icalendar 2.11.2 → 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d1b88e3b63c6a6c5865093d45c296bfc4f0d807922b79bbd071355f5bdf72a2
4
- data.tar.gz: 2b0e5e61d9dbab1ec76aab2a97bf3055da0ee42bfcb294867e1cba5d60841d17
3
+ metadata.gz: c581285bbacae9839202046bea20f80be28315865013f9f59e50851fbbed7a34
4
+ data.tar.gz: 8be871aab3ba233a56a6442783f6a32cbf138ae145fc777bed68fdf4beff0826
5
5
  SHA512:
6
- metadata.gz: 070602bed48d0b45c04dc6801944b0f2d8916a67ecc39194ff93a5bc6619aca5eafad06843cb39df72910f9babbe010310dcada5bce1a27d046297586a0d1042
7
- data.tar.gz: f4e43c98d659fa97b4cffadb0c79ba34ef0ba077d181272747eaf0065301d1b540a3fa5169dcd8d1701a60076f1f748ec8d28a44d1aefaf179197437ecab2958
6
+ metadata.gz: e847fc2a27df55033006bfc9b9d1572563a9a9bafc8cf286959e82e4a47f0c64fcda9a7b7470e37031e635a1e9b086eda769b75108a02e020782c1683fad763e
7
+ data.tar.gz: 3a8f8503ace1467d352d586ede3e4c8c91b7ab5d1fec5271c5add7658014a1cc61fc4b2f2ec32b09d481746ff14139fb83b7d14f63b23526056b183bb9133de6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.12.1 - 2025-10-19
4
+ - Fix a problem with invalid ics generation for calendars with custom properties that include a `tzid` parameter.
5
+
6
+ ## 2.12.0 - 2025-09-26
7
+ - Support timezone lookup by Windows names - Ronak Gothi
8
+
3
9
  ## 2.11.2 - 2025-06-21
4
10
  - Deal with more bad value parameter values by falling back to the property default type
5
11
 
@@ -3,32 +3,65 @@
3
3
  require 'delegate'
4
4
 
5
5
  module Icalendar
6
- class DowncasedHash < ::SimpleDelegator
6
+ class DowncasedHash
7
7
 
8
8
  def initialize(base)
9
- super Hash.new
9
+ @obj = Hash.new
10
10
  base.each do |key, value|
11
11
  self[key] = value
12
12
  end
13
13
  end
14
14
 
15
15
  def []=(key, value)
16
- __getobj__[key.to_s.downcase] = value
16
+ obj[key.to_s.downcase] = value
17
17
  end
18
18
 
19
19
  def [](key)
20
- __getobj__[key.to_s.downcase]
20
+ obj[key.to_s.downcase]
21
21
  end
22
22
 
23
23
  def has_key?(key)
24
- __getobj__.has_key? key.to_s.downcase
24
+ obj.has_key? key.to_s.downcase
25
25
  end
26
26
  alias_method :include?, :has_key?
27
27
  alias_method :member?, :has_key?
28
28
 
29
29
  def delete(key, &block)
30
- __getobj__.delete key.to_s.downcase, &block
30
+ obj.delete key.to_s.downcase, &block
31
31
  end
32
+
33
+ def merge(*other_hashes)
34
+ Icalendar::DowncasedHash.new(obj).merge!(*other_hashes)
35
+ end
36
+
37
+ def merge!(*other_hashes)
38
+ other_hashes.each do |hash|
39
+ hash.each do |key, value|
40
+ self[key] = value
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ def empty?
47
+ obj.empty?
48
+ end
49
+
50
+ def each(&block)
51
+ obj.each &block
52
+ end
53
+
54
+ def map(&block)
55
+ obj.map &block
56
+ end
57
+
58
+ def ==(other)
59
+ obj == Icalendar::DowncasedHash(other).obj
60
+ end
61
+
62
+ protected
63
+
64
+ attr_reader :obj
32
65
  end
33
66
 
34
67
  def self.DowncasedHash(base)
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module contains mappings from Windows timezone identifiers to Olson timezone identifiers.
4
+ #
5
+ # The data is taken from the unicode consortium [0], the proposal and rationale
6
+ # for this mapping is also available at the unicode consortium [1].
7
+ #
8
+ # [0] https://www.unicode.org/cldr/cldr-aux/charts/29/supplemental/zone_tzid.html
9
+ # [1] https://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping
10
+
11
+ module Icalendar
12
+ class Offset
13
+ class WindowsToIana < Offset
14
+ WINDOWS_TO_IANA = {
15
+ 'AUS Central Standard Time' => 'Australia/Darwin',
16
+ 'AUS Eastern Standard Time' => 'Australia/Sydney',
17
+ 'Afghanistan Standard Time' => 'Asia/Kabul',
18
+ 'Alaskan Standard Time' => 'America/Anchorage',
19
+ 'Arab Standard Time' => 'Asia/Riyadh',
20
+ 'Arabian Standard Time' => 'Asia/Dubai',
21
+ 'Arabic Standard Time' => 'Asia/Baghdad',
22
+ 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires',
23
+ 'Atlantic Standard Time' => 'America/Halifax',
24
+ 'Azerbaijan Standard Time' => 'Asia/Baku',
25
+ 'Azores Standard Time' => 'Atlantic/Azores',
26
+ 'Bahia Standard Time' => 'America/Bahia',
27
+ 'Bangladesh Standard Time' => 'Asia/Dhaka',
28
+ 'Belarus Standard Time' => 'Europe/Minsk',
29
+ 'Canada Central Standard Time' => 'America/Regina',
30
+ 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
31
+ 'Caucasus Standard Time' => 'Asia/Yerevan',
32
+ 'Cen. Australia Standard Time' => 'Australia/Adelaide',
33
+ 'Central America Standard Time' => 'America/Guatemala',
34
+ 'Central Asia Standard Time' => 'Asia/Almaty',
35
+ 'Central Brazilian Standard Time' => 'America/Cuiaba',
36
+ 'Central Europe Standard Time' => 'Europe/Budapest',
37
+ 'Central European Standard Time' => 'Europe/Warsaw',
38
+ 'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
39
+ 'Central Standard Time' => 'America/Chicago',
40
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
41
+ 'China Standard Time' => 'Asia/Shanghai',
42
+ 'Dateline Standard Time' => 'Etc/GMT+12',
43
+ 'E. Africa Standard Time' => 'Africa/Nairobi',
44
+ 'E. Australia Standard Time' => 'Australia/Brisbane',
45
+ 'E. Europe Standard Time' => 'Europe/Chisinau',
46
+ 'E. South America Standard Time' => 'America/Sao_Paulo',
47
+ 'Eastern Standard Time' => 'America/New_York',
48
+ 'Eastern Standard Time (Mexico)' => 'America/Cancun',
49
+ 'Egypt Standard Time' => 'Africa/Cairo',
50
+ 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
51
+ 'FLE Standard Time' => 'Europe/Kyiv',
52
+ 'Fiji Standard Time' => 'Pacific/Fiji',
53
+ 'GMT Standard Time' => 'Europe/London',
54
+ 'GTB Standard Time' => 'Europe/Bucharest',
55
+ 'Georgian Standard Time' => 'Asia/Tbilisi',
56
+ 'Greenland Standard Time' => 'America/Nuuk',
57
+ 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
58
+ 'Hawaiian Standard Time' => 'Pacific/Honolulu',
59
+ 'India Standard Time' => 'Asia/Kolkata',
60
+ 'Iran Standard Time' => 'Asia/Tehran',
61
+ 'Israel Standard Time' => 'Asia/Jerusalem',
62
+ 'Jordan Standard Time' => 'Asia/Amman',
63
+ 'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
64
+ 'Korea Standard Time' => 'Asia/Seoul',
65
+ 'Libya Standard Time' => 'Africa/Tripoli',
66
+ 'Line Islands Standard Time' => 'Pacific/Kiritimati',
67
+ 'Magadan Standard Time' => 'Asia/Magadan',
68
+ 'Mauritius Standard Time' => 'Indian/Mauritius',
69
+ 'Middle East Standard Time' => 'Asia/Beirut',
70
+ 'Montevideo Standard Time' => 'America/Montevideo',
71
+ 'Morocco Standard Time' => 'Africa/Casablanca',
72
+ 'Mountain Standard Time' => 'America/Denver',
73
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
74
+ 'Myanmar Standard Time' => 'Asia/Yangon',
75
+ 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
76
+ 'Namibia Standard Time' => 'Africa/Windhoek',
77
+ 'Nepal Standard Time' => 'Asia/Kathmandu',
78
+ 'New Zealand Standard Time' => 'Pacific/Auckland',
79
+ 'Newfoundland Standard Time' => 'America/St_Johns',
80
+ 'North Asia East Standard Time' => 'Asia/Irkutsk',
81
+ 'North Asia Standard Time' => 'Asia/Krasnoyarsk',
82
+ 'North Korea Standard Time' => 'Asia/Pyongyang',
83
+ 'Pacific SA Standard Time' => 'America/Santiago',
84
+ 'Pacific Standard Time' => 'America/Los_Angeles',
85
+ 'Pakistan Standard Time' => 'Asia/Karachi',
86
+ 'Paraguay Standard Time' => 'America/Asuncion',
87
+ 'Romance Standard Time' => 'Europe/Paris',
88
+ 'Russia Time Zone 10' => 'Asia/Srednekolymsk',
89
+ 'Russia Time Zone 11' => 'Asia/Kamchatka',
90
+ 'Russia Time Zone 3' => 'Europe/Samara',
91
+ 'Russian Standard Time' => 'Europe/Moscow',
92
+ 'SA Eastern Standard Time' => 'America/Cayenne',
93
+ 'SA Pacific Standard Time' => 'America/Bogota',
94
+ 'SA Western Standard Time' => 'America/La_Paz',
95
+ 'SE Asia Standard Time' => 'Asia/Bangkok',
96
+ 'Samoa Standard Time' => 'Pacific/Apia',
97
+ 'Singapore Standard Time' => 'Asia/Singapore',
98
+ 'South Africa Standard Time' => 'Africa/Johannesburg',
99
+ 'Sri Lanka Standard Time' => 'Asia/Colombo',
100
+ 'Syria Standard Time' => 'Asia/Damascus',
101
+ 'Taipei Standard Time' => 'Asia/Taipei',
102
+ 'Tasmania Standard Time' => 'Australia/Hobart',
103
+ 'Tokyo Standard Time' => 'Asia/Tokyo',
104
+ 'Tonga Standard Time' => 'Pacific/Tongatapu',
105
+ 'Turkey Standard Time' => 'Europe/Istanbul',
106
+ 'US Eastern Standard Time' => 'America/Indiana/Indianapolis',
107
+ 'US Mountain Standard Time' => 'America/Phoenix',
108
+ 'UTC' => 'Etc/GMT',
109
+ 'UTC+12' => 'Etc/GMT-12',
110
+ 'UTC-02' => 'Etc/GMT+2',
111
+ 'UTC-11' => 'Etc/GMT+11',
112
+ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
113
+ 'Venezuela Standard Time' => 'America/Caracas',
114
+ 'Vladivostok Standard Time' => 'Asia/Vladivostok',
115
+ 'W. Australia Standard Time' => 'Australia/Perth',
116
+ 'W. Central Africa Standard Time' => 'Africa/Lagos',
117
+ 'W. Europe Standard Time' => 'Europe/Berlin',
118
+ 'West Asia Standard Time' => 'Asia/Tashkent',
119
+ 'West Pacific Standard Time' => 'Pacific/Port_Moresby',
120
+ 'Yakutsk Standard Time' => 'Asia/Yakutsk'
121
+ }.freeze
122
+
123
+ def valid?
124
+ support_classes_defined? && tz
125
+ end
126
+
127
+ def normalized_value
128
+ Icalendar.logger.debug("Plan a - parsing #{value}/#{tzid} as ActiveSupport::TimeWithZone")
129
+ # plan a - use ActiveSupport::TimeWithZone
130
+ Icalendar::Values::Helpers::ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
131
+ end
132
+
133
+ def normalized_tzid
134
+ [WINDOWS_TO_IANA[tzid]].compact
135
+ end
136
+
137
+ private
138
+
139
+ def tz
140
+ @tz ||= ActiveSupport::TimeZone[normalized_tzid.first || '']
141
+ end
142
+ end
143
+ end
144
+ end
@@ -10,6 +10,7 @@ module Icalendar
10
10
  [
11
11
  Icalendar::Offset::ActiveSupportExact,
12
12
  Icalendar::Offset::TimeZoneStore,
13
+ Icalendar::Offset::WindowsToIana,
13
14
  Icalendar::Offset::ActiveSupportPartial,
14
15
  Icalendar::Offset::Null
15
16
  ].lazy.map { |klass| klass.new(tzid, value, timezone_store) }.detect(&:valid?)
