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 +4 -4
- data/.github/workflows/main.yml +0 -2
- data/CHANGELOG.md +10 -0
- data/icalendar.gemspec +1 -2
- data/lib/icalendar/calendar.rb +1 -1
- data/lib/icalendar/event.rb +1 -1
- data/lib/icalendar/has_properties.rb +6 -4
- data/lib/icalendar/journal.rb +1 -1
- data/lib/icalendar/parser.rb +11 -1
- data/lib/icalendar/todo.rb +1 -1
- data/lib/icalendar/tzinfo.rb +5 -3
- data/lib/icalendar/values/date.rb +1 -0
- data/lib/icalendar/values/duration.rb +2 -3
- data/lib/icalendar/values/recur.rb +18 -3
- data/lib/icalendar/values/utc_offset.rb +3 -3
- data/lib/icalendar/version.rb +1 -1
- data/spec/calendar_spec.rb +7 -0
- data/spec/fixtures/invalid_nesting.ics +47 -0
- data/spec/parser_spec.rb +8 -0
- data/spec/tzinfo_spec.rb +84 -1
- data/spec/values/duration_spec.rb +18 -0
- data/spec/values/period_spec.rb +1 -2
- data/spec/values/recur_spec.rb +19 -0
- data/spec/values/utc_offset_spec.rb +18 -0
- metadata +5 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7437b0ae5b86b89cb49c3db3c405aabe0a07ad4a9b1a6fd333faf4ed48d82384
|
|
4
|
+
data.tar.gz: 7a73813c5bbccc460acf9b2e80398dc17b48d65b16fe568e433f7fb6e50df107
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22a6861abb0548b74eb3ea814e4c8632da9d35b6ad0bc798faed60ee053b215d7cd32450b7317606b17f0d73fda188974f18d9d14cddd24cde92b1d3511872d1
|
|
7
|
+
data.tar.gz: 72ac340771b89b5d7eb6628a00e85aea8c19833ac0e74e15313de71feaac42073604ba1dd3e964cbff843cf9e832f430480bbd9976af19f9f851e076009b7c49
|
data/.github/workflows/main.yml
CHANGED
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.
|
|
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'
|
data/lib/icalendar/calendar.rb
CHANGED
|
@@ -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
|
data/lib/icalendar/event.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/icalendar/journal.rb
CHANGED
|
@@ -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
|
data/lib/icalendar/parser.rb
CHANGED
|
@@ -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
|
|
data/lib/icalendar/todo.rb
CHANGED
|
@@ -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
|
data/lib/icalendar/tzinfo.rb
CHANGED
|
@@ -63,7 +63,8 @@ module Icalendar
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def rrule
|
|
66
|
-
|
|
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?(:
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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 =
|
|
12
|
+
value = Parts.new(**parse_fields(value))
|
|
13
13
|
end
|
|
14
14
|
super value, *args
|
|
15
15
|
end
|
data/lib/icalendar/version.rb
CHANGED
data/spec/calendar_spec.rb
CHANGED
|
@@ -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:
|
|
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
|
data/spec/values/period_spec.rb
CHANGED
|
@@ -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) {
|
|
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 }
|
data/spec/values/recur_spec.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|