module CalInvite::IcalTimezone
Builds RFC 5545 VTIMEZONE components and converts UTC times to local wall-clock time for a given IANA/Olson timezone identifier, using TZInfo (already pulled in transitively via activesupport).
Both entry points fail soft: if tzid isn’t a TZInfo-recognized identifier (e.g. a raw UTC offset string like “+01:00”, or ‘UTC’ itself), they return nil so callers can fall back to their previous behavior instead of raising.
@api private
Public Instance Methods
Source
# File lib/cal_invite/ical_timezone.rb 102 def fixed_observance_lines(period) 103 [ 104 "BEGIN:STANDARD", 105 "DTSTART:16010101T000000", 106 "TZOFFSETFROM:#{format_offset(period.offset.observed_utc_offset)}", 107 "TZOFFSETTO:#{format_offset(period.offset.observed_utc_offset)}", 108 "TZNAME:#{period.offset.abbreviation}", 109 "END:STANDARD" 110 ] 111 end
@api private
Source
# File lib/cal_invite/ical_timezone.rb 126 def format_offset(seconds) 127 sign = seconds.negative? ? '-' : '+' 128 abs = seconds.abs 129 format('%<sign>s%<hours>02d%<minutes>02d', sign: sign, hours: abs / 3600, minutes: (abs % 3600) / 60) 130 end
@api private
Source
# File lib/cal_invite/ical_timezone.rb 25 def local_time(tzid, utc_time) 26 return nil if tzid.nil? || tzid.to_s.strip.empty? || tzid.to_s.upcase == 'UTC' 27 28 TZInfo::Timezone.get(tzid.to_s).to_local(utc_time.utc) 29 rescue TZInfo::InvalidTimezoneIdentifier 30 nil 31 end
Converts a UTC time to local wall-clock time for the given timezone.
@param tzid [String] An IANA timezone identifier, e.g. “America/New_York” @param utc_time [Time] The time to convert (interpreted as UTC) @return [Time, nil] The local wall-clock time, or nil if tzid is unrecognized
Source
# File lib/cal_invite/ical_timezone.rb 87 def observance_lines(kind, from_period, to_period) 88 transition_time = from_period.local_ends_at.to_time 89 90 [ 91 "BEGIN:#{kind}", 92 "DTSTART:#{transition_time.strftime('%Y%m%dT%H%M%S')}", 93 "TZOFFSETFROM:#{format_offset(from_period.offset.observed_utc_offset)}", 94 "TZOFFSETTO:#{format_offset(to_period.offset.observed_utc_offset)}", 95 "TZNAME:#{to_period.offset.abbreviation}", 96 rrule_line(transition_time), 97 "END:#{kind}" 98 ] 99 end
@api private
Source
# File lib/cal_invite/ical_timezone.rb 80 def period_after(tz, period) 81 return nil unless period&.ends_at 82 83 tz.period_for(period.ends_at.to_time) 84 end
@api private
Source
# File lib/cal_invite/ical_timezone.rb 73 def period_before(tz, period) 74 return nil unless period.starts_at 75 76 tz.period_for(period.starts_at.to_time - 1) 77 end
@api private
Source
# File lib/cal_invite/ical_timezone.rb 116 def rrule_line(time) 117 wday_names = %w[SU MO TU WE TH FR SA] 118 days_in_month = Date.new(time.year, time.month, -1).day 119 nth = (time.day - 1) / 7 + 1 120 nth = -1 if time.day + 7 > days_in_month 121 122 "RRULE:FREQ=YEARLY;BYMONTH=#{time.month};BYDAY=#{nth}#{wday_names[time.wday]}" 123 end
Derives a YEARLY RRULE (e.g. “2nd Sunday in March”) from a transition date.
@api private
Source
# File lib/cal_invite/ical_timezone.rb 40 def vtimezone_lines(tzid) 41 return nil if tzid.nil? || tzid.to_s.strip.empty? || tzid.to_s.upcase == 'UTC' 42 43 tz = TZInfo::Timezone.get(tzid.to_s) 44 current = tz.period_for(Time.now.utc) 45 46 if current.dst? 47 dst_period = current 48 std_period = period_before(tz, dst_period) 49 else 50 std_period = current 51 dst_period = period_after(tz, std_period) 52 dst_period = nil unless dst_period&.dst? 53 end 54 55 lines = ["BEGIN:VTIMEZONE", "TZID:#{tzid}"] 56 57 if std_period && dst_period 58 lines.concat(observance_lines('STANDARD', dst_period, std_period)) 59 lines.concat(observance_lines('DAYLIGHT', std_period, dst_period)) 60 else 61 lines.concat(fixed_observance_lines(std_period || dst_period || current)) 62 end 63 64 lines << "END:VTIMEZONE" 65 lines 66 rescue StandardError 67 # Never let an exotic/edge-case timezone break calendar generation — 68 # worst case the VEVENT ends up without a VTIMEZONE definition. 69 nil 70 end
Builds a complete VTIMEZONE component (STANDARD/DAYLIGHT observances with RRULEs derived from the timezone’s actual transition rules) for the given timezone identifier.
@param tzid [String] An IANA timezone identifier, e.g. “America/New_York” @return [Array<String>, nil] iCalendar lines for the VTIMEZONE component, or nil if tzid is unrecognized, is UTC, or the component can’t be built