@@ -36,7 +37,8 @@ module Icalendar
36
37
  end
37
38
  end
38
39
 
39
- require_relative "offset/active_support_exact"
40
- require_relative "offset/active_support_partial"
41
- require_relative "offset/null"
42
- require_relative "offset/time_zone_store"
40
+ require_relative 'offset/active_support_exact'
41
+ require_relative 'offset/active_support_partial'
42
+ require_relative 'offset/null'
43
+ require_relative 'offset/time_zone_store'
44
+ require_relative 'offset/windows_to_iana'
@@ -87,9 +87,10 @@ module Icalendar
87
87
  Icalendar::Values::Helpers::Array.new fields[:value].split(WRAP_PROPERTY_VALUE_SPLIT_REGEX),
88
88
  klass,
89
89
  fields[:params],
90
- delimiter: delimiter
90
+ delimiter: delimiter,
91
+ timezone_store: timezone_store
91
92
  else
92
- klass.new fields[:value], fields[:params]
93
+ klass.new fields[:value], fields[:params], timezone_store: timezone_store
93
94
  end
94
95
  rescue Icalendar::Values::DateTime::FormatError => fe
95
96
  raise fe if strict?
@@ -215,9 +216,6 @@ module Icalendar
215
216
  if param_value.size > 0
216
217
  param_value = param_value.gsub(PVALUE_GSUB_REGEX, '')
217
218
  params[param_name] << param_value
218
- if param_name == 'tzid'
219
- params['x-tz-store'] = timezone_store
220
- end
221
219
  end
222
220
  end
223
221
  end
@@ -8,10 +8,11 @@ module Icalendar
8
8
 
9
9
  class Value < ::SimpleDelegator
10
10
 
11
- attr_accessor :ical_params
11
+ attr_accessor :ical_params, :context
12
12
 
13
- def initialize(value, params = {})
13
+ def initialize(value, params = {}, context = {})
14
14
  @ical_params = Icalendar::DowncasedHash(params)
15
+ @context = Icalendar::DowncasedHash(context)
15
16
  super value
16
17
  end
17
18
 
@@ -5,8 +5,8 @@ module Icalendar
5
5
 
6
6
  class Boolean < Value
7
7
 
8
- def initialize(value, params = {})
9
- super value.to_s.downcase == 'true', params
8
+ def initialize(value, *args)
9
+ super value.to_s.downcase == 'true', *args
10
10
  end
11
11
 
12
12
  def value_ical
@@ -16,4 +16,4 @@ module Icalendar
16
16
  end
17
17
 
18
18
  end
19
- end
19
+ end
@@ -8,9 +8,8 @@ module Icalendar
8
8
  class Date < Value
9
9
  FORMAT = '%Y%m%d'
10
10
 
11
- def initialize(value, params = {})
11
+ def initialize(value, params = {}, *args)
12
12
  params.delete 'tzid'
13
- params.delete 'x-tz-store'
14
13
  if value.is_a? String
15
14
  begin
16
15
  parsed_date = ::Date.strptime(value, FORMAT)
@@ -18,9 +17,9 @@ module Icalendar
18
17
  raise FormatError.new("Failed to parse \"#{value}\" - #{e.message}")
19
18
  end
20
19
 
21
- super parsed_date, params
20
+ super parsed_date, params, *args
22
21
  elsif value.respond_to? :to_date
23
- super value.to_date, params
22
+ super value.to_date, params, *args
24
23
  else
25
24
  super
26
25
  end
@@ -11,7 +11,7 @@ module Icalendar
11
11
 
12
12
  FORMAT = '%Y%m%dT%H%M%S'
13
13
 
14
- def initialize(value, params = {})
14
+ def initialize(value, params = {}, *args)
15
15
  if value.is_a? String
16
16
  params['tzid'] = 'UTC' if value.end_with? 'Z'
17
17
 
@@ -21,9 +21,9 @@ module Icalendar
21
21
  raise FormatError.new("Failed to parse \"#{value}\" - #{e.message}")
22
22
  end
23
23
 
24
- super parsed_date, params
24
+ super parsed_date, params, *args
25
25
  elsif value.respond_to? :to_datetime
26
- super value.to_datetime, params
26
+ super value.to_datetime, params, *args
27
27
  else
28
28
  super
29
29
  end
@@ -7,11 +7,11 @@ module Icalendar
7
7
 
8
8
  class Duration < Value
9
9
 
10
- def initialize(value, params = {})
10
+ def initialize(value, *args)
11
11
  if value.is_a? Icalendar::Values::Duration
12
- super value.value, params
12
+ super value.value, *args
13
13
  else
14
- super OpenStruct.new(parse_fields value), params
14
+ super OpenStruct.new(parse_fields value), *args
15
15
  end
16
16
  end
17
17
 
@@ -5,8 +5,8 @@ module Icalendar
5
5
 
6
6
  class Float < Value
7
7
 
8
- def initialize(value, params = {})
9
- super value.to_f, params
8
+ def initialize(value, *args)
9
+ super value.to_f, *args
10
10
  end
11
11
 
12
12
  def value_ical
@@ -16,4 +16,4 @@ module Icalendar
16
16
  end
17
17
 
18
18
  end
19
- end
19
+ end
@@ -8,22 +8,23 @@ module Icalendar
8
8
 
9
9
  attr_reader :value_delimiter
10
10
 
11
- def initialize(value, klass, params = {}, options = {})
12
- @value_delimiter = options[:delimiter] || ','
11
+ def initialize(value, klass, params = {}, context = {})
12
+ context = Icalendar::DowncasedHash(context)
13
+ @value_delimiter = context['delimiter'] || ','
13
14
  mapped = if value.is_a? ::Array
14
15
  value.map do |v|
15
16
  if v.is_a? Icalendar::Values::Helpers::Array
16
- Icalendar::Values::Helpers::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
17
+ Icalendar::Values::Helpers::Array.new v.value, klass, v.ical_params, context.merge(delimiter: v.value_delimiter)
17
18
  elsif v.is_a? ::Array
18
- Icalendar::Values::Helpers::Array.new v, klass, params, delimiter: value_delimiter
19
+ Icalendar::Values::Helpers::Array.new v, klass, params, context.merge(delimiter: value_delimiter)
19
20
  elsif v.is_a? Icalendar::Value
20
21
  v
21
22
  else
22
- klass.new v, params
23
+ klass.new v, params, context
23
24
  end
24
25
  end
25
26
  else
26
- [klass.new(value, params)]
27
+ [klass.new(value, params, context)]
27
28
  end
28
29
  super mapped
29
30
  end
@@ -19,17 +19,20 @@ module Icalendar
19
19
  module TimeWithZone
20
20
  attr_reader :tz_utc, :timezone_store
21
21
 
