fugit 1.12.3 → 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: f588451db0cf7643afd0ecd89313863b4ca89913872547f0dd8342343fec1b57
4
- data.tar.gz: a0832d2aecbe4b158a3deef01ac7c230cd8de3ff2c3dc5a1da4e871a516d27c6
3
+ metadata.gz: 15c7da961f6616177b4837e7db2b8d2cbef8a73af0e39d482bbde88ca927d420
4
+ data.tar.gz: bdb787682ba39f3bd873c935378d7097291ff275c9c2443a4bf42990e71dfa92
5
5
  SHA512:
6
- metadata.gz: 5fca6043aef60a7c895b991f364714f1d26f3ef0922d477fc624a4f4cdc51dc948542fca4638567e2416fa4ff4c75e267f03991d7624f2515745d8e8be203314
7
- data.tar.gz: 8ddb37f7d0f961dc341757fff83ef1cf844df7a7ffc7aaedc1eed0dca6fea4be217029e2a266cb8ce774303e174f4e27713854b4058cf6f12b3c2aa6eb78d1ca
6
+ metadata.gz: c920de689a07b8e5b082afa2d67ff26a2ad3422e0a35afc0c02fee71d92d6a76da391d7ec6d03e55d62cbc1a46cbf1296bccb4a37ec9df9d72f35d3e4ecaf0ad
7
+ data.tar.gz: 11694200215f7506f64f862d7f63e86b3fc79be5ad4a4ddfe9278143c40991bcd2c89356e9e314d5caad125b0b26623daf3da64ee2e92b49ca1480b59fc220f3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
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
+
5
10
  ## fugit 1.12.3 released 2026-07-01
6
11
 
7
12
  * Wrap-around for weekday ranges with step value, gh-119 Nicolas Bachschmidt
data/CREDITS.md CHANGED
@@ -8,7 +8,7 @@
8
8
  * Mark R. James, https://github.com/mrj, AM vs America/Los_Angeles, gh-113
9
9
  * Tejas Bubane, https://github.com/tejasbubane, r3.4 in test matrix, gh-109
10
10
  * Luis Castillo, https://github.com/lekastillo, nice_hash gh-108
11
- * Geremia Taglialatela, https://github.com/tagliala, gh-105 gh-107
11
+ * Geremia Taglialatela, https://github.com/tagliala, gh-105 gh-107 gh-122
12
12
  * https://github.com/personnumber3377, gh-104 Fugit.parse choke on long input
13
13
  * Michael Scrivo, https://github.com/mscrivo, gh-103
14
14
  * Benjamin Darcet, https://github.com/bdarcet gh-95 gh-96 et-orbi #rweek
data/README.md CHANGED
@@ -486,6 +486,29 @@ Here's the output:
486
486
  ```
487
487
 
488
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
+
489
512
  ### the second extension
490
513
 
491
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/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,9 +582,9 @@ 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
  #
@@ -593,6 +593,11 @@ module Fugit
593
593
 
594
594
  return false if sla && sla < 1
595
595
 
596
+ if sep == :tilde
597
+ sta = random(min, max, sta, edn, sla, opts)
598
+ edn = nil unless sla
599
+ end
600
+
596
601
  edn = max if sla && edn.nil?
597
602
 
598
603
  return nil if sta.nil? && edn.nil? && sla.nil?
@@ -605,6 +610,24 @@ module Fugit
605
610
  range(min, max, sta, edn, sla)
606
611
  end
607
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
+
608
631
  def range(min, max, sta, edn, sla)
609
632
 
610
633
  return [ nil ] if sta == min && edn == max && sla == 1
@@ -647,13 +670,13 @@ module Fugit
647
670
  .uniq
648
671
  end
649
672
 
650
- def do_determine(key, arr, min, max)
673
+ def do_determine(key, arr, min, max, opts)
651
674
 
652
675
  null = false
653
676
 
654
677
  r = arr
655
678
  .collect { |v|
656
- expand(min, max, v) }
679
+ expand(min, max, opts, v) }
657
680
  .flatten(1)
658
681
  .collect { |e|
659
682
  return false if e == false
@@ -664,31 +687,36 @@ module Fugit
664
687
  r.uniq.sort
665
688
  end
666
689
 
667
- def determine_seconds(arr)
668
- (@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
669
692
  end
670
693
 
671
- def determine_minutes(arr)
672
- (@minutes = do_determine(:minutes, arr, 0, 59)) != false
694
+ def determine_minutes(arr, opts)
695
+ (@minutes = do_determine(:minutes, arr, 0, 59, opts)) != false
673
696
  end
674
697
 
675
- def determine_hours(arr)
676
- (@hours = do_determine(:hours, arr, 0, 23)) != false
698
+ def determine_hours(arr, opts)
699
+ (@hours = do_determine(:hours, arr, 0, 23, opts)) != false
677
700
  end
678
701
 
679
- def determine_monthdays(arr)
680
- (@monthdays = do_determine(:monthdays, arr, 1, 31)) != false
702
+ def determine_monthdays(arr, opts)
703
+ (@monthdays = do_determine(:monthdays, arr, 1, 31, opts)) != false
681
704
  end
682
705
 
683
- def determine_months(arr)
684
- (@months = do_determine(:months, arr, 1, 12)) != false
706
+ def determine_months(arr, opts)
707
+ (@months = do_determine(:months, arr, 1, 12, opts)) != false
685
708
  end
686
709
 
687
- def determine_weekdays(arr)
710
+ def determine_weekdays(arr, opts)
688
711
 
689
712
  @weekdays = []
690
713
 
691
- 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
+
692
720
  if ha || mo
693
721
  @weekdays << [ a, ha || mo ]
694
722
  elsif sl
@@ -759,7 +787,8 @@ module Fugit
759
787
 
760
788
  def s(i); rex(nil, i, /[ \t]+/); end
761
789
  def star(i); str(nil, i, '*'); end
762
- def hyphen(i); str(nil, i, '-'); end
790
+ def hyphen(i); str(:hyphen, i, '-'); end
791
+ def tilde(i); str(:tilde, i, '~'); end
763
792
  def comma(i); rex(nil, i, /,([ \t]*,)*/); end
764
793
  def comma?(i); rex(nil, i, /([ \t]*,)*/); end
765
794
  def and?(i); rex(nil, i, /&?/); end
@@ -787,12 +816,19 @@ module Fugit
787
816
  def r_mon(i); seq(nil, i, :mon, :_mon, '?'); end
788
817
  def r_dow(i); seq(nil, i, :dow, :_dow, '?'); end
789
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
+
790
826
  # sor: star or range
791
- def sor_mos(i); alt(nil, i, :star, :r_mos); end
792
- def sor_hou(i); alt(nil, i, :star, :r_hou); end
793
- def sor_dom(i); alt(nil, i, :star, :r_dom); end
794
- def sor_mon(i); alt(nil, i, :star, :r_mon); end
795
- 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
796
832
 
797
833
  # sorws: star or range with[out] slash
798
834
  def sorws_mos(i); seq(nil, i, :sor_mos, :slash, '?'); end
@@ -872,12 +908,16 @@ module Fugit
872
908
 
873
909
  def rewrite_elt(k, t)
874
910
 
875
- 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|
876
914
  case tt.name
877
915
  when :slash then slt = tt
878
916
  when :hash then hat = tt
879
917
  when :mod then mot = tt
880
- 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
881
921
  end
882
922
  end
883
923
 
@@ -891,11 +931,12 @@ module Fugit
891
931
 
892
932
  a = at ? rewrite_bound(k, at) : nil
893
933
  z = zt ? rewrite_bound(k, zt) : nil
934
+ sp = spt ? spt.name : nil
894
935
 
895
936
  #a, z = z, a if a && z && a > z
896
937
  # handled downstream since gh-27
897
938
 
898
- [ a, z, sl, ha, mo ]
939
+ [ a, z, sp, sl, ha, mo ]
899
940
  end
900
941
 
901
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,13 +6,13 @@ 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
11
  def parse_nat(s, opts={}); ::Fugit::Nat.parse(s, opts || {}); end
12
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
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
@@ -22,7 +22,7 @@ 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
27
  (opts[:nat] != false && parse_nat(s, opts || {})) || # 2s # 35s
28
28
  (opts[:at] != false && parse_at(s, opts || {})) || # 568ms 622ms
@@ -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.3'
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.3
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: 2026-07-01 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: