periodoxical 0.8.3 → 1.0.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: 78125f7c8e33a8588c933d7b0438daa5d0913394fe9c141e752dd73fc1968d88
4
- data.tar.gz: 60acbba0684d36991ac7bffcb21dc8a97e27bfb313d1ab9641668463e73c718c
3
+ metadata.gz: 3836bd7c49ec987409e70592d93cb8c021a009ce3587e48a6419c12121af3b1d
4
+ data.tar.gz: 510bb79570482c8019e911c8900ec15ee5016b59c1d51677d38d53f7c632523f
5
5
  SHA512:
6
- metadata.gz: df7a8cc7abe1139af195aebe455feb7c66b75e27e3a8d43e4c862ec362111756a461b7293efe3c3fdcca50eb1f70d5fdb5de04e740014a4fc33fe3f324e8d6ec
7
- data.tar.gz: '070876a087c2712b12f497054966f9cd98ac3d0e3eab1e4337e165e2ea42cef5ea4dd5b43b714393d92d14ddab2119b426128cf9d85f5de845b9e32959f72b1d'
6
+ metadata.gz: 0afbe7ebba86480f7d04766a087020734135bb0075245fbe6ba03f75590eb0c74710991b14e5fad653779ba7d6a74811d11adaeb7cfa28ec194dc9eea05adfb9
7
+ data.tar.gz: ddc843c9de7c3ae536f939f0c7bfc6c6d5ce2a37d4bde07d2941bd2ef7f7e0a41328d347fe1e9d8d1c1e1ba9bf916648424bd18795ad8642531d37529841d913
data/CODE_OF_CONDUCT.md CHANGED
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
55
55
  ## Enforcement
56
56
 
57
57
  Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at steven@fountain.com. All
58
+ reported by contacting the project team at stevenJLi@gmail.com. All
59
59
  complaints will be reviewed and investigated and will result in a response that
60
60
  is deemed necessary and appropriate to the circumstances. The project team is
61
61
  obligated to maintain confidentiality with regard to the reporter of an incident.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- periodoxical (0.8.3)
4
+ periodoxical (1.0.0)
5
5
  tzinfo (~> 2.0, >= 2.0.0)
6
6
  week_of_month (= 1.2.6)
7
7
 
data/README.md CHANGED
@@ -41,8 +41,8 @@ Periodoxical.generate(
41
41
  end_time: '10:30AM'
42
42
  },
43
43
  ],
44
- start_date: '2024-05-23',
45
- end_date: '2024-05-27',
44
+ starting_from: '2024-05-23',
45
+ ending_at: '2024-05-26',
46
46
  )
47
47
  #=>
48
48
  [
@@ -65,6 +65,30 @@ Periodoxical.generate(
65
65
  ]
66
66
  ```
67
67
 
68
+ The `starting_from` and `ending_at` params can also accept datetimes in ISO 8601 format for more precision. This example generate all the datetime blocks of **9:00AM - 10:30AM** but starting from **May 23, 2024 at 9:30AM**.
69
+
70
+ ```rb
71
+ Periodoxical.generate(
72
+ time_zone: 'America/Los_Angeles',
73
+ time_blocks: [
74
+ {
75
+ start_time: '9:00AM',
76
+ end_time: '10:30AM'
77
+ },
78
+ ],
79
+ starting_from: '2024-05-23T09:30:00-07:00', # can be string in iso8601 format
80
+ ending_at: DateTime.parse('2024-05-26T17:00:00-07:00'), # or an instance of DateTime
81
+ )
82
+ #=> [
83
+ # 2024-05-23 was skipped because the 9AM timeslot was before the `starting_from` of '2024-05-23T09:30:00-07:00'
84
+ {
85
+ start_time: #<DateTime: 2024-05-24T09:00:00-0700>,
86
+ end_time: #<DateTime: 2024-05-24T10:30:00-0700>,
87
+ },
88
+ ...
89
+ ]
90
+ ```
91
+
68
92
  ### Example 2 - specify days of the week
69
93
  As a Ruby dev, I want to generate all the datetime blocks of **9:00AM - 10:30AM** and **2:00PM - 2:30PM**, on **Mondays**, **Wednesdays**, and **Thursdays**, between the dates of **May 23, 2024** and **June 12, 2024**, inclusive. This can be represented visually as:
70
94
 
@@ -89,8 +113,8 @@ Periodoxical.generate(
89
113
  end_time: '2:30PM'
90
114
  }
91
115
  ],
92
- start_date: '2024-05-23',
93
- end_date: '2024-06-12',
116
+ starting_from: '2024-05-23',
117
+ ending_at: '2024-06-12',
94
118
  )
95
119
  # returns an array of hashes, each with :start and :end keys
96
120
  #=>
@@ -133,7 +157,7 @@ Periodoxical.generate(
133
157
  end_time: '2:30PM'
134
158
  }
135
159
  ],
136
- start_date: Date.parse('2024-05-23'), # Can also pass in `Date` object.
160
+ starting_from: Date.parse('2024-05-23'), # Can also pass in `Date` object.
137
161
  limit: 3
138
162
  )
139
163
  # =>
@@ -167,8 +191,8 @@ As a ruby dev, I want to generate all the timeblocks between **May 23, 2024** an
167
191
  ```rb
