fugit 1.9.0 → 1.11.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: '0099e4aa474e4886e99da33f4b76abd19f259d3a6eb64cd2bfe12f99f6256a23'
4
- data.tar.gz: 38c3fbe7b620cc6460f1b7b7d3e96298400c82d7dec7ef9d4b543925dff6e74a
3
+ metadata.gz: da93feee9bdfd44a793ce2b45b81109c0f7cd08ff9a930d15629a29c6a381e07
4
+ data.tar.gz: 32d9b447734805d3b58ae64c70a6699b931ef4fb4e6a76d8f279e66d4b5cb7df
5
5
  SHA512:
6
- metadata.gz: 75e41e393e779e06ec6280e6e2065877b4771fcca8b63307af13f7fe2407fc9a1fef75eca5a690fe9f3cec6b791deaa4c8b23c925326b24753c5ae8197d5a5bd
7
- data.tar.gz: e9ac1c2b0412a9e2549257b1d73490f4319335c847a6faff212dd8c8a9cbaa52787617491d76648763efbd24caffc39a91e08f06910970905dc810ef9fec36cc
6
+ metadata.gz: e7ac7afec4cfdcfdac22e5d2113892a0c7cb1f790d205bc32ecaea75c30f6ceb73af811824712c6781b480831f2a09479a6344af0d9b3c0ee92ae5cff7ef6c38
7
+ data.tar.gz: d87ecc490a49f19680891a82979903987843b5853ecfdc9ebee57d0b047fa4dffa96dfbbde43ef5d3011dbd07d492ac8f7a151fa6354478050c18efae9b911e4
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
  # CHANGELOG.md
3
3
 
4
4
 
5
+ ## fugit 1.11.1 released 2024-08-15
6
+
7
+ * Prevent nat parsing chocking on long input (> 256 chars), gh-104
8
+
9
+
10
+ ## fugit 1.11.0 released 2024-04-24
11
+
12
+ * Revert gh-86 ban on `every 27 hours` / `* */27 * * *` for gh-103
13
+
14
+
15
+ ## fugit 1.10.1 released 2024-02-29
16
+
17
+ * Fix on Ruby 2.2.6 thanks to @aunghtain, gh-93
18
+
19
+
20
+ ## fugit 1.10.0 released 2024-02-22
21
+
22
+ * Implement `Fugit::Cron#within(time_start, time_end)`
23
+ * Implement `Fugit::Cron#within(time_range)`
24
+ * Implement iterator-returning `Fugit::Cron#next` and `#prev`
25
+
26
+
5
27
  ## fugit 1.9.0 released 2023-10-24
6
28
 
7
29
  * Let nat parse "last", gh-88
data/CREDITS.md CHANGED
@@ -1,7 +1,12 @@
1
1
 
2
2
  # fugit credits
3
3
 
4
- * Marcos Belluci, https://github.com/delbetu, gh-88 , 1st and last nat
4
+ * https://github.com/personnumber3377, gh-104 Fugit.parse choke on long input
5
+ * Michael Scrivo, https://github.com/mscrivo, gh-103
6
+ * Benjamin Darcet, https://github.com/bdarcet gh-95 gh-96 et-orbi #rweek
7
+ * https://github.com/franckduche gh-95 gh-96 et-orbi #rweek
8
+ * https://hithub.com/aunghtain, gh-93, include oneliner vs Ruby 2.6.6
9
+ * Marcos Belluci, https://github.com/delbetu, gh-88, 1st and last nat
5
10
  * Michael Reinsch, https://github.com/mreinsch, gh-84 and gh-85
6
11
  * Marc Anguera, https://github.com/markets, gh-70 and Sidekiq-Cron
7
12
  * 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-2024, 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
