periodoxical 2.2.1 → 2.3.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/Gemfile.lock +1 -1
- data/README.md +33 -0
- data/lib/periodoxical/version.rb +1 -1
- data/lib/periodoxical.rb +72 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 169a25ff421b59755693ae84a7b909dde7346c3c3d05a7ebef0e363227ba247d
|
4
|
+
data.tar.gz: a4ff2b417b4be13c3c98ee4c9843aab54d7f346447a486084dcba32e20ea388a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd1a0ed4ba0f34ccf10fede5d036bddf317e801abdd8734931c3f2cdad707d7057ef6eee25410bf8e3af8c46c1299e982fa029bbc10a48acbc6e5360ab8ead68
|
7
|
+
data.tar.gz: 0d6c4234da26fe9c67653b32c69978e9703390bd10d6571703c6f56b5db2f5b2684f3ebd9543a37b94716d18c78e24584616f89ffd2b019c244a99bd0a917326
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -562,6 +562,39 @@ Periodoxical.generate(
|
|
562
562
|
]
|
563
563
|
```
|
564
564
|
|
565
|
+
### Daylight Saving Time (DST) handling
|
566
|
+
|
567
|
+
Periodoxical supports explicit strategies for DST edge cases when converting local times to UTC:
|
568
|
+
|
569
|
+
- ambiguous_time: how to resolve fall-back ambiguous local times (e.g., 01:30 occurs twice)
|
570
|
+
- :first → choose the first occurrence (usually daylight time)
|
571
|
+
- :last → choose the second occurrence (standard time)
|
572
|
+
- :raise → raise TZInfo::AmbiguousTime (default)
|
573
|
+
- gap_strategy: how to handle spring-forward missing local times (e.g., 02:30 does not exist)
|
574
|
+
- :advance → move forward by gap_shift_minutes to the next valid local time
|
575
|
+
- :skip → drop the invalid block
|
576
|
+
- :raise → raise TZInfo::PeriodNotFound (default)
|
577
|
+
- gap_shift_minutes: integer minutes to advance when gap_strategy is :advance (default: 60)
|
578
|
+
|
579
|
+
Example: be resilient around DST changes
|
580
|
+
|
581
|
+
```rb
|
582
|
+
Periodoxical.generate(
|
583
|
+
time_zone: 'America/Los_Angeles',
|
584
|
+
ambiguous_time: :last, # pick standard time on fall back
|
585
|
+
gap_strategy: :advance, # move to next valid time on spring forward
|
586
|
+
gap_shift_minutes: 60,
|
587
|
+
starting_from: '2025-03-01',
|
588
|
+
ending_at: '2025-11-30',
|
589
|
+
time_blocks: [ { start_time: '1:30AM', end_time: '2:30AM' } ]
|
590
|
+
)
|
591
|
+
```
|
592
|
+
|
593
|
+
Notes:
|
594
|
+
- Period offsets are applied based on the period at that instant, not the current period.
|
595
|
+
- If you choose `gap_strategy: :skip`, blocks that convert to invalid local times will be omitted.
|
596
|
+
|
597
|
+
|
565
598
|
## Development
|
566
599
|
|
567
600
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/periodoxical/version.rb
CHANGED
data/lib/periodoxical.rb
CHANGED
@@ -71,6 +71,14 @@ module Periodoxical
|
|
71
71
|
# - 9:00AM - 9:20AM
|
72
72
|
# - 9:20AM - 9:40AM
|
73
73
|
# - 9:40AM - 10:00AM
|
74
|
+
# @param [Symbol] ambiguous_time
|
75
|
+
# How to resolve DST fall-back ambiguous local times (e.g. 01:30 occurs twice).
|
76
|
+
# Allowed: :first (daylight time), :last (standard time), :raise (default).
|
77
|
+
# @param [Symbol] gap_strategy
|
78
|
+
# How to handle DST spring-forward missing local times (e.g. 02:30 does not exist).
|
79
|
+
# Allowed: :advance (shift forward), :skip (omit block), :raise (default).
|
80
|
+
# @param [Integer] gap_shift_minutes
|
81
|
+
# Minutes to advance when gap_strategy is :advance. Default: 60.
|
74
82
|
def initialize(
|
75
83
|
starting_from:,
|
76
84
|
ending_at: nil,
|
@@ -84,10 +92,16 @@ module Periodoxical
|
|
84
92
|
nth_day_of_week_in_month: nil,
|
85
93
|
days_of_month: nil,
|
86
94
|
duration: nil,
|
87
|
-
months: nil
|
95
|
+
months: nil,
|
96
|
+
ambiguous_time: :raise,
|
97
|
+
gap_strategy: :raise,
|
98
|
+
gap_shift_minutes: 60
|
88
99
|
)
|
89
100
|
|
90
101
|
@time_zone = TZInfo::Timezone.get(time_zone)
|
102
|
+
@ambiguous_time = ambiguous_time
|
103
|
+
@gap_strategy = gap_strategy
|
104
|
+
@gap_shift_minutes = gap_shift_minutes
|
91
105
|
if days_of_week.is_a?(Array)
|
92
106
|
@days_of_week = deep_symbolize_keys(days_of_week)
|
93
107
|
elsif days_of_week.is_a?(Hash)
|
@@ -144,9 +158,16 @@ module Periodoxical
|
|
144
158
|
|
145
159
|
private
|
146
160
|
|
161
|
+
# @param [Date] date
|
147
162
|
# @param [String] time_str
|
148
163
|
# Ex: '9:00AM'
|
149
|
-
#
|
164
|
+
#
|
165
|
+
# @return [DateTime]
|
166
|
+
# Converts a local date and time string to UTC and returns a DateTime with the
|
167
|
+
# timezone offset corresponding to the zone period at that instant.
|
168
|
+
# Handles DST transitions explicitly:
|
169
|
+
# - Ambiguous (fall-back) times resolved via `@ambiguous_time`.
|
170
|
+
# - Missing (spring-forward) times handled via `@gap_strategy` and `@gap_shift_minutes`.
|
150
171
|
def time_str_to_object(date, time_str)
|
151
172
|
time = Time.strptime(time_str, '%I:%M%p')
|
152
173
|
date_time = DateTime.new(
|
@@ -157,7 +178,51 @@ module Periodoxical
|
|
157
178
|
time.min,
|
158
179
|
time.sec,
|
159
180
|
)
|
160
|
-
|
181
|
+
utc_time = begin
|
182
|
+
@time_zone.local_to_utc(date_time) do |periods|
|
183
|
+
case @ambiguous_time
|
184
|
+
when :first
|
185
|
+
periods.first
|
186
|
+
when :last
|
187
|
+
periods.last
|
188
|
+
when :raise
|
189
|
+
raise TZInfo::AmbiguousTime, date_time.to_s
|
190
|
+
else
|
191
|
+
periods.last
|
192
|
+
end
|
193
|
+
end
|
194
|
+
rescue TZInfo::AmbiguousTime
|
195
|
+
case @ambiguous_time
|
196
|
+
when :first
|
197
|
+
@time_zone.local_to_utc(date_time) { |periods| periods.first }
|
198
|
+
when :last
|
199
|
+
@time_zone.local_to_utc(date_time) { |periods| periods.last }
|
200
|
+
else
|
201
|
+
raise
|
202
|
+
end
|
203
|
+
rescue TZInfo::PeriodNotFound
|
204
|
+
case @gap_strategy
|
205
|
+
when :advance
|
206
|
+
step = Rational(@gap_shift_minutes, 24 * 60)
|
207
|
+
shifted = date_time
|
208
|
+
attempts = 0
|
209
|
+
begin
|
210
|
+
shifted += step
|
211
|
+
attempts += 1
|
212
|
+
raise TZInfo::PeriodNotFound if attempts > 240
|
213
|
+
@time_zone.local_to_utc(shifted)
|
214
|
+
rescue TZInfo::PeriodNotFound
|
215
|
+
retry
|
216
|
+
end
|
217
|
+
when :skip
|
218
|
+
return nil
|
219
|
+
else
|
220
|
+
raise
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
period_at_utc = @time_zone.period_for_utc(utc_time)
|
225
|
+
utc_time.new_offset(period_at_utc.offset.utc_total_offset)
|
161
226
|
end
|
162
227
|
|
163
228
|
# @param [Date] date
|
@@ -212,6 +277,8 @@ module Periodoxical
|
|
212
277
|
strtm = time_str_to_object(@current_date, time_block[:start_time])
|
213
278
|
endtm = time_str_to_object(@current_date, time_block[:end_time])
|
214
279
|
|
280
|
+
return if strtm.nil? || endtm.nil?
|
281
|
+
|
215
282
|
if @duration
|
216
283
|
split_by_duration_and_append(strtm, endtm)
|
217
284
|
else
|
@@ -395,6 +462,7 @@ module Periodoxical
|
|
395
462
|
|
396
463
|
if @starting_from.is_a?(DateTime)
|
397
464
|
start_time = time_str_to_object(@current_date, time_block[:start_time])
|
465
|
+
return false if start_time.nil?
|
398
466
|
|
399
467
|
# If the candidate time block is starting earlier than @starting_from, we want to skip it
|
400
468
|
return true if start_time < @starting_from
|
@@ -402,6 +470,7 @@ module Periodoxical
|
|
402
470
|
|
403
471
|
if @ending_at.is_a?(DateTime)
|
404
472
|
end_time = time_str_to_object(@current_date, time_block[:end_time])
|
473
|
+
return false if end_time.nil?
|
405
474
|
|
406
475
|
# If the candidate time block is ending after @ending_at, we want to skip it
|
407
476
|
return true if end_time > @ending_at
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: periodoxical
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steven Li
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tzinfo
|