icalendar 2.12.4 → 2.13.0

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: 6a9a656fd81bc80eb43cf61e14d95a3b582f66bae80417da16a7e5d621910581
4
- data.tar.gz: df88f0f0a1619dbdcba157316aba616019d146fe74e46aba6d7603dc30b40c26
3
+ metadata.gz: 7437b0ae5b86b89cb49c3db3c405aabe0a07ad4a9b1a6fd333faf4ed48d82384
4
+ data.tar.gz: 7a73813c5bbccc460acf9b2e80398dc17b48d65b16fe568e433f7fb6e50df107
5
5
  SHA512:
6
- metadata.gz: a0ad4eb07a4123fba9260916530046bd008a9f076a26a276f09a31cce789aa0801a38fff3d553b7b11fcb3de072c72aff758921ad601d7e643b808495425b0df
7
- data.tar.gz: 06bab135b068613fdf8c4d10554a327a35b2072eb5e37c5e7bc7509b8e62b05e596153cfa8985065b2739e99eb7bc90dd8282c173972929900aa763d58e218b1
6
+ metadata.gz: 22a6861abb0548b74eb3ea814e4c8632da9d35b6ad0bc798faed60ee053b215d7cd32450b7317606b17f0d73fda188974f18d9d14cddd24cde92b1d3511872d1
7
+ data.tar.gz: 72ac340771b89b5d7eb6628a00e85aea8c19833ac0e74e15313de71feaac42073604ba1dd3e964cbff843cf9e832f430480bbd9976af19f9f851e076009b7c49
@@ -18,8 +18,6 @@ jobs:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  ruby:
21
- - 3.1
22
- - 3.2
23
21
  - 3.3
24
22
  - 3.4
25
23
  - "4.0"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.13.0 - 2026-10-11
4
+ - Generate `CATEGORIES` as a single comma-separated line instead of one line per value, matching how Google Calendar and Nextcloud expect it (#226). Setting `categories = [...]` now stores the joined value as one item, so code that reads `#categories` expecting one entry per category will see a single combined entry instead; `append_categories` is unaffected and still adds a separate line. - Carlos Kvasir
5
+ - Replace OpenStruct with Structs in Duration, Recur and UtcOffset values and drop the ostruct dependency (#329). Requires Ruby 2.5 or newer, for `Struct`'s `keyword_init:`. The parts keep the same readers and setters, but `#value` is now a Struct, so assigning an attribute that isn't one of its parts raises `NoMethodError`. - David Corson-Knowles
6
+ - Correct negative fractional-hour TZInfo offsets - ac1982
7
+
8
+ ## 2.12.5 - 2026-09-25
9
+ - Generate timezone transition onsets and recurrence dates using the previous UTC offset. - Ben Abulafia
10
+ - Limit parsing depth
11
+ - Remove TZID from DATE values - Henry Blyth
12
+
3
13
  ## 2.12.4 - 2026-07-31
4
14
  - TEXT unescape fix - Vincent Gao
5
15
 
data/icalendar.gemspec CHANGED
@@ -29,12 +29,11 @@ ActiveSupport is required for TimeWithZone support, but not required for general
29
29
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
30
30
  s.require_paths = ['lib']
31
31
 
32
- s.required_ruby_version = '>= 2.4.0'
32
+ s.required_ruby_version = '>= 2.5.0'
33
33
 
34
34
  s.add_dependency 'base64'
35
35
  s.add_dependency 'ice_cube', '~> 0.16'
36
36
  s.add_dependency 'logger'
37
- s.add_dependency 'ostruct'
38
37
 
39
38
  s.add_development_dependency 'rake', '~> 13.0'
40
39
  s.add_development_dependency 'bundler'
@@ -12,7 +12,7 @@ module Icalendar
12
12
  optional_single_property :uid
13
13
  optional_single_property :last_modified, Icalendar::Values::DateTime, true
14
14
  optional_single_property :url, Icalendar::Values::Uri, true
15
- optional_property :categories
15
+ optional_property :categories, Icalendar::Values::Text, false, false, true
16
16
  optional_single_property :refresh_interval, Icalendar::Values::Duration, true
17
17
  optional_single_property :source, Icalendar::Values::Uri, true
18
18
  optional_single_property :color
@@ -32,7 +32,7 @@ module Icalendar
32
32
  optional_property :rrule, Icalendar::Values::Recur, true
33
33
  optional_property :attach, Icalendar::Values::Uri
34
34
  optional_property :attendee, Icalendar::Values::CalAddress
35
- optional_property :categories
35
+ optional_property :categories, Icalendar::Values::Text, false, false, true
36
36
  optional_property :comment
37
37
  optional_property :contact
38
38
  optional_property :exdate, Icalendar::Values::DateTime
@@ -123,9 +123,9 @@ module Icalendar
123
123
  self.mutex_properties << properties
124
124
  end
125
125
 
126
- def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false, new_property = false)
126
+ def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false, new_property = false, single_line = false)
127
127
  self.suggested_single_properties << prop if suggested_single
