Module: CalInvite::IcalTimezone Private

Defined in:
lib/cal_invite/ical_timezone.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

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.

Class Method Summary collapse

Class Method Details

.fixed_observance_lines(period) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



102
103
104
105
106
107
108
109
110
111
# File 'lib/cal_invite/ical_timezone.rb', line 102

def fixed_observance_lines(period)
  [
    "BEGIN:STANDARD",
    "DTSTART:16010101T000000",
    "TZOFFSETFROM:#{format_offset(period.offset.observed_utc_offset)}",
    "TZOFFSETTO:#{format_offset(period.offset.observed_utc_offset)}",
    "TZNAME:#{period.offset.abbreviation}",
    "END:STANDARD"
  ]
end

.format_offset(seconds) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



126
127
128
129
130
# File 'lib/cal_invite/ical_timezone.rb', line 126

def format_offset(seconds)
  sign = seconds.negative? ? '-' : '+'
  abs = seconds.abs
  format('%<sign>s%<hours>02d%<minutes>02d', sign: sign, hours: abs / 3600, minutes: (abs % 3600) / 60)
end

.local_time(tzid, utc_time) ⇒ Time?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Converts a UTC time to local wall-clock time for the given timezone.

Parameters:

  • tzid (String)

    An IANA timezone identifier, e.g. "America/New_York"

  • utc_time (Time)

    The time to convert (interpreted as UTC)

Returns:

  • (Time, nil)

    The local wall-clock time, or nil if tzid is unrecognized



25
26
27
28
29
30
31
# File 'lib/cal_invite/ical_timezone.rb', line 25

def local_time(tzid, utc_time)
  return nil if tzid.nil? || tzid.to_s.strip.empty? || tzid.to_s.upcase == 'UTC'

  TZInfo::Timezone.get(tzid.to_s).to_local(utc_time.utc)
rescue TZInfo::InvalidTimezoneIdentifier
  nil
end

.observance_lines(kind, from_period, to_period) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/cal_invite/ical_timezone.rb', line 87

def observance_lines(kind, from_period, to_period)
  transition_time = from_period.local_ends_at.to_time

  [
    "BEGIN:#{kind}",
    "DTSTART:#{transition_time.strftime('%Y%m%dT%H%M%S')}",
    "TZOFFSETFROM:#{format_offset(from_period.offset.observed_utc_offset)}",
    "TZOFFSETTO:#{format_offset(to_period.offset.observed_utc_offset)}",
    "TZNAME:#{to_period.offset.abbreviation}",
    rrule_line(transition_time),
    "END:#{kind}"
  ]
end

.period_after(tz, period) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



80
81
82
83
84
# File 'lib/cal_invite/ical_timezone.rb', line 80

def period_after(tz, period)
  return nil unless period&.ends_at

  tz.period_for(period.ends_at.to_time)
end

.period_before(tz, period) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



73
74
75
76
77
# File 'lib/cal_invite/ical_timezone.rb', line 73

def period_before(tz, period)
  return nil unless period.starts_at

  tz.period_for(period.starts_at.to_time - 1)
end

.rrule_line(time) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Derives a YEARLY RRULE (e.g. "2nd Sunday in March") from a transition date.



116
117
118
119
120
121
122
123
# File 'lib/cal_invite/ical_timezone.rb', line 116

def rrule_line(time)
  wday_names = %w[SU MO TU WE TH FR SA]
  days_in_month = Date.new(time.year, time.month, -1).day
  nth = (time.day - 1) / 7 + 1
  nth = -1 if time.day + 7 > days_in_month

  "RRULE:FREQ=YEARLY;BYMONTH=#{time.month};BYDAY=#{nth}#{wday_names[time.wday]}"
end

.vtimezone_lines(tzid) ⇒ Array<String>?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds a complete VTIMEZONE component (STANDARD/DAYLIGHT observances with RRULEs derived from the timezone's actual transition rules) for the given timezone identifier.

Parameters:

  • tzid (String)

    An IANA timezone identifier, e.g. "America/New_York"

Returns:

  • (Array<String>, nil)

    iCalendar lines for the VTIMEZONE component, or nil if tzid is unrecognized, is UTC, or the component can't be built



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/cal_invite/ical_timezone.rb', line 40

def vtimezone_lines(tzid)
  return nil if tzid.nil? || tzid.to_s.strip.empty? || tzid.to_s.upcase == 'UTC'

  tz = TZInfo::Timezone.get(tzid.to_s)
  current = tz.period_for(Time.now.utc)

  if current.dst?
    dst_period = current
    std_period = period_before(tz, dst_period)
  else
    std_period = current
    dst_period = period_after(tz, std_period)
    dst_period = nil unless dst_period&.dst?
  end

  lines = ["BEGIN:VTIMEZONE", "TZID:#{tzid}"]

  if std_period && dst_period
    lines.concat(observance_lines('STANDARD', dst_period, std_period))
    lines.concat(observance_lines('DAYLIGHT', std_period, dst_period))
  else
    lines.concat(fixed_observance_lines(std_period || dst_period || current))
  end

  lines << "END:VTIMEZONE"
  lines
rescue StandardError
  # Never let an exotic/edge-case timezone break calendar generation —
  # worst case the VEVENT ends up without a VTIMEZONE definition.
  nil
end