icalendar 2.12.0 → 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: 9aaf6132f4baffabc0b6a45fb4a1a5049e58b4ca264eddaac67648575e6702c6
4
- data.tar.gz: ac0ee8ac873a3a9d97ad8914aa6f2bf3edb545f1d58d0d152988850cce9fca07
3
+ metadata.gz: c581285bbacae9839202046bea20f80be28315865013f9f59e50851fbbed7a34
4
+ data.tar.gz: 8be871aab3ba233a56a6442783f6a32cbf138ae145fc777bed68fdf4beff0826
5
5
  SHA512:
6
- metadata.gz: a330a0e6568705cc448eef9090d54c72b59fc78fec537ca715523689b5954875c76e0a16957ec344bce71fcb4b8f1f9e62af7c57591eef49d4682acee431a3e1
7
- data.tar.gz: 2f49d14ad8759d16324a04f1472eee65e05fc5a1a9167ab5a93da5e2cdddab11cd8a61bad95718533747f10f7f8bf4b7a664e67e8b743d0cf490a1657f55dcdd
6
+ metadata.gz: e847fc2a27df55033006bfc9b9d1572563a9a9bafc8cf286959e82e4a47f0c64fcda9a7b7470e37031e635a1e9b086eda769b75108a02e020782c1683fad763e
7
+ data.tar.gz: 3a8f8503ace1467d352d586ede3e4c8c91b7ab5d1fec5271c5add7658014a1cc61fc4b2f2ec32b09d481746ff14139fb83b7d14f63b23526056b183bb9133de6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
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
+
3
6
  ## 2.12.0 - 2025-09-26
4
7
  - Support timezone lookup by Windows names - Ronak Gothi
5
8
 
@@ -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)
@@ -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.12.0'
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.0
4
+ version: 2.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
@@ -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
@@ -343,6 +344,7 @@ test_files:
343
344
  - spec/fixtures/two_day_events.ics
344
345
  - spec/fixtures/two_events.ics
345
346
  - spec/fixtures/two_time_events.ics
347
+ - spec/fixtures/tz_store_param_bug.ics
346
348
  - spec/fixtures/tzid_search.ics
347
349
  - spec/freebusy_spec.rb
348
350
  - spec/journal_spec.rb