icalendar 2.8.0 → 2.10.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +4 -4
  3. data/History.txt +13 -0
  4. data/icalendar.gemspec +4 -0
  5. data/lib/icalendar/alarm.rb +2 -0
  6. data/lib/icalendar/calendar.rb +38 -0
  7. data/lib/icalendar/component.rb +8 -2
  8. data/lib/icalendar/downcased_hash.rb +2 -0
  9. data/lib/icalendar/event.rb +3 -1
  10. data/lib/icalendar/freebusy.rb +2 -0
  11. data/lib/icalendar/has_components.rb +7 -2
  12. data/lib/icalendar/has_properties.rb +4 -2
  13. data/lib/icalendar/journal.rb +2 -0
  14. data/lib/icalendar/logger.rb +2 -0
  15. data/lib/icalendar/marshable.rb +2 -0
  16. data/lib/icalendar/parser.rb +38 -14
  17. data/lib/icalendar/timezone.rb +2 -0
  18. data/lib/icalendar/timezone_store.rb +2 -0
  19. data/lib/icalendar/todo.rb +2 -0
  20. data/lib/icalendar/tzinfo.rb +2 -0
  21. data/lib/icalendar/value.rb +11 -3
  22. data/lib/icalendar/values/binary.rb +5 -1
  23. data/lib/icalendar/values/boolean.rb +2 -0
  24. data/lib/icalendar/values/cal_address.rb +2 -0
  25. data/lib/icalendar/values/date.rb +2 -0
  26. data/lib/icalendar/values/date_time.rb +4 -2
  27. data/lib/icalendar/values/duration.rb +15 -6
  28. data/lib/icalendar/values/float.rb +2 -0
  29. data/lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb +16 -0
  30. data/lib/icalendar/values/helpers/array.rb +62 -0
  31. data/lib/icalendar/values/helpers/time_with_zone.rb +61 -0
  32. data/lib/icalendar/values/integer.rb +2 -0
  33. data/lib/icalendar/values/period.rb +5 -1
  34. data/lib/icalendar/values/recur.rb +31 -14
  35. data/lib/icalendar/values/text.rb +5 -1
  36. data/lib/icalendar/values/time.rb +4 -2
  37. data/lib/icalendar/values/uri.rb +2 -0
  38. data/lib/icalendar/values/utc_offset.rb +6 -1
  39. data/lib/icalendar/version.rb +3 -1
  40. data/lib/icalendar.rb +2 -0
  41. data/spec/calendar_spec.rb +49 -0
  42. data/spec/event_spec.rb +4 -2
  43. data/spec/roundtrip_spec.rb +1 -1
  44. data/spec/values/date_time_spec.rb +16 -0
  45. metadata +8 -7
  46. data/lib/icalendar/values/active_support_time_with_zone_adapter.rb +0 -12
  47. data/lib/icalendar/values/array.rb +0 -58
  48. data/lib/icalendar/values/time_with_zone.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: becd9ceba3342d0b602f1bb8a33858fb2176ceafd457a0fbca76d24ff7800dc7
4
- data.tar.gz: f11054752ca18d026b1339e936ab30d4f1ff61219ecfd94d3f913b3de6b14106
3
+ metadata.gz: 4a4c654b36ffe5a09f521ad3c012686f12e582ba5d380242c646334f47153360
4
+ data.tar.gz: 184399a11e6e8a39b18ceeef59de8a9b9f7cd581f0c7169cce5c7dca63575e3b
5
5
  SHA512:
6
- metadata.gz: 8262dd28d571778ecc7dd638d6df2a44e4c26bea2221183d82a644c777d100a75027c83ac83fd53c801d43cd75b9521be104111900d6653119a0593a281b8203
7
- data.tar.gz: 1dea941db6e27188b9ab0d769a0df2335fdc2bc492a4ca6339a5e4a90e0d91f9821282aaed13b2cc4f8a4921e24a3d20f14aa34fea97c50c6326d3a1dc29f3cc
6
+ metadata.gz: 61837b5d19193d80d72f37edff89290fc2e5ffa0c6b9ee500036de58b0fbd9aacb5e0e9bc9f1a0a4e1ce7a3ad494ed0147e87bf697b54523d8b799e9eaa275fa
7
+ data.tar.gz: 00d54cc0ab4b7fc6acf616a537b426d110ddbe65dda1d6ab8ec938ba2df64e87def99023916c6193ab25d1885a2a60e8a1706617ed5a6e0adb5cb34671dae1ec
@@ -17,12 +17,12 @@ jobs:
17
17
  strategy:
18
18
  matrix:
19
19
  ruby:
20
- - 2.7.6
21
- - 3.0.4
22
- - 3.1.2
20
+ - 3.0.6
21
+ - 3.1.4
22
+ - 3.2.2
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v4
26
26
  - name: Set up Ruby
27
27
  uses: ruby/setup-ruby@v1
28
28
  with:
data/History.txt CHANGED
@@ -1,3 +1,16 @@
1
+ === Unreleased
2
+
3
+ === 2.10.0 2023-11-01
4
+ * Add changelog metadata to gemspec - Juri Hahn
5
+ * Attempt to rescue timezone info when given a nonstandard tzid with no VTIMEZONE
6
+ * Move Values classes that shouldn't be directly used into Helpers module
7
+
8
+ === 2.9.0 2023-08-11
9
+ * Always include the VALUE of Event URLs for improved compatibility - Sean Kelley
10
+ * Improved parse performance - Thomas Cannon
11
+ * Add helper methods for other Calendar method verbs
12
+ * bugfix: Require stringio before use in Parser - vwyu
13
+
1
14
  === 2.8.0 2022-07-10
