icalendar 2.12.5 → 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/CHANGELOG.md +5 -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/todo.rb +1 -1
- data/lib/icalendar/tzinfo.rb +2 -1
- 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/tzinfo_spec.rb +40 -0
- 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 +2 -16
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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
|
+
|
|
3
8
|
## 2.12.5 - 2026-09-25
|
|
4
9
|
- Generate timezone transition onsets and recurrence dates using the previous UTC offset. - Ben Abulafia
|
|
5
10
|
- Limit parsing depth
|
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/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
|
@@ -86,7 +86,8 @@ module Icalendar
|
|
|
86
86
|
module TimezoneOffset
|
|
87
87
|
def ical_offset
|
|
88
88
|
o = utc_total_offset
|
|
89
|
-
|
|
89
|
+
magnitude = o.abs
|
|
90
|
+
sprintf '%s%02d%02d', o < 0 ? '-' : '+', magnitude / 3600, (magnitude / 60) % 60
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
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
|
|
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
|
|
@@ -151,3 +172,22 @@ END:VTIMEZONE
|
|
|
151
172
|
end
|
|
152
173
|
|
|
153
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
|
|
@@ -314,7 +300,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
314
300
|
requirements:
|
|
315
301
|
- - ">="
|
|
316
302
|
- !ruby/object:Gem::Version
|
|
317
|
-
version: 2.
|
|
303
|
+
version: 2.5.0
|
|
318
304
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
319
305
|
requirements:
|
|
320
306
|
- - ">="
|