icalendar 2.5.1 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a5b21473741cf53f97e0855df7898df74760fd9c4941dea1a9876cb91dfdc95
4
- data.tar.gz: 743ad24dbf1057eee1a676c3de5184a52e481d5c88a8d96bb16bccb8fc002a12
3
+ metadata.gz: b22d9bc37f8f10ba08d5b8aa155dfb1b97a9876c800c547271f43ce15f09b89d
4
+ data.tar.gz: 91260efab7eafc852e509258fa0163cd1ac2c41352b9482fcae85a45547fa325
5
5
  SHA512:
6
- metadata.gz: 3e139149e08d9d8bc25dfb0fd03a6e0e029ecfc31112751128f2f020767207ee8b17b2d9e000f9197dbca0650171836e1ef5c470e8f613684ba6dea88013dda6
7
- data.tar.gz: 4a9ff7fcdc3f81b9252cef04f8211a90e71142b94e093b2b95dcbf15b631b576fbcd5ef937cb38ba62a94e22565d8d6f8583b42ceee34d51fc8e42de139ca932
6
+ metadata.gz: a6466ed9d1ae91fd9007b42a8d9001879e601ea5f03c338fceb5dcda03b3b32e27ca115cb3a359560b9c04c8944891a174d3bade9596f84d2ca12808dad3046d
7
+ data.tar.gz: a587f1166316ad6a87602d3026144c3d3e9ca09a6d3ef2a40a0664aa77cd497817894df6d53503f6cdcfd95a99fcc2e66c6a8688af19870de2a199cf9b47511d
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ pkg/
6
6
  .bundle
7
7
  coverage/
8
8
  tags
9
+ .idea
@@ -3,17 +3,15 @@ before_install:
3
3
  - gem install bundler
4
4
  language: ruby
5
5
  rvm:
6
+ - 2.7
7
+ - 2.6
6
8
  - 2.5
7
- - 2.4
8
- - 2.3
9
- - 2.2
10
9
  - jruby-19mode
11
- - rbx-3
12
10
  - ruby-head
13
11
  - jruby-head
12
+ - truffleruby-head
14
13
  matrix:
15
14
  allow_failures:
16
- - rvm: 2.2
17
15
  - rvm: ruby-head
18
16
  - rvm: jruby-head
19
17
  script: bundle exec rake spec
@@ -1,3 +1,24 @@
1
+ === 2.7.0 2020-09-12
2
+ * Handle custom component names, with and without X- prefix
3
+ * Fix Component lookup to avoid namespace collisions
4
+
5
+ === 2.6.1 2019-12-07
6
+ * Improve performance when generating large ICS files - Alex Balhatchet
7
+
8
+ === 2.6.0 2019-11-26
9
+ * Improve performance for calculating timezone offsets - Justin Howard
10
+ * Make it possible to de/serialize with Marshal - Pawel Niewiadomski
11
+ * Avoid FrozenError when running with frozen_string_literal
12
+ * Update minimum Ruby version to supported versions
13
+
14
+ === 2.5.3 2019-03-04
15
+ * Improve parsing performance - nehresma
16
+ * Support tzinfo 2.0 - Misty De Meo
17
+
18
+ === 2.5.2 2018-12-08
19
+ * Remove usage of the global TimezoneStore instance, in favor of a local variable in the parser
20
+ * Deprecate TimezoneStore class methods
21
+
1
22
  === 2.5.1 2018-10-30
2
23
  * Fix usage without ActiveSupport installed.
3
24
 
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  iCalendar -- Internet calendaring, Ruby style
2
2
  ===
3
3
 
