fugit 1.8.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: b1839fa07acc6287be95f8076c19afa2c9d6c9b5500ac1b03febab61959b5057
4
- data.tar.gz: 839ff5c0f85ccd51625febfdbfedd017e46962d288cf313307b706830dc838c9
3
+ metadata.gz: 15a3ea665c020e35ce51dcbc82be9a74e6f4abcaa2441316b514c630ffdbc094
4
+ data.tar.gz: f99246722de255325b57ea46f7b3f24bbfc11d65f1eb98fde9a2a43a3c249df0
5
5
  SHA512:
6
- metadata.gz: 5336bf251441fa3f7372d48aec2196cf86831b24c64110955f2ccf2df2e6e1ec84a68b318b53be8282f87794f40c09778bddd47abec3f65fe1469544a758fabb
7
- data.tar.gz: 0fdd70455097838c21c3f03c9dca20889b4aba07f67d790136e40be2d8686b0a6ad8a594fff1fe28e7cd4d322ad66a8a31aa71750cd435c7601345487ae89bdb
6
+ metadata.gz: cc55e7332f5f111ddf27d145fed7fbfeaeed30f492ba343d1f4067e776c96874b42bad9488b9683203040cc50333f74bab56fa6e61744ef92fb059f2a2c9f076
7
+ data.tar.gz: 5ee65240dd04fddb7777bee327f8fee0cfef5260ed55db9ec71ece64c5e5e6b53283aa7d26ba52facbca3882aee0920ff0dd3715fea5ad9153caca82ea712450
data/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
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
+
25
+ ## fugit 1.11.1 released 2024-08-15
26
+
27
+ * Prevent nat parsing chocking on long input (> 256 chars), gh-104
28
+
29
+
30
+ ## fugit 1.11.0 released 2024-04-24
31
+
32
+ * Revert gh-86 ban on `every 27 hours` / `* */27 * * *` for gh-103
33
+
34
+
35
+ ## fugit 1.10.1 released 2024-02-29
36
+
37
+ * Fix on Ruby 2.2.6 thanks to @aunghtain, gh-93
38
+
39
+
40
+ ## fugit 1.10.0 released 2024-02-22
41
+
42
+ * Implement `Fugit::Cron#within(time_start, time_end)`
43
+ * Implement `Fugit::Cron#within(time_range)`
44
+ * Implement iterator-returning `Fugit::Cron#next` and `#prev`
45
+
46
+
47
+ ## fugit 1.9.0 released 2023-10-24
48
+
49
+ * Let nat parse "last", gh-88
50
+ * Change that I am not sure about, gh-86
51
+
52
+
5
53
  ## fugit 1.8.1 released 2023-01-20
6
54
 
7
55
  * Fix for month subtraction, gh-84, @mreinsch
data/CREDITS.md CHANGED
@@ -1,6 +1,18 @@
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
10
+ * https://github.com/personnumber3377, gh-104 Fugit.parse choke on long input
11
+ * Michael Scrivo, https://github.com/mscrivo, gh-103
12
+ * Benjamin Darcet, https://github.com/bdarcet gh-95 gh-96 et-orbi #rweek
13
+ * https://github.com/franckduche gh-95 gh-96 et-orbi #rweek
14
+ * https://hithub.com/aunghtain, gh-93, include oneliner vs Ruby 2.6.6
15
+ * Marcos Belluci, https://github.com/delbetu, gh-88, 1st and last nat
4
16
  * Michael Reinsch, https://github.com/mreinsch, gh-84 and gh-85
5
17
  * Marc Anguera, https://github.com/markets, gh-70 and Sidekiq-Cron
6
18
  * ski-nine, https://github.com/ski-nine, gh-81
data/LICENSE.txt CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- Copyright (c) 2017-2023, 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
@@ -3,7 +3,6 @@
3
3
 
4
4
  [![tests](https://github.com/floraison/fugit/workflows/test/badge.svg)](https://github.com/floraison/fugit/actions)
5
5
  [![Gem Version](https://badge.fury.io/rb/fugit.svg)](http://badge.fury.io/rb/fugit)
6
- [![Join the chat at https://gitter.im/floraison/fugit](https://badges.gitter.im/floraison/fugit.svg)](https://gitter.im/floraison/fugit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7
6
 
8
7
  Time tools for [flor](https://github.com/floraison/flor) and the floraison group.
9
8
 
@@ -18,26 +17,33 @@ Fugit is a core dependency of [rufus-scheduler](https://github.com/jmettraux/ruf
18
17
 
19
18
  The intersection of those two projects is where fugit is born:
20
19
 
21
- * [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
22
- * [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
23
22
 
24
23
  ### Similar, sometimes overlapping projects
25
24
 
26
- * [chronic](https://github.com/mojombo/chronic) - a pure Ruby natural language date parser
27
- * [parse-cron](https://github.com/siebertm/parse-cron) - parses cron expressions and calculates the next occurrence after a given date
28
- * [ice_cube](https://github.com/seejohnrun/ice_cube) - Ruby date recurrence library
29
- * [ISO8601](https://github.com/arnau/ISO8601) - Ruby parser to work with ISO8601 dateTimes and durations
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
30
33
  * ...
31
34
 
32
35
  ### Projects using fugit
33
36
 
34
- * [arask](https://github.com/Ebbe/arask) - "Automatic RAils taSKs" uses fugit to parse cron strings
35
- * [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) - recent versions of Sidekiq-Cron use fugit to parse cron strings
36
- * [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) -
37
- * [flor](https://github.com/floraison/flor) - used in the [cron](https://github.com/floraison/flor/blob/master/doc/procedures/cron.md) procedure
38
- * [que-scheduler](https://github.com/hlascelles/que-scheduler) - a reliable job scheduler for [que](https://github.com/chanks/que)
39
- * [serial_scheduler](https://github.com/grosser/serial_scheduler) - ruby task scheduler without threading
40
- * [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
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
41
47
  * ...
42
48
 
43
49
  ## `Fugit.parse(s)`
@@ -93,7 +99,7 @@ As `Fugit.parse(s)` returns nil when it doesn't grok its input, and `Fugit.do_pa
93
99
 
94
100
  Sometimes you know a cron expression or an "every" natural expression will come in and you want to discard the rest.
95
101
 
96
- ```
102
+ ```ruby
97
103
  require 'fugit'
98
104
 
99
105
  Fugit.parse_cronish('0 0 1 jan *').class # ==> ::Fugit::Cron
@@ -108,6 +114,7 @@ Fugit.parse_cronish('12y12M') # ==> nil
108
114
 
109
115
  Introduced in fugit 1.8.0.
110
116
 
117
+
111
118
  ## `Fugit::Cron`
112
119
 
113
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.
@@ -123,8 +130,48 @@ c = Fugit::Cron.new('0 0 * * sun')
123
130
 
124
131
  p Time.now # => 2017-01-03 09:53:27 +0900
125
132
 
126
- p c.next_time # => 2017-01-08 00:00:00 +0900
127
- p c.previous_time # => 2017-01-01 00:00:00 +0900
133
+ p c.next_time.to_s # => 2017-01-08 00:00:00 +0900
134
+ p c.previous_time.to_s # => 2017-01-01 00:00:00 +0900
135
+
136
+ p c.next_time(Time.parse('2024-06-01')).to_s
137
+ # => "2024-06-02 00:00:00 +0900"
138
+ p c.previous_time(Time.parse('2024-06-01')).to_s
139
+ # => "2024-05-26 00:00:00 +0900"
140
+ #
141
+ # `Fugit::Cron#next_time` and `#previous_time` accept a "start time"
142
+
143
+ c = Fugit.parse_cron('0 12 * * mon#2')
144
+
145
+ # `#next` and `#prev` return Enumerable instances
146
+ #
147
+ # These two methods are available since fugit 1.10.0.
148
+ #
149
+ c.next(Time.parse('2024-02-16 12:00:00'))
150
+ .take(3)
151
+ .map(&:to_s)
152
+ # => [ '2024-03-11 12:00:00',
153
+ # '2024-04-08 12:00:00',
154
+ # '2024-05-13 12:00:00' ]
155
+ c.prev(Time.parse('2024-02-16 12:00:00'))
156
+ .take(3)
157
+ .map(&:to_s)
158
+ # => [ '2024-02-12 12:00:00',
159
+ # '2024-01-08 12:00:00',
160
+ # '2023-12-11 12:00:00' ]
161
+
162
+ # `#within` accepts a time range and returns an array of Eo::EoTime
163
+ # instances that correspond to the occurrences of the cron within
164
+ # the time range
165
+ #
166
+ # This method is available since fugit 1.10.0.
167
+ #
168
+ c.within(Time.parse('2024-02-16 12:00')..Time.parse('2024-08-01 12:00'))
169
+ .map(&:to_s)
170
+ # => [ '2024-03-11 12:00:00',
171
+ # '2024-04-08 12:00:00',
172
+ # '2024-05-13 12:00:00',
173
+ # '2024-06-10 12:00:00',
174
+ # '2024-07-08 12:00:00' ]
128
175
 
129
176
  p c.brute_frequency # => [ 604800, 604800, 53 ]
130
177
  # [ delta min, delta max, occurrence count ]
@@ -160,13 +207,28 @@ Example of cron strings understood by fugit:
160
207
 
161
208
  Please note that `'15/30 * * * *'` is interpreted as `'15-59/30 * * * *'` since fugit 1.4.6.
162
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
+
163
225
  ### the first Monday of the month
164
226
 
165
227
  Fugit tries to follow the `man 5 crontab` documentation.
166
228
 
167
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 `*`).
168
230
 
169
- 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.
170
232
 
171
233
  The man page says:
172
234
 
@@ -212,6 +274,8 @@ p Fugit.parse_cron('59 6 1-7& * 2&').next_time('2020-03-15').to_s
212
274
 
213
275
  Fugit understands `0 5 * * 1#1` or `0 5 * * mon#1` as "each first Monday of the month, at 05:00".
214
276
 
277
+ The hash extension can only be used in the day-of-week field.
278
+
215
279
  ```ruby
216
280
  '0 5 * * 1#1' #
217
281
  '0 5 * * mon#1' # the first Monday of the month at 05:00
@@ -232,125 +296,201 @@ Fugit understands `0 5 * * 1#1` or `0 5 * * mon#1` as "each first Monday of the
232
296
 
233
297
  ### the modulo extension
234
298
 
235
- 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".
300
+
301
+ The modulo extension can only be used in the day-of-week field.
236
302
 
237
303
  For odd Sundays, one can write `9 0 * * sun%2+1`.
238
304
 
239
- It can be combined, as in `9 0 * * sun%2,tue%3+2`
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).
240
306
 
241
- But what does it reference to? It starts at 1 on 2019-01-01.
307
+ What does `sun%2` actually mean?
308
+ ```ruby
309
+ t.wday == 0 && t.rweek % 2 == 0
310
+ ```
242
311
 
312
+ What does `tue%3+2` mean?
243
313
  ```ruby
244
- require 'et-orbi' # >= 1.1.8
314
+ t.wday == 2 && t.rweek % 3 == 2
315
+ ```
245
316
 
246
- # the reference
247
- p EtOrbi.parse('2019-01-01').wday # => 2
248
- p EtOrbi.parse('2019-01-01').rweek # => 1
249
- p EtOrbi.parse('2019-01-01').rweek % 2 # => 1
317
+ #### et-orbi < 1.4.0 : reference set on Tuesday 2019-01-01
250
318
 
251
- # today (as of this coding...)
252
- p EtOrbi.parse('2019-04-11').wday # => 4
253
- p EtOrbi.parse('2019-04-11').rweek # => 15
254
- p EtOrbi.parse('2019-04-11').rweek % 2 # => 1
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.
255
320
 
256
- c = Fugit.parse('* * * * tue%2')
257
- c.match?('2019-01-01') # => false, since rweek % 2 == 1
258
- c.match?('2019-01-08') # => true, since rweek % 2 == 0
321
+ Consider this iteration through the days around 2019-01-01.
322
+ ```ruby
323
+ require 'fugit'
259
324
 
260
- c = Fugit.parse('* * * * tue%2+1')
261
- c.match?('2019-01-01') # => true, since (rweek + 1) % 2 == 0
262
- c.match?('2019-01-08') # => false, since (rweek + 1) % 2 == 1
325
+ t = EtOrbi.parse('2018-12-28 12:00')
263
326
 
264
- # ...
265
- ```
327
+ 15.times do |i|
266
328
 
267
- `sun%2` matches if Sunday and `current_date.rweek % 2 == 0`
268
- `tue%3+2` matches if Tuesday and `current_date.rweek + 2 % 3 == 0`
269
- `tue%x+y` matches if Tuesday and `current_date.rweek + y % x == 0`
329
+ puts " * %14s / rday: %5d / rweek: %5d" % [
330
+ t.strftime('%F %a'), t.rday, t.rweek ]
270
331
 
332
+ w = t.rweek
333
+ t = t.add(24 * 3600)
334
+ puts if t.rweek != w
271
335
 
272
- ## `Fugit::Duration`
336
+ if i == 7
337
+ puts "\n (...)\n\n"
338
+ t = EtOrbi.parse('2025-10-04 12:00')
339
+ end
340
+ end
341
+ ```
273
342
 
274
- 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).
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
365
+ ```
275
366
 
276
- Provides duration arithmetic tools.
367
+ This [was problematic](https://github.com/floraison/fugit/issues/114), since the week started on, well, Tuesday.
277
368
 
278
- ```ruby
279
- require 'fugit'
280
369
 
281
- d = Fugit::Duration.parse('1y2M1d4h')
370
+ #### et-orbi >= 1.4.0 : reference set on Monday 2018-12-31
282
371
 
283
- p d.to_plain_s # => "1Y2M1D4h"
284
- p d.to_iso_s # => "P1Y2M1DT4H" ISO 8601 duration
285
- p d.to_long_s # => "1 year, 2 months, 1 day, and 4 hours"
372
+ Since 1.4.0, et-orbi starts by default on Monday (2018-12-31), as rday 0 with rweek 0.
286
373
 
287
- d += Fugit::Duration.parse('1y1h')
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
+ ```
288
397
 
289
- p d.to_long_s # => "2 years, 2 months, 1 day, and 5 hours"
290
398
 
291
- d += 3600
399
+ #### modulo and et-orbi >= 1.4.0 sanity check
292
400
 
293
- p d.to_plain_s # => "2Y2M1D5h3600s"
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.
294
402
 
295
- p Fugit::Duration.parse('1y2M1d4h').to_sec # => 36820800
296
- ```
403
+ ```ruby
404
+ require 'fugit'
297
405
 
298
- There is a `#deflate` method
406
+ c = Fugit.parse_cron('0 12 * * mon%2,wed%3+1')
299
407
 
300
- ```ruby
301
- Fugit::Duration.parse(1000).to_plain_s # => "1000s"
302
- Fugit::Duration.parse(3600).to_plain_s # => "3600s"
303
- Fugit::Duration.parse(1000).deflate.to_plain_s # => "16m40s"
304
- Fugit::Duration.parse(3600).deflate.to_plain_s # => "1h"
408
+ t = EtOrbi.parse('2025-09-20 12:00')
305
409
 
306
- # or event shorter
307
- Fugit.parse(1000).deflate.to_plain_s # => "16m40s"
308
- Fugit.parse(3600).deflate.to_plain_s # => "1h"
309
- ```
410
+ 44.times do
310
411
 
311
- There is also an `#inflate` method
412
+ wd = t.strftime('%a')
413
+ wd = %w[ Mon Wed ].include?(wd) ? '*' + wd.upcase : ' ' + wd.downcase
312
414
 
313
- ```ruby
314
- Fugit::Duration.parse('1h30m12').inflate.to_plain_s # => "5412s"
315
- Fugit.parse('1h30m12').inflate.to_plain_s # => "5412s"
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 }
316
421
 
317
- Fugit.parse('1h30m12').to_sec # => 5412
318
- Fugit.parse('1h30m12').to_sec.to_s + 's' # => "5412s"
422
+ w = t.rweek
423
+ t = t.add(24 * 3600)
424
+ puts if t.rweek != w
425
+ end
319
426
  ```
320
427
 
321
- The `to_*_s` methods are also available as class methods:
322
- ```ruby
323
- p Fugit::Duration.to_plain_s('1y2M1d4h')
324
- # => "1Y2M1D4h"
325
- p Fugit::Duration.to_iso_s('1y2M1d4h')
326
- # => "P1Y2M1DT4H" ISO 8601 duration
327
- p Fugit::Duration.to_long_s('1y2M1d4h')
328
- # => "1 year, 2 months, 1 day, and 4 hours"
428
+ Here's the output:
429
+ ```
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
329
480
  ```
330
481
 
331
- ## `Fugit::At`
332
-
333
- Points in time are parsed and given back as EtOrbi::EoTime instances.
334
482
 
335
- ```ruby
336
- Fugit::At.parse('2017-12-12').to_s
337
- # ==> "2017-12-12 00:00:00 +0900" (at least here in Hiroshima)
483
+ ### the second extension
338
484
 
339
- Fugit::At.parse('2017-12-12 12:00:00 America/New_York').to_s
340
- # ==> "2017-12-12 12:00:00 -0500"
341
- ```
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`.
342
486
 
343
- Directly with `Fugit.parse_at(s)` is OK too:
344
487
  ```ruby
345
- Fugit.parse_at('2017-12-12 12:00:00 America/New_York').to_s
346
- # ==> "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
347
492
  ```
348
493
 
349
- Directly with `Fugit.parse(s)` is OK too:
350
- ```ruby
351
- Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s
352
- # ==> "2017-12-12 12:00:00 -0500"
353
- ```
354
494
 
355
495
  ## `Fugit::Nat`
356
496
 
@@ -402,7 +542,8 @@ Fugit::Nat.parse('every day at 16:15 and 18:30', multi: true)
402
542
  # ==> [ '15 16 * * *', '30 18 * * *' ] (two Fugit::Cron instances)
403
543
 
404
544
  Fugit::Nat.parse('every day at 16:15 and 18:30', multi: :fail)
405
- # ==> ArgumentError: multiple crons in "every day at 16:15 and 18:30" (15 16 * * * | 30 18 * * *)
545
+ # ==> ArgumentError: multiple crons in "every day at 16:15 and 18:30"
546
+ # (15 16 * * * | 30 18 * * *)
406
547
  Fugit::Nat.parse('every day at 16:15 nada 18:30', multi: true)
407
548
  # ==> nil
408
549
  ```
@@ -413,6 +554,8 @@ Fugit::Nat.parse('every day at 16:15 nada 18:30', multi: true)
413
554
 
414
555
  `multi: false` is the default behaviour, return a single `Fugit::Cron` instance or nil when it cannot parse.
415
556
 
557
+ Please note that "nat" input is limited to 256 characters (fugit 1.11.1).
558
+
416
559
  ### Nat Midnight
417
560
 
418
561
  `"Every day at midnight"` is supported, but `"Every monday at midnight"` will be interpreted (as of Fugit <= 1.4.x) as `"Every monday at 00:00"`. Sorry about that.
@@ -448,6 +591,96 @@ p Fugit.parse('every day at 12:15 midnight').original # ==> "15 24 * * *"
448
591
  ```
449
592
 
450
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
+
451
684
  ## LICENSE
452
685
 
453
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.7'
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,23 +32,42 @@ 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
- return nil unless h
44
-
45
- self.allocate.send(:init, s, h)
51
+ self.allocate.send(:init, s0, h)
46
52
  end
47
53
 
48
54
  def do_parse(s)
49
55
 
50
56
  parse(s) ||
51
- fail(ArgumentError.new("invalid cron string #{s.inspect}"))
57
+ fail(ArgumentError.new("invalid cron string #{trunc(s)}"))
58
+ end
59
+
60
+ protected
61
+
62
+ def trunc(s)
63
+
64
+ if s.is_a?(String)
65
+ r = s.length > 28 ? s[0, 28] + "... len #{s.length}" : s
66
+ r.inspect
67
+ else
68
+ r = s.inspect
69
+ r.length > 35 ? s[0, 35] + '...' : r
70
+ end
52
71
  end
53
72
  end
54
73
 
@@ -156,34 +175,30 @@ module Fugit
156
175
 
157
176
  def weekday_hash_match?(nt, hsh)
158
177
 
178
+ return false unless hsh.is_a?(Integer)
179
+
159
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
160
184
 
161
- if hsh > 0
162
- hsh == phsh # positive wday, from the beginning of the month
163
- else
164
- hsh == nhsh # negative wday, from the end of the month, -1 == last
165
- end
185
+ (hsh == phsh) || (hsh == nhsh)
166
186
  end
167
187
 
168
188
  def weekday_modulo_match?(nt, mod)
169
189
 
170
- (nt.rweek % mod[0]) == (mod[1] % mod[0])
190
+ mod.is_a?(Array) &&
191
+ ((nt.rweek % mod[0]) == (mod[1] % mod[0]))
171
192
  end
172
193
 
173
194
  def weekday_match?(nt)
174
195
 
175
- return true if @weekdays.nil?
176
-
177
- wd, hom = @weekdays.find { |d, _| d == nt.wday }
178
-
179
- return false unless wd
180
- return true if hom.nil?
181
-
182
- if hom.is_a?(Array)
183
- weekday_modulo_match?(nt, hom)
184
- else
185
- weekday_hash_match?(nt, hom)
186
- 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)) }
187
202
  end
188
203
 
189
204
  def monthday_match?(nt)
@@ -266,6 +281,8 @@ module Fugit
266
281
  "please fill an issue at https://git.io/fjJC9"
267
282
  ) if (i += 1) > MAX_ITERATION_COUNT
268
283
 
284
+ #tt = t.time;
285
+ #puts " #{tt.strftime('%F %T %:z %A')} #{tt.rweek} #{tt.rweek % 2}"
269
286
  (ifrom == t.to_i) && (t.inc(1); next)
270
287
  month_match?(t) || (t.inc_month; next)
271
288
  day_match?(t) || (t.inc_day; next)
@@ -309,6 +326,8 @@ module Fugit
309
326
  "please fill an issue at https://git.io/fjJCQ"
310
327
  ) if (i += 1) > MAX_ITERATION_COUNT
311
328
 
329
+ #tt = t.time;
330
+ #puts " #{tt.strftime('%F %T %:z %A')} #{tt.rweek} #{tt.rweek % 4}"
312
331
  month_match?(t) || (t.dec_month; next)
313
332
  day_match?(t) || (t.dec_day; next)
314
333
  hour_match?(t) || (t.dec_hour; next)
@@ -320,6 +339,60 @@ module Fugit
320
339
  t.time.translate(from.zone)
321
340
  end
322
341
 
342
+ # Used by Fugit::Cron#next and Fugit::Cron#prev
343
+ #
344
+ class CronIterator
345
+ include ::Enumerable
346
+
347
+ attr_reader :cron, :start, :current, :direction
348
+
349
+ def initialize(cron, direction, start)
350
+
351
+ @cron = cron
352
+ @start = start
353
+ @current = start.dup
354
+ @direction = direction
355
+ end
356
+
357
+ def each
358
+
359
+ loop do
360
+
361
+ yield(@current = @cron.send(@direction, @current))
362
+ end
363
+ end
364
+ end
365
+
366
+ # Returns an ::Enumerable instance that yields each "next time" in
367
+ # succession
368
+ #
369
+ def next(from=::EtOrbi::EoTime.now)
370
+
371
+ CronIterator.new(self, :next_time, from)
372
+ end
373
+
374
+ # Returns an ::Enumerable instance that yields each "previous time" in
375
+ # succession
376
+ #
377
+ def prev(from=::EtOrbi::EoTime.now)
378
+
379
+ CronIterator.new(self, :previous_time, from)
380
+ end
381
+
382
+ # Returns an array of EtOrbi::EoTime instances that correspond to
383
+ # the occurrences of the cron within the given time range
384
+ #
385
+ def within(time_range, time_end=nil)
386
+
387
+ sta, ned =
388
+ time_range.is_a?(::Range) ? [ time_range.begin, time_range.end ] :
389
+ [ ::EtOrbi.make_time(time_range), ::EtOrbi.make_time(time_end) ]
390
+
391
+ CronIterator
392
+ .new(self, :next_time, sta)
393
+ .take_while { |eot| eot.to_t < ned }
394
+ end
395
+
323
396
  # Mostly used as a #next_time sanity check.
324
397
  # Avoid for "business" use, it's slow.
325
398
  #
@@ -488,18 +561,22 @@ module Fugit
488
561
 
489
562
  def init(original, h)
490
563
 
564
+ return nil unless h
565
+
491
566
  @original = original
492
567
  @cron_s = nil # just to be sure
493
568
  @day_and = h[:&]
494
569
 
495
- determine_seconds(h[:sec])
496
- determine_minutes(h[:min])
497
- determine_hours(h[:hou])
498
- determine_monthdays(h[:dom])
499
- determine_months(h[:mon])
500
- determine_weekdays(h[:dow])
501
- determine_timezone(h[:tz])
570
+ valid =
571
+ determine_seconds(h[:sec]) &&
572
+ determine_minutes(h[:min]) &&
573
+ determine_hours(h[:hou]) &&
574
+ determine_monthdays(h[:dom]) &&
575
+ determine_months(h[:mon]) &&
576
+ determine_weekdays(h[:dow]) &&
577
+ determine_timezone(h[:tz])
502
578
 
579
+ return nil unless valid
503
580
  return nil unless compact_month_days
504
581
 
505
582
  self
@@ -509,10 +586,15 @@ module Fugit
509
586
 
510
587
  sta, edn, sla = r
511
588
 
589
+ #return false if sla && sla > max
590
+ #
591
+ # let it go, "* */24 * * *" and "* */27 * * *" are okay
592
+ # gh-86 and gh-103
593
+
512
594
  edn = max if sla && edn.nil?
513
595
 
514
- return [ nil ] if sta.nil? && edn.nil? && sla.nil?
515
- return [ sta ] if sta && edn.nil?
596
+ return nil if sta.nil? && edn.nil? && sla.nil?
597
+ return sta if sta && edn.nil?
516
598
 
517
599
  sla = 1 if sla == nil
518
600
  sta = min if sta == nil
@@ -563,42 +645,41 @@ module Fugit
563
645
  .uniq
564
646
  end
565
647
 
566
- def compact(key)
648
+ def do_determine(key, arr, min, max)
567
649
 
568
- arr = instance_variable_get(key)
650
+ null = false
569
651
 
570
- return instance_variable_set(key, nil) if arr.include?(nil)
571
- # reductio ad astrum
652
+ r = arr
653
+ .collect { |v|
654
+ expand(min, max, v) }
655
+ .flatten(1)
656
+ .collect { |e|
657
+ return false if e == false
658
+ null = null || e == nil
659
+ (key == :hours && e == 24) ? 0 : e }
572
660
 
573
- arr.uniq!
574
- arr.sort!
661
+ return nil if null
662
+ r.uniq.sort
575
663
  end
576
664
 
577
665
  def determine_seconds(arr)
578
- @seconds = (arr || [ 0 ]).inject([]) { |a, s| a.concat(expand(0, 59, s)) }
579
- compact(:@seconds)
666
+ (@seconds = do_determine(:seconds, arr || [ 0 ], 0, 59)) != false
580
667
  end
581
668
 
582
669
  def determine_minutes(arr)
583
- @minutes = arr.inject([]) { |a, m| a.concat(expand(0, 59, m)) }
584
- compact(:@minutes)
670
+ (@minutes = do_determine(:minutes, arr, 0, 59)) != false
585
671
  end
586
672
 
587
673
  def determine_hours(arr)
588
- @hours = arr
589
- .inject([]) { |a, h| a.concat(expand(0, 23, h)) }
590
- .collect { |h| h == 24 ? 0 : h }
591
- compact(:@hours)
674
+ (@hours = do_determine(:hours, arr, 0, 23)) != false
592
675
  end
593
676
 
594
677
  def determine_monthdays(arr)
595
- @monthdays = arr.inject([]) { |a, d| a.concat(expand(1, 31, d)) }
596
- compact(:@monthdays)
678
+ (@monthdays = do_determine(:monthdays, arr, 1, 31)) != false
597
679
  end
598
680
 
599
681
  def determine_months(arr)
600
- @months = arr.inject([]) { |a, m| a.concat(expand(1, 12, m)) }
601
- compact(:@months)
682
+ (@months = do_determine(:months, arr, 1, 12)) != false
602
683
  end
603
684
 
604
685
  def determine_weekdays(arr)
@@ -624,11 +705,15 @@ module Fugit
624
705
  @weekdays.uniq!
625
706
  @weekdays.sort!
626
707
  @weekdays = nil if @weekdays.empty?
708
+
709
+ true
627
710
  end
628
711
 
629
712
  def determine_timezone(z)
630
713
 
631
714
  @zone, @timezone = z
715
+
716
+ true
632
717
  end
633
718
 
634
719
  def weekdays_to_cron_s
@@ -646,7 +731,8 @@ module Fugit
646
731
  else
647
732
  a.collect(&:to_s).join('#')
648
733
  end }
649
- .join(',')
734
+ .join(',') +
735
+ (@day_and ? '&' : '')
650
736
  end
651
737
 
652
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
 
@@ -258,7 +267,7 @@ module Fugit
258
267
  when Numeric then add_numeric(a)
259
268
  when Fugit::Duration then add_duration(a)
260
269
  when String then add_duration(self.class.parse(a))
261
- when ::Time, EtOrbi::EoTime then add_to_time(a)
270
+ when ::Time, ::EtOrbi::EoTime then add_to_time(a)
262
271
  else fail ArgumentError.new(
263
272
  "cannot add #{a.class} instance to a Fugit::Duration")
264
273
  end
data/lib/fugit/nat.rb CHANGED
@@ -7,6 +7,8 @@ module Fugit
7
7
  #
8
8
  module Nat
9
9
 
10
+ MAX_INPUT_LENGTH = 256
11
+
10
12
  class << self
11
13
 
12
14
  def parse(s, opts={})
@@ -17,6 +19,16 @@ module Fugit
17
19
 
18
20
  s = s.strip
19
21
 
22
+ if s.length > MAX_INPUT_LENGTH
23
+
24
+ fail ArgumentError.new(
25
+ "input too long for a nat string, " +
26
+ "#{s.length} > #{MAX_INPUT_LENGTH}"
27
+ ) if opts[:do_parse]
28
+
29
+ return nil
30
+ end
31
+
20
32
  #p s; Raabro.pp(Parser.parse(s, debug: 3), colours: true)
21
33
  #(p s; Raabro.pp(Parser.parse(s, debug: 1), colours: true)) rescue nil
22
34
 
@@ -29,7 +41,7 @@ module Fugit
29
41
 
30
42
  def do_parse(s, opts={})
31
43
 
32
- parse(s, opts) ||
44
+ parse(s, opts.merge(do_parse: true)) ||
33
45
  fail(ArgumentError.new("could not parse a nat #{s.inspect}"))
34
46
  end
35
47
  end
@@ -42,7 +54,7 @@ module Fugit
42
54
  %w[ zero ] + one_to_nine +
43
55
  %w[ ten eleven twelve thirteen fourteen fifteen sixteen seventeen
44
56
  eighteen nineteen ] +
45
- %w[ twenty thirty fourty fifty ]
57
+ %w[ twenty thirty forty fifty ]
46
58
  .collect { |a|
47
59
  ([ nil ] + one_to_nine)
48
60
  .collect { |b| [ a, b ].compact.join('-') } }
@@ -198,7 +210,7 @@ module Fugit
198
210
  end
199
211
 
200
212
  def ampm(i)
201
- 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)/)
202
214
  end
203
215
  def dark(i)
204
216
  rex(:dark, i, /[ \t]*dark/i)
@@ -642,9 +654,9 @@ module Fugit
642
654
  fail(ArgumentError.new(
643
655
  "multiple crons in #{opts[:_s].inspect} - #{@slots.inspect}"))
644
656
  elsif multi == true
645
- hms.collect { |hm| parse_cron(hm) }
657
+ hms.collect { |hm| parse_cron(hm, opts) }
646
658
  else
647
- parse_cron(hms.first)
659
+ parse_cron(hms.first, opts)
648
660
  end
649
661
  end
650
662
 
@@ -678,7 +690,7 @@ module Fugit
678
690
  .to_a
679
691
  end
680
692
 
681
- def parse_cron(hm)
693
+ def parse_cron(hm, opts)
682
694
 
683
695
  a = [
684
696
  slot(:second, '0'),
@@ -691,14 +703,46 @@ module Fugit
691
703
  a << tz.data0 if tz
692
704
  a.shift if a.first == [ '0' ]
693
705
 
706
+ letters_last = lambda { |x| x.is_a?(Numeric) ? x : 999_999 }
707
+
694
708
  s = a
695
- .collect { |e| e.uniq.sort.collect(&:to_s).join(',') }
709
+ .collect { |e|
710
+ e.uniq.sort_by(&letters_last).collect(&:to_s).join(',') }
696
711
  .join(' ')
697
712
 
698
- Fugit::Cron.parse(s)
713
+ c = Fugit::Cron.parse(s)
714
+
715
+ if opts[:strict]
716
+ restrict(a, c)
717
+ else
718
+ c
719
+ end
720
+ end
721
+
722
+ SLASH_REGEX = /^(\d+|\*)\/(\d+)$/.freeze
723
+
724
+ # Return nil if the cron is "not strict"
725
+ #
726
+ # For example, "0 0/17 * * *" (gh-86) is a perfectly valid
727
+ # cron string, but makes not much sense when derived via `.parse_nat`
728
+ # from "every 17 hours".
729
+ #
730
+ # It happens here because it's nat being strict, not cron.
731
+ #
732
+ def restrict(a, cron)
733
+
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)
739
+ end
740
+
741
+ cron
699
742
  end
700
743
 
701
744
  def slot(key, default)
745
+
702
746
  s = @slots[key]
703
747
  s ? s.data0 : [ default ]
704
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
@@ -20,17 +22,34 @@ module Fugit
20
22
 
21
23
  opts[:at] = opts[:in] if opts.has_key?(:in)
22
24
 
23
- (opts[:cron] != false && parse_cron(s)) ||
24
- (opts[:duration] != false && parse_duration(s)) ||
25
- (opts[:nat] != false && parse_nat(s, opts)) ||
26
- (opts[:at] != false && parse_at(s)) ||
25
+ (opts[:cron] != false && parse_cron(s)) || # 542ms 616ms
26
+ (opts[:duration] != false && parse_duration(s)) || # 645ms # 534ms
27
+ (opts[:nat] != false && parse_nat(s, opts)) || # 2s # 35s
28
+ (opts[:at] != false && parse_at(s)) || # 568ms 622ms
27
29
  nil
28
30
  end
29
31
 
30
32
  def do_parse(s, opts={})
31
33
 
32
- parse(s, opts) ||
33
- fail(ArgumentError.new("found no time information in #{s.inspect}"))
34
+ opts[:at] = opts[:in] if opts.has_key?(:in)
35
+
36
+ result = nil
37
+ errors = []
38
+
39
+ DO_PARSE_ORDER
40
+ .each { |k|
41
+ begin
42
+ result ||= (opts[k] != false && self.send("do_parse_#{k}", s))
43
+ rescue => err
44
+ errors << err
45
+ end }
46
+
47
+ return result if result
48
+
49
+ raise(
50
+ errors.find { |r| r.class != ArgumentError } ||
51
+ errors.first ||
52
+ ArgumentError.new("found no time information in #{s.inspect}"))
34
53
  end
35
54
 
36
55
  def parse_cronish(s, opts={})
@@ -46,6 +65,24 @@ module Fugit
46
65
  fail(ArgumentError.new("not cron or 'natural' cron string: #{s.inspect}"))
47
66
  end
48
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
+
49
86
  def determine_type(s)
50
87
 
51
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.8.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.8.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: 2023-01-20 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.7
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.7
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.1.6
114
+ rubygems_version: 3.4.19
120
115
  signing_key:
121
116
  specification_version: 4
122
117
  summary: time tools for flor