icalendar-rrule 0.1.7 → 0.2.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 +33 -0
- data/lib/icalendar/rrule/version.rb +1 -1
- data/lib/icalendar/scannable-calendar.rb +16 -1
- data/lib/icalendar/schedulable-component.rb +237 -22
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b891ac836e382f65aa522d6fc6b049483b3296deef1fe9d2bb196ed6bb2de9bc
|
|
4
|
+
data.tar.gz: 5f99a1568a014411c01559505dd71068e630fca5cdbeb3e32fbb246fecd24190
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab80482e0f12d0cc47b11e28bbcf7f887bf42e50ed9e6d74dea0f24202b42215c62585347e94e72e8cb5dd38e04bf0e507745520092c7a11af36bc48182ffea1
|
|
7
|
+
data.tar.gz: 6a62fab4d640445562924807ebd7ed8eeadba97d1a5630f2c8bcf399977e6d1ece789efe17dbf21c50e05ee04b289bf81220e81075a3a698ef79531dcba5dbe2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2025-12-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Support for tasks (VTODOs) with RRULE expansion
|
|
9
|
+
- New `single_timestamp?` predicate for zero-duration events/deadline-only tasks
|
|
10
|
+
- Comprehensive timezone extraction with multiple fallback strategies
|
|
11
|
+
- Better system timezone detection (ENV['TZ'], /etc/timezone, TZInfo)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **BREAKING**: Events without explicit timezone now use system timezone instead of UTC
|
|
15
|
+
- All-day events without DTEND now correctly compute 1-day duration in date-space
|
|
16
|
+
- Relaxed `ice_cube` dependency to >= 0.16 (tested with 0.17.0)
|
|
17
|
+
- `all_day?` now returns false for tasks (only applies to events)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Timezone handling for recurring events (RRULE expansion now preserves timezone)
|
|
21
|
+
- Floating time interpretation (DateTime with offset 0 no longer treated as UTC)
|
|
22
|
+
- All-day events no longer experience timezone shift errors
|
|
23
|
+
- Compatibility with icalendar gem 2.12.1 (DowncasedHash handling)
|
|
24
|
+
- Task time handling (zero-duration tasks with only DUE)
|
|
25
|
+
|
|
26
|
+
### Internal
|
|
27
|
+
- Enhanced `_extract_explicit_timezone` for better timezone detection
|
|
28
|
+
- Added `_dtstart_is_all_day?` helper
|
|
29
|
+
- Improved `_guess_system_timezone` with 5 fallback methods
|
|
30
|
+
- Better test coverage (145+ tests, including exotic timezones)
|
|
31
|
+
|
|
32
|
+
## [0.1.7] - 2020-xx-xx
|
|
33
|
+
- Previous stable release
|
|
@@ -52,8 +52,23 @@ module Icalendar
|
|
|
52
52
|
result = []
|
|
53
53
|
components.each do |comp|
|
|
54
54
|
occurrences = comp.schedule.occurrences_between(begin_time, closing_time)
|
|
55
|
+
|
|
56
|
+
# Get the target timezone from the component
|
|
57
|
+
target_tz = comp.start_time.time_zone
|
|
58
|
+
|
|
55
59
|
occurrences.each do |oc|
|
|
56
|
-
|
|
60
|
+
# Interpret the time components AS IF they're already in target timezone
|
|
61
|
+
# (don't convert, just reconstruct in the right zone)
|
|
62
|
+
start_tz = target_tz.local(
|
|
63
|
+
oc.start_time.year, oc.start_time.month, oc.start_time.day,
|
|
64
|
+
oc.start_time.hour, oc.start_time.min, oc.start_time.sec
|
|
65
|
+
)
|
|
66
|
+
end_tz = target_tz.local(
|
|
67
|
+
oc.end_time.year, oc.end_time.month, oc.end_time.day,
|
|
68
|
+
oc.end_time.hour, oc.end_time.min, oc.end_time.sec
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
new_oc = Icalendar::Rrule::Occurrence.new(self, comp, start_tz, end_tz)
|
|
57
72
|
result << new_oc
|
|
58
73
|
end
|
|
59
74
|
end
|
|
@@ -93,8 +93,15 @@ module Icalendar
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
##
|
|
96
|
-
#
|
|
97
|
-
#
|
|
96
|
+
# Returns the explicit duration from DURATION property or guessed duration.
|
|
97
|
+
#
|
|
98
|
+
# WARNING: This does NOT compute the actual duration between start_time and end_time!
|
|
99
|
+
# For events with DTEND but no DURATION property, this returns 0 or the guessed duration,
|
|
100
|
+
# even though the actual duration may be longer.
|
|
101
|
+
#
|
|
102
|
+
# To get the actual duration, use: (end_time.to_i - start_time.to_i)
|
|
103
|
+
#
|
|
104
|
+
# @return [Integer] explicit duration in seconds, or 0 if not specified
|
|
98
105
|
# @api private
|
|
99
106
|
def _duration_seconds # rubocop:disable Metrics/AbcSize
|
|
100
107
|
return _guessed_duration unless _duration
|
|
@@ -103,6 +110,25 @@ module Icalendar
|
|
|
103
110
|
d.seconds + (d.minutes * SEC_MIN) + (d.hours * SEC_HOUR) + (d.days * SEC_DAY) + (d.weeks * SEC_WEEK)
|
|
104
111
|
end
|
|
105
112
|
|
|
113
|
+
# Check if dtstart looks like an all-day event (starts at midnight)
|
|
114
|
+
# This is used internally to determine if we should apply the 1-day duration rule
|
|
115
|
+
# @return [Boolean] true if dtstart is a Date or starts at midnight
|
|
116
|
+
# @api private
|
|
117
|
+
def _dtstart_is_all_day?
|
|
118
|
+
# Only apply the 1-day rule to Events, not Tasks
|
|
119
|
+
return false unless self.is_a?(Icalendar::Event)
|
|
120
|
+
|
|
121
|
+
return true if _dtstart.is_a?(Icalendar::Values::Date)
|
|
122
|
+
|
|
123
|
+
# If it's a DateTime, check if it's at midnight (00:00:00)
|
|
124
|
+
if _dtstart.respond_to?(:to_time) || _dtstart.respond_to?(:to_datetime)
|
|
125
|
+
time = _to_time_with_zone(_dtstart)
|
|
126
|
+
return time == time.beginning_of_day
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
106
132
|
##
|
|
107
133
|
# Make an educated guess how long this event might last according to the following definition from RFC 5545:
|
|
108
134
|
#
|
|
@@ -114,7 +140,7 @@ module Icalendar
|
|
|
114
140
|
# @return [Integer] the number of seconds this task might last.
|
|
115
141
|
# @api private
|
|
116
142
|
def _guessed_duration
|
|
117
|
-
if
|
|
143
|
+
if _dtstart_is_all_day? && _dtend.nil? && _duration.nil? && _due.nil?
|
|
118
144
|
SEC_DAY
|
|
119
145
|
else
|
|
120
146
|
0
|
|
@@ -127,8 +153,11 @@ module Icalendar
|
|
|
127
153
|
def start_time
|
|
128
154
|
if _dtstart
|
|
129
155
|
_to_time_with_zone(_dtstart)
|
|
130
|
-
elsif _due
|
|
156
|
+
elsif _due && _duration_seconds > 0
|
|
131
157
|
_to_time_with_zone(_due.to_i - _duration_seconds)
|
|
158
|
+
elsif _due
|
|
159
|
+
# Task with only DUE, no duration: start == end (zero-duration/deadline-only)
|
|
160
|
+
_to_time_with_zone(_due)
|
|
132
161
|
else
|
|
133
162
|
_to_time_with_zone(NULL_TIME)
|
|
134
163
|
end
|
|
@@ -143,17 +172,50 @@ module Icalendar
|
|
|
143
172
|
elsif _dtend
|
|
144
173
|
_to_time_with_zone(_dtend)
|
|
145
174
|
elsif _dtstart
|
|
146
|
-
|
|
175
|
+
# Special handling for all-day events without explicit end
|
|
176
|
+
if _dtstart_is_all_day? && _dtend.nil?
|
|
177
|
+
# Stay in date space: add days to the date, not seconds to timestamp
|
|
178
|
+
start_date = _dtstart_all_day_event_as_date
|
|
179
|
+
end_date = start_date + (_duration_seconds / SEC_DAY).days
|
|
180
|
+
_date_to_time_with_zone(end_date, component_timezone)
|
|
181
|
+
else
|
|
182
|
+
_to_time_with_zone(start_time.to_i + _duration_seconds)
|
|
183
|
+
end
|
|
147
184
|
else
|
|
148
185
|
_to_time_with_zone(NULL_TIME + _duration_seconds)
|
|
149
186
|
end
|
|
150
187
|
end
|
|
151
188
|
|
|
189
|
+
# Extract the date component from dtstart, assuming it's an all-day event
|
|
190
|
+
# @return [Date]
|
|
191
|
+
# @api private
|
|
192
|
+
def _dtstart_all_day_event_as_date
|
|
193
|
+
raise ArgumentError, "dtstart is not an all-day event" unless _dtstart_is_all_day?
|
|
194
|
+
|
|
195
|
+
if _dtstart.is_a?(Icalendar::Values::Date)
|
|
196
|
+
_dtstart.to_date
|
|
197
|
+
elsif _dtstart.respond_to?(:to_date)
|
|
198
|
+
_dtstart.to_date
|
|
199
|
+
else
|
|
200
|
+
# Fallback: convert via TimeWithZone
|
|
201
|
+
time_with_zone = _to_time_with_zone(_dtstart)
|
|
202
|
+
raise ArgumentError, "Cannot convert dtstart to date" unless time_with_zone.respond_to?(:to_date)
|
|
203
|
+
time_with_zone.to_date
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
152
207
|
##
|
|
153
208
|
# Heuristic to determine whether the event is scheduled
|
|
154
|
-
# for a date without
|
|
155
|
-
#
|
|
209
|
+
# for a date without specifying the exact time of day.
|
|
210
|
+
#
|
|
211
|
+
# Note: This method always returns false for tasks (VTODOs),
|
|
212
|
+
# as the all-day concept only applies to events (VEVENTs).
|
|
213
|
+
#
|
|
214
|
+
# @return [Boolean] true if the component is an Event scheduled for an entire day,
|
|
215
|
+
# false for tasks or timed events
|
|
156
216
|
def all_day?
|
|
217
|
+
return false unless self.is_a?(Icalendar::Event)
|
|
218
|
+
|
|
157
219
|
_dtstart.is_a?(Icalendar::Values::Date) ||
|
|
158
220
|
(start_time == start_time.beginning_of_day && end_time == end_time.beginning_of_day)
|
|
159
221
|
end
|
|
@@ -164,6 +226,19 @@ module Icalendar
|
|
|
164
226
|
start_time.next_day.beginning_of_day < end_time
|
|
165
227
|
end
|
|
166
228
|
|
|
229
|
+
##
|
|
230
|
+
# Indicates whether this component represents a single point in time
|
|
231
|
+
# rather than a time range. Common for:
|
|
232
|
+
# - Open-ended events (e.g., concert start time without known end)
|
|
233
|
+
# - Tasks with only a deadline (no start time specified)
|
|
234
|
+
#
|
|
235
|
+
# @return [Boolean] true if the component has no duration
|
|
236
|
+
def single_timestamp?
|
|
237
|
+
return false if start_time.nil? || end_time.nil?
|
|
238
|
+
# Compare at second precision (ignore potential microsecond differences)
|
|
239
|
+
start_time.to_i == end_time.to_i
|
|
240
|
+
end
|
|
241
|
+
|
|
167
242
|
##
|
|
168
243
|
# Make sure, that we can always query for a _rrule_ array.
|
|
169
244
|
# @return [array] an array of _ical repeat-rules_ (or an empty array
|
|
@@ -264,9 +339,13 @@ module Icalendar
|
|
|
264
339
|
# Creates a schedule for this event
|
|
265
340
|
# @return [IceCube::Schedule]
|
|
266
341
|
def schedule # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
342
|
+
# Calculate duration in seconds
|
|
343
|
+
duration_seconds = (end_time.to_i - start_time.to_i) # Integer seconds
|
|
344
|
+
|
|
345
|
+
# Create a schedule with start_time and duration
|
|
346
|
+
# Convert to Ruby Time for IceCube compatibility
|
|
347
|
+
schedule = IceCube::Schedule.new(start_time.to_time, duration: duration_seconds)
|
|
348
|
+
|
|
270
349
|
_rrules.each do |rrule|
|
|
271
350
|
ice_cube_recurrence_rule = IceCube::Rule.from_ical(rrule)
|
|
272
351
|
schedule.add_recurrence_rule(ice_cube_recurrence_rule)
|
|
@@ -299,6 +378,9 @@ module Icalendar
|
|
|
299
378
|
#
|
|
300
379
|
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
301
380
|
def _to_time_with_zone(date_time, timezone = nil)
|
|
381
|
+
# Try to extract timezone from the date_time parameter first
|
|
382
|
+
timezone ||= _extract_explicit_timezone(date_time)
|
|
383
|
+
# Fall back to component timezone if no timezone could be extracted
|
|
302
384
|
timezone ||= component_timezone
|
|
303
385
|
|
|
304
386
|
# For Icalendar::Values::DateTime, we can extract the ical value. Which probably is already what we want.
|
|
@@ -317,6 +399,19 @@ module Icalendar
|
|
|
317
399
|
return date_time_value.in_time_zone(timezone)
|
|
318
400
|
|
|
319
401
|
elsif date_time_value.is_a?(DateTime)
|
|
402
|
+
# If DateTime has offset 0, treat it as "floating time" in the target timezone
|
|
403
|
+
# rather than converting from UTC
|
|
404
|
+
if date_time_value.offset.zero?
|
|
405
|
+
return timezone.local(
|
|
406
|
+
date_time_value.year,
|
|
407
|
+
date_time_value.month,
|
|
408
|
+
date_time_value.day,
|
|
409
|
+
date_time_value.hour,
|
|
410
|
+
date_time_value.min,
|
|
411
|
+
date_time_value.sec
|
|
412
|
+
)
|
|
413
|
+
end
|
|
414
|
+
# DateTime with explicit non-zero offset: convert to target timezone
|
|
320
415
|
return date_time_value.in_time_zone(timezone)
|
|
321
416
|
|
|
322
417
|
elsif date_time_value.is_a?(Icalendar::Values::Date)
|
|
@@ -325,6 +420,10 @@ module Icalendar
|
|
|
325
420
|
elsif date_time_value.is_a?(Date)
|
|
326
421
|
return _date_to_time_with_zone(date_time_value, timezone)
|
|
327
422
|
|
|
423
|
+
elsif date_time_value.is_a?(Time)
|
|
424
|
+
# Preserve Time's timezone by converting to UTC first, then to target timezone
|
|
425
|
+
return timezone.at(date_time_value.getutc)
|
|
426
|
+
|
|
328
427
|
elsif date_time_value.respond_to?(:to_time)
|
|
329
428
|
return timezone.at(date_time_value.to_time)
|
|
330
429
|
|
|
@@ -353,10 +452,11 @@ module Icalendar
|
|
|
353
452
|
# @return [ActiveSupport::TimeZone] the unique timezone used in this component
|
|
354
453
|
def component_timezone
|
|
355
454
|
# let's try sequentially, the first non-nil wins.
|
|
356
|
-
timezone ||=
|
|
357
|
-
timezone ||=
|
|
358
|
-
timezone ||=
|
|
455
|
+
timezone ||= _extract_explicit_timezone(_dtend)
|
|
456
|
+
timezone ||= _extract_explicit_timezone(_dtstart)
|
|
457
|
+
timezone ||= _extract_explicit_timezone(_due)
|
|
359
458
|
timezone ||= _extract_calendar_timezone
|
|
459
|
+
timezone ||= _guess_system_timezone
|
|
360
460
|
|
|
361
461
|
# as a last resort we'll use the Coordinated Universal Time (UTC).
|
|
362
462
|
timezone || ActiveSupport::TimeZone['UTC']
|
|
@@ -373,7 +473,6 @@ module Icalendar
|
|
|
373
473
|
return nil unless parent.is_a?(Icalendar::Calendar)
|
|
374
474
|
calendar_timezones = parent.timezones
|
|
375
475
|
calendar_timezones.each do |tz|
|
|
376
|
-
break unless tz.valid?(true)
|
|
377
476
|
ugly_tzid = tz.tzid
|
|
378
477
|
break unless ugly_tzid
|
|
379
478
|
tzid = Array(ugly_tzid).first.to_s.gsub(/^(["'])|(["'])$/, '')
|
|
@@ -387,14 +486,24 @@ module Icalendar
|
|
|
387
486
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
388
487
|
|
|
389
488
|
##
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
489
|
+
# Extracts an explicitly set timezone from the given object.
|
|
490
|
+
#
|
|
491
|
+
# This method only returns a timezone if it was explicitly specified through:
|
|
492
|
+
# - An iCalendar TZID parameter (e.g., tzid: 'Europe/Berlin')
|
|
493
|
+
# - An existing ActiveSupport::TimeWithZone object
|
|
494
|
+
# - A wrapped value that is already a TimeWithZone
|
|
495
|
+
#
|
|
496
|
+
# Unlike _guess_timezone_from_offset, this method does NOT guess or infer
|
|
497
|
+
# timezones from UTC offsets. It returns nil if no explicit timezone is found.
|
|
498
|
+
#
|
|
499
|
+
# @param date_time [Object] an object from which to extract the timezone.
|
|
500
|
+
# Typically, an Icalendar::Value, Time, DateTime, or ActiveSupport::TimeWithZone.
|
|
501
|
+
# @return [ActiveSupport::TimeZone, nil] the explicitly set timezone, or nil if none found.
|
|
393
502
|
# @api private
|
|
394
|
-
def
|
|
395
|
-
timezone ||= _extract_ical_time_zone(date_time)
|
|
396
|
-
timezone ||= _extract_act_sup_timezone(date_time)
|
|
397
|
-
timezone || _extract_value_time_zone(date_time)
|
|
503
|
+
def _extract_explicit_timezone(date_time)
|
|
504
|
+
timezone ||= _extract_ical_time_zone(date_time) # try with ical TZID parameter (most specific)
|
|
505
|
+
timezone ||= _extract_act_sup_timezone(date_time) # is the given value already ActiveSupport::TimeWithZone?
|
|
506
|
+
timezone || _extract_value_time_zone(date_time) # is the ical.value of type ActiveSupport::TimeWithZone?
|
|
398
507
|
end
|
|
399
508
|
|
|
400
509
|
##
|
|
@@ -419,6 +528,51 @@ module Icalendar
|
|
|
419
528
|
ical_value.value.time_zone
|
|
420
529
|
end
|
|
421
530
|
|
|
531
|
+
##
|
|
532
|
+
# Guesses the corresponding ActiveSupport timezone from a given time object's UTC offset.
|
|
533
|
+
# This method extracts the UTC offset from objects that respond to :utc_offset
|
|
534
|
+
# (such as Time, DateTime, or their wrapped values in Icalendar::Values)
|
|
535
|
+
# and matches it to an equivalent ActiveSupport::TimeZone.
|
|
536
|
+
#
|
|
537
|
+
# Note: Since multiple timezones can share the same UTC offset (e.g., Berlin,
|
|
538
|
+
# Amsterdam, Paris all use +01:00), this method returns an arbitrary timezone
|
|
539
|
+
# with the matching offset - hence "guess" rather than "extract".
|
|
540
|
+
#
|
|
541
|
+
# If the input does not respond to :utc_offset or an error occurs during processing,
|
|
542
|
+
# the method returns nil.
|
|
543
|
+
#
|
|
544
|
+
# @param date_time [Object] the object to extract the UTC offset from.
|
|
545
|
+
# Should respond to :utc_offset (e.g., Time, DateTime, Icalendar::Values::DateTime).
|
|
546
|
+
# @return [ActiveSupport::TimeZone, nil] an ActiveSupport::TimeZone matching the UTC offset,
|
|
547
|
+
# or nil if no match is found or an error occurs.
|
|
548
|
+
# @api private
|
|
549
|
+
def _guess_timezone_from_offset(date_time)
|
|
550
|
+
# Extract value from Icalendar::Values::DateTime if needed
|
|
551
|
+
value = date_time.is_a?(Icalendar::Value) && date_time.respond_to?(:value) ? date_time.value : date_time
|
|
552
|
+
|
|
553
|
+
return nil unless value.respond_to?(:utc_offset)
|
|
554
|
+
|
|
555
|
+
# Get the timezone offset from the Time or DateTime object
|
|
556
|
+
offset_seconds = value.utc_offset
|
|
557
|
+
return nil unless offset_seconds.is_a?(Integer)
|
|
558
|
+
|
|
559
|
+
# First try: check if the system's default timezone matches the offset
|
|
560
|
+
system_tz = _guess_system_timezone
|
|
561
|
+
|
|
562
|
+
# Return `system timezone` if it matches the offset
|
|
563
|
+
return system_tz if system_tz && system_tz.now.utc_offset == offset_seconds
|
|
564
|
+
|
|
565
|
+
# Fallback: find any timezone matching the offset
|
|
566
|
+
# For offset 0, always use UTC to avoid ambiguous timezones with DST
|
|
567
|
+
if offset_seconds.zero?
|
|
568
|
+
ActiveSupport::TimeZone['UTC']
|
|
569
|
+
else
|
|
570
|
+
ActiveSupport::TimeZone[offset_seconds]
|
|
571
|
+
end
|
|
572
|
+
rescue StandardError
|
|
573
|
+
nil
|
|
574
|
+
end
|
|
575
|
+
|
|
422
576
|
##
|
|
423
577
|
# Get the timezone from the given object, assuming it can be extracted from ical params.
|
|
424
578
|
# @param [Icalendar::Value] ical_value an ical value that (probably) supports a time zone identifier.
|
|
@@ -427,11 +581,72 @@ module Icalendar
|
|
|
427
581
|
def _extract_ical_time_zone(ical_value)
|
|
428
582
|
return nil unless ical_value.is_a?(Icalendar::Value)
|
|
429
583
|
return nil unless ical_value.respond_to?(:ical_params)
|
|
430
|
-
|
|
584
|
+
|
|
585
|
+
ical_params = ical_value.ical_params
|
|
586
|
+
return nil unless ical_params
|
|
587
|
+
|
|
588
|
+
ugly_tzid = ical_params['tzid'] || ical_params[:tzid] || ical_params['TZID'] || ical_params[:TZID]
|
|
431
589
|
return nil if ugly_tzid.nil?
|
|
590
|
+
|
|
432
591
|
tzid = Array(ugly_tzid).first.to_s.gsub(/^(["'])|(["'])$/, '')
|
|
592
|
+
return nil if tzid.empty?
|
|
593
|
+
|
|
433
594
|
ActiveSupport::TimeZone[tzid]
|
|
595
|
+
rescue StandardError
|
|
596
|
+
# Uncomment for debugging icalendar gem compatibility issues:
|
|
597
|
+
# warn "[icalendar-rrule] Failed to extract timezone: #{e.message}"
|
|
598
|
+
nil
|
|
434
599
|
end
|
|
600
|
+
|
|
601
|
+
##
|
|
602
|
+
# Attempts to determine the system's timezone.
|
|
603
|
+
# Tries multiple methods in order of reliability.
|
|
604
|
+
#
|
|
605
|
+
# @return [ActiveSupport::TimeZone, nil] the system timezone or nil if it cannot be determined.
|
|
606
|
+
# @api private
|
|
607
|
+
def _guess_system_timezone
|
|
608
|
+
# Method 1: Rails/ActiveSupport Time.zone (most reliable if set)
|
|
609
|
+
return Time.zone if Time.zone.is_a?(ActiveSupport::TimeZone)
|
|
610
|
+
|
|
611
|
+
# Method 2: ENV['TZ'] environment variable
|
|
612
|
+
if ENV['TZ']
|
|
613
|
+
tz = ActiveSupport::TimeZone[ENV['TZ']]
|
|
614
|
+
return tz if tz
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# Method 3: Try TZInfo if available (optional dependency)
|
|
618
|
+
begin
|
|
619
|
+
require 'tzinfo'
|
|
620
|
+
tz_identifier = TZInfo::Timezone.default_timezone.identifier
|
|
621
|
+
tz = ActiveSupport::TimeZone[tz_identifier]
|
|
622
|
+
return tz if tz
|
|
623
|
+
rescue LoadError, StandardError
|
|
624
|
+
# TZInfo not available or failed, continue
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Method 4: Read /etc/timezone on Linux (Debian/Ubuntu style)
|
|
628
|
+
if File.readable?('/etc/timezone')
|
|
629
|
+
tz_name = File.read('/etc/timezone').strip
|
|
630
|
+
tz = ActiveSupport::TimeZone[tz_name]
|
|
631
|
+
return tz if tz
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# Method 5: Parse /etc/localtime symlink (common on many Unix systems)
|
|
635
|
+
if File.symlink?('/etc/localtime')
|
|
636
|
+
link = File.readlink('/etc/localtime')
|
|
637
|
+
# Extract timezone name from path like /usr/share/zoneinfo/Europe/Berlin
|
|
638
|
+
if link =~ %r{zoneinfo/(.+)$}
|
|
639
|
+
tz = ActiveSupport::TimeZone[$1]
|
|
640
|
+
return tz if tz
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
nil
|
|
645
|
+
rescue StandardError
|
|
646
|
+
nil
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
|
|
435
650
|
end
|
|
436
651
|
end
|
|
437
652
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: icalendar-rrule
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harald Postner
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -149,6 +148,7 @@ files:
|
|
|
149
148
|
- ".rubocop.yml"
|
|
150
149
|
- ".travis.yml"
|
|
151
150
|
- ".yardopts"
|
|
151
|
+
- CHANGELOG.md
|
|
152
152
|
- Gemfile
|
|
153
153
|
- LICENSE.txt
|
|
154
154
|
- README.md
|
|
@@ -167,7 +167,6 @@ metadata:
|
|
|
167
167
|
homepage_uri: https://github.com/free-creations/icalendar-rrule
|
|
168
168
|
source_code_uri: https://github.com/free-creations/icalendar-rrule
|
|
169
169
|
bug_tracker_uri: https://github.com/free-creations/icalendar-rrule/issues
|
|
170
|
-
post_install_message:
|
|
171
170
|
rdoc_options: []
|
|
172
171
|
require_paths:
|
|
173
172
|
- lib
|
|
@@ -182,8 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
182
181
|
- !ruby/object:Gem::Version
|
|
183
182
|
version: '0'
|
|
184
183
|
requirements: []
|
|
185
|
-
rubygems_version:
|
|
186
|
-
signing_key:
|
|
184
|
+
rubygems_version: 4.0.1
|
|
187
185
|
specification_version: 4
|
|
188
186
|
summary: Helper for ICalendars with recurring events.
|
|
189
187
|
test_files: []
|