4
- [![Build Status](https://travis-ci.org/icalendar/icalendar.svg?branch=master)](https://travis-ci.org/icalendar/icalendar)
4
+ [![Build Status](https://travis-ci.com/icalendar/icalendar.svg?branch=master)](https://travis-ci.com/icalendar/icalendar)
5
5
  [![Code Climate](https://codeclimate.com/github/icalendar/icalendar.png)](https://codeclimate.com/github/icalendar/icalendar)
6
6
 
7
7
  <http://github.com/icalendar/icalendar>
@@ -115,7 +115,7 @@ cal.event do |e|
115
115
  a.attendee = %w(mailto:me@my-domain.com mailto:me-too@my-domain.com) # one or more email recipients (required)
116
116
  a.append_attendee "mailto:me-three@my-domain.com"
117
117
  a.trigger = "-PT15M" # 15 minutes before
118
- a.append_attach Icalendar::Values::Uri.new "ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary" # email attachments (optional)
118
+ a.append_attach Icalendar::Values::Uri.new("ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary") # email attachments (optional)
119
119
  end
120
120
 
121
121
  e.alarm do |a|
@@ -24,22 +24,23 @@ ActiveSupport is required for TimeWithZone support, but not required for general
24
24
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
25
25
  s.require_paths = ['lib']
26
26
 
27
- s.required_ruby_version = '>= 2.2.0'
27
+ s.required_ruby_version = '>= 2.4.0'
28
28
 
29
29
  s.add_dependency 'ice_cube', '~> 0.16'
30
30
 
31
- s.add_development_dependency 'rake', '~> 12.0'
32
- s.add_development_dependency 'bundler', '~> 1.16'
31
+ s.add_development_dependency 'rake', '~> 13.0'
32
+ s.add_development_dependency 'bundler', '~> 2.0'
33
33
 
34
- # test with both groups of tzinfo dependencies
34
+ # test with all groups of tzinfo dependencies
35
+ # tzinfo 2.x
36
+ # s.add_development_dependency 'tzinfo', '~> 2.0'
37
+ # s.add_development_dependency 'tzinfo-data', '~> 1.2020'
35
38
  # tzinfo 1.x
36
- s.add_development_dependency 'activesupport', '~> 5.2'
37
- s.add_development_dependency 'i18n', '~> 1.1'
39
+ s.add_development_dependency 'activesupport', '~> 6.0'
40
+ s.add_development_dependency 'i18n', '~> 1.8'
38
41
  s.add_development_dependency 'tzinfo', '~> 1.2'
39
- s.add_development_dependency 'tzinfo-data', '~> 1.2018'
42
+ s.add_development_dependency 'tzinfo-data', '~> 1.2020'
40
43
  # tzinfo 0.x
41
- # s.add_development_dependency 'activesupport', '~> 4.0'
42
- # s.add_development_dependency 'i18n', '~> 0.7'
43
44
  # s.add_development_dependency 'tzinfo', '~> 0.3'
44
45
  # end tzinfo
45
46
 
@@ -13,7 +13,7 @@ module Icalendar
13
13
  end
14
14
 
15
15
  def self.parse(source, single = false)
16
- warn "**** DEPRECATION WARNING ****\nIcalendar.parse will be removed. Please switch to Icalendar::Calendar.parse."
16
+ warn "**** DEPRECATION WARNING ****\nIcalendar.parse will be removed in 3.0. Please switch to Icalendar::Calendar.parse."
17
17
  calendars = Parser.new(source).parse
18
18
  single ? calendars.first : calendars
19
19
  end
@@ -22,6 +22,7 @@ end
22
22
 
23
23
  require 'icalendar/has_properties'
24
24
  require 'icalendar/has_components'
25
+ require 'icalendar/marshable'
25
26
  require 'icalendar/component'
26
27
  require 'icalendar/value'
27
28
  require 'icalendar/alarm'
@@ -71,6 +71,8 @@ module Icalendar
71
71
  # than 75 octets, but you need to split between characters, not bytes.
72
72
  # This is challanging with Unicode composing accents, for example.
73
73
 
74
+ return long_line if long_line.bytesize <= Icalendar::MAX_LINE_LENGTH
75
+
74
76
  chars = long_line.scan(/\P{M}\p{M}*/u) # split in graphenes
75
77
  folded = ['']
76
78
  bytes = 0
@@ -10,7 +10,7 @@ module Icalendar
10
10
  end
11
11
 
12
12
  def initialize(*args)
13
- @custom_components = Hash.new { |h, k| h[k] = [] }
13
+ @custom_components = Hash.new
14
14
  super
15
15
  end
16
16
 
@@ -21,21 +21,33 @@ module Icalendar
21
21
  c
22
22
  end
23
23
 
24
+ def add_custom_component(component_name, c)
25
+ c.parent = self
26
+ yield c if block_given?
27
+ (custom_components[component_name.downcase.gsub("-", "_")] ||= []) << c
28
+ c
29
+ end
30
+
31
+ def custom_component(component_name)
32
+ custom_components[component_name.downcase.gsub("-", "_")] || []
33
+ end
34
+
24
35
  def method_missing(method, *args, &block)
25
36
  method_name = method.to_s
26
37
  if method_name =~ /^add_(x_\w+)$/
27
38
  component_name = $1
28
39
  custom = args.first || Component.new(component_name, component_name.upcase)
29
- custom_components[component_name] << custom
30
- yield custom if block_given?
31
- custom
40
+ add_custom_component(component_name, custom, &block)
41
+ elsif method_name =~ /^x_/ && custom_component(method_name).size > 0
42
+ custom_component method_name
32
43
  else
33
44
  super
34
45
  end
35
46
  end
36
47
 
37
48
  def respond_to_missing?(method_name, include_private = false)
38
- method_name.to_s.start_with?('add_x_') || super
49
+ string_method = method_name.to_s
50
+ string_method.start_with?('add_x_') || custom_component(string_method).size > 0 || super
39
51
  end
40
52
 
41
53
  module ClassMethods
@@ -10,7 +10,7 @@ module Icalendar
10
10
  end
11
11
 
12
12
  def initialize(*args)
13
- @custom_properties = Hash.new { |h, k| h[k] = [] }
13
+ @custom_properties = Hash.new
14
14
  super
15
15
  end
16
16
 
@@ -39,7 +39,7 @@ module Icalendar
39
39
  end
40
40
 
41
41
  def custom_property(property_name)
42
- custom_properties[property_name.downcase]
42
+ custom_properties[property_name.downcase] || []
43
43
  end
44
44
 
45
45
  def append_custom_property(property_name, value)
@@ -49,9 +49,9 @@ module Icalendar
49
49
  elsif self.class.multiple_properties.include? property_name
50
50
  send "append_#{property_name}", value
51
51
  elsif value.is_a? Icalendar::Value
52
- custom_properties[property_name] << value
52
+ (custom_properties[property_name] ||= []) << value
53
53
  else
54
- custom_properties[property_name] << Icalendar::Values::Text.new(value)
54
+ (custom_properties[property_name] ||= []) << Icalendar::Values::Text.new(value)
55
55
  end
56
56
  end
57
57
 
@@ -0,0 +1,34 @@
1
+ module Icalendar
2
+ module Marshable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def marshal_dump
8
+ instance_variables
9
+ .reject { |ivar| self.class.transient_variables.include?(ivar) }
10
+ .each_with_object({}) do |ivar, serialized|
11
+
12
+ serialized[ivar] = instance_variable_get(ivar)
13
+ end
14
+ end
15
+
16
+ def marshal_load(serialized)
17
+ serialized.each do |ivar, value|
18
+ unless self.class.transient_variables.include?(ivar)
19
+ instance_variable_set(ivar, value)
20
+ end
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def transient_variables
26
+ @transient_variables ||= [:@transient_variables]
27
+ end
28
+
29
+ def transient_variable(name)
30
+ transient_variables.push(name.to_sym)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -4,7 +4,7 @@ module Icalendar
4
4
 
5
5
  class Parser
6
6
  attr_writer :component_class
7
- attr_reader :source, :strict
7
+ attr_reader :source, :strict, :timezone_store
8
8
 
9
9
  def initialize(source, strict = false)
10
10
  if source.respond_to? :gets
@@ -18,6 +18,7 @@ module Icalendar
18
18
  end
19
19
  read_in_data
20
20
  @strict = strict
21
+ @timezone_store = TimezoneStore.new
21
22
  end
22
23
 
23
24
  def parse
@@ -81,8 +82,8 @@ module Icalendar
81
82
  if !fields[:params]['value'].nil?
82
83
  klass_name = fields[:params].delete('value').first
83
84
  unless klass_name.upcase == klass.value_type
84
- klass_name = klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }
85
- klass = Icalendar::Values.const_get klass_name if Icalendar::Values.const_defined?(klass_name)
85
+ klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }}"
86
+ klass = Object.const_get klass_name if Object.const_defined?(klass_name)
86
87
  end
87
88
  end
88
89
  klass
@@ -102,17 +103,17 @@ module Icalendar
102
103
  while (fields = next_fields)
103
104
  if fields[:name] == 'end'
104
105
  klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
105
- TimezoneStore.store(component) if klass_name == 'Timezone'
106
+ timezone_store.store(component) if klass_name == 'Timezone'
106
107
  break
107
108
  elsif fields[:name] == 'begin'
108
- klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
109
+ klass_name = fields[:value].gsub(/\AV/, '').gsub("-", "_").downcase.capitalize
109
110
  Icalendar.logger.debug "Adding component #{klass_name}"
110
- if Icalendar.const_defined? klass_name
111
- component.add_component parse_component(Icalendar.const_get(klass_name).new)
112
- elsif Icalendar::Timezone.const_defined? klass_name
113
- component.add_component parse_component(Icalendar::Timezone.const_get(klass_name).new)
111
+ if Object.const_defined? "Icalendar::#{klass_name}"
112
+ component.add_component parse_component(Object.const_get("Icalendar::#{klass_name}").new)
113
+ elsif Object.const_defined? "Icalendar::Timezone::#{klass_name}"
114
+ component.add_component parse_component(Object.const_get("Icalendar::Timezone::#{klass_name}").new)
114
115
  else
115
- component.add_component parse_component(Component.new klass_name.downcase, fields[:value])
116
+ component.add_custom_component klass_name, parse_component(Component.new klass_name.downcase, fields[:value])
116
117
  end
117
118
  else
118
119
  parse_property component, fields
@@ -162,10 +163,20 @@ module Icalendar
162
163
  param_name = match[0].downcase
163
164
  params[param_name] ||= []
164
165
  match[1].scan %r{#{PVALUE}} do |param_value|
165
- params[param_name] << param_value.gsub(/\A"|"\z/, '') if param_value.size > 0
166
+ if param_value.size > 0
167
+ param_value = param_value.gsub(/\A"|"\z/, '')
168
+ params[param_name] << param_value
169
+ if param_name == 'tzid'
170
+ params['x-tz-info'] = timezone_store.retrieve param_value
171
+ end
172
+ end
166
173
  end
167
174
  end
168
- Icalendar.logger.debug "Found fields: #{parts.inspect} with params: #{params.inspect}"
175
+ # Building the string to send to the logger is expensive.
176
+ # Only do it if the logger is at the right log level.
177
+ if ::Logger::DEBUG >= Icalendar.logger.level
178
+ Icalendar.logger.debug "Found fields: #{parts.inspect} with params: #{params.inspect}"
179
+ end
169
180
  {
170
181
  name: parts[:name].downcase.gsub('-', '_'),
171
182
  params: params,
@@ -14,10 +14,40 @@ module Icalendar
14
14
  optional_property :comment
15
15
  optional_property :rdate, Icalendar::Values::DateTime
16
16
  optional_property :tzname
17
+
18
+ transient_variable :@cached_occurrences
19
+ transient_variable :@occurrences
20
+ end
21
+ end
22
+
23
+ def occurrences
24
+ @occurrences ||= IceCube::Schedule.new(dtstart.to_time) do |s|
25
+ rrule.each do |rule|
26
+ s.add_recurrence_rule IceCube::Rule.from_ical(rule.value_ical)
27
+ end
28
+ rdate.each do |date|
29
+ s.add_recurrence_time date.to_time
30
+ end
31
+ end.all_occurrences_enumerator
32
+ end
33
+
34
+ def previous_occurrence(from)
35
+ from = IceCube::TimeUtil.match_zone(from, dtstart.to_time)
36
+
37
+ @cached_occurrences ||= []
38
+ while @cached_occurrences.empty? || @cached_occurrences.last <= from
39
+ begin
40
+ @cached_occurrences << occurrences.next
41
+ rescue StopIteration
42
+ break
43
+ end
17
44
  end
45
+
46
+ @cached_occurrences.reverse_each.find { |occurrence| occurrence < from }
18
47
  end
19
48
  end
20
49
  class Daylight < Component
50
+ include Marshable
21
51
  include TzProperties
22
52
 
23
53
  def initialize
@@ -25,6 +55,7 @@ module Icalendar
25
55
  end
26
56
  end
27
57
  class Standard < Component
58
+ include Marshable
28
59
  include TzProperties
29
60
 
30
61
  def initialize
@@ -75,30 +106,14 @@ module Icalendar
75
106
 
76
107
  def standard_for(local)
77
108
  possible = standards.map do |std|
78
- schedule = IceCube::Schedule.new(std.dtstart.to_time) do |s|
79
- std.rrule.each do |rule|
80
- s.add_recurrence_rule IceCube::Rule.from_ical(rule.value_ical)
81
- end
82
- std.rdate.each do |date|
83
- s.add_recurrence_date date
84
- end
85
- end
86
- [schedule.previous_occurrence(local.to_time), std]
109
+ [std.previous_occurrence(local.to_time), std]
87
110
  end
88
111
  possible.sort_by(&:first).last
89
112
  end
90
113
 
91
114
  def daylight_for(local)
92
115
  possible = daylights.map do |day|
93
- schedule = IceCube::Schedule.new(day.dtstart.to_time) do |s|
94
- day.rrule.each do |rule|
95
- s.add_recurrence_rule IceCube::Rule.from_ical(rule.value_ical)
96
- end
97
- day.rdate.each do |date|
98
- s.add_recurrence_date date
99
- end
100
- end
101
- [schedule.previous_occurrence(local.to_time), day]
116
+ [day.previous_occurrence(local.to_time), day]
102
117
  end
103
118
  possible.sort_by(&:first).last
104
119
  end
@@ -9,14 +9,17 @@ module Icalendar
9
9
  end
10
10
 
11
11
  def self.instance
12
+ warn "**** DEPRECATION WARNING ****\nTimezoneStore.instance will be removed in 3.0. Please instantiate a TimezoneStore object."
12
13
  @instance ||= new
13
14
  end
14
15
 
15
16
  def self.store(timezone)
17
+ warn "**** DEPRECATION WARNING ****\nTimezoneStore.store will be removed in 3.0. Please use instance methods."
16
18
  instance.store timezone
17
19
  end
18
20
 
19
21
  def self.retrieve(tzid)
22
+ warn "**** DEPRECATION WARNING ****\nTimezoneStore.retrieve will be removed in 3.0. Please use instance methods."
20
23
  instance.retrieve tzid
21
24
  end
22
25
 
@@ -61,7 +61,7 @@ module Icalendar
61
61
  end
62
62
 
63
63
  def rrule
64
- start = local_start.to_datetime
64
+ start = (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime
65
65
  # this is somewhat of a hack, but seems to work ok
66
66
  # assumes that no timezone transition is in law as "4th X of the month"
67
67
  # but only as 1st X, 2nd X, 3rd X, or Last X
@@ -76,7 +76,7 @@ module Icalendar
76
76
  end
77
77
 
78
78
  def dtstart
79
- local_start.to_datetime.strftime '%Y%m%dT%H%M%S'
79
+ (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime.strftime '%Y%m%dT%H%M%S'
80
80
  end
81
81
  end
82
82
 
@@ -22,7 +22,7 @@ module Icalendar
22
22
  else
23
23
  [klass.new(value, params)]
24
24
  end
25
- super mapped, params
25
+ super mapped
26
26
  end
27
27
 
28
28
  def params_ical
@@ -7,6 +7,8 @@ module Icalendar
7
7
  FORMAT = '%Y%m%d'
8
8
 
9
9
  def initialize(value, params = {})
10
+ params.delete 'tzid'
11
+ params.delete 'x-tz-info'
10
12
  if value.is_a? String
11
13
  begin
12
14
  parsed_date = ::Date.strptime(value, FORMAT)
@@ -18,6 +18,7 @@ module Icalendar
18
18
  def initialize(value, params = {})
19
19
  params = Icalendar::DowncasedHash(params)
20
20
  @tz_utc = params['tzid'] == 'UTC'
21
+ x_tz_info = params.delete 'x-tz-info'
21
22
 
22
23
  offset_value = unless params['tzid'].nil?
23
24
  tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
@@ -25,12 +26,12 @@ module Icalendar
25
26
  defined?(ActiveSupportTimeWithZoneAdapter) &&
26
27
  (tz = ActiveSupport::TimeZone[tzid])
27
28
  ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
28
- elsif (tz = TimezoneStore.retrieve(tzid))
29
- offset = tz.offset_for_local(value).to_s
29
+ elsif !x_tz_info.nil?
30
+ offset = x_tz_info.offset_for_local(value).to_s
30
31
  if value.respond_to?(:change)
31
32
  value.change offset: offset
32
33
  else
33
- ::Time.new(value.year, value.month, value.day, value.hour, value.min, value.sec, offset)
34
+ ::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
34
35
  end
35
36
  end
36
37
  end
@@ -37,8 +37,7 @@ module Icalendar
37
37
  end
38
38
 
39
39
  def parse_fields(value)
40
- value.gsub!(/\s+/, '')
41
- md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value
40
+ md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value.gsub(/\s+/, '')
42
41
  {
43
42
  behind: (md[:behind] == '-'),
44
43
  hours: md[:hours].to_i,
@@ -1,5 +1,5 @@
1
1
  module Icalendar
2
2
 
3
- VERSION = '2.5.1'
3
+ VERSION = '2.7.0'
4
4
 
5
5
  end
@@ -2,6 +2,12 @@ require 'spec_helper'
2
2
 
3
3
  describe Icalendar::Calendar do
4
4
 
5
+ context 'marshalling' do
6
+ it 'can be de/serialized' do
7
+ Marshal.load(Marshal.dump(subject))
8
+ end
9
+ end
10
+
5
11
  context 'values' do
6
12
  let(:property) { 'my-value' }
7
13
 
@@ -113,6 +113,13 @@ describe Icalendar::Event do
113
113
  end
114
114
  end
115
115
 
116
+ describe "#custom_property" do
117
+ it "returns a default for missing properties" do
118
+ expect(subject.x_doesnt_exist).to eq([])
119
+ expect(subject.custom_property "x_doesnt_exist").to eq([])
120
+ end
121
+ end
122
+
116
123
  describe '.parse' do
117
124
  let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', fn) }
118
125
  let(:fn) { 'event.ics' }
@@ -0,0 +1,158 @@
1
+ BEGIN:VCALENDAR
2
+ PRODID:-//Atlassian Confluence//Calendar Plugin 1.0//EN
3
+ VERSION:2.0
4
+ CALSCALE:GREGORIAN
5
+ X-WR-CALNAME:Grimsell testkalender
6
+ X-WR-CALDESC:
7
+ X-WR-TIMEZONE:Europe/Stockholm
8
+ X-MIGRATED-FOR-USER-KEY:true
9
+ METHOD:PUBLISH
10
+ BEGIN:VTIMEZONE
11
+ TZID:Europe/Stockholm
12
+ TZURL:http://tzurl.org/zoneinfo/Europe/Stockholm
13
+ SEQUENCE:9206
14
+ BEGIN:DAYLIGHT
15
+ TZOFFSETFROM:+0100
16
+ TZOFFSETTO:+0200
17
+ TZNAME:CEST
18
+ DTSTART:19810329T020000
19
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
20
+ END:DAYLIGHT
21
+ BEGIN:STANDARD
22
+ TZOFFSETFROM:+0200
23
+ TZOFFSETTO:+0100
24
+ TZNAME:CET
25
+ DTSTART:19961027T030000
26
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
27
+ END:STANDARD
28
+ BEGIN:STANDARD
29
+ TZOFFSETFROM:+011212
30
+ TZOFFSETTO:+010014
31
+ TZNAME:SET
32
+ DTSTART:18790101T000000
33
+ RDATE:18790101T000000
34
+ END:STANDARD
35
+ BEGIN:STANDARD
36
+ TZOFFSETFROM:+010014
37
+ TZOFFSETTO:+0100
38
+ TZNAME:CET
39
+ DTSTART:19000101T000000
40
+ RDATE:19000101T000000
41
+ END:STANDARD
42
+ BEGIN:DAYLIGHT
43
+ TZOFFSETFROM:+0100
44
+ TZOFFSETTO:+0200
45
+ TZNAME:CEST
46
+ DTSTART:19160514T230000
47
+ RDATE:19160514T230000
48
+ RDATE:19800406T020000
49
+ END:DAYLIGHT
50
+ BEGIN:STANDARD
51
+ TZOFFSETFROM:+0200
52
+ TZOFFSETTO:+0100
53
+ TZNAME:CET
54
+ DTSTART:19161001T010000
55
+ RDATE:19161001T010000
56
+ RDATE:19800928T030000
57
+ RDATE:19810927T030000
58
+ RDATE:19820926T030000
59
+ RDATE:19830925T030000
60
+ RDATE:19840930T030000
61
+ RDATE:19850929T030000
62
+ RDATE:19860928T030000
63
+ RDATE:19870927T030000
64
+ RDATE:19880925T030000
65
+ RDATE:19890924T030000
66
+ RDATE:19900930T030000
67
+ RDATE:19910929T030000
68
+ RDATE:19920927T030000
69
+ RDATE:19930926T030000
70
+ RDATE:19940925T030000
71
+ RDATE:19950924T030000
72
+ END:STANDARD
73
+ BEGIN:STANDARD
74
+ TZOFFSETFROM:+0100
75
+ TZOFFSETTO:+0100
76
+ TZNAME:CET
77
+ DTSTART:19800101T000000
78
+ RDATE:19800101T000000
79
+ END:STANDARD
80
+ END:VTIMEZONE
81
+ BEGIN:VEVENT
82
+ DTSTAMP:20151203T104347Z
83
+ SUMMARY:Kaffepaus
84
+ UID:20151203T101856Z-1543254179@intranet.idainfront.se
85
+ DESCRIPTION:
86
+ ORGANIZER;X-CONFLUENCE-USER-KEY=ff80818141e4d3b60141e4d4b75700a2;CN=Magnu
87
+ s Grimsell;CUTYPE=INDIVIDUAL:mailto:magnus.grimsell@idainfront.se
88
+ CREATED:20151203T101856Z
89
+ LAST-MODIFIED:20151203T101856Z
90
+ SEQUENCE:1
91
+ X-CONFLUENCE-SUBCALENDAR-TYPE:other
92
+ TRANSP:OPAQUE
93
+ STATUS:CONFIRMED
94
+ DTSTART:20151203T090000Z
95
+ DTEND:20151203T091500Z
96
+ END:VEVENT
97
+ BEGIN:X-EVENT-SERIES
98
+ SUMMARY:iipax - Sprints
99
+ DESCRIPTION:
100
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
101
+ URL:jira://8112ef9a-343e-30e6-b469-4f993bf0371d?projectKey=II&dateFieldNa
102
+ me=sprint
103
+ END:X-EVENT-SERIES
104
+ BEGIN:VEVENT
105
+ DTSTAMP:20151203T104348Z
106
+ DTSTART;TZID=Europe/Stockholm:20130115T170800
107
+ DTEND;TZID=Europe/Stockholm:20130204T170800
108
+ UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-3@jira.idainfront.se
109
+ X-GREENHOPPER-SPRINT-CLOSED:false
110
+ X-GREENHOPPER-SPRINT-EDITABLE:true
111
+ X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax
112
+ X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG
113
+ oToBoard.jspa?sprintId=3
114
+ SEQUENCE:0
115
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
116
+ TRANSP:OPAQUE
117
+ STATUS:CONFIRMED
118
+ DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=3
119
+
120
+ SUMMARY:iipax - Callisto-6
121
+ END:VEVENT
122
+ BEGIN:VEVENT
123
+ DTSTAMP:20151203T104348Z
124
+ DTSTART;TZID=Europe/Stockholm:20130219T080000
125
+ DTEND;TZID=Europe/Stockholm:20130319T095900
126
+ UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-5@jira.idainfront.se
127
+ X-GREENHOPPER-SPRINT-CLOSED:false
128
+ X-GREENHOPPER-SPRINT-EDITABLE:true
129
+ X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax
130
+ X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG
131
+ oToBoard.jspa?sprintId=5
132
+ SEQUENCE:0
133
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
134
+ TRANSP:OPAQUE
135
+ STATUS:CONFIRMED
136
+ DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=5
137
+
138
+ SUMMARY:iipax - Bulbasaur
139
+ END:VEVENT
140
+ BEGIN:VEVENT
141
+ DTSTAMP:20151203T104348Z
142
+ DTSTART;TZID=Europe/Stockholm:20130326T105900
143
+ DTEND;TZID=Europe/Stockholm:20130419T105900
144
+ UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-7@jira.idainfront.se
145
+ X-GREENHOPPER-SPRINT-CLOSED:true
146
+ X-GREENHOPPER-SPRINT-EDITABLE:true
147
+ X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax
148
+ X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG
149
+ oToBoard.jspa?sprintId=7
150
+ SEQUENCE:0
151
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
152
+ TRANSP:OPAQUE
153
+ STATUS:CONFIRMED
154
+ DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=7
155
+
156
+ SUMMARY:iipax - Ivysaur
157
+ END:VEVENT
158
+ END:VCALENDAR
@@ -66,6 +66,16 @@ describe Icalendar::Parser do
66
66
  expect(event.dtstart.utc).to eq Time.parse("20180104T150000Z")
67
67
  end
68
68
  end
69
+ context 'custom_component.ics' do
70
+ let(:fn) { 'custom_component.ics' }
71
+
72
+ it 'correctly handles custom named components' do
73
+ parsed = subject.parse
74
+ calendar = parsed.first
75
+ expect(calendar.custom_component('x_event_series').size).to eq 1
76
+ expect(calendar.custom_component('X-EVENT-SERIES').size).to eq 1
77
+ end
78
+ end
69
79
  end
70
80
 
71
81
  describe '#parse with bad line' do
@@ -15,7 +15,7 @@ describe Icalendar do
15
15
  parsed = Icalendar::Calendar.parse(source).first
16
16
  event.rdate = parsed.events.first.rdate
17
17
  expect(event.rdate.first).to be_kind_of Icalendar::Values::Array
18
- expect(event.rdate.first.ical_params).to eq 'tzid' => ['US-Mountain']
18
+ expect(event.rdate.first.params_ical).to eq ";TZID=US-Mountain"
19
19
  end
20
20
  end
21
21
 
@@ -135,6 +135,15 @@ describe Icalendar do
135
135
  ical = subject.parse.first.to_ical
136
136
  expect(ical).to include 'CUSTOMFIELD:Not properly noted as custom with X- prefix.'
137
137
  end
138
+
139
+ context 'custom components' do
140
+ let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'custom_component.ics') }
141
+
142
+ it 'can output the custom component' do
143
+ ical = subject.parse.first.to_ical
144
+ expect(ical).to include 'BEGIN:X-EVENT-SERIES'
145
+ end
146
+ end
138
147
  end
139
148
  end
140
149
  end
@@ -28,4 +28,52 @@ describe Icalendar::Timezone do
28
28
  it { should_not be_valid }
29
29
  end
30
30
  end
31
+
32
+ context 'marshalling' do
33
+ context 'with standard/daylight components' do
34
+ before do
35
+ subject.standard do |standard|
36
+ standard.rrule = Icalendar::Values::Recur.new("FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10")
37
+ standard.dtstart = Icalendar::Values::DateTime.new("16010101T030000")
38
+ standard.tzoffsetfrom = Icalendar::Values::UtcOffset.new("+0200")
39
+ standard.tzoffsetto = Icalendar::Values::UtcOffset.new("+0100")
40
+ end
41
+
42
+ subject.daylight do |daylight|
43
+ daylight.rrule = Icalendar::Values::Recur.new("FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3")
44
+ daylight.dtstart = Icalendar::Values::DateTime.new("16010101T020000")
45
+ daylight.tzoffsetfrom = Icalendar::Values::UtcOffset.new("+0100")
46
+ daylight.tzoffsetto = Icalendar::Values::UtcOffset.new("+0200")
47
+ end
48
+ end
49
+
50
+ it 'can be de/serialized' do
51
+ first_standard = subject.standards.first
52
+ first_daylight = subject.daylights.first
53
+
54
+ expect(first_standard.valid?).to be_truthy
55
+ expect(first_daylight.valid?).to be_truthy
56
+
57
+ # calling previous_occurrence intializes @cached_occurrences with a time that's not handled by ruby marshaller
58
+ first_occurence_for = Time.new(1601, 10, 31)
59
+
60
+ standard_previous_occurrence = first_standard.previous_occurrence(first_occurence_for)
61
+ expect(standard_previous_occurrence).not_to be_nil
62
+
63
+ daylight_previous_occurrence = first_daylight.previous_occurrence(first_occurence_for)
64
+ expect(daylight_previous_occurrence).not_to be_nil
65
+
66
+ deserialized = nil
67
+
68
+ expect { deserialized = Marshal.load(Marshal.dump(subject)) }.not_to raise_exception
69
+
70
+ expect(deserialized.standards.first.previous_occurrence(first_occurence_for)).to eq(standard_previous_occurrence)
71
+ expect(deserialized.daylights.first.previous_occurrence(first_occurence_for)).to eq(daylight_previous_occurrence)
72
+ end
73
+ end
74
+
75
+ it 'can be de/serialized' do
76
+ expect { Marshal.load(Marshal.dump(subject)) }.not_to raise_exception
77
+ end
78
+ end
31
79
  end
@@ -29,7 +29,6 @@ describe 'TZInfo::Timezone' do
29
29
  let(:tz) { TZInfo::Timezone.get 'Asia/Shanghai' }
30
30
  let(:date) { DateTime.now }
31
31
 
32
- # TODO only run this test with tzinfo ≥ 1.0
33
32
  it 'only creates a standard component' do
34
33
  expect(subject.to_ical).to eq <<-EXPECTED.gsub "\n", "\r\n"
35
34
  BEGIN:VTIMEZONE
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.1
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-30 00:00:00.000000000 Z
11
+ date: 2020-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ice_cube
@@ -30,56 +30,56 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '12.0'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '12.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.16'
47
+ version: '2.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.16'
54
+ version: '2.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: activesupport
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '5.2'
61
+ version: '6.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '5.2'
68
+ version: '6.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: i18n
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '1.1'
75
+ version: '1.8'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '1.1'
82
+ version: '1.8'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: tzinfo
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '1.2018'
103
+ version: '1.2020'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '1.2018'
110
+ version: '1.2020'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: timecop
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -180,6 +180,7 @@ files:
180
180
  - lib/icalendar/has_properties.rb
181
181
  - lib/icalendar/journal.rb
182
182
  - lib/icalendar/logger.rb
183
+ - lib/icalendar/marshable.rb
183
184
  - lib/icalendar/parser.rb
184
185
  - lib/icalendar/timezone.rb
185
186
  - lib/icalendar/timezone_store.rb
@@ -209,6 +210,7 @@ files:
209
210
  - spec/calendar_spec.rb
210
211
  - spec/downcased_hash_spec.rb
211
212
  - spec/event_spec.rb
213
+ - spec/fixtures/custom_component.ics
212
214
  - spec/fixtures/event.ics
213
215
  - spec/fixtures/nondefault_values.ics
214
216
  - spec/fixtures/nonstandard.ics
@@ -245,7 +247,7 @@ metadata: {}
245
247
  post_install_message: 'ActiveSupport is required for TimeWithZone support, but not
246
248
  required for general use.
247
249
 
248
- '
250
+ '
249
251
  rdoc_options: []
250
252
  require_paths:
251
253
  - lib
@@ -253,16 +255,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
253
255
  requirements:
254
256
  - - ">="
255
257
  - !ruby/object:Gem::Version
256
- version: 2.2.0
258
+ version: 2.4.0
257
259
  required_rubygems_version: !ruby/object:Gem::Requirement
258
260
  requirements:
259
261
  - - ">="
260
262
  - !ruby/object:Gem::Version
261
263
  version: '0'
262
264
  requirements: []
263
- rubyforge_project:
264
- rubygems_version: 2.7.6
265
- signing_key:
265
+ rubygems_version: 3.1.2
266
+ signing_key:
266
267
  specification_version: 4
267
268
  summary: A ruby implementation of the iCalendar specification (RFC-5545).
268
269
  test_files:
@@ -270,6 +271,7 @@ test_files:
270
271
  - spec/calendar_spec.rb
271
272
  - spec/downcased_hash_spec.rb
272
273
  - spec/event_spec.rb
274
+ - spec/fixtures/custom_component.ics
273
275
  - spec/fixtures/event.ics
274
276
  - spec/fixtures/nondefault_values.ics
275
277
  - spec/fixtures/nonstandard.ics