fugit 1.11.1 → 1.12.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da93feee9bdfd44a793ce2b45b81109c0f7cd08ff9a930d15629a29c6a381e07
4
- data.tar.gz: 32d9b447734805d3b58ae64c70a6699b931ef4fb4e6a76d8f279e66d4b5cb7df
3
+ metadata.gz: 15a3ea665c020e35ce51dcbc82be9a74e6f4abcaa2441316b514c630ffdbc094
4
+ data.tar.gz: f99246722de255325b57ea46f7b3f24bbfc11d65f1eb98fde9a2a43a3c249df0
5
5
  SHA512:
6
- metadata.gz: e7ac7afec4cfdcfdac22e5d2113892a0c7cb1f790d205bc32ecaea75c30f6ceb73af811824712c6781b480831f2a09479a6344af0d9b3c0ee92ae5cff7ef6c38
7
- data.tar.gz: d87ecc490a49f19680891a82979903987843b5853ecfdc9ebee57d0b047fa4dffa96dfbbde43ef5d3011dbd07d492ac8f7a151fa6354478050c18efae9b911e4
6
+ metadata.gz: cc55e7332f5f111ddf27d145fed7fbfeaeed30f492ba343d1f4067e776c96874b42bad9488b9683203040cc50333f74bab56fa6e61744ef92fb059f2a2c9f076
7
+ data.tar.gz: 5ee65240dd04fddb7777bee327f8fee0cfef5260ed55db9ec71ece64c5e5e6b53283aa7d26ba52facbca3882aee0920ff0dd3715fea5ad9153caca82ea712450
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
  # CHANGELOG.md
3
3
 
4
4
 
5
+ ## fugit 1.12.1 released 2025-10-14
6
+
7
+ * Fix Fugit::Cron#to_cron_s to include & if @day_and
8
+
9
+
10
+ ## fugit 1.12.0 released 2025-09-30
11
+
12
+ * Upgrade et-orbi to ~> 1.4.0 for EtOrbi.rweek_ref=, gh-114
13
+ this changes the rweek reference point to 2018-12-31 (Monday)
14
+ * Fix `12 0 * * wed%4+1,wed%4` issue where 1 wed was ignored, gh-114
15
+
16
+
17
+ ## fugit 1.11.2 released 2025-08-22
18
+
19
+ * Fix "every day at midnight America/Los_Angeles", gh-113, Mark R. James
20
+ * Avoid Hash#partition https://bugs.ruby-lang.org/issues/16252
21
+ * Fix Fugit::Nat "zero dark forty", gh-107
22
+ * Ensure @yearly and other specials accept a timezone
23
+
24
+
5
25
  ## fugit 1.11.1 released 2024-08-15
6
26
 
7
27
  * Prevent nat parsing chocking on long input (> 256 chars), gh-104
data/CREDITS.md CHANGED
@@ -1,6 +1,12 @@
1
1
 
2
2
  # fugit credits
3
3
 
4
+ * Eric Claerhout https://github.com/swebra gh-114 rweek readme clarity
5
+ * Hugh Kelsey https://github.com/hughkelsey gh-114 rweek/rday / wed%4+1,wed%4
6
+ * Mark R. James, https://github.com/mrj, AM vs America/Los_Angeles, gh-113
7
+ * Tejas Bubane, https://github.com/tejasbubane, r3.4 in test matrix, gh-109
8
+ * Luis Castillo, https://github.com/lekastillo, nice_hash gh-108
9
+ * Geremia Taglialatela, https://github.com/tagliala, gh-105 gh-107
4
10
  * https://github.com/personnumber3377, gh-104 Fugit.parse choke on long input
5
11
  * Michael Scrivo, https://github.com/mscrivo, gh-103
6
12
  * Benjamin Darcet, https://github.com/bdarcet gh-95 gh-96 et-orbi #rweek
