fugit 1.12.1 → 1.13.0

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: 15a3ea665c020e35ce51dcbc82be9a74e6f4abcaa2441316b514c630ffdbc094
4
- data.tar.gz: f99246722de255325b57ea46f7b3f24bbfc11d65f1eb98fde9a2a43a3c249df0
3
+ metadata.gz: 15c7da961f6616177b4837e7db2b8d2cbef8a73af0e39d482bbde88ca927d420
4
+ data.tar.gz: bdb787682ba39f3bd873c935378d7097291ff275c9c2443a4bf42990e71dfa92
5
5
  SHA512:
6
- metadata.gz: cc55e7332f5f111ddf27d145fed7fbfeaeed30f492ba343d1f4067e776c96874b42bad9488b9683203040cc50333f74bab56fa6e61744ef92fb059f2a2c9f076
7
- data.tar.gz: 5ee65240dd04fddb7777bee327f8fee0cfef5260ed55db9ec71ece64c5e5e6b53283aa7d26ba52facbca3882aee0920ff0dd3715fea5ad9153caca82ea712450
6
+ metadata.gz: c920de689a07b8e5b082afa2d67ff26a2ad3422e0a35afc0c02fee71d92d6a76da391d7ec6d03e55d62cbc1a46cbf1296bccb4a37ec9df9d72f35d3e4ecaf0ad
7
+ data.tar.gz: 11694200215f7506f64f862d7f63e86b3fc79be5ad4a4ddfe9278143c40991bcd2c89356e9e314d5caad125b0b26623daf3da64ee2e92b49ca1480b59fc220f3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
  # CHANGELOG.md
3
3
 
4
4
 
5
+ ## fugit 1.13.0 released 2026-07-10
6
+
7
+ * Random values support in cron string `"10 ~ * * *"` gh-121 Nicols Bachschmidt
8
+
9
+
10
+ ## fugit 1.12.3 released 2026-07-01
11
+
12
+ * Wrap-around for weekday ranges with step value, gh-119 Nicolas Bachschmidt
13
+
14
+
15
+ ## fugit 1.12.2 released 2026-05-28
16
+
17
+ * Fix "divide by zero" gh-117
18
+ * Let at parse 'now', gh-111
19
+ * Let Fugit::At pass options to Chronic, gh-116
20
+
21
+
5
22
  ## fugit 1.12.1 released 2025-10-14
6
23
 
7
24
  * Fix Fugit::Cron#to_cron_s to include & if @day_and
data/CREDITS.md CHANGED
@@ -1,12 +1,14 @@
1
1
 
2
2
  # fugit credits
3
3
 
4
+ * Nicolas Bachschmidt https://github.com/baarde gh-120 weekday wrap fix
5
+ * JebeenLee https://github.com/JebeenLee gh-117 divide by zero
4
6
  * Eric Claerhout https://github.com/swebra gh-114 rweek readme clarity
5
7
  * Hugh Kelsey https://github.com/hughkelsey gh-114 rweek/rday / wed%4+1,wed%4
6
8
  * Mark R. James, https://github.com/mrj, AM vs America/Los_Angeles, gh-113
7
9
  * Tejas Bubane, https://github.com/tejasbubane, r3.4 in test matrix, gh-109
8
10
  * Luis Castillo, https://github.com/lekastillo, nice_hash gh-108
9
- * Geremia Taglialatela, https://github.com/tagliala, gh-105 gh-107
11
+ * Geremia Taglialatela, https://github.com/tagliala, gh-105 gh-107 gh-122
10
12
  * https://github.com/personnumber3377, gh-104 Fugit.parse choke on long input
11
13
  * Michael Scrivo, https://github.com/mscrivo, gh-103
12
14
  * 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-2025, John Mettraux, jmettraux+flor@gmail.com
2
+ Copyright (c) 2017-2026, 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
@@ -1,6 +1,12 @@
1
1
 
2
2
  # fugit
3
3
 
4
+ > Sed fugit interea, fugit irreparabile tempus
5
+
6
+ Virgil, _Georgics_ Book III
7
+
8
+ ---
9
+
4
10
  [![tests](https://github.com/floraison/fugit/workflows/test/badge.svg)](https://github.com/floraison/fugit/actions)
5
11
  [![Gem Version](https://badge.fury.io/rb/fugit.svg)](http://badge.fury.io/rb/fugit)
6
12
 
@@ -480,6 +486,29 @@ Here's the output:
480
486
  ```
481
487
 
482
488
 
489
+ ### the random extension
490
+
491
+ Fugit accepts the `~` character to specify a range from which a random value is picked. Either or both the start and the end of the range may be omitted.
492
+
493
+ ```ruby
494
+ '~ * * * *' # every hour on a random minute
495
+ '~29 * * * *' # every hour on a random minute between 0 and 29
496
+ '30~ * * * *' # every hour on a random minute between 30 and 59
497
+ '~/10 * * * *' # every 10 minutes starting on a random minute between 0 and 9
498
+ '0 12 * * 1~5' # every week at noon on a random workday
499
+ ```
500
+
501
+ You may customize how random values are generated by adding the `:random` option with a boolean or an object that implements the `#rand` method.
502
+
503
+ ```ruby
504
+ Fugit.parse('~ * * * *', random: false) # on a non-random minute ==> '0 * * * *'
505
+ Fugit.parse('~ * * * *', random: true) # on a random minute (default)
506
+ Fugit.parse('~ * * * *', random: SecureRandom) # on a cryptographically secure random minute
507
+ ```
508
+
509
+ Please note that the random values are determined at parse time. So `"~29 * * * *"` at parse time could become `"17 * * * *"` for example and go on triggering every hour at minute 17, until a new parsing occurs and another (or the same) minute is picked. This aligns with the OpenBSD [`man 5 crontab`](https://man.openbsd.org/crontab.5), OpenBSD `crond` rolling the dice at load time.
510
+
511
+
483
512
  ### the second extension
484
513
 
485
514
  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`.
data/fugit.gemspec CHANGED
@@ -45,6 +45,7 @@ Time tools for flor and the floraison project. Cron parsing and occurrence compu
45
45
 
46
46
  s.add_development_dependency 'rspec', '~> 3.8'
47
47
  s.add_development_dependency 'chronic', '~> 0.10'
48
+ s.add_development_dependency 'ostruct'
48
49
 
49
50
  s.require_path = 'lib'
50
51
  end
data/lib/fugit/at.rb CHANGED
@@ -6,14 +6,19 @@ module Fugit
6
6
 
7
7
  class << self
8
8
 
9
- def parse(s)
9
+ def parse(s, opts={})
10
10
 
11
- ::EtOrbi.make_time(s) rescue nil
11
+ do_parse(s, opts || {}) rescue nil
12
12
  end
13
13
 
14
- def do_parse(s)
14
+ def do_parse(s, opts={})
15
15
 
16
- ::EtOrbi.make_time(s)
16
+ case s
17
+ when /^now$/i then ::EtOrbi::EoTime.now
18
+ when String then ::EtOrbi.parse(s, opts || {})
19
+ when ::EtOrbi::EoTime then s
20
+ else ::EtOrbi.make_time(s)
21
+ end
17
22
  end
18
23
  end
19
24
  end
data/lib/fugit/cron.rb CHANGED
@@ -29,7 +29,7 @@ module Fugit
29
29
  parse(original)
30
30
  end
31
31
 
32
- def parse(s)
32
+ def parse(s, opts={})
33
33
 
34
34
  return s if s.is_a?(self)
35
35
  return nil unless s.is_a?(String)
@@ -48,12 +48,12 @@ module Fugit
48
48
  #p s; Raabro.pp(Parser.parse(s, debug: 3), colors: true)
49
49
  h = Parser.parse(s)
50
50
 
51
- self.allocate.send(:init, s0, h)
51
+ self.allocate.send(:init, s0, h, opts)
52
52
  end
53
53
 
54
- def do_parse(s)
54
+ def do_parse(s, opts={})
55
55
 
56
- parse(s) ||
56
+ parse(s, opts) ||
57
57
  fail(ArgumentError.new("invalid cron string #{trunc(s)}"))
58
58
  end
59
59
 
@@ -559,7 +559,7 @@ module Fugit
559
559
 
560
560
  FREQUENCY_CACHE = {}
561
561
 
562
- def init(original, h)
562
+ def init(original, h, opts)
563
563
 
564
564
  return nil unless h
565
565
 
@@ -568,12 +568,12 @@ module Fugit
568
568
  @day_and = h[:&]
569
569
 
570
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]) &&
571
+ determine_seconds(h[:sec], opts) &&
572
+ determine_minutes(h[:min], opts) &&
573
+ determine_hours(h[:hou], opts) &&
574
+ determine_monthdays(h[:dom], opts) &&
575
+ determine_months(h[:mon], opts) &&
576
+ determine_weekdays(h[:dow], opts) &&
577
577
  determine_timezone(h[:tz])
578
578
 
579
579
  return nil unless valid
@@ -582,15 +582,22 @@ module Fugit
582
582
  self
583
583
  end
584
584
 
585
- def expand(min, max, r)
585
+ def expand(min, max, opts, r)
586
586
 
587
- sta, edn, sla = r
587
+ sta, edn, sep, sla = r
588
588
 
589
589
  #return false if sla && sla > max
590
590
  #
591
591
  # let it go, "* */24 * * *" and "* */27 * * *" are okay
592
592
  # gh-86 and gh-103
593
593
 
594
+ return false if sla && sla < 1
595
+
596
+ if sep == :tilde
597
+ sta = random(min, max, sta, edn, sla, opts)
598
+ edn = nil unless sla
599
+ end
600
+
594
601
  edn = max if sla && edn.nil?
595
602
 
596
603
  return nil if sta.nil? && edn.nil? && sla.nil?
@@ -603,6 +610,24 @@ module Fugit
603
610
  range(min, max, sta, edn, sla)
604
611
  end
605
612
 
613
+ def random(min, max, sta, edn, sla, opts)
614
+
615
+ sta = min if sta.nil?
616
+ edn = max if edn.nil?
617
+
618
+ random = opts.fetch(:random, true)
619
+ random = Random if random == true
620
+ return sta unless random
621
+
622
+ n = edn - sta + 1
623
+ n = max - min + 1 + n if n <= 0
624
+ n = sla if sla && sla < n
625
+
626
+ r = sta + random.rand(n)
627
+ r = r + min - max - 1 if r > max
628
+ r
629
+ end
630
+
606
631
  def range(min, max, sta, edn, sla)
607
632
 
608
633
  return [ nil ] if sta == min && edn == max && sla == 1
@@ -645,13 +670,13 @@ module Fugit
645
670
  .uniq
646
671
  end
647
672
 
648
- def do_determine(key, arr, min, max)
673
+ def do_determine(key, arr, min, max, opts)
649
674
 
650
675
  null = false
651
676
 
652
677
  r = arr
653
678
  .collect { |v|
654
- expand(min, max, v) }
679
+ expand(min, max, opts, v) }
655
680
  .flatten(1)
656
681
  .collect { |e|
657
682
  return false if e == false
@@ -662,36 +687,44 @@ module Fugit
662
687
  r.uniq.sort
663
688
  end
664
689
 
665
- def determine_seconds(arr)
666
- (@seconds = do_determine(:seconds, arr || [ 0 ], 0, 59)) != false
690
+ def determine_seconds(arr, opts)
691
+ (@seconds = do_determine(:seconds, arr || [ 0 ], 0, 59, opts)) != false
667
692
  end
668
693
 
669
- def determine_minutes(arr)
670
- (@minutes = do_determine(:minutes, arr, 0, 59)) != false
694
+ def determine_minutes(arr, opts)
695
+ (@minutes = do_determine(:minutes, arr, 0, 59, opts)) != false
671
696
  end
672
697
 
673
- def determine_hours(arr)
674
- (@hours = do_determine(:hours, arr, 0, 23)) != false
698
+ def determine_hours(arr, opts)
699
+ (@hours = do_determine(:hours, arr, 0, 23, opts)) != false
675
700
  end
676
701
 
677
- def determine_monthdays(arr)
678
- (@monthdays = do_determine(:monthdays, arr, 1, 31)) != false
702
+ def determine_monthdays(arr, opts)
703
+ (@monthdays = do_determine(:monthdays, arr, 1, 31, opts)) != false
679
704
  end
680
705
 
681
- def determine_months(arr)
682
- (@months = do_determine(:months, arr, 1, 12)) != false
706
+ def determine_months(arr, opts)
707
+ (@months = do_determine(:months, arr, 1, 12, opts)) != false
683
708
  end
684
709
 
685
- def determine_weekdays(arr)
710
+ def determine_weekdays(arr, opts)
686
711
 
687
712
  @weekdays = []
688
713
 
689
- arr.each do |a, z, sl, ha, mo| # a to z, slash, hash, and mod
714
+ arr.each do |a, z, sp, sl, ha, mo| # a to z, seperator, slash, hash, and mod
715
+ if sp == :tilde
716
+ a = random(0, 6, a, z, sl, opts)
717
+ z = nil unless sl
718
+ end
719
+
690
720
  if ha || mo
691
721
  @weekdays << [ a, ha || mo ]
692
722
  elsif sl
693
- ((a || 0)..(z || (a ? a : 6))).step(sl < 1 ? 1 : sl)
694
- .each { |i| @weekdays << [ i ] }
723
+ z ||= (a ? a : 6)
724
+ a ||= 0
725
+ z = z + 7 if a > z
726
+ (a..z).step(sl < 1 ? 1 : sl)
727
+ .each { |i| @weekdays << [ (i > 6) ? i - 7 : i ] }
695
728
  elsif z
696
729
  z = z + 7 if a > z
697
730
  (a..z).each { |i| @weekdays << [ (i > 6) ? i - 7 : i ] }
@@ -754,7 +787,8 @@ module Fugit
754
787
 
755
788
  def s(i); rex(nil, i, /[ \t]+/); end
756
789
  def star(i); str(nil, i, '*'); end
757
- def hyphen(i); str(nil, i, '-'); end
790
+ def hyphen(i); str(:hyphen, i, '-'); end
791
+ def tilde(i); str(:tilde, i, '~'); end
758
792
  def comma(i); rex(nil, i, /,([ \t]*,)*/); end
759
793
  def comma?(i); rex(nil, i, /([ \t]*,)*/); end
760
794
  def and?(i); rex(nil, i, /&?/); end
@@ -782,12 +816,19 @@ module Fugit
782
816
  def r_mon(i); seq(nil, i, :mon, :_mon, '?'); end
783
817
  def r_dow(i); seq(nil, i, :dow, :_dow, '?'); end
784
818
 
819
+ # t: tilde range
820
+ def t_mos(i); seq(nil, i, :mos, '?', :tilde, :mos, '?'); end
821
+ def t_hou(i); seq(nil, i, :hou, '?', :tilde, :hou, '?'); end
822
+ def t_dom(i); seq(nil, i, :dom, '?', :tilde, :dom, '?'); end
823
+ def t_mon(i); seq(nil, i, :mon, '?', :tilde, :mon, '?'); end
824
+ def t_dow(i); seq(nil, i, :dow, '?', :tilde, :dow, '?'); end
825
+
785
826
  # sor: star or range
786
- def sor_mos(i); alt(nil, i, :star, :r_mos); end
787
- def sor_hou(i); alt(nil, i, :star, :r_hou); end
788
- def sor_dom(i); alt(nil, i, :star, :r_dom); end
789
- def sor_mon(i); alt(nil, i, :star, :r_mon); end
790
- def sor_dow(i); alt(nil, i, :star, :r_dow); end
827
+ def sor_mos(i); alt(nil, i, :star, :t_mos, :r_mos); end
828
+ def sor_hou(i); alt(nil, i, :star, :t_hou, :r_hou); end
829
+ def sor_dom(i); alt(nil, i, :star, :t_dom, :r_dom); end
830
+ def sor_mon(i); alt(nil, i, :star, :t_mon, :r_mon); end
831
+ def sor_dow(i); alt(nil, i, :star, :t_dow, :r_dow); end
791
832
 
792
833
  # sorws: star or range with[out] slash
793
834
  def sorws_mos(i); seq(nil, i, :sor_mos, :slash, '?'); end
@@ -867,12 +908,16 @@ module Fugit
867
908
 
868
909
  def rewrite_elt(k, t)
869
910
 
870
- at, zt, slt, hat, mot = nil; t.subgather(nil).each do |tt|
911
+ at, zt, spt, slt, hat, mot = nil
912
+
913
+ t.subgather(nil).each do |tt|
871
914
  case tt.name
872
915
  when :slash then slt = tt
873
916
  when :hash then hat = tt
874
917
  when :mod then mot = tt
875
- else if at; zt ||= tt; else; at = tt; end
918
+ when :hyphen then spt = tt
919
+ when :tilde then spt = tt
920
+ else if spt; zt ||= tt; else; at = tt; end
876
921
  end
877
922
  end
878
923
 
@@ -886,11 +931,12 @@ module Fugit
886
931
 
887
932
  a = at ? rewrite_bound(k, at) : nil
888
933
  z = zt ? rewrite_bound(k, zt) : nil
934
+ sp = spt ? spt.name : nil
889
935
 
890
936
  #a, z = z, a if a && z && a > z
891
937
  # handled downstream since gh-27
892
938
 
893
- [ a, z, sl, ha, mo ]
939
+ [ a, z, sp, sl, ha, mo ]
894
940
  end
895
941
 
896
942
  def rewrite_entry(t)
data/lib/fugit/nat.rb CHANGED
@@ -710,7 +710,7 @@ module Fugit
710
710
  e.uniq.sort_by(&letters_last).collect(&:to_s).join(',') }
711
711
  .join(' ')
712
712
 
713
- c = Fugit::Cron.parse(s)
713
+ c = Fugit::Cron.parse(s, opts)
714
714
 
715
715
  if opts[:strict]
716
716
  restrict(a, c)
data/lib/fugit/parse.rb CHANGED
@@ -6,15 +6,15 @@ module Fugit
6
6
 
7
7
  class << self
8
8
 
9
- def parse_cron(s); ::Fugit::Cron.parse(s); end
9
+ def parse_cron(s, opts={}); ::Fugit::Cron.parse(s, opts || {}); end
10
10
  def parse_duration(s); ::Fugit::Duration.parse(s); end
11
- def parse_nat(s, opts={}); ::Fugit::Nat.parse(s, opts); end
12
- def parse_at(s); ::Fugit::At.parse(s); end
11
+ def parse_nat(s, opts={}); ::Fugit::Nat.parse(s, opts || {}); end
12
+ def parse_at(s, opts={}); ::Fugit::At.parse(s, opts || {}); end
13
13
  def parse_in(s); parse_duration(s); end
14
14
 
15
- def do_parse_cron(s); ::Fugit::Cron.do_parse(s); end
15
+ def do_parse_cron(s, opts={}); ::Fugit::Cron.do_parse(s, opts || {}); end
16
16
  def do_parse_duration(s); ::Fugit::Duration.do_parse(s); end
17
- def do_parse_nat(s, opts={}); ::Fugit::Nat.do_parse(s, opts); end
17
+ def do_parse_nat(s, opts={}); ::Fugit::Nat.do_parse(s, opts || {}); end
18
18
  def do_parse_at(s); ::Fugit::At.do_parse(s); end
19
19
  def do_parse_in(s); do_parse_duration(s); end
20
20
 
@@ -22,10 +22,10 @@ module Fugit
22
22
 
23
23
  opts[:at] = opts[:in] if opts.has_key?(:in)
24
24
 
25
- (opts[:cron] != false && parse_cron(s)) || # 542ms 616ms
25
+ (opts[:cron] != false && parse_cron(s, opts || {})) || # 542ms 616ms
26
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
+ (opts[:nat] != false && parse_nat(s, opts || {})) || # 2s # 35s
28
+ (opts[:at] != false && parse_at(s, opts || {})) || # 568ms 622ms
29
29
  nil
30
30
  end
31
31
 
@@ -54,14 +54,14 @@ module Fugit
54
54
 
55
55
  def parse_cronish(s, opts={})
56
56
 
57
- r = parse_cron(s) || parse_nat(s, opts)
57
+ r = parse_cron(s, opts) || parse_nat(s, opts)
58
58
 
59
59
  r.is_a?(::Fugit::Cron) ? r : nil
60
60
  end
61
61
 
62
62
  def do_parse_cronish(s, opts={})
63
63
 
64
- parse_cronish(s) ||
64
+ parse_cronish(s, opts) ||
65
65
  fail(ArgumentError.new("not cron or 'natural' cron string: #{s.inspect}"))
66
66
  end
67
67
 
data/lib/fugit.rb CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  module Fugit
5
5
 
6
- VERSION = '1.12.1'
6
+ VERSION = '1.13.0'
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.12.1
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Mettraux
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-14 00:00:00.000000000 Z
11
+ date: 2026-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: raabro
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ostruct
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  description: Time tools for flor and the floraison project. Cron parsing and occurrence
70
84
  computing. Timestamps and more.
71
85
  email: