periodoxical 0.5.2 → 0.7.2

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: fa1a430d11ca7fdd0cea524011b7164f926f87bad37a49c4848bdbca934bdc79
4
- data.tar.gz: fea338a55f3b095b4ac6c1ff57f235e8490a9b6683ae48b9a573b0b1afba22f0
3
+ metadata.gz: 01bdfec5ba5930f743cc1c4197e0964631c8dde5be1bb14c79082fe363bf72e1
4
+ data.tar.gz: 6aaaf40f368acbdd78632455889298e6eb6ced8c0ee80f4ed9e96387e7d05536
5
5
  SHA512:
6
- metadata.gz: 74ec882770d5567ad292ad26c020777d9075cbb0bf77259f37fa0713ab239772dea1dd52fadb2d415ceb8820a10cbc2572ee4847ac181ae208d81d6d759b0517
7
- data.tar.gz: e2a7c7b00eec3522ce4d2bd15a999a7f86de8826245c464f0e0844b3f7cd42d7eb942d80e7cad2ecbc48d66399f4962fd9f7be7d8a9d4a025e51d79b477112e2
6
+ metadata.gz: dc697912db1f3a5439ffd3cdf3371c4be7f4475bb2bbae1f01c2ac42887d6041c45763e1977eabf8f68e5f6ee7c7c0d728e80153796acd419d9741a7a3126d5f
7
+ data.tar.gz: 7a0c713a31ccca86c48b08bdcf1908056475a589c7adc0b93ed94dd13af0ebd35fe58151340fe2e88ff3da13d653673064c9c1d3500e9dbe61845528ecdbb207
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- periodoxical (0.5.2)
4
+ periodoxical (0.6.2)
5
5
  tzinfo (~> 2.0, >= 2.0.0)
6
+ week_of_month (= 1.2.6)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -40,6 +41,7 @@ GEM
40
41
  rspec-support (3.13.1)
41
42
  tzinfo (2.0.6)
42
43
  concurrent-ruby (~> 1.0)
44
+ week_of_month (1.2.6)
43
45
 
44
46
  PLATFORMS
45
47
  arm64-darwin-22
data/README.md CHANGED
@@ -29,8 +29,44 @@ Or install it yourself as:
29
29
 
30
30
  ## Usage
31
31
 
32
- #### Example 1
33
- As a Ruby dev, I want to generate all the datetimes 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:
32
+ ### Example 1
33
+ As a Ruby dev, I want to generate all the datetime blocks of **9:00AM - 10:30AM** for all days from **May 23, 2024** to **May 26, 2024** inclusive.
34
+
35
+ ```rb
36
+ Periodoxical.generate(
37
+ time_zone: 'America/Los_Angeles',
38
+ time_blocks: [
39
+ {
40
+ start_time: '9:00AM',
41
+ end_time: '10:30AM'
42
+ },
43
+ ],
44
+ start_date: '2024-05-23',
45
+ end_date: '2024-05-27',
46
+ )
47
+ #=>
48
+ [
49
+ {
50
+ start_time: #<DateTime: 2024-05-23T09:00:00-0700>,
51
+ end_time: #<DateTime: 2024-05-23T10:30:00-0700>,
52
+ },
53
+ {
54
+ start_time: #<DateTime: 2024-05-24T09:00:00-0700>,
55
+ end_time: #<DateTime: 2024-05-24T10:30:00-0700>,
56
+ },
57
+ {
58
+ start_time: #<DateTime: 2024-05-25T09:00:00-0700>,
59
+ end_time: #<DateTime: 2024-05-25T10:30:00-0700>,
60
+ },
61
+ {
62
+ start_time: #<DateTime: 2024-05-26T09:00:00-0700>,
63
+ end_time: #<DateTime: 2024-05-26T10:30:00-0700>,
64
+ }
65
+ ]
66
+ ```
67
+
68
+ ### Example 2 - specify days of the week
69
+ 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:
34
70
 
35
71
  <div align="center">
36
72
  <img width="558" alt="calendar_image_1" src="https://github.com/StevenJL/periodoxical/assets/2191808/e92fc6ff-03fd-44ed-a955-d3a0dd0f5d0a">
@@ -79,9 +115,9 @@ Periodoxical.generate(
79
115
  ]
80
116
  ```
81
117
 
82
- #### Example 2 - using the `limit` key.
118
+ ### Example 3 - using the `limit` key.
83
119
 
84
- As a ruby dev, I want to generate the next 3 datetime blocks of **9:00AM - 10:30AM** and **2:00PM - 2:30PM** on **Sundays**, after **May 23, 2024** using the `limit` key.
120
+ As a ruby dev, I want to generate the next **3** datetime blocks of **9:00AM - 10:30AM** and **2:00PM - 2:30PM** on **Sundays**, after **May 23, 2024** using the `limit` key.
85
121
 
86
122
  ```rb
87
123
  Periodoxical.generate(
@@ -117,7 +153,7 @@ Periodoxical.generate(
117
153
  ]
118
154
  ```
119
155
 
120
- #### Example 3 - when time blocks vary between days
156
+ ### Example 4 - when time blocks vary between days
121
157
 
122
158
  As a ruby dev, I want to generate all the timeblocks between **May 23, 2024** and **June 12, 2024** where the time should be **8AM-9AM** on **Mondays**, but **10:45AM-12:00PM** and **2:00PM-4:00PM** on **Wednesdays**, and **2:30PM-4:15PM** on **Thursdays**.
123
159
 
@@ -148,8 +184,115 @@ Periodoxical.generate(
148
184
  )
149
185
  ```
150
186
 
151
- #### Example 4 - Exclude time blocks using the `exclusion_dates` parameter
152
- As a Ruby dev, I want to generate slots for 8AM - 9AM on Mondays, except for the Monday of June 10, 2024.
187
+ ### Example 5 - when specifying time blocks using day-of-month and/or week-of-month and/or month.
188
+
189
+ As a Ruby dev, I want to generate the next 3 slots for **8AM - 9AM** for the **5th** and **10th** day of every month starting from **June**
190
+
191
+ ```rb
192
+ Periodoxical.generate(
193
+ time_zone: 'America/Los_Angeles',
194
+ start_date: '2024-06-1',
195
+ limit: 3,
196
+ days_of_month: [5, 10],
197
+ time_blocks: [
198
+ { start_time: '8:00AM', end_time: '9:00AM' },
199
+ ],
200
+ )
201
+ #=>
202
+ [
203
+ {
204
+ start: #<DateTime: 2024-06-05T08:00:00-0700>,
205
+ end: #<DateTime: 2024-06-05T09:00:00-0700>,
206
+ },
207
+ {
208
+ start: #<DateTime: 2024-06-10T08:00:00-0700>,
209
+ end: #<DateTime: 2024-06-10T09:00:00-0700>,
210
+ },
211
+ {
212
+ start: #<DateTime: 2024-07-05T08:00:00-0700>,
213
+ end: #<DateTime: 2024-07-05T09:00:00-0700>,
214
+ },
215
+ ]
216
+ ```
217
+
218
+ As a Ruby dev, I want to generate **4** slots for **8AM - 9AM** on **Mondays** but only in the **first two weeks** in the months of **April, May, and June**
219
+
220
+ ```
221
+ Periodoxical.generate(
222
+ time_zone: 'America/Los_Angeles',
223
+ start_date: '2024-04-1',
224
+ limit: 4,
225
+ weeks_of_month: [1 2],
226
+ months: [4, 5, 6],
227
+ days_of_week: %w(mon),
228
+ time_blocks: [
229
+ { start_time: '8:00AM', end_time: '9:00AM' },
230
+ ],
231
+ )
232
+ #=>
233
+ [
234
+ {
235
+ start_time: #<DateTime: 2024-04-01T08:00:00-0700>,
236
+ end_time: #<DateTime: 2024-04-01T09:00:00-0700>,
237
+ },
238
+ {
239
+ start_time: #<DateTime: 2024-04-08T08:00:00-0700>,
240
+ end_time: #<DateTime: 2024-04-08T09:00:00-0700>,
241
+ },
242
+ {
243
+ start_time: #<DateTime: 2024-05-06T08:00:00-0700>,
244
+ end_time: #<DateTime: 2024-05-06T09:00:00-0700>,
245
+ },
246
+ {
247
+ start_time: #<DateTime: 2024-06-03T08:00:00-0700>,
248
+ end_time: #<DateTime: 2024-06-03T09:00:00-0700>,
249
+ },
250
+ ]
251
+ ```
252
+
253
+ ### Example 6 - Specify nth day-of-week in month (ie. first Monday of the Month, second Tuesday of the Month, last Friday of Month)
254
+ As a Ruby dev, I want to generate slots for **8AM - 9AM** on the **first and second Mondays** and **last Fridays** of every month starting in June 2024. I can do this with the `nth_day_of_week_in_month` param.
255
+
256
+ ```rb
257
+ Periodoxical.generate(
258
+ time_zone: 'America/Los_Angeles',
259
+ start_date: '2024-06-01',
260
+ limit: 5,
261
+ nth_day_of_week_in_month: {
262
+ mon: [1, 2], # valid values: -1,1,2,3,4,5
263
+ fri: [-1], # Use -1 to specify "last" of the month.
264
+ },
265
+ time_blocks: [
266
+ { start_time: '8:00AM', end_time: '9:00AM' },
267
+ ],
268
+ )
269
+ # =>
270
+ [
271
+ {
272
+ start: #<DateTime: 2024-06-03T08:00:00-0700>, # First Monday of June 2024
273
+ end: #<DateTime: 2024-06-03T09:00:00-0700>,
274
+ },
275
+ {
276
+ start: #<DateTime: 2024-06-10T08:00:00-0700>, # second Monday of June 2024
277
+ end: #<DateTime: 2024-06-10T09:00:00-0700>,
278
+ },
279
+ {
280
+ start: #<DateTime: 2024-06-28 08:00:00 -0700>, # last Friday of June 2024
281
+ end: #<DateTime: 2024-06-28 09:00:00 -0700>,
282
+ },
283
+ {
284
+ start: #<DateTime: 2024-07-01 08:00:00 -0700>, # First Monday of July 2024
285
+ end: #<DateTime: 2024-07-01 09:00:00 -0700>,
286
+ },
287
+ {
288
+ start: #<DateTime: 2024-07-08 08:00:00 -0700>, # Second Monday of July 2024
289
+ end: #<DateTime: 2024-07-08 09:00:00 -0700>,
290
+ },
291
+ ]
292
+ ```
293
+
294
+ ### Example 7 - Exclude time blocks using the `exclusion_dates` parameter
295
+ As a Ruby dev, I want to generate slots for **8AM - 9AM** on **Mondays**, except for the **Monday of June 10, 2024**.
153
296
 
154
297
  ```rb
155
298
  Periodoxical.generate(
@@ -163,24 +306,101 @@ Periodoxical.generate(
163
306
  ],
164
307
  }
165
308
  )
166
- # Returns all Monday 8AM - 9AM blocks except for the june on June 10, 2024
309
+ # Returns all Monday 8AM - 9AM blocks except for the Monday on June 10, 2024
167
310
  # =>
168
311
  [
169
312
  {
170
- start: <#DateTime: 2024-06-03T08:00:00-0700>,
171
- end: <#DateTime: 2024-06-03T09:00:00-0700>,
313
+ start: #<DateTime: 2024-06-03T08:00:00-0700>,
314
+ end: #<DateTime: 2024-06-03T09:00:00-0700>,
315
+ }
316
+ {
317
+ start: #<DateTime: 2024-06-17T08:00:00-0700>,
318
+ end: #<DateTime: 2024-06-17T09:00:00-0700>,
172
319
  }
173
320
  {
174
- start: <#DateTime: 2024-06-17T08:00:00-0700>,
175
- end: <#DateTime: 2024-06-17T09:00:00-0700>,
321
+ start: #<DateTime: 2024-06-24T08:00:00-0700>,
322
+ end: #<DateTime: 2024-06-24T09:00:00-0700>,
176
323
  }
177
324
  {
178
- start: <#DateTime: 2024-06-24T08:00:00-0700>,
179
- end: <#DateTime: 2024-06-24T09:00:00-0700>,
325
+ start: #<DateTime: 2024-07-01T08:00:00-0700>,
326
+ end: #<DateTime: 2024-07-01T09:00:00-0700>,
180
327
  }
328
+ ]
329
+ ```
330
+
331
+ ### Having Some Fun
332
+
333
+ Generate all the Friday the 13ths ever since May 1980 (when the first Friday the 13th film was released).
334
+
335
+ ```rb
336
+ Periodoxical.generate(
337
+ time_zone: 'America/Los_Angeles',
338
+ start_date: '1980-05-01',
339
+ days_of_week: %w(fri),
340
+ days_of_month: [13],
341
+ limit: 100,
342
+ time_blocks: [
343
+ { start_time: '11:00PM', end_time: '12:00AM' },
344
+ ],
345
+ )
346
+ # =>
347
+ [
348
+ {
349
+ start: #<DateTime: 1980-06-13T23:00:00-0700>,
350
+ end: #<DateTime: 1980-06-13T00:00:00-0700>,
351
+ },
352
+ {
353
+ start: #<DateTime: 1981-02-13T23:00:00-0800>,
354
+ end: #<DateTime: 1981-02-13T00:00:00-0800>,
355
+ },
356
+ {
357
+ start: #<DateTime: 1981-03-13T23:00:00-0800>,
358
+ end: #<DateTime: 1981-03-13T00:00:00-0800>,
359
+ },
360
+ {
361
+ start: #<DateTime: 1981-11-13T23:00:00-0800>,
362
+ end: #<DateTime: 1981-11-13T00:00:00-0800>,
363
+ }
364
+ ...
365
+ ]
366
+ ```
367
+
368
+ Generate the next 10 Thanksgivings from now on (Thanksgivings is defined as the 4th Thursday in November).
369
+
370
+ ```rb
371
+ Periodoxical.generate(
372
+ time_zone: 'America/Los_Angeles',
373
+ start_date: '2024-05-01',
374
+ months: [11],
375
+ nth_day_of_week_in_month: {
376
+ thu: [4],
377
+ },
378
+ limit: 10,
379
+ time_blocks: [
380
+ { start_time: '5:00PM', end_time: '6:00PM' },
381
+ ],
382
+ )
383
+ #=>
384
+ [
385
+ {
386
+ start: #<DateTime: 2024-11-28T17:00:00-0800>,
387
+ end: #<DateTime: 2024-11-28T18:00:00-0800>,
388
+ },
389
+ {
390
+ start: #<DateTime: 2025-11-27T17:00:00-0800>,
391
+ end: #<DateTime: 2025-11-27T18:00:00-0800>,
392
+ },
393
+ {
394
+ start: #<DateTime: 2026-11-26T17:00:00-0800>,
395
+ end: #<DateTime: 2026-11-26T18:00:00-0800>,
396
+ },
397
+ {
398
+ start: #<DateTime: 2027-11-25T17:00:00-0800>,
399
+ end: #<DateTime: 2027-11-25T18:00:00-0800>,
400
+ },
181
401
  {
182
- start: <#DateTime: 2024-07-01T08:00:00-0700>,
183
- end: <#DateTime: 2024-07-01T09:00:00-0700>,
402
+ start: #<DateTime: 2028-11-23T17:00:00-0800>,
403
+ end: #<DateTime: 2028-11-23T18:00:00-0800>,
184
404
  }
185
405
  ]
186
406
  ```
@@ -1,3 +1,3 @@
1
1
  module Periodoxical
2
- VERSION = "0.5.2"
2
+ VERSION = "0.7.2"
3
3
  end
data/lib/periodoxical.rb CHANGED
@@ -2,6 +2,7 @@ require "periodoxical/version"
2
2
  require "date"
3
3
  require "time"
4
4
  require "tzinfo"
5
+ require "week_of_month"
5
6
 
6
7
  module Periodoxical
7
8
  class << self
@@ -32,22 +33,44 @@ module Periodoxical
32
33
  # Days of the week to generate the times for, if nil, then times are generated
33
34
  # for every day.
34
35
  # Ex: %w(mon tue wed sat)
36
+ # @param [Array<Integer>, nil] days_of_month
37
+ # Days of month to generate times for.
38
+ # Ex: %w(5 10) - The 5th and 10th days of every month
39
+ # @param [Array<Integer>, nil] months
40
+ # Months as integers, where 1 = Jan, 12 = Dec
35
41
  # @param [Integer] limit
36
42
  # How many date times to generate. To be used when `end_date` is nil.
37
43
  # @param [Aray<String>] exclusion_dates
38
44
  # Dates to be excluded when generating the time blocks
39
45
  # Ex: ['2024-06-10', '2024-06-14']
40
- # @param [Hash<Hash>] day_of_week_time_blocks
46
+ # @param [Hash<Array<Hash>>] day_of_week_time_blocks
41
47
  # To be used when hours are different between days of the week
42
48
  # Ex: {
43
49
  # mon: [{ start_time: '10:15AM', end_time: '11:35AM' }, { start_time: '9:00AM' }, {end_time: '4:30PM'} ],
44
50
  # tue: { start_time: '11:30PM', end_time: '12:00AM' },
45
51
  # fri: { start_time: '7:00PM', end_time: '9:00PM' },
46
52
  # }
47
- def initialize(time_zone: 'Etc/UTC', days_of_week: nil,
48
- start_date:, end_date: nil, time_blocks: nil, day_of_week_time_blocks: nil, limit: nil, exclusion_dates: nil)
53
+ def initialize(
54
+ start_date:,
55
+ end_date: nil,
56
+ time_blocks: nil,
57
+ day_of_week_time_blocks: nil,
58
+ limit: nil,
59
+ exclusion_dates: nil,
60
+ time_zone: 'Etc/UTC',
61
+ days_of_week: nil,
62
+ nth_day_of_week_in_month: nil,
63
+ days_of_month: nil,
64
+ weeks_of_month: nil,
65
+ months: nil
66
+ )
67
+
49
68
  @time_zone = TZInfo::Timezone.get(time_zone)
50
69
  @days_of_week = days_of_week
70
+ @nth_day_of_week_in_month = nth_day_of_week_in_month
71
+ @days_of_month = days_of_month
72
+ @weeks_of_month = weeks_of_month
73
+ @months = months
51
74
  @time_blocks = time_blocks
52
75
  @day_of_week_time_blocks = day_of_week_time_blocks
53
76
  @start_date = start_date.is_a?(String) ? Date.parse(start_date) : start_date
@@ -67,74 +90,35 @@ module Periodoxical
67
90
  # }
68
91
  # ]
69
92
  def generate
70
- if @days_of_week && @time_blocks
71
- generate_when_same_time_blocks_for_all_days
72
- elsif @day_of_week_time_blocks
73
- generate_when_different_time_blocks_between_days
93
+ initialize_looping_variables!
94
+ while @keep_generating
95
+ if should_add_time_blocks_from_current_date?
96
+ add_time_blocks_from_current_date!
97
+ end
98
+ advance_current_date_and_check_if_reached_end_date
74
99
  end
100
+ @output
75
101
  end
76
102
 
77
103
  private
78
104
 
79
- def generate_when_different_time_blocks_between_days
80
- times_output = []
81
- current_date = @start_date
82
- current_count = 0
83
- keep_generating = true
84
- while keep_generating
85
- day_of_week = day_of_week_long_to_short(current_date.strftime("%A"))
86
- if @day_of_week_time_blocks[day_of_week.to_sym] && !excluded_date?(current_date)
87
- time_blocks = @day_of_week_time_blocks[day_of_week.to_sym]
88
- time_blocks.each do |tb|
89
- times_output << {
90
- start: time_str_to_object(current_date, tb[:start_time]),
91
- end: time_str_to_object(current_date, tb[:end_time])
92
- }
93
- current_count = current_count + 1
94
- if @limit && current_count == @limit
95
- keep_generating = false
96
- break
97
- end
98
- end
99
- end
100
- current_date = current_date + 1
101
- if @end_date && (current_date > @end_date)
102
- keep_generating = false
103
- end
105
+ def validate!
106
+ unless @day_of_week_time_blocks || @time_blocks
107
+ raise "`day_of_week_time_blocks` or `time_blocks` need to be provided"
104
108
  end
105
- times_output
106
- end
107
109
 
108
- def generate_when_same_time_blocks_for_all_days
109
- times_output = []
110
- current_date = @start_date
111
- current_count = 0
112
- keep_generating = true
113
- while keep_generating
114
- day_of_week = day_of_week_long_to_short(current_date.strftime("%A"))
115
- if @days_of_week.include?(day_of_week) && !excluded_date?(current_date)
116
- @time_blocks.each do |tb|
117
- times_output << {
118
- start: time_str_to_object(current_date, tb[:start_time]),
119
- end: time_str_to_object(current_date, tb[:end_time])
120
- }
121
- current_count = current_count + 1
122
- if @limit && current_count == @limit
123
- keep_generating = false
124
- break
125
- end
126
- end
127
- end
128
- current_date = current_date + 1
110
+ if @days_of_week && @day_of_week_time_blocks
111
+ raise "`days_of_week` and `day_of_week_time_blocks` are both provided, which leads to ambiguity. Please use only one of these parameters."
112
+ end
129
113
 
130
- if @end_date && (current_date > @end_date)
131
- keep_generating = false
114
+ if @weeks_of_month
115
+ @weeks_of_month.each do |wom|
116
+ unless wom.is_a?(Integer) && wom.between?(1, 5)
117
+ raise "weeks_of_month must be an array of integers between 1 and 5"
118
+ end
132
119
  end
133
120
  end
134
- times_output
135
- end
136
121
 
137
- def validate!
138
122
  # days of week are valid
139
123
  if @days_of_week
140
124
  @days_of_week.each do |day|
@@ -144,6 +128,32 @@ module Periodoxical
144
128
  end
145
129
  end
146
130
 
131
+ if @nth_day_of_week_in_month
132
+ @nth_day_of_week_in_month.keys.each do |day|
133
+ unless VALID_DAYS_OF_WEEK.include?(day.to_s)
134
+ raise "#{day} is not valid day of week format. Must be: #{VALID_DAYS_OF_WEEK}"
135
+ end
136
+ end
137
+ @nth_day_of_week_in_month.each do |k,v|
138
+ unless v.is_a?(Array)
139
+ raise "nth_day_of_week_in_month parameter is invalid. Please look at the README for examples."
140
+ end
141
+ v.each do |num|
142
+ unless [-1,1,2,3,4,5].include?(num)
143
+ raise "nth_day_of_week_in_month parameter is invalid. Please look at the README for examples. "
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ if @days_of_week && @nth_day_of_week_in_month
150
+ # If both `days_of_week` and `nth_day_of_week_in_month` are provided for the same days-of-the-week, then it is ambiguous. (ie. I want this timeslot for every Monday, but also only for the first Mondays, well which one is it?)
151
+ overlapping_days = @days_of_week & @nth_day_of_week_in_month.keys.map(&:to_s)
152
+ unless overlapping_days.empty?
153
+ raise "#{overlapping_days} is specified in both `days_of_week` and also `nth_day_of_week_in_month`, which leads to ambiguity. Pleasee look at the README for examples."
154
+ end
155
+ end
156
+
147
157
  if @day_of_week_time_blocks
148
158
  @day_of_week_time_blocks.keys.each do |d|
149
159
  unless VALID_DAYS_OF_WEEK.include?(d.to_s)
@@ -152,8 +162,20 @@ module Periodoxical
152
162
  end
153
163
  end
154
164
 
155
- unless (@days_of_week && @time_blocks) || (@day_of_week_time_blocks)
156
- raise "Need to provide either `days_of_week` and `time_blocks` or `day_of_week_time_blocks`"
165
+ if @days_of_month
166
+ @days_of_month.each do |dom|
167
+ unless dom.is_a?(Integer) && dom.between?(1,31)
168
+ raise 'days_of_months must be array of integers between 1 and 31'
169
+ end
170
+ end
171
+ end
172
+
173
+ if @months
174
+ @months.each do |mon|
175
+ unless mon.is_a?(Integer) && mon.between?(1, 12)
176
+ raise 'months must be array of integers between 1 and 12'
177
+ end
178
+ end
157
179
  end
158
180
 
159
181
  unless( @limit || @end_date)
@@ -189,17 +211,166 @@ module Periodoxical
189
211
  @time_zone.local_to_utc(date_time).new_offset(@time_zone.current_period.offset.utc_total_offset)
190
212
  end
191
213
 
192
- # @param [Date] current_date
214
+ # @param [Date] date
193
215
  # @return [Boolean]
194
216
  # Whether or not the date is excluded
195
- def excluded_date?(current_date)
217
+ def excluded_date?(date)
196
218
  return false unless @exclusion_dates
197
219
 
198
220
  @exclusion_dates.each do |ed|
199
- return true if current_date == ed
221
+ return true if date == ed
200
222
  end
201
223
 
202
224
  false
203
225
  end
226
+
227
+ # Variables which manage flow of looping through time and generating slots
228
+ def initialize_looping_variables!
229
+ @output = []
230
+ @current_date = @start_date
231
+ @current_count = 0
232
+ @keep_generating = true
233
+ end
234
+
235
+ # @param [Hash] time_block
236
+ # Ex:
237
+ # {
238
+ # start_time: "9:00AM",
239
+ # start_time: "10:00AM",
240
+ # }
241
+ # Generates time block but also checks if we should stop generating
242
+ def append_to_output_and_check_limit(time_block)
243
+ @output << {
244
+ start: time_str_to_object(@current_date, time_block[:start_time]),
245
+ end: time_str_to_object(@current_date, time_block[:end_time])
246
+ }
247
+
248
+ # increment count, if `limit` is used to stop generating
249
+ @current_count = @current_count + 1
250
+ if @limit && @current_count == @limit
251
+ @keep_generating = false
252
+ throw :done
253
+ end
254
+ end
255
+
256
+ def advance_current_date_and_check_if_reached_end_date
257
+ @current_date = @current_date + 1
258
+
259
+ if @end_date && (@current_date > @end_date)
260
+ @keep_generating = false
261
+ end
262
+ end
263
+
264
+ # @return [Boolean]
265
+ # Should time blocks be added based on the current_date?
266
+ def should_add_time_blocks_from_current_date?
267
+ # return false if current_date is explicitly excluded
268
+ if @exclusion_dates
269
+ return false if @exclusion_dates.include?(@current_date)
270
+ end
271
+
272
+ # If weeks_of_months are specified but not satisified, return false
273
+ if @weeks_of_month
274
+ return false unless @weeks_of_month.include?(@current_date.week_of_month)
275
+ end
276
+
277
+ # If months are specified, but current_date does not satisfy months,
278
+ # return false
279
+ if @months
280
+ return false unless @months.include?(@current_date.month)
281
+ end
282
+
283
+ # If days of months are specified, but current_date does not satisfy it,
284
+ # return false
285
+ if @days_of_month
286
+ return false unless @days_of_month.include?(@current_date.day)
287
+ end
288
+
289
+ # The following conditions depend on the day-of-week of current_date.
290
+ day_of_week = day_of_week_long_to_short(@current_date.strftime("%A"))
291
+
292
+ # If days of week are specified, but current_date does not satisfy it,
293
+ # return false
294
+ if @days_of_week
295
+ return false unless @days_of_week.include?(day_of_week)
296
+ end
297
+
298
+ if @day_of_week_time_blocks
299
+ dowtb = @day_of_week_time_blocks[day_of_week.to_sym]
300
+ return false if dowtb.nil?
301
+ return false if dowtb.empty?
302
+ end
303
+
304
+ if @nth_day_of_week_in_month
305
+ # If the day of week is specified in nth_day_of_week_in_month,
306
+ # we need to investigate it whether or not to exclude it.
307
+ if @nth_day_of_week_in_month[day_of_week.to_sym]
308
+ n_occurence_of_day_of_week_in_month = ((@current_date.day - 1) / 7) + 1
309
+ # -1 is a special case and requires extra-math
310
+ if @nth_day_of_week_in_month[day_of_week.to_sym].include?(-1)
311
+ # We basically want to convert the -1 into its 'positive-equivalent` in this month, and compare it with that.
312
+ # For example, in June 2024, the Last Friday is also the 4th Friday. So in that case, we convert the -1 into a 4.
313
+ positivized_indices = @nth_day_of_week_in_month[day_of_week.to_sym].map { |indx| positivize_index(indx, day_of_week) }
314
+ return positivized_indices.include?(n_occurence_of_day_of_week_in_month)
315
+ else
316
+ return @nth_day_of_week_in_month[day_of_week.to_sym].include?(n_occurence_of_day_of_week_in_month)
317
+ end
318
+ else
319
+ # if day-of-week was not specified in nth_day_of_week_in_month,
320
+ # it could have been specified in either `days_of_week` or `day_of_week_time_blocks and we unfortunately need to re-check those here. I cant think of a way to further DRY-it up.
321
+ return false unless @days_of_week && @day_of_week_time_blocks
322
+ if @day_of_week
323
+ return false unless @days_of_week.include?(day_of_week)
324
+ end
325
+
326
+ if @day_of_week_time_blocks
327
+ dowtb = @day_of_week_time_blocks[day_of_week.to_sym]
328
+ return false if dowtb.nil?
329
+ return false if dowtb.empty?
330
+ end
331
+ end
332
+ end
333
+
334
+ # Otherwise, return true
335
+ true
336
+ end
337
+
338
+ def add_time_blocks_from_current_date!
339
+ if @day_of_week_time_blocks
340
+ day_of_week = day_of_week_long_to_short(@current_date.strftime("%A"))
341
+ time_blocks = @day_of_week_time_blocks[day_of_week.to_sym]
342
+ catch :done do
343
+ time_blocks.each do |tb|
344
+ append_to_output_and_check_limit(tb)
345
+ end
346
+ end
347
+ elsif @time_blocks
348
+ catch :done do
349
+ @time_blocks.each do |tb|
350
+ append_to_output_and_check_limit(tb)
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ # What is the positive index of the last day-of-week for the given month-year?
357
+ # For example, the last Friday in June 2024 is also the nth Friday. What is this n?
358
+ # @return [Integer]
359
+ def positivize_index(indx, day_of_week)
360
+ # If index is already positive, just return it
361
+ return indx if indx > 0
362
+
363
+ # get last_day_of month
364
+ month = @current_date.month
365
+ year = @current_date.year
366
+ last_date = Date.new(year, month, -1)
367
+
368
+ # walk backwords until you get to the right day of the week
369
+ while day_of_week_long_to_short(last_date.strftime("%A")) != day_of_week
370
+ last_date = last_date - 1
371
+ end
372
+
373
+ ((last_date.day - 1) / 7) + 1
374
+ end
204
375
  end
205
376
  end
data/periodoxical.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.require_paths = ["lib"]
36
36
 
37
37
  spec.add_dependency 'tzinfo', '~> 2.0', '>= 2.0.0'
38
+ spec.add_dependency 'week_of_month', '1.2.6'
38
39
 
39
40
  spec.add_development_dependency "bundler", "~> 2.4"
40
41
  spec.add_development_dependency "rake", "~> 12.3.3"
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.5.2
4
+ version: 0.7.2
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-01 00:00:00.000000000 Z
11
+ date: 2024-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tzinfo
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 2.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: week_of_month
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 1.2.6
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 1.2.6
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: bundler
35
49
  requirement: !ruby/object:Gem::Requirement