22
- def initialize(value, params = {})
22
+ def initialize(value, params = {}, context = {})
23
23
  params = Icalendar::DowncasedHash(params)
24
+ context = Icalendar::DowncasedHash(context)
24
25
  @tz_utc = params['tzid'] == 'UTC'
25
- @timezone_store = params.delete 'x-tz-store'
26
+ @timezone_store = context['timezone_store']
26
27
 
27
28
  offset = Icalendar::Offset.build(value, params, timezone_store)
28
29
 
29
- @offset_value = offset&.normalized_value
30
- params['tzid'] = offset.normalized_tzid if offset
30
+ unless offset.nil?
31
+ @offset_value = offset.normalized_value
32
+ params['tzid'] = offset.normalized_tzid
33
+ end
31
34
 
32
- super (@offset_value || value), params
35
+ super (@offset_value || value), params, context
33
36
  end
34
37
 
35
38
  def __getobj__
@@ -5,8 +5,8 @@ module Icalendar
5
5
 
6
6
  class Integer < Value
7
7
 
8
- def initialize(value, params = {})
9
- super value.to_i, params
8
+ def initialize(value, *args)
9
+ super value.to_i, *args
10
10
  end
11
11
 
12
12
  def value_ical
@@ -16,4 +16,4 @@ module Icalendar
16
16
  end
17
17
 
18
18
  end
19
- end
19
+ end
@@ -7,7 +7,7 @@ module Icalendar
7
7
 
8
8
  PERIOD_LAST_PART_REGEX = /\A[+-]?P.+\z/.freeze
9
9
 
10
- def initialize(value, params = {})
10
+ def initialize(value, *args)
11
11
  parts = value.split '/'
12
12
  period_start = Icalendar::Values::DateTime.new parts.first
13
13
  if parts.last =~ PERIOD_LAST_PART_REGEX
@@ -15,7 +15,7 @@ module Icalendar
15
15
  else
16
16
  period_end = Icalendar::Values::DateTime.new parts.last
17
17
  end
18
- super [period_start, period_end], params
18
+ super [period_start, period_end], *args
19
19
  end
20
20
 
21
21
  def value_ical
@@ -47,4 +47,4 @@ module Icalendar
47
47
  end
48
48
  end
49
49
  end
50
- end
50
+ end
@@ -12,11 +12,11 @@ module Icalendar
12
12
  MONTHDAY = '[+-]?\d{1,2}'
13
13
  YEARDAY = '[+-]?\d{1,3}'
14
14
 
15
- def initialize(value, params = {})
15
+ def initialize(value, *args)
16
16
  if value.is_a? Icalendar::Values::Recur
17
- super value.value, params
17
+ super value.value, *args
18
18
  else
19
- super OpenStruct.new(parse_fields value), params
19
+ super OpenStruct.new(parse_fields value), *args
20
20
  end
21
21
  end
22
22
 
@@ -3,12 +3,12 @@
3
3
  module Icalendar
4
4
  module Values
5
5
  class Text < Value
6
- def initialize(value, params = {})
6
+ def initialize(value, *args)
7
7
  value = value.gsub('\n', "\n")
8
8
  value.gsub!('\,', ',')
9
9
  value.gsub!('\;', ';')
10
10
  value.gsub!('\\\\') { '\\' }
11
- super value, params
11
+ super value, *args
12
12
  end
13
13
 
14
14
  VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX = /\r?\n/.freeze
@@ -11,12 +11,12 @@ module Icalendar
11
11
 
12
12
  FORMAT = '%H%M%S'
13
13
 
14
- def initialize(value, params = {})
14
+ def initialize(value, params = {}, *args)
15
15
  if value.is_a? String
16
16
  params['tzid'] = 'UTC' if value.end_with? 'Z'
17
- super ::DateTime.strptime(value, FORMAT).to_time, params
17
+ super ::DateTime.strptime(value, FORMAT).to_time, params, *args
18
18
  elsif value.respond_to? :to_time
19
- super value.to_time, params
19
+ super value.to_time, params, *args
20
20
  else
21
21
  super
22
22
  end
@@ -33,4 +33,4 @@ module Icalendar
33
33
  end
34
34
 
35
35
  end
36
- end
36
+ end
@@ -7,9 +7,9 @@ module Icalendar
7
7
 
8
8
  class Uri < Value
9
9
 
10
- def initialize(value, params = {})
10
+ def initialize(value, *args)
11
11
  parsed = URI.parse(value) rescue value
12
- super parsed, params
12
+ super parsed, *args
13
13
  end
14
14
 
15
15
  def value_ical
@@ -5,13 +5,13 @@ require 'ostruct'
5
5
  module Icalendar
6
6
  module Values
7
7
  class UtcOffset < Value
8
- def initialize(value, params = {})
8
+ def initialize(value, *args)
9
9
  if value.is_a? Icalendar::Values::UtcOffset
10
10
  value = value.value
11
11
  else
12
12
  value = OpenStruct.new parse_fields(value)
13
13
  end
14
- super value, params
14
+ super value, *args
15
15
  end
16
16
 
17
17
  def behind?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Icalendar
4
4
 
5
- VERSION = '2.11.2'
5
+ VERSION = '2.12.1'
6
6
 
7
7
  end
@@ -0,0 +1,30 @@
1
+ BEGIN:VCALENDAR
2
+ METHOD:COUNTER
3
+ PRODID:Microsoft Exchange Server 2010
4
+ VERSION:2.0
5
+ BEGIN:VTIMEZONE
6
+ TZID:Pacific Standard Time
7
+ BEGIN:STANDARD
8
+ DTSTART:16010101T020000
9
+ TZOFFSETFROM:-0700
10
+ TZOFFSETTO:-0800
11
+ RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
12
+ END:STANDARD
13
+ BEGIN:DAYLIGHT
14
+ DTSTART:16010101T020000
15
+ TZOFFSETFROM:-0800
16
+ TZOFFSETTO:-0700
17
+ RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
18
+ END:DAYLIGHT
19
+ END:VTIMEZONE
20
+ BEGIN:VEVENT
21
+ DTSTART;TZID=Pacific Standard Time:20251008T103000
22
+ DTEND;TZID=Pacific Standard Time:20251008T113000
23
+ X-MS-OLK-ORIGINALSTART;TZID=Pacific Standard Time:20251008T110000
24
+ X-MS-OLK-ORIGINALEND;TZID=Pacific Standard Time:20251008T120000
25
+ UID:1sqt3u5rn2fprt7g7u75obcgav@google.com
26
+ SEQUENCE:0
27
+ DTSTAMP:20251007T184328Z
28
+ COMMENT;LANGUAGE=en-US:\n
29
+ END:VEVENT
30
+ END:VCALENDAR
data/spec/parser_spec.rb CHANGED
@@ -126,4 +126,18 @@ describe Icalendar::Parser do
126
126
  expect(event.location.value).to eq "1000 Main St Example, State 12345"
127
127
  end
128
128
  end
129
+
130
+ describe 'custom properties with tzid' do
131
+ let(:fn) { 'tz_store_param_bug.ics' }
132
+
133
+ it 'parses without error' do
134
+ expect(subject.parse.first).to be_a Icalendar::Calendar
135
+ end
136
+
137
+ it 'can be output to ics and re-parsed without error' do
138
+ cal = subject.parse.first
139
+ new_cal = Icalendar::Parser.new(cal.to_ical, false).parse.first
140
+ expect(new_cal).to be_a Icalendar::Calendar
141
+ end
142
+ end
129
143
  end
@@ -17,7 +17,7 @@ describe Icalendar::Values::DateTime do
17
17
  expect(subject.value_ical).to eq value
18
18
  end
19
19
 
20
- context 'local time' do
20
+ context 'tzid in IANA format' do
21
21
  let(:value) { '20140209T160652' }
22
22
  let(:params) { {'tzid' => 'America/Denver'} }
23
23
 
@@ -28,14 +28,15 @@ describe Icalendar::Values::DateTime do
28
28
  end