128
- multi_property prop, klass, new_property
128
+ multi_property prop, klass, new_property, single_line
129
129
  end
130
130
 
131
131
  def single_property(prop, klass, new_property)
@@ -141,14 +141,16 @@ module Icalendar
141
141
  end
142
142
  end
143
143
 
144
- def multi_property(prop, klass, new_property)
144
+ def multi_property(prop, klass, new_property, single_line = false)
145
145
  self.multiple_properties << prop.to_s
146
146
  self.default_property_types[prop.to_s] = klass
147
147
  property_var = "@#{prop}"
148
148
 
149
149
  define_method "#{prop}=" do |value|
150
150
  mapped = map_property_value value, klass, true, new_property
151
- if mapped.is_a? Icalendar::Values::Helpers::Array
151
+ if single_line
152
+ instance_variable_set property_var, [mapped].compact
153
+ elsif mapped.is_a? Icalendar::Values::Helpers::Array
152
154
  instance_variable_set property_var, mapped.to_a.compact
153
155
  else
154
156
  instance_variable_set property_var, [mapped].compact
@@ -22,7 +22,7 @@ module Icalendar
22
22
  optional_property :rrule, Icalendar::Values::Recur, true
23
23
  optional_property :attach, Icalendar::Values::Uri
24
24
  optional_property :attendee, Icalendar::Values::CalAddress
25
- optional_property :categories
25
+ optional_property :categories, Icalendar::Values::Text, false, false, true
26
26
  optional_property :comment
27
27
  optional_property :contact
28
28
  optional_property :description
@@ -6,8 +6,9 @@ require 'stringio'
6
6
  module Icalendar
7
7
 
8
8
  class Parser
9
- attr_writer :component_class
9
+ attr_writer :component_class, :parse_depth_limit
10
10
  attr_reader :source, :strict, :timezone_store, :verbose
11
+ attr_accessor :parse_depth
11
12
 
12
13
  CLEAN_BAD_WRAPPING_GSUB_REGEX = /\r?\n[ \t]/.freeze
13
14
 
@@ -39,6 +40,7 @@ module Icalendar
39
40
  read_in_data
40
41
  @strict = strict
41
42
  @verbose = verbose
43
+ @parse_depth = 0
42
44
  @timezone_store = TimezoneStore.new
43
45
  end
44
46
 
@@ -139,9 +141,16 @@ module Icalendar
139
141
  @component_class ||= Icalendar::Calendar
140
142
  end
141
143
 
144
+ def parse_depth_limit
145
+ # a valid ics file should only have 3 levels. Leaving some room for non-compliant custom components
146
+ @parse_depth_limit ||= 5
147
+ end
148
+
142
149
  PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX = /\AV/.freeze
143
150
 
144
151
  def parse_component(component)
152
+ self.parse_depth += 1
153
+ raise ParseError.new("ics file nested too deeply") if parse_depth > parse_depth_limit
145
154
  while (fields = next_fields)
146
155
  if fields[:name] == 'end'
147
156
  klass_name = fields[:value].gsub(PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX, '').downcase.capitalize
@@ -161,6 +170,7 @@ module Icalendar
161
170
  parse_property component, fields
162
171
  end
163
172
  end
173
+ self.parse_depth -= 1
164
174
  component
165
175
  end
166
176
 
@@ -33,7 +33,7 @@ module Icalendar
33
33
  optional_property :rrule, Icalendar::Values::Recur, true
34
34
  optional_property :attach, Icalendar::Values::Uri
35
35
  optional_property :attendee, Icalendar::Values::CalAddress
36
- optional_property :categories
36
+ optional_property :categories, Icalendar::Values::Text, false, false, true
37
37
  optional_property :comment
38
38
  optional_property :contact
39
39
  optional_property :exdate, Icalendar::Values::DateTime
@@ -63,7 +63,8 @@ module Icalendar
63
63
  end
64
64
 
65
65
  def rrule