data/LICENSE.txt CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- Copyright (c) 2017-2024, John Mettraux, jmettraux+flor@gmail.com
2
+ Copyright (c) 2017-2025, John Mettraux, jmettraux+flor@gmail.com
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -17,31 +17,33 @@ Fugit is a core dependency of [rufus-scheduler](https://github.com/jmettraux/ruf
17
17
 
18
18
  The intersection of those two projects is where fugit is born:
19
19
 
20
- * [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) - a cron/at/in/every/interval in-process scheduler, in fact, it's the father project to this fugit project
21
- * [flor](https://github.com/floraison/flor) - a Ruby workflow engine, fugit provides the foundation for its time scheduling capabilities
20
+ * [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) a cron/at/in/every/interval in-process scheduler, in fact, it's the father project to this fugit project
21
+ * [flor](https://github.com/floraison/flor) a Ruby workflow engine, fugit provides the foundation for its time scheduling capabilities
22
22
 
23
23
  ### Similar, sometimes overlapping projects
24
24
 
25
- * [chronic](https://github.com/mojombo/chronic) - a pure Ruby natural language date parser
26
- * [parse-cron](https://github.com/siebertm/parse-cron) - parses cron expressions and calculates the next occurrence after a given date
27
- * [ice_cube](https://github.com/seejohnrun/ice_cube) - Ruby date recurrence library
28
- * [ISO8601](https://github.com/arnau/ISO8601) - Ruby parser to work with ISO8601 dateTimes and durations
29
- * [chrono](https://github.com/r7kamura/chrono) - a chain of logics about chronology
30
- * [CronCalc](https://github.com/mizinsky/cron_calc) - calculates cron job occurrences
31
- * [Recurrence](https://github.com/fnando/recurrence) - a simple library to handle recurring events
25
+ * [chronic](https://github.com/mojombo/chronic) a pure Ruby natural language date parser
26
+ * [parse-cron](https://github.com/siebertm/parse-cron) parses cron expressions and calculates the next occurrence after a given date
27
+ * [ice_cube](https://github.com/seejohnrun/ice_cube) Ruby date recurrence library
28
+ * [ISO8601](https://github.com/arnau/ISO8601) Ruby parser to work with ISO8601 dateTimes and durations
29
+ * [chrono](https://github.com/r7kamura/chrono) a chain of logics about chronology
30
+ * [CronCalc](https://github.com/mizinsky/cron_calc) calculates cron job occurrences
31
+ * [Recurrence](https://github.com/fnando/recurrence) a simple library to handle recurring events
32
+ * [CronConfigParser](https://github.com/madogiwa0124/cron_config_parser) — parse the cron configuration for readability
32
33
  * ...
33
34
 
34
35
  ### Projects using fugit
35
36
 
36
- * [arask](https://github.com/Ebbe/arask) - "Automatic RAils taSKs" uses fugit to parse cron strings
37
- * [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) - uses fugit to parse cron strings since version 1.0.0, it was using rufus-scheduler previously
38
- * [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) - as seen above
39
- * [flor](https://github.com/floraison/flor) - used in the [cron](https://github.com/floraison/flor/blob/master/doc/procedures/cron.md) procedure
40
- * [que-scheduler](https://github.com/hlascelles/que-scheduler) - a reliable job scheduler for [que](https://github.com/chanks/que)
41
- * [serial_scheduler](https://github.com/grosser/serial_scheduler) - ruby task scheduler without threading
42
- * [delayed_cron_job](https://github.com/codez/delayed_cron_job) - an extension to Delayed::Job that allows you to set cron expressions for your jobs
43
- * [GoodJob](https://github.com/bensheldon/good_job) - a multithreaded, Postgres-based, Active Job backend for Ruby on Rails
44
- * [Solid Queue](https://github.com/rails/solid_queue) - a DB-based queuing backend for Active Job, designed with simplicity and performance in mind
37
+ * [arask](https://github.com/Ebbe/arask) "Automatic RAils taSKs" uses fugit to parse cron strings
38
+ * [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) uses fugit to parse cron strings since version 1.0.0, it was using rufus-scheduler previously
39
+ * [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) as seen above
40
+ * [flor](https://github.com/floraison/flor) used in the [cron](https://github.com/floraison/flor/blob/master/doc/procedures/cron.md) procedure
41
+ * [que-scheduler](https://github.com/hlascelles/que-scheduler) a reliable job scheduler for [que](https://github.com/chanks/que)
42
+ * [serial_scheduler](https://github.com/grosser/serial_scheduler) ruby task scheduler without threading
43
+ * [delayed_cron_job](https://github.com/codez/delayed_cron_job) an extension to Delayed::Job that allows you to set cron expressions for your jobs
44
+ * [GoodJob](https://github.com/bensheldon/good_job) a multithreaded, Postgres-based, Active Job backend for Ruby on Rails
45
+ * [Solid Queue](https://github.com/rails/solid_queue) a DB-based queuing backend for Active Job, designed with simplicity and performance in mind
46
+ * [qron](https://github.com/floraison/qron) — stupid cron thread that wakes up from time to time to do what's in its crontab
45
47
  * ...
46
48
 
47
49
  ## `Fugit.parse(s)`
@@ -112,6 +114,7 @@ Fugit.parse_cronish('12y12M') # ==> nil
112
114
 
113
115
  Introduced in fugit 1.8.0.
114
116
 
117
+
115
118
  ## `Fugit::Cron`
116
119
 
117
120
  A class `Fugit::Cron` to parse cron strings and then `#next_time` and `#previous_time` to compute the next or the previous occurrence respectively.
@@ -204,13 +207,28 @@ Example of cron strings understood by fugit:
204
207
 
205
208
  Please note that `'15/30 * * * *'` is interpreted as `'15-59/30 * * * *'` since fugit 1.4.6.
206
209
 
210
+ ### time zones
211
+
212
+ Fugit accepts a IANA timezone identifier right after a cron string:
213
+ ```ruby
214
+ '5 0 * * * Europe/Rome' # 5 minutes after midnight, every day, Rome tz
215
+ '0 22 * * 1-5 Asia/Tbilisi' # at 2200 on weekdays in Georgia
216
+
217
+ '@yearly Asia/Kuala_Lumpur' # turns into '0 0 1 1 * Asia/Kuala_Lumpur'
218
+ '@monthly Asia/Jakarta' # turns into '0 0 1 * * Asia/Jakarta'
219
+ #
220
+ # those two "ats" and friends since fugit 1.11.2...
221
+ ```
222
+
223
+ When no time zone is specified, fugit uses Ruby's provided timezone.
224
+
207
225
  ### the first Monday of the month
208
226
 
209
227
  Fugit tries to follow the `man 5 crontab` documentation.
210
228
 
211
229
  There is a surprising thing about this canon, all the columns are joined by ANDs, except for monthday and weekday which are joined together by OR if they are both set (they are not `*`).
212
230
 
213
- Many people (me included) [are suprised](https://superuser.com/questions/428807/run-a-cron-job-on-the-first-monday-of-every-month) when they try to specify "at 05:00 on the first Monday of the month" as `0 5 1-7 * 1` or `0 5 1-7 * mon` and the results are off.
231
+ Many people (me included) [are surprised](https://superuser.com/questions/428807/run-a-cron-job-on-the-first-monday-of-every-month) when they try to specify "at 05:00 on the first Monday of the month" as `0 5 1-7 * 1` or `0 5 1-7 * mon` and the results are off.
214
232
 
215
233
  The man page says:
216
234
 
@@ -278,139 +296,201 @@ The hash extension can only be used in the day-of-week field.
278
296
 
279
297
  ### the modulo extension
280
298
 
281
- Fugit, since 1.1.10, also understands cron strings like "`9 0 * * sun%2`" which can be read as "every other Sunday at 9am".
299
+ Since 1.1.10, fugit also understands cron strings like `9 0 * * sun%2` which can be read as "every other Sunday at 9am" or `12 0 * * mon%4` for "every fourth monday at noon".
282
300
 
283
301
  The modulo extension can only be used in the day-of-week field.
284
302
 
285
303
  For odd Sundays, one can write `9 0 * * sun%2+1`.
286
304
 
287
- It can be combined, as in `9 0 * * sun%2,tue%3+2`
288
-
289
- But what does it reference to? It starts at 1 on 2019-01-01.
305
+ It can be combined, as in `9 0 * * sun%2,tue%3+2`, which will match every other Sunday and 1 in 3 Tuesdays (with an offset of 2).
290
306
 
307
+ What does `sun%2` actually mean?
291
308
  ```ruby
292
- require 'et-orbi' # >= 1.1.8
309
+ t.wday == 0 && t.rweek % 2 == 0
310
+ ```
293
311
 
294
- # the reference
295
- p EtOrbi.parse('2019-01-01').wday # => 2
296
- p EtOrbi.parse('2019-01-01').rweek # => 1
297
- p EtOrbi.parse('2019-01-01').rweek % 2 # => 1
312
+ What does `tue%3+2` mean?
313
+ ```ruby
314
+ t.wday == 2 && t.rweek % 3 == 2
315
+ ```
298
316
 
299
- # today (as of this coding...)
300
- p EtOrbi.parse('2019-04-11').wday # => 4
301
- p EtOrbi.parse('2019-04-11').rweek # => 15
302
- p EtOrbi.parse('2019-04-11').rweek % 2 # => 1
317
+ #### et-orbi < 1.4.0 : reference set on Tuesday 2019-01-01
303
318
 
304
- c = Fugit.parse('* * * * tue%2')
305
- c.match?('2019-01-01') # => false, since rweek % 2 == 1
306
- c.match?('2019-01-08') # => true, since rweek % 2 == 0
319
+ The original implementation of `#rweek` (and `#rday`) found in [et-orbi](https://github.com/floraison/et-orbi) was initially pointing to "Tuesday 2019-01-01" and it was set as rday 1 and rweek 1.
307
320
 
308
- c = Fugit.parse('* * * * tue%2+1')
309
- c.match?('2019-01-01') # => true, since (rweek + 1) % 2 == 0
310
- c.match?('2019-01-08') # => false, since (rweek + 1) % 2 == 1
321
+ Consider this iteration through the days around 2019-01-01.
322
+ ```ruby
323
+ require 'fugit'
311
324
 
312
- # ...
313
- ```
325
+ t = EtOrbi.parse('2018-12-28 12:00')
314
326
 
315
- `sun%2` matches if Sunday and `current_date.rweek % 2 == 0`
316
- `tue%3+2` matches if Tuesday and `current_date.rweek + 2 % 3 == 0`
317
- `tue%x+y` matches if Tuesday and `current_date.rweek + y % x == 0`
327
+ 15.times do |i|
318
328
 
329
+ puts " * %14s / rday: %5d / rweek: %5d" % [
330
+ t.strftime('%F %a'), t.rday, t.rweek ]
319
331
 
320
- ### the second extension
332
+ w = t.rweek
333
+ t = t.add(24 * 3600)
334
+ puts if t.rweek != w
321
335
 
322
- Fugit accepts cron strings with five elements, `minute hour day-of-month month day-of-week`, the standard cron format or six elements `second minute hour day-of-month month day-of-week`.
336
+ if i == 7
337
+ puts "\n (...)\n\n"
338
+ t = EtOrbi.parse('2025-10-04 12:00')
339
+ end
340
+ end
341
+ ```
323
342
 
324
- ```ruby
325
- c = Fugit.parse('* * * * *') # every minute
326
- c = Fugit.parse('5 * * * *') # every hour at minute 5
327
- c = Fugit.parse('* * * * * *') # every second
328
- c = Fugit.parse('5 * * * * *') # every minute at second 5
343
+ For et-orbi 1.2.11, it yields:
344
+ ```
345
+ * 2018-12-28 Fri / rday: -3 / rweek: 0
346
+ * 2018-12-29 Sat / rday: -2 / rweek: 0
347
+ * 2018-12-30 Sun / rday: -1 / rweek: 0
348
+ * 2018-12-31 Mon / rday: 0 / rweek: 0
349
+
350
+ * 2019-01-01 Tue / rday: 1 / rweek: 1
351
+ * 2019-01-02 Wed / rday: 2 / rweek: 1
352
+ * 2019-01-03 Thu / rday: 3 / rweek: 1
353
+ * 2019-01-04 Fri / rday: 4 / rweek: 1
354
+ * 2019-01-05 Sat / rday: 5 / rweek: 1
355
+
356
+ (...)
357
+
358
+ * 2025-10-04 Sat / rday: 2469 / rweek: 353
359
+ * 2025-10-05 Sun / rday: 2470 / rweek: 353
360
+ * 2025-10-06 Mon / rday: 2471 / rweek: 353
361
+
362
+ * 2025-10-07 Tue / rday: 2472 / rweek: 354
363
+ * 2025-10-08 Wed / rday: 2473 / rweek: 354
364
+ * 2025-10-09 Thu / rday: 2474 / rweek: 354
329
365
  ```
330
366
 
367
+ This [was problematic](https://github.com/floraison/fugit/issues/114), since the week started on, well, Tuesday.
331
368
 
332
- ## `Fugit::Duration`
333
369
 
334
- A class `Fugit::Duration` to parse duration strings (vanilla [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) ones and [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) ones).
370
+ #### et-orbi >= 1.4.0 : reference set on Monday 2018-12-31
335
371
 
336
- Provides duration arithmetic tools.
372
+ Since 1.4.0, et-orbi starts by default on Monday (2018-12-31), as rday 0 with rweek 0.
337
373
 
338
- ```ruby
339
- require 'fugit'
374
+ Thus, the above code yields:
375
+ ```
376
+ * 2018-12-28 Fri / rday: -3 / rweek: -1
377
+ * 2018-12-29 Sat / rday: -2 / rweek: -1
378
+ * 2018-12-30 Sun / rday: -1 / rweek: -1
379
+
380
+ * 2018-12-31 Mon / rday: 0 / rweek: 0
381
+ * 2019-01-01 Tue / rday: 1 / rweek: 0
382
+ * 2019-01-02 Wed / rday: 2 / rweek: 0
383
+ * 2019-01-03 Thu / rday: 3 / rweek: 0
384
+ * 2019-01-04 Fri / rday: 4 / rweek: 0
385
+ * 2019-01-05 Sat / rday: 5 / rweek: 0
386
+
387
+ (...)
388
+
389
+ * 2025-10-04 Sat / rday: 2469 / rweek: 352
390
+ * 2025-10-05 Sun / rday: 2470 / rweek: 352
391
+
392
+ * 2025-10-06 Mon / rday: 2471 / rweek: 353
393
+ * 2025-10-07 Tue / rday: 2472 / rweek: 353
394
+ * 2025-10-08 Wed / rday: 2473 / rweek: 353
395
+ * 2025-10-09 Thu / rday: 2474 / rweek: 353
396
+ ```
340
397
 
341
- d = Fugit::Duration.parse('1y2M1d4h')
342
398
 
343
- p d.to_plain_s # => "1Y2M1D4h"
344
- p d.to_iso_s # => "P1Y2M1DT4H" ISO 8601 duration
345
- p d.to_long_s # => "1 year, 2 months, 1 day, and 4 hours"
399
+ #### modulo and et-orbi >= 1.4.0 sanity check
346
400
 
347
- d += Fugit::Duration.parse('1y1h')
401
+ Given the cron `"0 12 * * mon%2,wed%3+1"`, here is a piece of code that considers a range of 44 days and tells in its last column if YES or no if each of the days matches the cron.
348
402
 
349
- p d.to_long_s # => "2 years, 2 months, 1 day, and 5 hours"
403
+ ```ruby
404
+ require 'fugit'
350
405
 
351
- d += 3600
406
+ c = Fugit.parse_cron('0 12 * * mon%2,wed%3+1')
352
407
 
353
- p d.to_plain_s # => "2Y2M1D5h3600s"
408
+ t = EtOrbi.parse('2025-09-20 12:00')
354
409
 
355
- p Fugit::Duration.parse('1y2M1d4h').to_sec # => 36820800
356
- ```
410
+ 44.times do
357
411
 
358
- There is a `#deflate` method
412
+ wd = t.strftime('%a')
413
+ wd = %w[ Mon Wed ].include?(wd) ? '*' + wd.upcase : ' ' + wd.downcase
359
414
 
360
- ```ruby
361
- Fugit::Duration.parse(1000).to_plain_s # => "1000s"
362
- Fugit::Duration.parse(3600).to_plain_s # => "3600s"
363
- Fugit::Duration.parse(1000).deflate.to_plain_s # => "16m40s"
364
- Fugit::Duration.parse(3600).deflate.to_plain_s # => "1h"
415
+ puts "%14s | rweek: %3d | %%2: %d == 0 | %%3: %d == 1 | ? %3s" % [
416
+ t.strftime('%F') + ' ' + wd,
417
+ t.rweek,
418
+ t.rweek % 2, t.rweek % 3,
419
+ c.match?(t)
420
+ ].map { |e| e == true ? 'YES' : e == false ? 'no' : e }
365
421
 
366
- # or event shorter
367
- Fugit.parse(1000).deflate.to_plain_s # => "16m40s"
368
- Fugit.parse(3600).deflate.to_plain_s # => "1h"
422
+ w = t.rweek
423
+ t = t.add(24 * 3600)
424
+ puts if t.rweek != w
425
+ end
369
426
  ```
370
427
 
371
- There is also an `#inflate` method
372
-
373
- ```ruby
374
- Fugit::Duration.parse('1h30m12').inflate.to_plain_s # => "5412s"
375
- Fugit.parse('1h30m12').inflate.to_plain_s # => "5412s"
376
-
377
- Fugit.parse('1h30m12').to_sec # => 5412
378
- Fugit.parse('1h30m12').to_sec.to_s + 's' # => "5412s"
428
+ Here's the output:
379
429
  ```
380
-
381
- The `to_*_s` methods are also available as class methods:
382
- ```ruby
383
- p Fugit::Duration.to_plain_s('1y2M1d4h')
384
- # => "1Y2M1D4h"
385
- p Fugit::Duration.to_iso_s('1y2M1d4h')
386
- # => "P1Y2M1DT4H" ISO 8601 duration
387
- p Fugit::Duration.to_long_s('1y2M1d4h')
388
- # => "1 year, 2 months, 1 day, and 4 hours"
430
+ 2025-09-20 sat | rweek: 350 | %2: 0 == 0 | %3: 2 == 1 | ? no
431
+ 2025-09-21 sun | rweek: 350 | %2: 0 == 0 | %3: 2 == 1 | ? no
432
+
433
+ 2025-09-22 *MON | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ? no
434
+ 2025-09-23 tue | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ? no
435
+ 2025-09-24 *WED | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ? no
436
+ 2025-09-25 thu | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ? no
437
+ 2025-09-26 fri | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ? no
438
+ 2025-09-27 sat | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ? no
439
+ 2025-09-28 sun | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ? no
440
+
441
+ 2025-09-29 *MON | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? YES
442
+ 2025-09-30 tue | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? no
443
+ 2025-10-01 *WED | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? YES
444
+ 2025-10-02 thu | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? no
445
+ 2025-10-03 fri | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? no
446
+ 2025-10-04 sat | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? no
447
+ 2025-10-05 sun | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? no
448
+
449
+ 2025-10-06 *MON | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ? no
450
+ 2025-10-07 tue | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ? no
451
+ 2025-10-08 *WED | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ? no
452
+ 2025-10-09 thu | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ? no
453
+ 2025-10-10 fri | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ? no
454
+ 2025-10-11 sat | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ? no
455
+ 2025-10-12 sun | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ? no
456
+
457
+ 2025-10-13 *MON | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? YES
458
+ 2025-10-14 tue | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? no
459
+ 2025-10-15 *WED | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? no
460
+ 2025-10-16 thu | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? no
461
+ 2025-10-17 fri | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? no
462
+ 2025-10-18 sat | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? no
463
+ 2025-10-19 sun | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? no
464
+
465
+ 2025-10-20 *MON | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? no
466
+ 2025-10-21 tue | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? no
467
+ 2025-10-22 *WED | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? YES
468
+ 2025-10-23 thu | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? no
469
+ 2025-10-24 fri | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? no
470
+ 2025-10-25 sat | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? no
471
+ 2025-10-26 sun | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? no
472
+
473
+ 2025-10-27 *MON | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? YES
474
+ 2025-10-28 tue | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? no
475
+ 2025-10-29 *WED | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? no
476
+ 2025-10-30 thu | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? no
477
+ 2025-10-31 fri | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? no
478
+ 2025-11-01 sat | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? no
479
+ 2025-11-02 sun | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? no
389
480
  ```
390
481
 
391
- ## `Fugit::At`
392
-
393
- Points in time are parsed and given back as EtOrbi::EoTime instances.
394
482
 
395
- ```ruby
396
- Fugit::At.parse('2017-12-12').to_s
397
- # ==> "2017-12-12 00:00:00 +0900" (at least here in Hiroshima)
483
+ ### the second extension
398
484
 
399
- Fugit::At.parse('2017-12-12 12:00:00 America/New_York').to_s
400
- # ==> "2017-12-12 12:00:00 -0500"
401
- ```
485
+ Fugit accepts cron strings with five elements, `minute hour day-of-month month day-of-week`, the standard cron format or six elements `second minute hour day-of-month month day-of-week`.
402
486
 
403
- Directly with `Fugit.parse_at(s)` is OK too:
404
487
  ```ruby
405
- Fugit.parse_at('2017-12-12 12:00:00 America/New_York').to_s
406
- # ==> "2017-12-12 12:00:00 -0500"
488
+ c = Fugit.parse('* * * * *') # every minute
489
+ c = Fugit.parse('5 * * * *') # every hour at minute 5
490
+ c = Fugit.parse('* * * * * *') # every second
491
+ c = Fugit.parse('5 * * * * *') # every minute at second 5
407
492
  ```
408
493
 
409
- Directly with `Fugit.parse(s)` is OK too:
410
- ```ruby
411
- Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s
412
- # ==> "2017-12-12 12:00:00 -0500"
413
- ```
414
494
 
415
495
  ## `Fugit::Nat`
416
496
 
@@ -511,6 +591,96 @@ p Fugit.parse('every day at 12:15 midnight').original # ==> "15 24 * * *"
511
591
  ```
512
592
 
513
593
 
594
+ ## `Fugit::Duration`
595
+
596
+ A class `Fugit::Duration` to parse duration strings (vanilla [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) ones and [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) ones).
597
+
598
+ Provides duration arithmetic tools.
599
+
600
+ ```ruby
601
+ require 'fugit'
602
+
603
+ d = Fugit::Duration.parse('1y2M1d4h')
604
+
605
+ p d.to_plain_s # => "1Y2M1D4h"
606
+ p d.to_iso_s # => "P1Y2M1DT4H" ISO 8601 duration
607
+ p d.to_long_s # => "1 year, 2 months, 1 day, and 4 hours"
608
+
609
+ d += Fugit::Duration.parse('1y1h')
610
+
611
+ p d.to_long_s # => "2 years, 2 months, 1 day, and 5 hours"
612
+
613
+ d += 3600
614
+
615
+ p d.to_plain_s # => "2Y2M1D5h3600s"
616
+
617
+ p Fugit::Duration.parse('1y2M1d4h').to_sec # => 36820800
618
+ ```
619
+
620
+ There is a `#deflate` method
621
+
622
+ ```ruby
623
+ Fugit::Duration.parse(1000).to_plain_s # => "1000s"
624
+ Fugit::Duration.parse(3600).to_plain_s # => "3600s"
625
+ Fugit::Duration.parse(1000).deflate.to_plain_s # => "16m40s"
626
+ Fugit::Duration.parse(3600).deflate.to_plain_s # => "1h"
627
+
628
+ # or event shorter
629
+ Fugit.parse(1000).deflate.to_plain_s # => "16m40s"
630
+ Fugit.parse(3600).deflate.to_plain_s # => "1h"
631
+ ```
632
+
633
+ There is also an `#inflate` method
634
+
635
+ ```ruby
636
+ Fugit::Duration.parse('1h30m12').inflate.to_plain_s # => "5412s"
637
+ Fugit.parse('1h30m12').inflate.to_plain_s # => "5412s"
638
+
639
+ Fugit.parse('1h30m12').to_sec # => 5412
640
+ Fugit.parse('1h30m12').to_sec.to_s + 's' # => "5412s"
641
+ ```
642
+
643
+ The `to_*_s` methods are also available as class methods:
644
+ ```ruby
645
+ p Fugit::Duration.to_plain_s('1y2M1d4h')
646
+ # => "1Y2M1D4h"
647
+ p Fugit::Duration.to_iso_s('1y2M1d4h')
648
+ # => "P1Y2M1DT4H" ISO 8601 duration
649
+ p Fugit::Duration.to_long_s('1y2M1d4h')
650
+ # => "1 year, 2 months, 1 day, and 4 hours"
651
+ ```
652
+
653
+
654
+ ## `Fugit::At`
655
+
656
+ Points in time are parsed and given back as EtOrbi::EoTime instances.
657
+
658
+ ```ruby
659
+ Fugit::At.parse('2017-12-12').to_s
660
+ # ==> "2017-12-12 00:00:00 +0900" (at least here in Hiroshima)
661
+
662
+ Fugit::At.parse('2017-12-12 12:00:00 America/New_York').to_s
663
+ # ==> "2017-12-12 12:00:00 -0500"
664
+ ```
665
+
666
+ Directly with `Fugit.parse_at(s)` is OK too:
667
+ ```ruby
668
+ Fugit.parse_at('2017-12-12 12:00:00 America/New_York').to_s
669
+ # ==> "2017-12-12 12:00:00 -0500"
670
+ ```
671
+
672
+ Directly with `Fugit.parse(s)` is OK too:
673
+ ```ruby
674
+ Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s
675
+ # ==> "2017-12-12 12:00:00 -0500"
676
+ ```
677
+
678
+
679
+ ## KNOWN ISSUES
680
+
681
+ The gem [nice_hash](https://github.com/MarioRuiz/nice_hash) gets in the way of `fugit`, as seen in [issue 108](https://github.com/floraison/fugit/issues/108). It prevents `fugit` from correctly parsing cron strings.
682
+
683
+
514
684
  ## LICENSE
515
685
 
516
686
  MIT, see [LICENSE.txt](LICENSE.txt)
data/fugit.gemspec CHANGED
@@ -20,18 +20,18 @@ Time tools for flor and the floraison project. Cron parsing and occurrence compu
20
20
 
21
21
  s.metadata = {
22
22
  'changelog_uri' => s.homepage + '/blob/master/CHANGELOG.md',
23
- 'documentation_uri' => s.homepage,
24
23
  'bug_tracker_uri' => s.homepage + '/issues',
25
- #'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/floraison',
24
+ 'documentation_uri' => s.homepage,
26
25
  'homepage_uri' => s.homepage,
27
26
  'source_code_uri' => s.homepage,
27
+ #'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/floraison',
28
28
  #'wiki_uri' => s.homepage + '/wiki',
29
+ 'rubygems_mfa_required' => 'true',
29
30
  }
30
31
 
31
32
  #s.files = `git ls-files`.split("\n")
32
33
  s.files = Dir[
33
- 'README.{md,txt}',
34
- 'CHANGELOG.{md,txt}', 'CREDITS.{md,txt}', 'LICENSE.{md,txt}',
34
+ '{README,CHANGELOG,CREDITS,LICENSE}.{md,txt}',
35
35
  #'Makefile',
36
36
  'lib/**/*.rb', #'spec/**/*.rb', 'test/**/*.rb',
37
37
  "#{s.name}.gemspec",
@@ -41,7 +41,7 @@ Time tools for flor and the floraison project. Cron parsing and occurrence compu
41
41
  # this dependency appears in 'et-orbi'
42
42
 
43
43
  s.add_runtime_dependency 'raabro', '~> 1.4'
44
- s.add_runtime_dependency 'et-orbi', '~> 1', '>= 1.2.11'
44
+ s.add_runtime_dependency 'et-orbi', '~> 1.4'
45
45
 
46
46
  s.add_development_dependency 'rspec', '~> 3.8'
47
47
  s.add_development_dependency 'chronic', '~> 0.10'
data/lib/fugit/cron.rb CHANGED
@@ -32,15 +32,23 @@ module Fugit
32
32
  def parse(s)
33
33
 
34
34
  return s if s.is_a?(self)
35
+ return nil unless s.is_a?(String)
35
36
 
36
- s = SPECIALS[s] || s
37
+ s0 = s
38
+ s = s.strip
37
39
 
38
- return nil unless s.is_a?(String)
40
+ s =
41
+ if s[0, 1] == '@'
42
+ ss = s.split(/\s+/, 2)
43
+ [ SPECIALS[ss[0]] || ss, *ss[1..-1] ].join(' ')
44
+ else
45
+ s
46
+ end
39
47
 
40
48
  #p s; Raabro.pp(Parser.parse(s, debug: 3), colors: true)
41
- h = Parser.parse(s.strip)
49
+ h = Parser.parse(s)
42
50
 
43
- self.allocate.send(:init, s, h)
51
+ self.allocate.send(:init, s0, h)
44
52
  end
45
53
 
46
54
  def do_parse(s)
@@ -167,34 +175,30 @@ module Fugit
167
175
 
168
176
  def weekday_hash_match?(nt, hsh)
169
177
 
178
+ return false unless hsh.is_a?(Integer)
179
+
170
180
  phsh, nhsh = nt.wday_in_month
181
+ #
182
+ # positive wday, from the beginning of the month
183
+ # negative wday, from the end of the month, -1 == last
171
184
 
172
- if hsh > 0
173
- hsh == phsh # positive wday, from the beginning of the month
174
- else
175
- hsh == nhsh # negative wday, from the end of the month, -1 == last
176
- end
185
+ (hsh == phsh) || (hsh == nhsh)
177
186
  end
178
187
 
179
188
  def weekday_modulo_match?(nt, mod)
180
189
 
181
- (nt.rweek % mod[0]) == (mod[1] % mod[0])
190
+ mod.is_a?(Array) &&
191
+ ((nt.rweek % mod[0]) == (mod[1] % mod[0]))
182
192
  end
183
193
 
184
194
  def weekday_match?(nt)
185
195
 
186
- return true if @weekdays.nil?
187
-
188
- wd, hom = @weekdays.find { |d, _| d == nt.wday }
189
-
190
- return false unless wd
191
- return true if hom.nil?
192
-
193
- if hom.is_a?(Array)
194
- weekday_modulo_match?(nt, hom)
195
- else
196
- weekday_hash_match?(nt, hom)
197
- end
196
+ @weekdays.nil? ||
197
+ @weekdays.find { |wd, hom|
198
+ (nt.wday == wd) &&
199
+ (hom.nil? ||
200
+ weekday_modulo_match?(nt, hom) ||
201
+ weekday_hash_match?(nt, hom)) }
198
202
  end
199
203
 
200
204
  def monthday_match?(nt)
@@ -727,7 +731,8 @@ module Fugit
727
731
  else
728
732
  a.collect(&:to_s).join('#')
729
733
  end }
730
- .join(',')
734
+ .join(',') +
735
+ (@day_and ? '&' : '')
731
736
  end
732
737
 
733
738
  module Parser include Raabro
@@ -67,11 +67,20 @@ module Fugit
67
67
  day: { a: 'D', r: 'd', i: 'D', s: DAY_S, I: true, l: 'day' },
68
68
  hou: { a: 'h', r: 'h', i: 'H', s: 3600, I: true, l: 'hour' },
69
69
  min: { a: 'm', r: 'm', i: 'M', s: 60, I: true, l: 'minute' },
70
- sec: { a: 's', r: 's', i: 'S', s: 1, I: true, l: 'second' } }.freeze
71
-
72
- INFLA_KEYS, NON_INFLA_KEYS = KEYS
73
- .partition { |k, v| v[:I] }
74
- .collect(&:freeze)
70
+ sec: { a: 's', r: 's', i: 'S', s: 1, I: true, l: 'second' }
71
+ }.freeze
72
+
73
+ #INFLA_KEYS, NON_INFLA_KEYS = KEYS
74
+ # .partition { |k, v| v[:I] }
75
+ # .collect(&:freeze)
76
+ #
77
+ # https://bugs.ruby-lang.org/issues/16252
78
+ #
79
+ kes = KEYS.entries
80
+ INFLA_KEYS,
81
+ NON_INFLA_KEYS =
82
+ kes.select { |_, v| v[:I] }.freeze,
83
+ kes.select { |_, v| ! v[:I] }.freeze
75
84
 
76
85
  def _to_s(key)
77
86
 
data/lib/fugit/nat.rb CHANGED
@@ -54,7 +54,7 @@ module Fugit
54
54
  %w[ zero ] + one_to_nine +
55
55
  %w[ ten eleven twelve thirteen fourteen fifteen sixteen seventeen
56
56
  eighteen nineteen ] +
57
- %w[ twenty thirty fourty fifty ]
57
+ %w[ twenty thirty forty fifty ]
58
58
  .collect { |a|
59
59
  ([ nil ] + one_to_nine)
60
60
  .collect { |b| [ a, b ].compact.join('-') } }
@@ -210,7 +210,7 @@ module Fugit
210
210
  end
211
211
 
212
212
  def ampm(i)
213
- rex(:ampm, i, /[ \t]*(am|pm|noon|midday|midnight)/i)
213
+ rex(:ampm, i, /[ \t]*(am|AM|pm|PM|[Nn]oon|[Mm]idday|[Mm]idnight)/)
214
214
  end
215
215
  def dark(i)
216
216
  rex(:dark, i, /[ \t]*dark/i)
@@ -719,6 +719,8 @@ module Fugit
719
719
  end
720
720
  end
721
721
 
722
+ SLASH_REGEX = /^(\d+|\*)\/(\d+)$/.freeze
723
+
722
724
  # Return nil if the cron is "not strict"
723
725
  #
724
726
  # For example, "0 0/17 * * *" (gh-86) is a perfectly valid
@@ -729,16 +731,18 @@ module Fugit
729
731
  #
730
732
  def restrict(a, cron)
731
733
 
732
- if m = ((a[1] && a[1][0]) || '').match(/^(\d+|\*)\/(\d+)$/)
733
- #p m
734
- sla = m[1].to_i
735
- return nil unless [ 1, 2, 3, 4, 5, 6, 8, 12 ].include?(sla)
734
+ if m = ((a[0] && a[0] != [ 0 ] && a[0][0]) || '').match(SLASH_REGEX)
735
+ return nil unless (1..60).include?(m[1].to_i)
736
+ end
737
+ if m = ((a[1] && a[1][0]) || '').match(SLASH_REGEX)
738
+ return nil unless [ 1, 2, 3, 4, 5, 6, 8, 12 ].include?(m[1].to_i)
736
739
  end
737
740
 
738
741
  cron
739
742
  end
740
743
 
741
744
  def slot(key, default)
745
+
742
746
  s = @slots[key]
743
747
  s ? s.data0 : [ default ]
744
748
  end
data/lib/fugit/parse.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Fugit
4
4
 
5
+ DO_PARSE_ORDER = %i[ cron duration nat at ].freeze
6
+
5
7
  class << self
6
8
 
7
9
  def parse_cron(s); ::Fugit::Cron.parse(s); end
@@ -34,7 +36,7 @@ module Fugit
34
36
  result = nil
35
37
  errors = []
36
38
 
37
- %i[ cron duration nat at ]
39
+ DO_PARSE_ORDER
38
40
  .each { |k|
39
41
  begin
40
42
  result ||= (opts[k] != false && self.send("do_parse_#{k}", s))
@@ -63,6 +65,24 @@ module Fugit
63
65
  fail(ArgumentError.new("not cron or 'natural' cron string: #{s.inspect}"))
64
66
  end
65
67
 
68
+ def parse_max(s, opts={})
69
+
70
+ s0 = s.lines.first
71
+
72
+ (0..[ ::Fugit::Nat::MAX_INPUT_LENGTH, s0.length - 1 ].min).each do |i|
73
+
74
+ s1 =
75
+ s0[0, s0.length - i].rstrip
76
+ f =
77
+ opts[:cronish] ? parse_cronish(s1, opts) :
78
+ parse(s1, opts)
79
+
80
+ return [ s1, f ] if f
81
+ end
82
+
83
+ nil
84
+ end
85
+
66
86
  def determine_type(s)
67
87
 
68
88
  case self.parse(s)
data/lib/fugit.rb CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  module Fugit
5
5
 
6
- VERSION = '1.11.1'
6
+ VERSION = '1.12.1'
7
7
  end
8
8
 
9
9
  require 'time'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fugit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.1
4
+ version: 1.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Mettraux
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-15 00:00:00.000000000 Z
11
+ date: 2025-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: raabro
@@ -30,20 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 1.2.11
33
+ version: '1.4'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
38
  - - "~>"
42
39
  - !ruby/object:Gem::Version
43
- version: '1'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 1.2.11
40
+ version: '1.4'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: rspec
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -97,10 +91,11 @@ licenses:
97
91
  - MIT
98
92
  metadata:
99
93
  changelog_uri: https://github.com/floraison/fugit/blob/master/CHANGELOG.md
100
- documentation_uri: https://github.com/floraison/fugit
101
94
  bug_tracker_uri: https://github.com/floraison/fugit/issues
95
+ documentation_uri: https://github.com/floraison/fugit
102
96
  homepage_uri: https://github.com/floraison/fugit
103
97
  source_code_uri: https://github.com/floraison/fugit
98
+ rubygems_mfa_required: 'true'
104
99
  post_install_message:
105
100
  rdoc_options: []
106
101
  require_paths:
@@ -116,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
111
  - !ruby/object:Gem::Version
117
112
  version: '0'
118
113
  requirements: []
119
- rubygems_version: 3.4.10
114
+ rubygems_version: 3.4.19
120
115
  signing_key:
121
116
  specification_version: 4
122
117
  summary: time tools for flor