2
15
  * Fix compatibility with ActiveSupport 7 - Pat Allan
3
16
  * Set default action of "DISPLAY" on alarms - Rikson
data/icalendar.gemspec CHANGED
@@ -20,6 +20,10 @@ variety of calendaring applications.
20
20
  ActiveSupport is required for TimeWithZone support, but not required for general use.
21
21
  EOM
22
22
 
23
+ s.metadata = {
24
+ 'changelog_uri' => 'https://github.com/icalendar/icalendar/blob/main/History.txt'
25
+ }
26
+
23
27
  s.files = `git ls-files`.split "\n"
24
28
  s.test_files = `git ls-files -- {test,spec,features}/*`.split "\n"
25
29
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Alarm < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Calendar < Component
@@ -31,6 +33,42 @@ module Icalendar
31
33
 
32
34
  def publish
33
35
  self.ip_method = 'PUBLISH'
36
+ self
37
+ end
38
+
39
+ def request
40
+ self.ip_method = 'REQUEST'
41
+ self
42
+ end
43
+
44
+ def reply
45
+ self.ip_method = 'REPLY'
46
+ self
47
+ end
48
+
49
+ def add
50
+ self.ip_method = 'ADD'
51
+ self
52
+ end
53
+
54
+ def cancel
55
+ self.ip_method = 'CANCEL'
56
+ self
57
+ end
58
+
59
+ def refresh
60
+ self.ip_method = 'REFRESH'
61
+ self
62
+ end
63
+
64
+ def counter
65
+ self.ip_method = 'COUNTER'
66
+ self
67
+ end
68
+
69
+ def decline_counter
70
+ self.ip_method = 'DECLINECOUNTER'
71
+ self
34
72
  end
35
73
 
36
74
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module Icalendar
@@ -53,10 +55,14 @@ module Icalendar
53
55
  end.compact.join "\r\n"
54
56
  end
55
57
 
58
+ ICAL_PROP_NAME_GSUB_REGEX = /\Aip_/.freeze
59
+
56
60
  def ical_prop_name(prop_name)
57
- prop_name.gsub(/\Aip_/, '').gsub('_', '-').upcase
61
+ prop_name.gsub(ICAL_PROP_NAME_GSUB_REGEX, '').gsub('_', '-').upcase
58
62
  end
59
63
 
64
+ ICAL_FOLD_LONG_LINE_SCAN_REGEX = /\P{M}\p{M}*/u.freeze
65
+
60
66
  def ical_fold(long_line, indent = "\x20")
61
67
  # rfc2445 says:
62
68
  # Lines of text SHOULD NOT be longer than 75 octets, excluding the line
@@ -74,7 +80,7 @@ module Icalendar
74
80
 
75
81
  return long_line if long_line.bytesize <= Icalendar::MAX_LINE_LENGTH
76
82
 
77
- chars = long_line.scan(/\P{M}\p{M}*/u) # split in graphenes
83
+ chars = long_line.scan(ICAL_FOLD_LONG_LINE_SCAN_REGEX) # split in graphenes
78
84
  folded = ['']
79
85
  bytes = 0
80
86
  while chars.count > 0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Event < Component
@@ -24,7 +26,7 @@ module Icalendar
24
26
  optional_single_property :status
25
27
  optional_single_property :summary
26
28
  optional_single_property :transp
27
- optional_single_property :url, Icalendar::Values::Uri
29
+ optional_single_property :url, Icalendar::Values::Uri, true
28
30
  optional_single_property :recurrence_id, Icalendar::Values::DateTime
29
31
 
30
32
  optional_property :rrule, Icalendar::Values::Recur, true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Freebusy < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  module HasComponents
@@ -32,13 +34,16 @@ module Icalendar
32
34
  custom_components[component_name.downcase.gsub("-", "_")] || []
33
35
  end
34
36
 
37
+ METHOD_MISSING_ADD_REGEX = /^add_(x_\w+)$/.freeze
38
+ METHOD_MISSING_X_FLAG_REGEX = /^x_/.freeze
39
+
35
40
  def method_missing(method, *args, &block)
36
41
  method_name = method.to_s
37
- if method_name =~ /^add_(x_\w+)$/
42
+ if method_name =~ METHOD_MISSING_ADD_REGEX
38
43
  component_name = $1
39
44
  custom = args.first || Component.new(component_name, component_name.upcase)
40
45
  add_custom_component(component_name, custom, &block)
41
- elsif method_name =~ /^x_/ && custom_component(method_name).size > 0
46
+ elsif method_name =~ METHOD_MISSING_X_FLAG_REGEX && custom_component(method_name).size > 0
42
47
  custom_component method_name
43
48
  else
44
49
  super
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  module HasProperties
@@ -146,7 +148,7 @@ module Icalendar
146
148
 
147
149
  define_method "#{prop}=" do |value|
148
150
  mapped = map_property_value value, klass, true, new_property
149
- if mapped.is_a? Icalendar::Values::Array
151
+ if mapped.is_a? Icalendar::Values::Helpers::Array
150
152
  instance_variable_set property_var, mapped.to_a.compact
151
153
  else
152
154
  instance_variable_set property_var, [mapped].compact
@@ -177,7 +179,7 @@ module Icalendar
177
179
  if value.nil? || value.is_a?(Icalendar::Value)
178
180
  value
179
181
  elsif value.is_a? ::Array
180
- Icalendar::Values::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
182
+ Icalendar::Values::Helpers::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
181
183
  else
182
184
  klass.new value, params
183
185
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Journal < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
  require 'logger'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Marshable
3
5
  def self.included(base)
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/timezone_store'
4
+ require 'stringio'
2
5
 
3
6
  module Icalendar
4
7
 
@@ -6,6 +9,8 @@ module Icalendar
6
9
  attr_writer :component_class
7
10
  attr_reader :source, :strict, :timezone_store, :verbose
8
11
 
12
+ CLEAN_BAD_WRAPPING_GSUB_REGEX = /\r?\n[ \t]/.freeze
13
+
9
14
  def self.clean_bad_wrapping(source)
10
15
  content = if source.respond_to? :read
11
16
  source.read
@@ -18,7 +23,7 @@ module Icalendar
18
23
  end
19
24
  encoding = content.encoding
20
25
  content.force_encoding(Encoding::ASCII_8BIT)
21
- content.gsub(/\r?\n[ \t]/, "").force_encoding(encoding)
26
+ content.gsub(CLEAN_BAD_WRAPPING_GSUB_REGEX, "").force_encoding(encoding)
22
27
  end
23
28
 
24
29
  def initialize(source, strict = false, verbose = false)
@@ -71,11 +76,15 @@ module Icalendar
71
76
  end
72
77
  end
73
78
 
79
+ WRAP_PROPERTY_VALUE_DELIMETER_REGEX = /(?<!\\)([,;])/.freeze
80
+ WRAP_PROPERTY_VALUE_SPLIT_REGEX = /(?<!\\)[;,]/.freeze
81
+
82
+
74
83
  def wrap_property_value(component, fields, multi_property)
75
84
  klass = get_wrapper_class component, fields
76
85
  if wrap_in_array? klass, fields[:value], multi_property
77
- delimiter = fields[:value].match(/(?<!\\)([,;])/)[1]
78
- Icalendar::Values::Array.new fields[:value].split(/(?<!\\)[;,]/),
86
+ delimiter = fields[:value].match(WRAP_PROPERTY_VALUE_DELIMETER_REGEX)[1]
87
+ Icalendar::Values::Helpers::Array.new fields[:value].split(WRAP_PROPERTY_VALUE_SPLIT_REGEX),
79
88
  klass,
80
89
  fields[:params],
81
90
  delimiter: delimiter
@@ -88,17 +97,22 @@ module Icalendar
88
97
  retry
89
98
  end
90
99
 
100
+ WRAP_IN_ARRAY_REGEX_1 = /(?<!\\)[,;]/.freeze
101
+ WRAP_IN_ARRAY_REGEX_2 = /(?<!\\);/.freeze
102
+
91
103
  def wrap_in_array?(klass, value, multi_property)
92
104
  klass.value_type != 'RECUR' &&
93
- ((multi_property && value =~ /(?<!\\)[,;]/) || value =~ /(?<!\\);/)
105
+ ((multi_property && value =~ WRAP_IN_ARRAY_REGEX_1) || value =~ WRAP_IN_ARRAY_REGEX_2)
94
106
  end
95
107
 
108
+ GET_WRAPPER_CLASS_GSUB_REGEX = /(?:\A|-)(.)/.freeze
109
+
96
110
  def get_wrapper_class(component, fields)
97
111
  klass = component.class.default_property_types[fields[:name]]
98
112
  if !fields[:params]['value'].nil?
99
113
  klass_name = fields[:params].delete('value').first
100
114
  unless klass_name.upcase == klass.value_type
101
- klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }}"
115
+ klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(GET_WRAPPER_CLASS_GSUB_REGEX) { |m| m[-1].upcase }}"
102
116
  klass = Object.const_get klass_name if Object.const_defined?(klass_name)
103
117
  end
104
118
  end
@@ -119,14 +133,16 @@ module Icalendar
119
133
  @component_class ||= Icalendar::Calendar
120
134
  end
121
135
 
136
+ PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX = /\AV/.freeze
137
+
122
138
  def parse_component(component)
123
139
  while (fields = next_fields)
124
140
  if fields[:name] == 'end'
125
- klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
141
+ klass_name = fields[:value].gsub(PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX, '').downcase.capitalize
126
142
  timezone_store.store(component) if klass_name == 'Timezone'
127
143
  break
128
144
  elsif fields[:name] == 'begin'
129
- klass_name = fields[:value].gsub(/\AV/, '').gsub("-", "_").downcase.capitalize
145
+ klass_name = fields[:value].gsub(PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX, '').gsub("-", "_").downcase.capitalize
130
146
  Icalendar.logger.debug "Adding component #{klass_name}"
131
147
  if Object.const_defined? "Icalendar::#{klass_name}"
132
148
  component.add_component parse_component(Object.const_get("Icalendar::#{klass_name}").new)
@@ -146,13 +162,16 @@ module Icalendar
146
162
  @data = source.gets and @data.chomp!
147
163
  end
148
164
 
165
+ NEXT_FIELDS_TAB_REGEX = /\A[ \t].+\z/.freeze
166
+ NEXT_FIELDS_WHITESPACE_REGEX = /\A\s*\z/.freeze
167
+
149
168
  def next_fields
150
169
  line = @data or return nil
151
170
  loop do
152
171
  read_in_data
153
- if @data =~ /\A[ \t].+\z/
172
+ if @data =~ NEXT_FIELDS_TAB_REGEX
154
173
  line << @data[1, @data.size]
155
- elsif @data !~ /\A\s*\z/
174
+ elsif @data !~ NEXT_FIELDS_WHITESPACE_REGEX
156
175
  break
157
176
  end
158
177
  end
@@ -167,24 +186,29 @@ module Icalendar
167
186
  VALUE = '.*'
168
187
  LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*):(?<value>#{VALUE})"
169
188
  BAD_LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*)"
189
+ LINE_REGEX = %r{#{LINE}}.freeze
190
+ BAD_LINE_REGEX = %r{#{BAD_LINE}}.freeze
191
+ PARAM_REGEX = %r{#{PARAM}}.freeze
192
+ PVALUE_REGEX = %r{#{PVALUE}}.freeze
193
+ PVALUE_GSUB_REGEX = /\A"|"\z/.freeze
170
194
 
171
195
  def parse_fields(input)
172
- if parts = %r{#{LINE}}.match(input)
196
+ if parts = LINE_REGEX.match(input)
173
197
  value = parts[:value]
174
198
  else
175
- parts = %r{#{BAD_LINE}}.match(input) unless strict?
199
+ parts = BAD_LINE_REGEX.match(input) unless strict?
176
200
  parts or fail "Invalid iCalendar input line: #{input}"
177
201
  # Non-strict and bad line so use a value of empty string
178
202
  value = ''
179
203
  end
180
204
 
181
205
  params = {}
182
- parts[:params].scan %r{#{PARAM}} do |match|
206
+ parts[:params].scan PARAM_REGEX do |match|
183
207
  param_name = match[0].downcase
184
208
  params[param_name] ||= []
185
- match[1].scan %r{#{PVALUE}} do |param_value|
209
+ match[1].scan PVALUE_REGEX do |param_value|
186
210
  if param_value.size > 0
187
- param_value = param_value.gsub(/\A"|"\z/, '')
211
+ param_value = param_value.gsub(PVALUE_GSUB_REGEX, '')
188
212
  params[param_name] << param_value
189
213
  if param_name == 'tzid'
190
214
  params['x-tz-info'] = timezone_store.retrieve param_value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ice_cube'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
  require 'icalendar/downcased_hash'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Todo < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  =begin
2
4
  Copyright (C) 2008 Sean Dague
3
5
 
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+
1
4
  require 'delegate'
2
5
  require 'icalendar/downcased_hash'
3
6
 
@@ -31,8 +34,11 @@ module Icalendar
31
34
  end
32
35
  end
33
36
 
37
+ VALUE_TYPE_GSUB_REGEX_1 = /\A.*::/.freeze
38
+ VALUE_TYPE_GSUB_REGEX_2 = /(?<!\A)[A-Z]/.freeze
39
+
34
40
  def self.value_type
35
- name.gsub(/\A.*::/, '').gsub(/(?<!\A)[A-Z]/, '-\0').upcase
41
+ name.gsub(VALUE_TYPE_GSUB_REGEX_1, '').gsub(VALUE_TYPE_GSUB_REGEX_2, '-\0').upcase
36
42
  end
37
43
 
38
44
  def value_type
@@ -54,9 +60,11 @@ module Icalendar
54
60
  "#{name.to_s.gsub('_', '-').upcase}=#{param_value}"
55
61
  end
56
62
 
63
+ ESCAPE_PARAM_VALUE_REGEX = /[;:,]/.freeze
64
+
57
65
  def escape_param_value(value)
58
66
  v = value.to_s.gsub('"', "'")
59
- v =~ /[;:,]/ ? %("#{v}") : v
67
+ v =~ ESCAPE_PARAM_VALUE_REGEX ? %("#{v}") : v
60
68
  end
61
69
 
62
70
  end
@@ -64,7 +72,7 @@ module Icalendar
64
72
  end
65
73
 
66
74
  # helpers; not actual iCalendar value type
67
- require_relative 'values/array'
75
+ require_relative 'values/helpers/array'
68
76
  require_relative 'values/date_or_date_time'
69
77
 
70
78
  # iCalendar value types
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Icalendar
@@ -21,9 +23,11 @@ module Icalendar
21
23
 
22
24
  private
23
25
 
26
+ BASE_64_REGEX = /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)\z/.freeze
27
+
24
28
  def base64?
25
29
  value.is_a?(String) &&
26
- value =~ /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)\z/
30
+ value =~ BASE_64_REGEX
27
31
  end
28
32
  end
29
33
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
 
3
5
  module Icalendar
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
- require_relative 'time_with_zone'
4
+ require_relative 'helpers/time_with_zone'
3
5
 
4
6
  module Icalendar
5
7
  module Values
6
8
 
7
9
  class DateTime < Value
8
- include TimeWithZone
10
+ include Helpers::TimeWithZone
9
11
 
10
12
  FORMAT = '%Y%m%dT%H%M%S'
11
13
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -36,14 +38,21 @@ module Icalendar
36
38
  hours > 0 || minutes > 0 || seconds > 0
37
39
  end
38
40
 
41
+ DURATION_PAST_REGEX = /\A([+-])P/.freeze
42
+ DURATION_WEEKS_REGEX = /(\d+)W/.freeze
43
+ DURATION_DAYS_REGEX = /(\d+)D/.freeze
44
+ DURATION_HOURS_REGEX = /(\d+)H/.freeze
45
+ DURATION_MINUTES_REGEX = /(\d+)M/.freeze
46
+ DURATION_SECONDS_REGEX = /(\d+)S/.freeze
47
+
39
48
  def parse_fields(value)
40
49
  {
41
- past: (value =~ /\A([+-])P/ ? $1 == '-' : false),
42
- weeks: (value =~ /(\d+)W/ ? $1.to_i : 0),
43
- days: (value =~ /(\d+)D/ ? $1.to_i : 0),
44
- hours: (value =~ /(\d+)H/ ? $1.to_i : 0),
45
- minutes: (value =~ /(\d+)M/ ? $1.to_i : 0),
46
- seconds: (value =~ /(\d+)S/ ? $1.to_i : 0)
50
+ past: (value =~ DURATION_PAST_REGEX ? $1 == '-' : false),
51
+ weeks: (value =~ DURATION_WEEKS_REGEX ? $1.to_i : 0),
52
+ days: (value =~ DURATION_DAYS_REGEX ? $1.to_i : 0),
53
+ hours: (value =~ DURATION_HOURS_REGEX ? $1.to_i : 0),
54
+ minutes: (value =~ DURATION_MINUTES_REGEX ? $1.to_i : 0),
55
+ seconds: (value =~ DURATION_SECONDS_REGEX ? $1.to_i : 0)
47
56
  }
48
57
  end
49
58
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalendar
4
+ module Values
5
+ module Helpers
6
+ class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
7
+ # ActiveSupport::TimeWithZone implements a #to_a method that will cause
8
+ # unexpected behavior in components with multi_property DateTime
9
+ # properties when the setters for those properties are invoked with an
10
+ # Icalendar::Values::DateTime that is delegating for an
11
+ # ActiveSupport::TimeWithZone. To avoid this behavior, undefine #to_a.
12
+ undef_method :to_a
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalendar
4
+ module Values
5
+ module Helpers
6
+
7
+ class Array < Value
8
+
9
+ attr_reader :value_delimiter
10
+
11
+ def initialize(value, klass, params = {}, options = {})
12
+ @value_delimiter = options[:delimiter] || ','
13
+ mapped = if value.is_a? ::Array
14
+ value.map do |v|
15
+ if v.is_a? Icalendar::Values::Helpers::Array
16
+ Icalendar::Values::Helpers::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
17
+ elsif v.is_a? ::Array
18
+ Icalendar::Values::Helpers::Array.new v, klass, params, delimiter: value_delimiter
19
+ elsif v.is_a? Icalendar::Value
20
+ v
21
+ else
22
+ klass.new v, params
23
+ end
24
+ end
25
+ else
26
+ [klass.new(value, params)]
27
+ end
28
+ super mapped
29
+ end
30
+
31
+ def params_ical
32
+ value.each do |v|
33
+ ical_params.merge! v.ical_params
34
+ end
35
+ super
36
+ end
37
+
38
+ def value_ical
39
+ value.map do |v|
40
+ v.value_ical
41
+ end.join value_delimiter
42
+ end
43
+
44
+ def valid?
45
+ klass = value.first.class
46
+ !value.all? { |v| v.class == klass }
47
+ end
48
+
49
+ def value_type
50
+ value.first.value_type
51
+ end
52
+
53
+ private
54
+
55
+ def needs_value_type?(default_type)
56
+ value.first.class != default_type
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'active_support/time'
5
+
6
+ if defined?(ActiveSupport::TimeWithZone)
7
+ require_relative 'active_support_time_with_zone_adapter'
8
+ end
9
+ rescue NameError
10
+ # ActiveSupport v7+ needs the base require to be run first before loading
11
+ # specific parts of it.
12
+ # https://guides.rubyonrails.org/active_support_core_extensions.html#stand-alone-active-support
13
+ require 'active_support'
14
+ retry
15
+ rescue LoadError
16
+ # tis ok, just a bit less fancy
17
+ end
18
+
19
+ module Icalendar
20
+ module Values
21
+ module Helpers
22
+ module TimeWithZone
23
+ attr_reader :tz_utc
24
+
25
+ def initialize(value, params = {})
26
+ params = Icalendar::DowncasedHash(params)
27
+ @tz_utc = params['tzid'] == 'UTC'
28
+ x_tz_info = params.delete 'x-tz-info'
29
+
30
+ offset_value = unless params['tzid'].nil?
31
+ tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
32
+ support_classes_defined = defined?(ActiveSupport::TimeZone) && defined?(ActiveSupportTimeWithZoneAdapter)
33
+ if support_classes_defined && (tz = ActiveSupport::TimeZone[tzid])
34
+ # plan a - use ActiveSupport::TimeWithZone
35
+ ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
36
+ elsif !x_tz_info.nil?
37
+ # plan b - use definition from provided `VTIMEZONE`
38
+ offset = x_tz_info.offset_for_local(value).to_s
39
+ if value.respond_to?(:change)
40
+ value.change offset: offset
41
+ else
42
+ ::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
43
+ end
44
+ elsif support_classes_defined && (tz = ActiveSupport::TimeZone[tzid.split.first])
45
+ # plan c - try to find an ActiveSupport::TimeWithZone based on the first word of the tzid
46
+ params['tzid'] = [tz.tzinfo.name]
47
+ ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
48
+ end
49
+ # plan d - just ignore the tzid
50
+ end
51
+ super((offset_value || value), params)
52
+ end
53
+
54
+ def params_ical
55
+ ical_params.delete 'tzid' if tz_utc
56
+ super
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
4
6
  class Period < Value
5
7
 
8
+ PERIOD_LAST_PART_REGEX = /\A[+-]?P.+\z/.freeze
9
+
6
10
  def initialize(value, params = {})
7
11
  parts = value.split '/'
8
12
  period_start = Icalendar::Values::DateTime.new parts.first
9
- if parts.last =~ /\A[+-]?P.+\z/
13
+ if parts.last =~ PERIOD_LAST_PART_REGEX
10
14
  period_end = Icalendar::Values::Duration.new parts.last
11
15
  else
12
16
  period_end = Icalendar::Values::DateTime.new parts.last
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -44,22 +46,37 @@ module Icalendar
44
46
 
45
47
  private
46
48
 
49
+ PARSE_FIELDS_FREQUENCY_REGEX = /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i.freeze
50
+ PARSE_FIELDS_UNTIL_REGEX = /UNTIL=([^;]*)/i.freeze
51
+ PARSE_FIELDS_COUNT_REGEX = /COUNT=(\d+)/i.freeze
52
+ PARSE_FIELDS_INTERVAL_REGEX = /INTERVAL=(\d+)/i.freeze
53
+ PARSE_FIELDS_BY_SECOND_REGEX = /BYSECOND=(#{NUM_LIST})(?:;|\z)/i.freeze
54
+ PARSE_FIELDS_BY_MINUTE_REGEX = /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i.freeze
55
+ PARSE_FIELDS_BY_HOUR_REGEX = /BYHOUR=(#{NUM_LIST})(?:;|\z)/i.freeze
56
+ PARSE_FIELDS_BY_DAY_REGEX = /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i.freeze
57
+ PARSE_FIELDS_BY_MONTH_DAY_REGEX = /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze
58
+ PARSE_FIELDS_BY_YEAR_DAY_REGEX = /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze
59
+ PARSE_FIELDS_BY_WEEK_NUMBER_REGEX = /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze
60
+ PARSE_FIELDS_BY_MONTH_REGEX = /BYMONTH=(#{NUM_LIST})(?:;|\z)/i.freeze
61
+ PARSE_FIELDS_BY_SET_POSITON_REGEX = /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze
62
+ PARSE_FIELDS_BY_WEEK_START_REGEX = /WKST=(#{DAYNAME})/i.freeze
63
+
47
64
  def parse_fields(value)
48
65
  {
49
- frequency: (value =~ /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i ? $1.upcase : nil),
50
- until: (value =~ /UNTIL=([^;]*)/i ? $1 : nil),
51
- count: (value =~ /COUNT=(\d+)/i ? $1.to_i : nil),
52
- interval: (value =~ /INTERVAL=(\d+)/i ? $1.to_i : nil),
53
- by_second: (value =~ /BYSECOND=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
54
- by_minute: (value =~ /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
55
- by_hour: (value =~ /BYHOUR=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
56
- by_day: (value =~ /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
57
- by_month_day: (value =~ /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
58
- by_year_day: (value =~ /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
59
- by_week_number: (value =~ /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
60
- by_month: (value =~ /BYMONTH=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
61
- by_set_position: (value =~ /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
62
- week_start: (value =~ /WKST=(#{DAYNAME})/i ? $1.upcase : nil)
66
+ frequency: (value =~ PARSE_FIELDS_FREQUENCY_REGEX ? $1.upcase : nil),
67
+ until: (value =~ PARSE_FIELDS_UNTIL_REGEX ? $1 : nil),
68
+ count: (value =~ PARSE_FIELDS_COUNT_REGEX ? $1.to_i : nil),
69
+ interval: (value =~ PARSE_FIELDS_INTERVAL_REGEX ? $1.to_i : nil),
70
+ by_second: (value =~ PARSE_FIELDS_BY_SECOND_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
71
+ by_minute: (value =~ PARSE_FIELDS_BY_MINUTE_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
72
+ by_hour: (value =~ PARSE_FIELDS_BY_HOUR_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
73
+ by_day: (value =~ PARSE_FIELDS_BY_DAY_REGEX ? $1.split(',') : nil),
74
+ by_month_day: (value =~ PARSE_FIELDS_BY_MONTH_DAY_REGEX ? $1.split(',') : nil),
75
+ by_year_day: (value =~ PARSE_FIELDS_BY_YEAR_DAY_REGEX ? $1.split(',') : nil),
76
+ by_week_number: (value =~ PARSE_FIELDS_BY_WEEK_NUMBER_REGEX ? $1.split(',') : nil),
77
+ by_month: (value =~ PARSE_FIELDS_BY_MONTH_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
78
+ by_set_position: (value =~ PARSE_FIELDS_BY_SET_POSITON_REGEX ? $1.split(',') : nil),
79
+ week_start: (value =~ PARSE_FIELDS_BY_WEEK_START_REGEX ? $1.upcase : nil)
63
80
  }
64
81
  end
65
82
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
  class Text < Value
@@ -9,12 +11,14 @@ module Icalendar
9
11
  super value, params
10
12
  end
11
13
 
14
+ VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX = /\r?\n/.freeze
15
+
12
16
  def value_ical
13
17
  value.dup.tap do |v|
14
18
  v.gsub!('\\') { '\\\\' }
15
19
  v.gsub!(';', '\;')
16
20
  v.gsub!(',', '\,')
17
- v.gsub!(/\r?\n/, '\n')
21
+ v.gsub!(VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX, '\n')
18
22
  end
19
23
  end
20
24
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
- require_relative 'time_with_zone'
4
+ require_relative 'helpers/time_with_zone'
3
5
 
4
6
  module Icalendar
5
7
  module Values
6
8
 
7
9
  class Time < Value
8
- include TimeWithZone
10
+ include Helpers::TimeWithZone
9
11
 
10
12
  FORMAT = '%H%M%S'
11
13
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -36,8 +38,11 @@ module Icalendar
36
38
  hours == 0 && minutes == 0 && seconds == 0
37
39
  end
38
40
 
41
+ PARSE_FIELDS_MD_REGEX = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.freeze
42
+ PARSE_FIELDS_WHITESPACE_GSUB_REGEX = /\s+/.freeze
43
+
39
44
  def parse_fields(value)
40
- md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value.gsub(/\s+/, '')
45
+ md = PARSE_FIELDS_MD_REGEX.match value.gsub(PARSE_FIELDS_WHITESPACE_GSUB_REGEX, '')
41
46
  {
42
47
  behind: (md[:behind] == '-'),
43
48
  hours: md[:hours].to_i,
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
- VERSION = '2.8.0'
5
+ VERSION = '2.10.0'
4
6
 
5
7
  end
data/lib/icalendar.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/logger'
2
4
 
3
5
  module Icalendar
@@ -190,6 +190,55 @@ END:VCALENDAR
190
190
  end
191
191
  end
192
192
 
193
+ describe '#request' do
194
+ it 'sets ip_method to "REQUEST"' do
195
+ subject.request
196
+ expect(subject.ip_method).to eq 'REQUEST'
197
+ end
198
+ end
199
+
200
+ describe '#reply' do
201
+ it 'sets ip_method to "REPLY"' do
202
+ subject.reply
203
+ expect(subject.ip_method).to eq 'REPLY'
204
+ end
205
+ end
206
+
207
+ describe '#add' do
208
+ it 'sets ip_method to "ADD"' do
209
+ subject.add
210
+ expect(subject.ip_method).to eq 'ADD'
211
+ end
212
+ end
213
+
214
+ describe '#cancel' do
215
+ it 'sets ip_method to "CANCEL"' do
216
+ subject.cancel
217
+ expect(subject.ip_method).to eq 'CANCEL'
218
+ end
219
+ end
220
+
221
+ describe '#refresh' do
222
+ it 'sets ip_method to "REFRESH"' do
223
+ subject.refresh
224
+ expect(subject.ip_method).to eq 'REFRESH'
225
+ end
226
+ end
227
+
228
+ describe '#counter' do
229
+ it 'sets ip_method to "COUNTER"' do
230
+ subject.counter
231
+ expect(subject.ip_method).to eq 'COUNTER'
232
+ end
233
+ end
234
+
235
+ describe '#decline_counter' do
236
+ it 'sets ip_method to "DECLINECOUNTER"' do
237
+ subject.decline_counter
238
+ expect(subject.ip_method).to eq 'DECLINECOUNTER'
239
+ end
240
+ end
241
+
193
242
  describe '.parse' do
194
243
  let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'bad_wrapping.ics') }
195
244
 
data/spec/event_spec.rb CHANGED
@@ -51,8 +51,8 @@ describe Icalendar::Event do
51
51
  context 'suggested single values' do
52
52
  before(:each) do
53
53
  subject.dtstart = DateTime.now
54
- subject.append_rrule double('RRule').as_null_object
55
- subject.append_rrule double('RRule').as_null_object
54
+ subject.append_rrule 'RRule'
55
+ subject.append_rrule 'RRule'
56
56
  end
57
57
 
58
58
  it 'is valid by default' do
@@ -156,6 +156,7 @@ describe Icalendar::Event do
156
156
  subject.dtend = "20131227T033000Z"
157
157
  subject.summary = 'My event, my ical, my test'
158
158
  subject.geo = [41.230896,-74.411774]
159
+ subject.url = 'https://example.com'
159
160
  subject.x_custom_property = 'customize'
160
161
  end
161
162
 
@@ -163,6 +164,7 @@ describe Icalendar::Event do
163
164
  it { expect(subject.to_ical).to include 'DTEND:20131227T033000Z' }
164
165
  it { expect(subject.to_ical).to include 'SUMMARY:My event\, my ical\, my test' }
165
166
  it { expect(subject.to_ical).to include 'X-CUSTOM-PROPERTY:customize' }
167
+ it { expect(subject.to_ical).to include 'URL;VALUE=URI:https://example.com' }
166
168
  it { expect(subject.to_ical).to include 'GEO:41.230896;-74.411774' }
167
169
 
168
170
  context 'simple organizer' do
@@ -14,7 +14,7 @@ describe Icalendar do
14
14
  event = Icalendar::Event.new
15
15
  parsed = Icalendar::Calendar.parse(source).first
16
16
  event.rdate = parsed.events.first.rdate
17
- expect(event.rdate.first).to be_kind_of Icalendar::Values::Array
17
+ expect(event.rdate.first).to be_kind_of Icalendar::Values::Helpers::Array
18
18
  expect(event.rdate.first.params_ical).to eq ";TZID=US-Mountain"
19
19
  end
20
20
  end
@@ -27,6 +27,22 @@ describe Icalendar::Values::DateTime do
27
27
  expect(subject.value.utc.hour).to eq 23
28
28
  end
29
29
  end
30
+
31
+ context 'nonstandard format tzid local time' do
32
+ let(:value) { '20230901T230404' }
33
+ let(:params) { {'tzid' => 'Singapore Standard Time'} }
34
+
35
+ it 'parses the value as local time' do
36
+ expect(subject.value.hour).to eq 23
37
+ expect(subject.value.utc_offset).to eq 28800
38
+ expect(subject.value.utc.hour).to eq 15
39
+ end
40
+
41
+ it 'updates the tzid' do
42
+ # use an array because that's how output from the parser will end up
43
+ expect(subject.ical_params['tzid']).to eq ['Asia/Singapore']
44
+ end
45
+ end
30
46
  end
31
47
 
32
48
  else
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.8.0
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-11 00:00:00.000000000 Z
11
+ date: 2023-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ice_cube
@@ -187,8 +187,6 @@ files:
187
187
  - lib/icalendar/todo.rb
188
188
  - lib/icalendar/tzinfo.rb
189
189
  - lib/icalendar/value.rb
190
- - lib/icalendar/values/active_support_time_with_zone_adapter.rb
191
- - lib/icalendar/values/array.rb
192
190
  - lib/icalendar/values/binary.rb
193
191
  - lib/icalendar/values/boolean.rb
194
192
  - lib/icalendar/values/cal_address.rb
@@ -197,12 +195,14 @@ files:
197
195
  - lib/icalendar/values/date_time.rb
198
196
  - lib/icalendar/values/duration.rb
199
197
  - lib/icalendar/values/float.rb
198
+ - lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb
199
+ - lib/icalendar/values/helpers/array.rb
200
+ - lib/icalendar/values/helpers/time_with_zone.rb
200
201
  - lib/icalendar/values/integer.rb
201
202
  - lib/icalendar/values/period.rb
202
203
  - lib/icalendar/values/recur.rb
203
204
  - lib/icalendar/values/text.rb
204
205
  - lib/icalendar/values/time.rb
205
- - lib/icalendar/values/time_with_zone.rb
206
206
  - lib/icalendar/values/uri.rb
207
207
  - lib/icalendar/values/utc_offset.rb
208
208
  - lib/icalendar/version.rb
@@ -247,7 +247,8 @@ licenses:
247
247
  - BSD-2-Clause
248
248
  - GPL-3.0-only
249
249
  - icalendar
250
- metadata: {}
250
+ metadata:
251
+ changelog_uri: https://github.com/icalendar/icalendar/blob/main/History.txt
251
252
  post_install_message: 'ActiveSupport is required for TimeWithZone support, but not
252
253
  required for general use.
253
254
 
@@ -266,7 +267,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
266
267
  - !ruby/object:Gem::Version
267
268
  version: '0'
268
269
  requirements: []
269
- rubygems_version: 3.3.7
270
+ rubygems_version: 3.4.10
270
271
  signing_key:
271
272
  specification_version: 4
272
273
  summary: A ruby implementation of the iCalendar specification (RFC-5545).
@@ -1,12 +0,0 @@
1
- module Icalendar
2
- module Values
3
- class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
4
- # ActiveSupport::TimeWithZone implements a #to_a method that will cause
5
- # unexpected behavior in components with multi_property DateTime
6
- # properties when the setters for those properties are invoked with an
7
- # Icalendar::Values::DateTime that is delegating for an
8
- # ActiveSupport::TimeWithZone. To avoid this behavior, undefine #to_a.
9
- undef_method :to_a
10
- end
11
- end
12
- end
@@ -1,58 +0,0 @@
1
- module Icalendar
2
- module Values
3
-
4
- class Array < Value
5
-
6
- attr_reader :value_delimiter
7
-
8
- def initialize(value, klass, params = {}, options = {})
9
- @value_delimiter = options[:delimiter] || ','
10
- mapped = if value.is_a? ::Array
11
- value.map do |v|
12
- if v.is_a? Icalendar::Values::Array
13
- Icalendar::Values::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
14
- elsif v.is_a? ::Array
15
- Icalendar::Values::Array.new v, klass, params, delimiter: value_delimiter
16
- elsif v.is_a? Icalendar::Value
17
- v
18
- else
19
- klass.new v, params
20
- end
21
- end
22
- else
23
- [klass.new(value, params)]
24
- end
25
- super mapped
26
- end
27
-
28
- def params_ical
29
- value.each do |v|
30
- ical_params.merge! v.ical_params
31
- end
32
- super
33
- end
34
-
35
- def value_ical
36
- value.map do |v|
37
- v.value_ical
38
- end.join value_delimiter
39
- end
40
-
41
- def valid?
42
- klass = value.first.class
43
- !value.all? { |v| v.class == klass }
44
- end
45
-
46
- def value_type
47
- value.first.value_type
48
- end
49
-
50
- private
51
-
52
- def needs_value_type?(default_type)
53
- value.first.class != default_type
54
- end
55
- end
56
-
57
- end
58
- end
@@ -1,53 +0,0 @@
1
- require 'icalendar/timezone_store'
2
-
3
- begin
4
- require 'active_support/time'
5
-
6
- if defined?(ActiveSupport::TimeWithZone)
7
- require 'icalendar/values/active_support_time_with_zone_adapter'
8
- end
9
- rescue NameError
10
- # ActiveSupport v7+ needs the base require to be run first before loading
11
- # specific parts of it.
12
- # https://guides.rubyonrails.org/active_support_core_extensions.html#stand-alone-active-support
13
- require 'active_support'
14
- retry
15
- rescue LoadError
16
- # tis ok, just a bit less fancy
17
- end
18
-
19
- module Icalendar
20
- module Values
21
- module TimeWithZone
22
- attr_reader :tz_utc
23
-
24
- def initialize(value, params = {})
25
- params = Icalendar::DowncasedHash(params)
26
- @tz_utc = params['tzid'] == 'UTC'
27
- x_tz_info = params.delete 'x-tz-info'
28
-
29
- offset_value = unless params['tzid'].nil?
30
- tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
31
- if defined?(ActiveSupport::TimeZone) &&
32
- defined?(ActiveSupportTimeWithZoneAdapter) &&
33
- (tz = ActiveSupport::TimeZone[tzid])
34
- ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
35
- elsif !x_tz_info.nil?
36
- offset = x_tz_info.offset_for_local(value).to_s
37
- if value.respond_to?(:change)
38
- value.change offset: offset
39
- else
40
- ::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
41
- end
42
- end
43
- end
44
- super((offset_value || value), params)
45
- end
46
-
47
- def params_ical
48
- ical_params.delete 'tzid' if tz_utc
49
- super
50
- end
51
- end
52
- end
53
- end