168
192
  Periodoxical.generate(
169
193
  time_zone: 'America/Los_Angeles',
170
- start_date: Date.parse('2024-05-23'), # can also pass in Date objects
171
- end_date: Date.parse('2024-06-12'), # can also pass in Date objects,
194
+ starting_from: Date.parse('2024-05-23'), # can also pass in Date objects
195
+ ending_at: Date.parse('2024-06-12'), # can also pass in Date objects,
172
196
  day_of_week_time_blocks: {
173
197
  mon: [
174
198
  { start_time: '8:00AM', end_time: '9:00AM' },
@@ -191,7 +215,7 @@ As a Ruby dev, I want to generate the next 3 timeblocks for **8AM - 9AM** for th
191
215
  ```rb
192
216
  Periodoxical.generate(
193
217
  time_zone: 'America/Los_Angeles',
194
- start_date: '2024-06-1',
218
+ starting_from: '2024-06-01',
195
219
  limit: 3,
196
220
  days_of_month: [5, 10],
197
221
  time_blocks: [
@@ -220,7 +244,7 @@ As a Ruby dev, I want to generate **4** timeblocks for **8AM - 9AM** on **Monday
220
244
  ```
221
245
  Periodoxical.generate(
222
246
  time_zone: 'America/Los_Angeles',
223
- start_date: '2024-04-1',
247
+ starting_from: '2024-04-01',
224
248
  limit: 4,
225
249
  weeks_of_month: [1 2],
226
250
  months: [4, 5, 6],
@@ -256,7 +280,7 @@ As a Ruby dev, I want to generate timeblocks for **8AM - 9AM** on the **first an
256
280
  ```rb
257
281
  Periodoxical.generate(
258
282
  time_zone: 'America/Los_Angeles',
259
- start_date: '2024-06-01',
283
+ starting_from: '2024-06-01',
260
284
  limit: 5,
261
285
  nth_day_of_week_in_month: {
262
286
  mon: [1, 2], # valid values: -1,1,2,3,4,5
@@ -291,13 +315,13 @@ Periodoxical.generate(
291
315
  ]
292
316
  ```
293
317
 
294
- ### Example 7 - Exclude time blocks using the `exclusion_dates` parameter
318
+ ### Example 7 - Exclude time blocks using the `exclusion_dates` and `exclusion_times` parameters
295
319
  As a Ruby dev, I want to generate timeblocks for **8AM - 9AM** on **Mondays**, except for the **Monday of June 10, 2024**.
296
320
 
297
321
  ```rb
298
322
  Periodoxical.generate(
299
323
  time_zone: 'America/Los_Angeles',
300
- start_date: '2024-06-3',
324
+ starting_from: '2024-06-03',
301
325
  limit: 4,
302
326
  exclusion_dates: %w(2024-06-10),
303
327
  day_of_week_time_blocks: {
@@ -328,18 +352,76 @@ Periodoxical.generate(
328
352
  ]
329
353
  ```
330
354
 
355
+ As a Ruby dev, I want to generate timeblocks for **8AM - 9AM**, and **10AM - 11AM** on **Mondays**, except for those that conflict (meaning overlap) with the time block of **10:30AM - 11:30AM** on the **Monday of June 10, 2024**. I can skip the conflicting time blocks by using the `exclusion_times` parameter.
356
+
357
+ ```rb
358
+ Periodoxical.generate(
359
+ time_zone: 'America/Los_Angeles',
360
+ starting_from: '2024-06-03',
361
+ limit: 4,
362
+ days_of_week: %(mon),
363
+ time_blocks: [
364
+ { start_time: '8:00AM', end_time: '9:00AM' },
365
+ { start_time: '10:00AM', end_time: '11:00AM' },
366
+ ],
367
+ exclusion_times: [
368
+ {
369
+ start: '2024-06-10T10:30:00-07:00',
370
+ end: '2024-06-10T11:30:00-07:00',
371
+ }
372
+ ],
373
+ )
374
+ # =>
375
+ [
376
+ {
377
+ start: #<DateTime 2024-06-03T08:00:00-0700>,
378
+ end: #<DateTime 2024-06-03T09:00:00-0700>,
379
+ },
380
+ {
381
+ start: #<DateTime 2024-06-03T10:00:00-0700>,
382
+ end: #<DateTime 2024-06-03T11:00:00-0700>,
383
+ },
384
+ {
385
+ start: #<DateTime 2024-06-10T08:00:00-0700>,
386
+ end: #<DateTime 2024-06-10T09:00:00-0700>,
387
+ },
388
+ # The June 10 10AM - 11AM was skipped because it overlapped with the June 10 10:30AM - 11:30AM exclusion time.
389
+ {
390
+ start: #<DateTime 2024-06-17T08:00:00-0700>,
391
+ end: #<DateTime 2024-06-17T09:00:00-0700>,
392
+ },
393
+ {
394
+ start: #<DateTime 2024-06-17T10:00:00-0700>,
395
+ end: #<DateTime 2024-06-17T11:00:00-0700>,
396
+ },
397
+ {
398
+ start: #<DateTime 2024-06-24T08:00:00-0700>,
399
+ end: #<DateTime 2024-06-24T09:00:00-0700>,
400
+ },
401
+ ]
402
+ ```
403
+
331
404
  ### Example 8 - Every-other-nth day-of-week rules (ie. every other Tuesday, every 3rd Wednesday, every 10th Friday)
332
405
 
333
406
  As a Ruby dev, I want to generate timeblocks for **9AM- 10AM** on **every Monday**, but **every other Tuesday**, and **every other 3rd Wednesday**. I can do this using the `days_of_week` parameter, but also using the `every` and `every_other_nth` keys to specify the every-other-nth-rules.
334
407
 
408
+ This can be visualized as:
409
+
410
+ <div align="center">
411
+ <img width="600" alt="alt_google_cal_image" src="https://github.com/StevenJL/periodoxical/assets/2191808/d663da17-a94a-4715-886a-8223b129dd60">
412
+ <p><i>(image courtesy of calendar.google.com)</i></p>
413
+ </div>
414
+
415
+ <br>
416
+
335
417
  ```rb
336
418
  Periodoxical.generate(
337
419
  time_zone: 'America/Los_Angeles',
338
- start_date: '2024-12-30',
420
+ starting_from: '2024-12-30',
339
421
  days_of_week: {
340
422
  mon: { every: true }, # every Monday (no skipping)
341
- tue: { every_other_nth: 2 }, # every other Tuesday starting at first Tuesday from start date
342
- wed: { every_other_nth: 3 }, # every 3rd Wednesday starting at first Wednesday from start date
423
+ tue: { every_other_nth: 2 }, # every other Tuesday starting at first Tuesday from `starting_from` date
424
+ wed: { every_other_nth: 3 }, # every 3rd Wednesday starting at first Wednesday from `starting_from` date
343
425
  },
344
426
  limit: 10,
345
427
  time_blocks: [
@@ -398,7 +480,7 @@ Generate all the Friday the 13ths ever since May 1980 (when the first Friday the
398
480
  ```rb
399
481
  Periodoxical.generate(
400
482
  time_zone: 'America/Los_Angeles',
401
- start_date: '1980-05-01',
483
+ starting_from: '1980-05-01',
402
484
  days_of_week: %w(fri),
403
485
  days_of_month: [13],
404
486
  limit: 100,
@@ -433,7 +515,7 @@ Generate the next 10 Thanksgivings from now on (Thanksgivings is defined as the
433
515
  ```rb
434
516
  Periodoxical.generate(
435
517
  time_zone: 'America/Los_Angeles',
436
- start_date: '2024-05-01',
518
+ starting_from: '2024-05-01',
437
519
  months: [11],
438
520
  nth_day_of_week_in_month: {
439
521
  thu: [4],
@@ -464,7 +546,8 @@ Periodoxical.generate(
464
546
  {
465
547
  start: #<DateTime: 2028-11-23T17:00:00-0800>,
466
548
  end: #<DateTime: 2028-11-23T18:00:00-0800>,
467
- }
549
+ },
550
+ ...
468
551
  ]
469
552
  ```
470
553
 
@@ -96,8 +96,16 @@ module Periodoxical
96
96
  end
97
97
  end
98
98
 
99
- unless( @limit || @end_date)
100
- raise "Either `limit` or `end_date` must be provided"
99
+ unless( @limit || @ending_at)
100
+ raise "Either `limit` or `ending_at` must be provided"
101
+ end
102
+
103
+ if @exclusion_times
104
+ @exclusion_times.each do |tb|
105
+ unless tb[:start] < tb[:end]
106
+ raise "Exclusion times must have `start` before `end`. #{tb[:start]} not before #{tb[:end]}"
107
+ end
108
+ end
101
109
  end
102
110
  end
103
111
  end
@@ -1,3 +1,3 @@
1
1
  module Periodoxical
2
- VERSION = "0.8.3"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/periodoxical.rb CHANGED
@@ -17,8 +17,8 @@ module Periodoxical
17
17
  # @param [String] time_zone
18
18
  # Ex: 'America/Los_Angeles', 'America/Chicago',
19
19
  # TZInfo::DataTimezone#name from the tzinfo gem (https://github.com/tzinfo/tzinfo)
20
- # @param [Date, String] start_date
21
- # @param [Date, String] end_date
20
+ # @param [Date, String] starting_from
21
+ # @param [Date, String] ending_at
22
22
  # @param [Array<Hash>] time_blocks
23
23
  # Ex: [
24
24
  # {
@@ -40,10 +40,23 @@ module Periodoxical
40
40
  # @param [Array<Integer>, nil] months
41
41
  # Months as integers, where 1 = Jan, 12 = Dec
42
42
  # @param [Integer] limit
43
- # How many date times to generate. To be used when `end_date` is nil.
43
+ # How many date times to generate. To be used when `ending_at` is nil.
44
44
  # @param [Aray<String>] exclusion_dates
45
45
  # Dates to be excluded when generating the time blocks
46
46
  # Ex: ['2024-06-10', '2024-06-14']
47
+ # @param [Aray<Hash>] exclusion_times
48
+ # Timeblocks to be excluded when generating the time blocks if there is conflict (ie. overlap)
49
+ # Ex: [
50
+ # {
51
+ # start: '2024-06-10T10:30:00-07:00',
52
+ # end: '2024-06-10T11:30:00-07:00'
53
+ # },
54
+ # {
55
+ # start: '2024-06-10T14:30:00-07:00',
56
+ # end: '2024-06-10T15:30:00-07:00'
57
+ # },
58
+ # ]
59
+ #
47
60
  # @param [Hash<Array<Hash>>] day_of_week_time_blocks
48
61
  # To be used when hours are different between days of the week
49
62
  # Ex: {
@@ -52,12 +65,13 @@ module Periodoxical
52
65
  # fri: { start_time: '7:00PM', end_time: '9:00PM' },
53
66
  # }
54
67
  def initialize(
55
- start_date:,
56
- end_date: nil,
68
+ starting_from:,
69
+ ending_at: nil,
57
70
  time_blocks: nil,
58
71
  day_of_week_time_blocks: nil,
59
72
  limit: nil,
60
73
  exclusion_dates: nil,
74
+ exclusion_times: nil,
61
75
  time_zone: 'Etc/UTC',
62
76
  days_of_week: nil,
63
77
  nth_day_of_week_in_month: nil,
@@ -78,12 +92,17 @@ module Periodoxical
78
92
  @months = months
79
93
  @time_blocks = time_blocks
80
94
  @day_of_week_time_blocks = day_of_week_time_blocks
81
- @start_date = start_date.is_a?(String) ? Date.parse(start_date) : start_date
82
- @end_date = end_date.is_a?(String) ? Date.parse(end_date) : end_date
95
+ @starting_from = date_object_from(starting_from)
96
+ @ending_at = date_object_from(ending_at)
83
97
  @limit = limit
84
98
  @exclusion_dates = if exclusion_dates && !exclusion_dates.empty?
85
99
  exclusion_dates.map { |ed| Date.parse(ed) }
86
100
  end
101
+ @exclusion_times = if exclusion_times
102
+ exclusion_times.map do |et|
103
+ { start: DateTime.parse(et[:start]), end: DateTime.parse(et[:end]) }
104
+ end
105
+ end
87
106
  validate!
88
107
  end
89
108
 
@@ -155,7 +174,11 @@ module Periodoxical
155
174
  # Variables which manage flow of looping through time and generating slots
156
175
  def initialize_looping_variables!
157
176
  @output = []
158
- @current_date = @start_date
177
+ if @starting_from.is_a?(DateTime)
178
+ @current_date = @starting_from.to_date
179
+ else
180
+ @current_date = @starting_from
181
+ end
159
182
  @current_day_of_week = day_of_week_long_to_short(@current_date.strftime("%A"))
160
183
  @current_count = 0
161
184
  @keep_generating = true
@@ -184,6 +207,10 @@ module Periodoxical
184
207
  # }
185
208
  # Generates time block but also checks if we should stop generating
186
209
  def append_to_output_and_check_limit(time_block)
210
+ # Check if this particular time is conflicts with any times from `exclusion_times`.
211
+ return if overlaps_with_an_excluded_time?(time_block)
212
+ return if before_starting_from_or_after_ending_at?(time_block)
213
+
187
214
  @output << {
188
215
  start: time_str_to_object(@current_date, time_block[:start_time]),
189
216
  end: time_str_to_object(@current_date, time_block[:end_time])
@@ -202,7 +229,7 @@ module Periodoxical
202
229
 
203
230
  @current_day_of_week = day_of_week_long_to_short(@current_date.strftime("%A"))
204
231
 
205
- if @end_date && (@current_date > @end_date)
232
+ if @ending_at && (@current_date > @ending_at)
206
233
  @keep_generating = false
207
234
  end
208
235
 
@@ -210,7 +237,7 @@ module Periodoxical
210
237
  # there is bug, or poorly specified rules. If @current_date goes into
211
238
  # 1000 years in the future, but still no dates have been generated yet, this is
212
239
  # most likely an infinite loop situation, and needs to be killed.
213
- if @limit && ((@current_date - @start_date).to_i > 365000) && @output.empty?
240
+ if @limit && ((@current_date - @starting_from).to_i > 365000) && @output.empty?
214
241
  raise "No end condition detected, causing infinite loop. Please check rules/conditions or raise github issue for potential bug fixed"
215
242
  end
216
243
  end
@@ -302,8 +329,8 @@ module Periodoxical
302
329
  # end_time: '10:30AM'
303
330
  # },
304
331
  # ],
305
- # start_date: '2024-05-23',
306
- # end_date: '2024-05-27',
332
+ # starting_from: '2024-05-23',
333
+ # ending_at: '2024-05-27',
307
334
  # )
308
335
  # where if we don't specify any date-of-week/month constraints, we return all consecutive dates.
309
336
  # In the future, if we don't support this case, we can use `false` as the return value.
@@ -350,5 +377,86 @@ module Periodoxical
350
377
  def update_days_of_week_running_tally!
351
378
  @days_of_week_running_tally[@current_day_of_week.to_sym] = @days_of_week_running_tally[@current_day_of_week.to_sym] + 1
352
379
  end
380
+
381
+ # @return [Boolean]
382
+ # Used only when `starting_from` and `ending_at` are instances of DateTime
383
+ # instead of Date, requiring more precision, calculation.
384
+ def before_starting_from_or_after_ending_at?(time_block)
385
+ return false unless @starting_from.is_a?(DateTime) || @ending_at.is_a?(DateTime)
386
+
387
+ if @starting_from.is_a?(DateTime)
388
+ start_time = time_str_to_object(@current_date, time_block[:start_time])
389
+
390
+ # If the candidate time block is starting earlier than @starting_from, we want to skip it
391
+ return true if start_time < @starting_from
392
+ end
393
+
394
+ if @ending_at.is_a?(DateTime)
395
+ end_time = time_str_to_object(@current_date, time_block[:end_time])
396
+
397
+ # If the candidate time block is ending after @ending_at, we want to skip it
398
+ return true if end_time > @ending_at
399
+ end
400
+
401
+ false
402
+ end
403
+
404
+ # @return [Boolean]
405
+ # Whether or not the given `time_block` in the @current_date and
406
+ # @time_zone overlaps with the times in `exclusion_times`.
407
+ def overlaps_with_an_excluded_time?(time_block)
408
+ return false unless @exclusion_times
409
+
410
+ @exclusion_times.each do |exclusion_timeblock|
411
+ return true if overlap?(
412
+ exclusion_timeblock,
413
+ {
414
+ start: time_str_to_object(@current_date, time_block[:start_time]),
415
+ end: time_str_to_object(@current_date, time_block[:end_time]),
416
+ }
417
+ )
418
+ end
419
+
420
+ false
421
+ end
422
+
423
+ # @param [Hash] time_block_1, time_block_2
424
+ # Ex: {
425
+ # start: #<DateTime>,
426
+ # end: #<DateTime>,
427
+ # }
428
+ def overlap?(time_block_1, time_block_2)
429
+ tb_1_start = time_block_1[:start]
430
+ tb_1_end = time_block_1[:end]
431
+ tb_2_start = time_block_2[:start]
432
+ tb_2_end = time_block_2[:end]
433
+
434
+ # Basicall overlap is when one starts before the other has ended
435
+ return true if tb_1_end > tb_2_start && tb_1_end < tb_2_end
436
+ # By symmetry
437
+ return true if tb_2_end > tb_1_start && tb_2_end < tb_1_end
438
+
439
+ false
440
+ end
441
+
442
+ def date_object_from(dt)
443
+ return unless dt
444
+ return dt if dt.is_a?(Date) || dt.is_a?(DateTime)
445
+
446
+ if dt.is_a?(String)
447
+ return Date.parse(dt) if /\A\d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[12]\d|3[01])\z/ =~ dt
448
+
449
+ if /\A\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?(Z|[+-][01]\d:[0-5]\d)?\z/ =~ dt
450
+ # convert to DateTime object
451
+ dt = DateTime.parse(dt)
452
+ # convert to given time_zone
453
+ return dt.to_time.localtime(@time_zone.utc_offset).to_datetime
454
+ end
455
+
456
+ raise "Could not parse date/datetime string #{dt}. Please README for examples."
457
+ else
458
+ raise "Invalid argument: #{dt}"
459
+ end
460
+ end
353
461
  end
354
462
  end
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: 0.8.3
4
+ version: 1.0.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: 2024-06-03 00:00:00.000000000 Z
11
+ date: 2024-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tzinfo