29
29
  end
30
30
 
31
- context 'nonstandard format tzid local time' do
31
+ context 'tzid with partial match' do
32
32
  let(:value) { '20230901T230404' }
33
- let(:params) { {'tzid' => 'Singapore Standard Time'} }
33
+ let(:params) { { 'tzid' => 'Singapore Time' } }
34
34
 
35
35
  it 'parses the value as local time' do
36
36
  expect(subject.value.hour).to eq 23
37
- expect(subject.value.utc_offset).to eq 28800
37
+ expect(subject.value.utc_offset).to eq 28_800
38
38
  expect(subject.value.utc.hour).to eq 15
39
+ expect(subject.value.utc.minute).to eq 04
39
40
  end
40
41
 
41
42
  it 'updates the tzid' do
@@ -43,8 +44,24 @@ describe Icalendar::Values::DateTime do
43
44
  expect(subject.ical_params['tzid']).to eq ['Asia/Singapore']
44
45
  end
45
46
  end
46
- end
47
47
 
48
+ context 'tzid in windows format' do
49
+ let(:value) { '20230901T230404' }
50
+ let(:params) { { 'tzid' => 'India Standard Time' } }
51
+
52
+ it 'parses the value as local time' do
53
+ expect(subject.value.hour).to eq 23
54
+ expect(subject.value.utc_offset).to eq 19_800
55
+ expect(subject.value.utc.hour).to eq 17
56
+ expect(subject.value.utc.minute).to eq 34
57
+ end
58
+
59
+ it 'updates the tzid' do
60
+ # use an array because that's how output from the parser will end up
61
+ expect(subject.ical_params['tzid']).to eq ['Asia/Kolkata']
62
+ end
63
+ end
64
+ end
48
65
  else
49
66
 
50
67
  context 'without ActiveSupport' do
@@ -63,10 +80,8 @@ describe Icalendar::Values::DateTime do
63
80
  end
64
81
  end
65
82
  end
66
-
67
83
  end
68
84
 
69
-
70
85
  context 'common tests' do
71
86
  it 'does not add any tzid parameter to output' do
72
87
  expect(subject.to_ical described_class).to eq ":#{value}"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.11.2
4
+ version: 2.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-06-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: base64
@@ -228,6 +227,7 @@ files:
228
227
  - lib/icalendar/offset/active_support_partial.rb
229
228
  - lib/icalendar/offset/null.rb
230
229
  - lib/icalendar/offset/time_zone_store.rb
230
+ - lib/icalendar/offset/windows_to_iana.rb
231
231
  - lib/icalendar/parser.rb
232
232
  - lib/icalendar/timezone.rb
233
233
  - lib/icalendar/timezone_store.rb
@@ -275,6 +275,7 @@ files:
275
275
  - spec/fixtures/two_day_events.ics
276
276
  - spec/fixtures/two_events.ics
277
277
  - spec/fixtures/two_time_events.ics
278
+ - spec/fixtures/tz_store_param_bug.ics
278
279
  - spec/fixtures/tzid_search.ics
279
280
  - spec/freebusy_spec.rb
280
281
  - spec/journal_spec.rb
@@ -317,8 +318,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
317
318
  - !ruby/object:Gem::Version
318
319
  version: '0'
319
320
  requirements: []
320
- rubygems_version: 3.3.27
321
- signing_key:
321
+ rubygems_version: 3.6.9
322
322
  specification_version: 4
323
323
  summary: A ruby implementation of the iCalendar specification (RFC-5545).
324
324
  test_files:
@@ -344,6 +344,7 @@ test_files:
344
344
  - spec/fixtures/two_day_events.ics
345
345
  - spec/fixtures/two_events.ics
346
346
  - spec/fixtures/two_time_events.ics
347
+ - spec/fixtures/tz_store_param_bug.ics
347
348
  - spec/fixtures/tzid_search.ics
348
349
  - spec/freebusy_spec.rb
349
350
  - spec/journal_spec.rb