66
- start = (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime
66
+ # RFC 5545 defines the onset in local time using TZOFFSETFROM.
67
+ start = (respond_to?(:local_end_at) ? local_end_at : local_end).to_datetime
67
68
  # this is somewhat of a hack, but seems to work ok
68
69
  # assumes that no timezone transition is in law as "4th X of the month"
69
70
  # but only as 1st X, 2nd X, 3rd X, or Last X
@@ -78,14 +79,15 @@ module Icalendar
78
79
  end
79
80
 
80
81
  def dtstart
81
- (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime.strftime '%Y%m%dT%H%M%S'
82
+ (respond_to?(:local_end_at) ? local_end_at : local_end).to_datetime.strftime '%Y%m%dT%H%M%S'
82
83
  end
83
84
  end
84
85
 
85
86
  module TimezoneOffset
86
87
  def ical_offset
87
88
  o = utc_total_offset
88
- sprintf '%+-2.2d%2.2d', (o / 3600).to_i, ((o / 60) % 60).to_i
89
+ magnitude = o.abs
90
+ sprintf '%s%02d%02d', o < 0 ? '-' : '+', magnitude / 3600, (magnitude / 60) % 60
89
91
  end
90
92
  end
91
93
  end
@@ -10,6 +10,7 @@ module Icalendar
10
10
 
11
11
  def initialize(value, params = {}, *args)
12
12
  params.delete 'tzid'
13
+ params.delete :tzid
13
14
  if value.is_a? String
14
15
  begin
15
16
  parsed_date = ::Date.strptime(value, FORMAT)
@@ -1,17 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
-
5
3
  module Icalendar
6
4
  module Values
7
5
 
8
6
  class Duration < Value
7
+ Parts = Struct.new(:past, :weeks, :days, :hours, :minutes, :seconds, keyword_init: true)
9
8
 
10
9
  def initialize(value, *args)
11
10
  if value.is_a? Icalendar::Values::Duration
12
11
  super value.value, *args
13
12
  else
14
- super OpenStruct.new(parse_fields value), *args
13
+ super Parts.new(**parse_fields(value)), *args
15
14
  end
16
15
  end
17
16
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
-
5
3
  module Icalendar
6
4
  module Values
7
5
 
@@ -11,12 +9,29 @@ module Icalendar
11
9
  WEEKDAY = "(?:[+-]?\\d{1,2})?(?:#{DAYNAME})"
12
10
  MONTHDAY = '[+-]?\d{1,2}'
13
11
  YEARDAY = '[+-]?\d{1,3}'
12
+ Parts = Struct.new(
13
+ :frequency,
14
+ :until,
15
+ :count,
16
+ :interval,
17
+ :by_second,
18
+ :by_minute,
19
+ :by_hour,
20
+ :by_day,
21
+ :by_month_day,
22
+ :by_year_day,
23
+ :by_week_number,
24
+ :by_month,
25
+ :by_set_position,
26
+ :week_start,
27
+ keyword_init: true
28
+ )
14
29
 
15
30
  def initialize(value, *args)
16
31
  if value.is_a? Icalendar::Values::Recur
17
32
  super value.value, *args
18
33
  else
19
- super OpenStruct.new(parse_fields value), *args
34
+ super Parts.new(**parse_fields(value)), *args
20
35
  end
21
36
  end
22
37
 
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
-
5
3
  module Icalendar
6
4
  module Values
7
5
  class UtcOffset < Value
6
+ Parts = Struct.new(:behind, :hours, :minutes, :seconds, keyword_init: true)
7
+
8
8
  def initialize(value, *args)
9
9
  if value.is_a? Icalendar::Values::UtcOffset
10
10
  value = value.value
11
11
  else
12
- value = OpenStruct.new parse_fields(value)
12
+ value = Parts.new(**parse_fields(value))
13
13
  end
14
14
  super value, *args
15
15
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Icalendar
4
4
 
5
- VERSION = '2.12.4'
5
+ VERSION = '2.13.0'
6
6
 
7
7
  end
@@ -52,6 +52,13 @@ describe Icalendar::Calendar do
52
52
  end
53
53
  end
54
54
 
55
+ describe '#categories' do
56
+ it 'renders multiple categories as a single comma-separated line' do
57
+ subject.categories = ['CAT A', 'CAT B']
58
+ expect(subject.to_ical).to include "CATEGORIES:CAT A,CAT B\r\n"
59
+ end
60
+ end
61
+
55
62
  context 'components' do
56
63
  let(:ical_component) { double 'Component', name: 'event', :'parent=' => nil }
57
64
 
@@ -0,0 +1,47 @@
1
+ BEGIN:VCALENDAR
2
+ VERSION:2.0
3
+ PRODID:bsprodidfortestabc123
4
+ BEGIN:VEVENT
5
+ UID:bsuidfortestabc123
6
+ ORGANIZER:mailto:joebob@random.net
7
+ ATTACH:http://bush.sucks.org/impeach/him.rhtml
8
+ ATTACH:http://corporations-dominate.existence.net/why.rhtml
9
+ SUMMARY:This is a really long summary
10
+ to test the method of unfolding lines\,
11
+ so I'm just going to ma
12
+ ke it
13
+ a whol
14
+ e
15
+ bunch of lines.
16
+ CLASS:PRIVATE
17
+ PRIORITY:2
18
+ CUSTOMFIELD:Not properly noted as custom with X- prefix.
19
+ GEO:37.386013;-122.0829322
20
+ DTSTART;TZID=US-Mountain:20050120T170000
21
+ DTEND:20050120T184500
22
+ DTSTAMP:20050118T211523Z
23
+ NAME:SomeName
24
+ BEGIN:X-NESTING
25
+ BEGIN:X-NESTING
26
+ BEGIN:X-NESTING
27
+ BEGIN:X-NESTING
28
+ BEGIN:X-NESTING
29
+ BEGIN:X-NESTING
30
+ BEGIN:X-NESTING
31
+ BEGIN:X-NESTING
32
+ BEGIN:X-NESTING
33
+ BEGIN:X-NESTING
34
+ BEGIN:X-NESTING
35
+ END:X-NESTING
36
+ END:X-NESTING
37
+ END:X-NESTING
38
+ END:X-NESTING
39
+ END:X-NESTING
40
+ END:X-NESTING
41
+ END:X-NESTING
42
+ END:X-NESTING
43
+ END:X-NESTING
44
+ END:X-NESTING
45
+ END:X-NESTING
46
+ END:VEVENT
47
+ END:VCALENDAR
data/spec/parser_spec.rb CHANGED
@@ -87,6 +87,14 @@ describe Icalendar::Parser do
87
87
  end
88
88
  end
89
89
 
90
+ describe '#parse with invalid nesting' do
91
+ let(:fn) { 'invalid_nesting.ics' }
92
+
93
+ it 'raises a ParseError' do
94
+ expect { subject.parse }.to raise_error Icalendar::Parser::ParseError
95
+ end
96
+ end
97
+
90
98
  describe '#parse with bad line' do
91
99
  let(:fn) { 'single_event_bad_line.ics' }
92
100
 
data/spec/tzinfo_spec.rb CHANGED
@@ -17,6 +17,27 @@ describe 'TZInfo::Timezone' do
17
17
  specify { expect(subject.standards.first.tzoffsetfrom.value_ical).to eq "+0200" }
18
18
  end
19
19
 
20
+ describe 'negative fractional-hour offsets' do
21
+ let(:tz) { TZInfo::Timezone.get 'America/St_Johns' }
22
+
23
+ [DateTime.new(2021, 1, 1), DateTime.new(2021, 7, 1)].each do |reference_date|
24
+ context reference_date.to_s do
25
+ let(:date) { reference_date }
26
+
27
+ it 'round-trips the standard and daylight offsets' do
28
+ calendar = Icalendar::Calendar.new
29
+ calendar.add_timezone subject
30
+ parsed = Icalendar::Calendar.parse(calendar.to_ical).first.timezones.first
31
+
32
+ expect(parsed.daylights.first.tzoffsetfrom.value_ical).to eq '-0330'
33
+ expect(parsed.daylights.first.tzoffsetto.value_ical).to eq '-0230'
34
+ expect(parsed.standards.first.tzoffsetfrom.value_ical).to eq '-0230'
35
+ expect(parsed.standards.first.tzoffsetto.value_ical).to eq '-0330'
36
+ end
37
+ end
38
+ end
39
+ end
40
+
20
41
  describe 'daylight recurrence rule' do
21
42
  specify { expect(subject.daylights.first.rrule.first.value_ical).to eq "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3" }
22
43
  end
@@ -25,6 +46,49 @@ describe 'TZInfo::Timezone' do
25
46
  specify { expect(subject.standards.first.rrule.first.value_ical).to eq "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10" }
26
47
  end
27
48
 
49
+ describe 'transition onset' do
50
+ let(:date) { DateTime.new 2021, 7, 1 }
51
+
52
+ {
53
+ 'Europe/Stockholm' => {
54
+ daylight: ['20210328T020000', '+0100', '20210328T010000Z'],
55
+ standard: ['20211031T030000', '+0200', '20211031T010000Z']
56
+ },
57
+ 'Australia/Lord_Howe' => {
58
+ daylight: ['20211003T020000', '+1030', '20211002T153000Z'],
59
+ standard: ['20210404T020000', '+1100', '20210403T150000Z']
60
+ }
61
+ }.each do |identifier, transitions|
62
+ context identifier do
63
+ let(:tz) { TZInfo::Timezone.get identifier }
64
+
65
+ transitions.each do |kind, (local, offset, utc)|
66
+ it "serializes the #{kind} onset using the previous offset" do
67
+ calendar = Icalendar::Calendar.new
68
+ calendar.add_timezone subject
69
+ parsed = Icalendar::Calendar.parse(calendar.to_ical).first.timezones.first
70
+ component = parsed.public_send("#{kind}s").first
71
+
72
+ expect(component.dtstart.value_ical).to eq local
73
+ expect(component.tzoffsetfrom.value_ical).to eq offset
74
+ onset = DateTime.strptime("#{component.dtstart.value_ical}#{offset}", '%Y%m%dT%H%M%S%z')
75
+ expect(onset.new_offset(0).strftime('%Y%m%dT%H%M%SZ')).to eq utc
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ context 'when the clock change crosses midnight' do
82
+ let(:tz) { TZInfo::Timezone.get 'America/Santiago' }
83
+
84
+ it 'uses the previous-offset date for both DTSTART and the recurrence rule' do
85
+ standard = subject.standards.first
86
+ expect(standard.dtstart.value_ical).to eq '20210404T000000'
87
+ expect(standard.rrule.first.value_ical).to eq 'FREQ=YEARLY;BYDAY=1SU;BYMONTH=4'
88
+ end
89
+ end
90
+ end
91
+
28
92
  describe 'no end transition' do
29
93
  let(:tz) { TZInfo::Timezone.get 'Asia/Shanghai' }
30
94
  let(:date) { DateTime.now }
@@ -34,7 +98,7 @@ describe 'TZInfo::Timezone' do
34
98
  BEGIN:VTIMEZONE
35
99
  TZID:Asia/Shanghai
36
100
  BEGIN:STANDARD
37
- DTSTART:19910915T010000
101
+ DTSTART:19910915T020000
38
102
  TZOFFSETFROM:+0900
39
103
  TZOFFSETTO:+0800
40
104
  TZNAME:CST
@@ -108,3 +172,22 @@ END:VTIMEZONE
108
172
  end
109
173
 
110
174
  end
175
+
176
+
177
+ describe Icalendar::TimezoneOffset do
178
+ {
179
+ -12600 => '-0330',
180
+ -34200 => '-0930',
181
+ -1800 => '-0030',
182
+ -3600 => '-0100',
183
+ 0 => '+0000',
184
+ 19800 => '+0530',
185
+ 20700 => '+0545'
186
+ }.each do |seconds, expected|
187
+ it "formats #{seconds} seconds as #{expected}" do
188
+ offset = Object.new.extend(described_class)
189
+ allow(offset).to receive(:utc_total_offset).and_return(seconds)
190
+ expect(offset.ical_offset).to eq expected
191
+ end
192
+ end
193
+ end
@@ -64,4 +64,22 @@ describe Icalendar::Values::Duration do
64
64
  expect(subject.value_ical).to eq 'P4D'
65
65
  end
66
66
  end
67
+
68
+ describe 'field access' do
69
+ let(:value) { '-P1DT2H' }
70
+
71
+ it 'is backed by a Struct of the duration parts' do
72
+ expect(subject.value).to be_a Icalendar::Values::Duration::Parts
73
+ expect(subject.value.to_h).to eq(past: true, weeks: 0, days: 1, hours: 2, minutes: 0, seconds: 0)
74
+ end
75
+
76
+ it 'writes parts through setters' do
77
+ subject.hours = 3
78
+ expect(subject.value_ical).to eq '-P1DT3H'
79
+ end
80
+
81
+ it 'copies another duration' do
82
+ expect(described_class.new(subject).value_ical).to eq '-P1DT2H'
83
+ end
84
+ end
67
85
  end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'ostruct'
3
2
 
4
3
  describe Icalendar::Values::Period do
5
4
 
@@ -24,7 +23,7 @@ describe Icalendar::Values::Period do
24
23
 
25
24
  context 'date-time/duration' do
26
25
  let(:value) { '19830507T000600Z/P1604W' }
27
- let(:expected_duration) { OpenStruct.new past: false, weeks: 1604, days: 0, hours: 0, minutes: 0, seconds: 0 }
26
+ let(:expected_duration) { Icalendar::Values::Duration::Parts.new(past: false, weeks: 1604, days: 0, hours: 0, minutes: 0, seconds: 0) }
28
27
 
29
28
  describe '#value_ical' do
30
29
  specify { expect(subject.value_ical).to eq value }
@@ -56,4 +56,23 @@ describe Icalendar::Values::Recur do
56
56
  expect(subject.value_ical).to eq 'FREQ=DAILY;BYDAY=SU,SA;BYYEARDAY=1,34,56,240'
57
57
  end
58
58
  end
59
+
60
+ describe 'field access' do
61
+ let(:value) { 'FREQ=WEEKLY;COUNT=4;BYDAY=MO,WE,FR' }
62
+
63
+ it 'is backed by a Struct of the RRULE parts' do
64
+ expect(subject.value).to be_a Icalendar::Values::Recur::Parts
65
+ expect(subject.value.to_h).to include(frequency: 'WEEKLY', count: 4, by_day: %w(MO WE FR), until: nil)
66
+ end
67
+
68
+ it 'writes parts through setters' do
69
+ subject.frequency = 'DAILY'
70
+ subject.count = 2
71
+ expect(subject.value_ical).to eq 'FREQ=DAILY;COUNT=2;BYDAY=MO,WE,FR'
72
+ end
73
+
74
+ it 'copies another recur' do
75
+ expect(described_class.new(subject).value_ical).to eq subject.value_ical
76
+ end
77
+ end
59
78
  end
@@ -38,4 +38,22 @@ describe Icalendar::Values::UtcOffset do
38
38
  end
39
39
  end
40
40
  end
41
+
42
+ describe 'field access' do
43
+ subject { described_class.new '-0530' }
44
+
45
+ it 'is backed by a Struct of the offset parts' do
46
+ expect(subject.value).to be_a Icalendar::Values::UtcOffset::Parts
47
+ expect(subject.value.to_h).to eq(behind: true, hours: 5, minutes: 30, seconds: 0)
48
+ end
49
+
50
+ it 'writes parts through setters' do
51
+ subject.minutes = 45
52
+ expect(subject.value_ical).to eq '-0545'
53
+ end
54
+
55
+ it 'copies another offset' do
56
+ expect(described_class.new(subject).value_ical).to eq '-0530'
57
+ end
58
+ end
41
59
  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.4
4
+ version: 2.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
@@ -51,20 +51,6 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
- - !ruby/object:Gem::Dependency
55
- name: ostruct
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: rake
70
56
  requirement: !ruby/object:Gem::Requirement
@@ -261,6 +247,7 @@ files:
261
247
  - spec/fixtures/bad_wrapping.ics
262
248
  - spec/fixtures/custom_component.ics
263
249
  - spec/fixtures/event.ics
250
+ - spec/fixtures/invalid_nesting.ics
264
251
  - spec/fixtures/nondefault_values.ics
265
252
  - spec/fixtures/nonstandard.ics
266
253
  - spec/fixtures/recurrence.ics
@@ -313,14 +300,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
313
300
  requirements:
314
301
  - - ">="
315
302
  - !ruby/object:Gem::Version
316
- version: 2.4.0
303
+ version: 2.5.0
317
304
  required_rubygems_version: !ruby/object:Gem::Requirement
318
305
  requirements:
319
306
  - - ">="
320
307
  - !ruby/object:Gem::Version
321
308
  version: '0'
322
309
  requirements: []
323
- rubygems_version: 4.0.6
310
+ rubygems_version: 4.0.16
324
311
  specification_version: 4
325
312
  summary: A ruby implementation of the iCalendar specification (RFC-5545).
326
313
  test_files:
@@ -331,6 +318,7 @@ test_files:
331
318
  - spec/fixtures/bad_wrapping.ics
332
319
  - spec/fixtures/custom_component.ics
333
320
  - spec/fixtures/event.ics
321
+ - spec/fixtures/invalid_nesting.ics
334
322
  - spec/fixtures/nondefault_values.ics
335
323
  - spec/fixtures/nonstandard.ics
336
324
  - spec/fixtures/recurrence.ics