@@ -26,17 +26,22 @@ The intersection of those two projects is where fugit is born:
26
26
  * [parse-cron](https://github.com/siebertm/parse-cron) - parses cron expressions and calculates the next occurrence after a given date
27
27
  * [ice_cube](https://github.com/seejohnrun/ice_cube) - Ruby date recurrence library
28
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
29
32
  * ...
30
33
 
31
34
  ### Projects using fugit
32
35
 
33
36
  * [arask](https://github.com/Ebbe/arask) - "Automatic RAils taSKs" uses fugit to parse cron strings
34
- * [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) - recent versions of Sidekiq-Cron use fugit to parse cron strings
35
- * [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) -
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
36
39
  * [flor](https://github.com/floraison/flor) - used in the [cron](https://github.com/floraison/flor/blob/master/doc/procedures/cron.md) procedure
37
40
  * [que-scheduler](https://github.com/hlascelles/que-scheduler) - a reliable job scheduler for [que](https://github.com/chanks/que)
38
41
  * [serial_scheduler](https://github.com/grosser/serial_scheduler) - ruby task scheduler without threading
39
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
40
45
  * ...
41
46
 
42
47
  ## `Fugit.parse(s)`
@@ -92,7 +97,7 @@ As `Fugit.parse(s)` returns nil when it doesn't grok its input, and `Fugit.do_pa
92
97
 
93
98
  Sometimes you know a cron expression or an "every" natural expression will come in and you want to discard the rest.
94
99
 
95
- ```
100
+ ```ruby
96
101
  require 'fugit'
97
102
 
98
103
  Fugit.parse_cronish('0 0 1 jan *').class # ==> ::Fugit::Cron
@@ -122,8 +127,48 @@ c = Fugit::Cron.new('0 0 * * sun')
122
127
 
123
128
  p Time.now # => 2017-01-03 09:53:27 +0900
124
129
 
125
- p c.next_time # => 2017-01-08 00:00:00 +0900
126
- p c.previous_time # => 2017-01-01 00:00:00 +0900
130
+ p c.next_time.to_s # => 2017-01-08 00:00:00 +0900
131
+ p c.previous_time.to_s # => 2017-01-01 00:00:00 +0900
132
+
133
+ p c.next_time(Time.parse('2024-06-01')).to_s
134
+ # => "2024-06-02 00:00:00 +0900"
135
+ p c.previous_time(Time.parse('2024-06-01')).to_s
136
+ # => "2024-05-26 00:00:00 +0900"
137
+ #
138
+ # `Fugit::Cron#next_time` and `#previous_time` accept a "start time"
139
+
140
+ c = Fugit.parse_cron('0 12 * * mon#2')
141
+
142
+ # `#next` and `#prev` return Enumerable instances
143
+ #
144
+ # These two methods are available since fugit 1.10.0.
145
+ #
146
+ c.next(Time.parse('2024-02-16 12:00:00'))
147
+ .take(3)
148
+ .map(&:to_s)
149
+ # => [ '2024-03-11 12:00:00',
150
+ # '2024-04-08 12:00:00',
151
+ # '2024-05-13 12:00:00' ]
152
+ c.prev(Time.parse('2024-02-16 12:00:00'))
153
+ .take(3)
154
+ .map(&:to_s)
155
+ # => [ '2024-02-12 12:00:00',
156
+ # '2024-01-08 12:00:00',
157
+ # '2023-12-11 12:00:00' ]
158
+
159
+ # `#within` accepts a time range and returns an array of Eo::EoTime
160
+ # instances that correspond to the occurrences of the cron within
161
+ # the time range
162
+ #
163
+ # This method is available since fugit 1.10.0.
164
+ #
165
+ c.within(Time.parse('2024-02-16 12:00')..Time.parse('2024-08-01 12:00'))
166
+ .map(&:to_s)
167
+ # => [ '2024-03-11 12:00:00',
168
+ # '2024-04-08 12:00:00',
169
+ # '2024-05-13 12:00:00',
170
+ # '2024-06-10 12:00:00',
171
+ # '2024-07-08 12:00:00' ]
127
172
 
128
173
  p c.brute_frequency # => [ 604800, 604800, 53 ]
129
174
  # [ delta min, delta max, occurrence count ]
@@ -211,6 +256,8 @@ p Fugit.parse_cron('59 6 1-7& * 2&').next_time('2020-03-15').to_s
211
256
 
212
257
  Fugit understands `0 5 * * 1#1` or `0 5 * * mon#1` as "each first Monday of the month, at 05:00".
213
258
 
259
+ The hash extension can only be used in the day-of-week field.
260
+
214
261
  ```ruby
215
262
  '0 5 * * 1#1' #
216
263
  '0 5 * * mon#1' # the first Monday of the month at 05:00
@@ -233,6 +280,8 @@ Fugit understands `0 5 * * 1#1` or `0 5 * * mon#1` as "each first Monday of the
233
280
 
234
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".
235
282
 
283
+ The modulo extension can only be used in the day-of-week field.
284
+
236
285
  For odd Sundays, one can write `9 0 * * sun%2+1`.
237
286
 
238
287
  It can be combined, as in `9 0 * * sun%2,tue%3+2`
@@ -268,6 +317,18 @@ c.match?('2019-01-08') # => false, since (rweek + 1) % 2 == 1
268
317
  `tue%x+y` matches if Tuesday and `current_date.rweek + y % x == 0`
269
318
 
270
319
 
320
+ ### the second extension
321
+
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`.
323
+
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
329
+ ```
330
+
331
+
271
332
  ## `Fugit::Duration`
272
333
 
273
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).
@@ -401,7 +462,8 @@ Fugit::Nat.parse('every day at 16:15 and 18:30', multi: true)
401
462
  # ==> [ '15 16 * * *', '30 18 * * *' ] (two Fugit::Cron instances)
402
463
 
403
464
  Fugit::Nat.parse('every day at 16:15 and 18:30', multi: :fail)
404
- # ==> ArgumentError: multiple crons in "every day at 16:15 and 18:30" (15 16 * * * | 30 18 * * *)
465
+ # ==> ArgumentError: multiple crons in "every day at 16:15 and 18:30"
466
+ # (15 16 * * * | 30 18 * * *)
405
467
  Fugit::Nat.parse('every day at 16:15 nada 18:30', multi: true)
406
468
  # ==> nil
407
469
  ```
@@ -412,6 +474,8 @@ Fugit::Nat.parse('every day at 16:15 nada 18:30', multi: true)
412
474
 
413
475
  `multi: false` is the default behaviour, return a single `Fugit::Cron` instance or nil when it cannot parse.
414
476
 
477
+ Please note that "nat" input is limited to 256 characters (fugit 1.11.1).
478
+
415
479
  ### Nat Midnight
416
480
 
417
481
  `"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.
data/fugit.gemspec CHANGED
@@ -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', '>= 1.2.11'
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
@@ -46,7 +46,20 @@ module Fugit
46
46
  def do_parse(s)
47
47
 
48
48
  parse(s) ||
49
- fail(ArgumentError.new("invalid cron string #{s.inspect}"))
49
+ fail(ArgumentError.new("invalid cron string #{trunc(s)}"))
50
+ end
51
+
52
+ protected
53
+
54
+ def trunc(s)
55
+
56
+ if s.is_a?(String)
57
+ r = s.length > 28 ? s[0, 28] + "... len #{s.length}" : s
58
+ r.inspect
59
+ else
60
+ r = s.inspect
61
+ r.length > 35 ? s[0, 35] + '...' : r
62
+ end
50
63
  end
51
64
  end
52
65
 
@@ -264,6 +277,8 @@ module Fugit
264
277
  "please fill an issue at https://git.io/fjJC9"
265
278
  ) if (i += 1) > MAX_ITERATION_COUNT
266
279
 
280
+ #tt = t.time;
281
+ #puts " #{tt.strftime('%F %T %:z %A')} #{tt.rweek} #{tt.rweek % 2}"
267
282
  (ifrom == t.to_i) && (t.inc(1); next)
268
283
  month_match?(t) || (t.inc_month; next)
269
284
  day_match?(t) || (t.inc_day; next)
@@ -307,6 +322,8 @@ module Fugit
307
322
  "please fill an issue at https://git.io/fjJCQ"
308
323
  ) if (i += 1) > MAX_ITERATION_COUNT
309
324
 
325
+ #tt = t.time;
326
+ #puts " #{tt.strftime('%F %T %:z %A')} #{tt.rweek} #{tt.rweek % 4}"
310
327
  month_match?(t) || (t.dec_month; next)
311
328
  day_match?(t) || (t.dec_day; next)
312
329
  hour_match?(t) || (t.dec_hour; next)
@@ -318,6 +335,60 @@ module Fugit
318
335
  t.time.translate(from.zone)
319
336
  end
320
337
 
338
+ # Used by Fugit::Cron#next and Fugit::Cron#prev
339
+ #
340
+ class CronIterator
341
+ include ::Enumerable
342
+
343
+ attr_reader :cron, :start, :current, :direction
344
+
345
+ def initialize(cron, direction, start)
346
+
347
+ @cron = cron
348
+ @start = start
349
+ @current = start.dup
350
+ @direction = direction
351
+ end
352
+
353
+ def each
354
+
355
+ loop do
356
+
357
+ yield(@current = @cron.send(@direction, @current))
358
+ end
359
+ end
360
+ end
361
+
362
+ # Returns an ::Enumerable instance that yields each "next time" in
363
+ # succession
364
+ #
365
+ def next(from=::EtOrbi::EoTime.now)
366
+
367
+ CronIterator.new(self, :next_time, from)
368
+ end
369
+
370
+ # Returns an ::Enumerable instance that yields each "previous time" in
371
+ # succession
372
+ #
373
+ def prev(from=::EtOrbi::EoTime.now)
374
+
375
+ CronIterator.new(self, :previous_time, from)
376
+ end
377
+
378
+ # Returns an array of EtOrbi::EoTime instances that correspond to
379
+ # the occurrences of the cron within the given time range
380
+ #
381
+ def within(time_range, time_end=nil)
382
+
383
+ sta, ned =
384
+ time_range.is_a?(::Range) ? [ time_range.begin, time_range.end ] :
385
+ [ ::EtOrbi.make_time(time_range), ::EtOrbi.make_time(time_end) ]
386
+
387
+ CronIterator
388
+ .new(self, :next_time, sta)
389
+ .take_while { |eot| eot.to_t < ned }
390
+ end
391
+
321
392
  # Mostly used as a #next_time sanity check.
322
393
  # Avoid for "business" use, it's slow.
323
394
  #
@@ -511,7 +582,10 @@ module Fugit
511
582
 
512
583
  sta, edn, sla = r
513
584
 
514
- return false if sla && sla > max
585
+ #return false if sla && sla > max
586
+ #
587
+ # let it go, "* */24 * * *" and "* */27 * * *" are okay
588
+ # gh-86 and gh-103
515
589
 
516
590
  edn = max if sla && edn.nil?
517
591
 
@@ -258,7 +258,7 @@ module Fugit
258
258
  when Numeric then add_numeric(a)
259
259
  when Fugit::Duration then add_duration(a)
260
260
  when String then add_duration(self.class.parse(a))
261
- when ::Time, EtOrbi::EoTime then add_to_time(a)
261
+ when ::Time, ::EtOrbi::EoTime then add_to_time(a)
262
262
  else fail ArgumentError.new(
263
263
  "cannot add #{a.class} instance to a Fugit::Duration")
264
264
  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
data/lib/fugit/parse.rb CHANGED
@@ -20,17 +20,34 @@ module Fugit
20
20
 
21
21
  opts[:at] = opts[:in] if opts.has_key?(:in)
22
22
 
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)) ||
23
+ (opts[:cron] != false && parse_cron(s)) || # 542ms 616ms
24
+ (opts[:duration] != false && parse_duration(s)) || # 645ms # 534ms
25
+ (opts[:nat] != false && parse_nat(s, opts)) || # 2s # 35s
26
+ (opts[:at] != false && parse_at(s)) || # 568ms 622ms
27
27
  nil
28
28
  end
29
29
 
30
30
  def do_parse(s, opts={})
31
31
 
32
- parse(s, opts) ||
33
- fail(ArgumentError.new("found no time information in #{s.inspect}"))
32
+ opts[:at] = opts[:in] if opts.has_key?(:in)
33
+
34
+ result = nil
35
+ errors = []
36
+
37
+ %i[ cron duration nat at ]
38
+ .each { |k|
39
+ begin
40
+ result ||= (opts[k] != false && self.send("do_parse_#{k}", s))
41
+ rescue => err
42
+ errors << err
43
+ end }
44
+
45
+ return result if result
46
+
47
+ raise(
48
+ errors.find { |r| r.class != ArgumentError } ||
49
+ errors.first ||
50
+ ArgumentError.new("found no time information in #{s.inspect}"))
34
51
  end
35
52
 
36
53
  def parse_cronish(s, opts={})
data/lib/fugit.rb CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  module Fugit
5
5
 
6
- VERSION = '1.9.0'
6
+ VERSION = '1.11.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.9.0
4
+ version: 1.11.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-10-23 00:00:00.000000000 Z
11
+ date: 2024-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: raabro
@@ -33,7 +33,7 @@ dependencies:
33
33
  version: '1'
34
34
  - - ">="
35
35
  - !ruby/object:Gem::Version
36
- version: 1.2.7
36
+ version: 1.2.11
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '1'
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 1.2.7
46
+ version: 1.2.11
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -116,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  requirements: []
119
- rubygems_version: 3.2.33
119
+ rubygems_version: 3.4.10
120
120
  signing_key:
121
121
  specification_version: 4
122
122
  summary: time tools for flor