Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package rubygem-fugit for openSUSE:Factory 
checked in at 2026-07-15 16:33:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rubygem-fugit (Old)
 and      /work/SRC/openSUSE:Factory/.rubygem-fugit.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rubygem-fugit"

Wed Jul 15 16:33:53 2026 rev:2 rq:1365323 version:1.13.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/rubygem-fugit/rubygem-fugit.changes      
2024-12-13 22:34:22.308147686 +0100
+++ /work/SRC/openSUSE:Factory/.rubygem-fugit.new.1991/rubygem-fugit.changes    
2026-07-15 16:50:24.392416100 +0200
@@ -1,0 +2,6 @@
+Mon Jul 13 09:20:20 UTC 2026 - Aleksei Burlakov <[email protected]>
+
+- update to version 1.13.0:
+  * Random values support in cron string "10 ~ * * *" gh-121 Nicols Bachschmidt
+
+-------------------------------------------------------------------

Old:
----
  fugit-1.11.1.gem

New:
----
  fugit-1.13.0.gem

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rubygem-fugit.spec ++++++
--- /var/tmp/diff_new_pack.r7Dbji/_old  2026-07-15 16:50:27.000504716 +0200
+++ /var/tmp/diff_new_pack.r7Dbji/_new  2026-07-15 16:50:27.000504716 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package rubygem-fugit
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -24,13 +24,13 @@
 #
 
 Name:           rubygem-fugit
-Version:        1.11.1
+Version:        1.13.0
 Release:        0
 %define mod_name fugit
 %define mod_full_name %{mod_name}-%{version}
-BuildRequires:  ruby-macros >= 5
-BuildRequires:  %{ruby}
 BuildRequires:  %{rubygem gem2rpm}
+BuildRequires:  %{ruby}
+BuildRequires:  ruby-macros >= 5
 URL:            https://github.com/floraison/fugit
 Source:         https://rubygems.org/gems/%{mod_full_name}.gem
 Source1:        gem2rpm.yml

++++++ fugit-1.11.1.gem -> fugit-1.13.0.gem ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/CHANGELOG.md new/CHANGELOG.md
--- old/CHANGELOG.md    2024-08-15 14:11:09.000000000 +0200
+++ new/CHANGELOG.md    2026-07-10 04:25:38.000000000 +0200
@@ -2,6 +2,43 @@
 # CHANGELOG.md
 
 
+## fugit 1.13.0 released 2026-07-10
+
+* Random values support in cron string `"10 ~ * * *"` gh-121 Nicols Bachschmidt
+
+
+## fugit 1.12.3 released 2026-07-01
+
+* Wrap-around for weekday ranges with step value, gh-119 Nicolas Bachschmidt
+
+
+## fugit 1.12.2 released 2026-05-28
+
+* Fix "divide by zero" gh-117
+* Let at parse 'now', gh-111
+* Let Fugit::At pass options to Chronic, gh-116
+
+
+## fugit 1.12.1 released 2025-10-14
+
+* Fix Fugit::Cron#to_cron_s to include & if @day_and
+
+
+## fugit 1.12.0 released 2025-09-30
+
+* Upgrade et-orbi to ~> 1.4.0 for EtOrbi.rweek_ref=, gh-114
+  this changes the rweek reference point to 2018-12-31 (Monday)
+* Fix `12 0 * * wed%4+1,wed%4` issue where 1 wed was ignored, gh-114
+
+
+## fugit 1.11.2 released 2025-08-22
+
+* Fix "every day at midnight America/Los_Angeles", gh-113, Mark R. James
+* Avoid Hash#partition https://bugs.ruby-lang.org/issues/16252
+* Fix Fugit::Nat "zero dark forty", gh-107
+* Ensure @yearly and other specials accept a timezone
+
+
 ## fugit 1.11.1 released 2024-08-15
 
 * Prevent nat parsing chocking on long input (> 256 chars), gh-104
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/CREDITS.md new/CREDITS.md
--- old/CREDITS.md      2024-08-15 14:11:09.000000000 +0200
+++ new/CREDITS.md      2026-07-10 04:25:38.000000000 +0200
@@ -1,6 +1,14 @@
 
 # fugit credits
 
+* Nicolas Bachschmidt https://github.com/baarde gh-120 weekday wrap fix
+* JebeenLee https://github.com/JebeenLee gh-117 divide by zero
+* Eric Claerhout https://github.com/swebra gh-114 rweek readme clarity
+* Hugh Kelsey https://github.com/hughkelsey gh-114 rweek/rday / wed%4+1,wed%4
+* Mark R. James, https://github.com/mrj, AM vs America/Los_Angeles, gh-113
+* Tejas Bubane, https://github.com/tejasbubane, r3.4 in test matrix, gh-109
+* Luis Castillo, https://github.com/lekastillo, nice_hash gh-108
+* Geremia Taglialatela, https://github.com/tagliala, gh-105 gh-107 gh-122
 * https://github.com/personnumber3377, gh-104 Fugit.parse choke on long input
 * Michael Scrivo, https://github.com/mscrivo, gh-103
 * Benjamin Darcet, https://github.com/bdarcet gh-95 gh-96 et-orbi #rweek
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/LICENSE.txt new/LICENSE.txt
--- old/LICENSE.txt     2024-08-15 14:11:09.000000000 +0200
+++ new/LICENSE.txt     2026-07-10 04:25:38.000000000 +0200
@@ -1,5 +1,5 @@
 
-Copyright (c) 2017-2024, John Mettraux, [email protected]
+Copyright (c) 2017-2026, John Mettraux, [email protected]
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/README.md new/README.md
--- old/README.md       2024-08-15 14:11:09.000000000 +0200
+++ new/README.md       2026-07-10 04:25:38.000000000 +0200
@@ -1,6 +1,12 @@
 
 # fugit
 
+> Sed fugit interea, fugit irreparabile tempus
+
+Virgil, _Georgics_ Book III
+
+---
+
 
[![tests](https://github.com/floraison/fugit/workflows/test/badge.svg)](https://github.com/floraison/fugit/actions)
 [![Gem 
Version](https://badge.fury.io/rb/fugit.svg)](http://badge.fury.io/rb/fugit)
 
@@ -17,31 +23,33 @@
 
 The intersection of those two projects is where fugit is born:
 
-* [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
-* [flor](https://github.com/floraison/flor) - a Ruby workflow engine, fugit 
provides the foundation for its time scheduling capabilities
+* [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
+* [flor](https://github.com/floraison/flor) — a Ruby workflow engine, fugit 
provides the foundation for its time scheduling capabilities
 
 ### Similar, sometimes overlapping projects
 
-* [chronic](https://github.com/mojombo/chronic) - a pure Ruby natural language 
date parser
-* [parse-cron](https://github.com/siebertm/parse-cron) - parses cron 
expressions and calculates the next occurrence after a given date
-* [ice_cube](https://github.com/seejohnrun/ice_cube) - Ruby date recurrence 
library
-* [ISO8601](https://github.com/arnau/ISO8601) - Ruby parser to work with 
ISO8601 dateTimes and durations
-* [chrono](https://github.com/r7kamura/chrono) - a chain of logics about 
chronology
-* [CronCalc](https://github.com/mizinsky/cron_calc) - calculates cron job 
occurrences
-* [Recurrence](https://github.com/fnando/recurrence) - a simple library to 
handle recurring events
+* [chronic](https://github.com/mojombo/chronic) — a pure Ruby natural language 
date parser
+* [parse-cron](https://github.com/siebertm/parse-cron) — parses cron 
expressions and calculates the next occurrence after a given date
+* [ice_cube](https://github.com/seejohnrun/ice_cube) — Ruby date recurrence 
library
+* [ISO8601](https://github.com/arnau/ISO8601) — Ruby parser to work with 
ISO8601 dateTimes and durations
+* [chrono](https://github.com/r7kamura/chrono) — a chain of logics about 
chronology
+* [CronCalc](https://github.com/mizinsky/cron_calc) — calculates cron job 
occurrences
+* [Recurrence](https://github.com/fnando/recurrence) — a simple library to 
handle recurring events
+* [CronConfigParser](https://github.com/madogiwa0124/cron_config_parser) — 
parse the cron configuration for readability
 * ...
 
 ### Projects using fugit
 
-* [arask](https://github.com/Ebbe/arask) - "Automatic RAils taSKs" uses fugit 
to parse cron strings
-* [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
-* [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) - as seen 
above
-* [flor](https://github.com/floraison/flor) - used in the 
[cron](https://github.com/floraison/flor/blob/master/doc/procedures/cron.md) 
procedure
-* [que-scheduler](https://github.com/hlascelles/que-scheduler) - a reliable 
job scheduler for [que](https://github.com/chanks/que)
-* [serial_scheduler](https://github.com/grosser/serial_scheduler) - ruby task 
scheduler without threading
-* [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
-* [GoodJob](https://github.com/bensheldon/good_job) - a multithreaded, 
Postgres-based, Active Job backend for Ruby on Rails
-* [Solid Queue](https://github.com/rails/solid_queue) - a DB-based queuing 
backend for Active Job, designed with simplicity and performance in mind
+* [arask](https://github.com/Ebbe/arask) — "Automatic RAils taSKs" uses fugit 
to parse cron strings
+* [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
+* [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) — as seen 
above
+* [flor](https://github.com/floraison/flor) — used in the 
[cron](https://github.com/floraison/flor/blob/master/doc/procedures/cron.md) 
procedure
+* [que-scheduler](https://github.com/hlascelles/que-scheduler) — a reliable 
job scheduler for [que](https://github.com/chanks/que)
+* [serial_scheduler](https://github.com/grosser/serial_scheduler) — ruby task 
scheduler without threading
+* [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
+* [GoodJob](https://github.com/bensheldon/good_job) — a multithreaded, 
Postgres-based, Active Job backend for Ruby on Rails
+* [Solid Queue](https://github.com/rails/solid_queue) — a DB-based queuing 
backend for Active Job, designed with simplicity and performance in mind
+* [qron](https://github.com/floraison/qron) — stupid cron thread that wakes up 
from time to time to do what's in its crontab
 * ...
 
 ## `Fugit.parse(s)`
@@ -112,6 +120,7 @@
 
 Introduced in fugit 1.8.0.
 
+
 ## `Fugit::Cron`
 
 A class `Fugit::Cron` to parse cron strings and then `#next_time` and 
`#previous_time` to compute the next or the previous occurrence respectively.
@@ -204,13 +213,28 @@
 
 Please note that `'15/30 * * * *'` is interpreted as `'15-59/30 * * * *'` 
since fugit 1.4.6.
 
+### time zones
+
+Fugit accepts a IANA timezone identifier right after a cron string:
+```ruby
+'5 0 * * *  Europe/Rome'      # 5 minutes after midnight, every day, Rome tz
+'0 22 * * 1-5  Asia/Tbilisi'  # at 2200 on weekdays in Georgia
+
+'@yearly Asia/Kuala_Lumpur'  # turns into '0 0 1 1 * Asia/Kuala_Lumpur'
+'@monthly Asia/Jakarta'      # turns into '0 0 1 * * Asia/Jakarta'
+  #
+  # those two "ats" and friends since fugit 1.11.2...
+```
+
+When no time zone is specified, fugit uses Ruby's provided timezone.
+
 ### the first Monday of the month
 
 Fugit tries to follow the `man 5 crontab` documentation.
 
 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 `*`).
 
-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.
+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.
 
 The man page says:
 
@@ -278,140 +302,225 @@
 
 ### the modulo extension
 
-Fugit, since 1.1.10, also understands cron strings like "`9 0 * * sun%2`" 
which can be read as "every other Sunday at 9am".
+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".
 
 The modulo extension can only be used in the day-of-week field.
 
 For odd Sundays, one can write `9 0 * * sun%2+1`.
 
-It can be combined, as in `9 0 * * sun%2,tue%3+2`
+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).
+
+What does `sun%2` actually mean?
+```ruby
+t.wday == 0 && t.rweek % 2 == 0
+```
+
+What does `tue%3+2` mean?
+```ruby
+t.wday == 2 && t.rweek % 3 == 2
+```
+
+#### et-orbi < 1.4.0 : reference set on Tuesday 2019-01-01
 
-But what does it reference to? It starts at 1 on 2019-01-01.
+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.
 
+Consider this iteration through the days around 2019-01-01.
 ```ruby
-require 'et-orbi' # >= 1.1.8
+require 'fugit'
 
-# the reference
-p EtOrbi.parse('2019-01-01').wday       # => 2
-p EtOrbi.parse('2019-01-01').rweek      # => 1
-p EtOrbi.parse('2019-01-01').rweek % 2  # => 1
+t = EtOrbi.parse('2018-12-28 12:00')
 
-# today (as of this coding...)
-p EtOrbi.parse('2019-04-11').wday       # => 4
-p EtOrbi.parse('2019-04-11').rweek      # => 15
-p EtOrbi.parse('2019-04-11').rweek % 2  # => 1
+15.times do |i|
 
-c = Fugit.parse('* * * * tue%2')
-c.match?('2019-01-01')  # => false, since rweek % 2 == 1
-c.match?('2019-01-08')  # => true, since rweek % 2 == 0
+  puts " * %14s / rday: %5d / rweek: %5d" % [
+    t.strftime('%F %a'), t.rday, t.rweek ]
 
-c = Fugit.parse('* * * * tue%2+1')
-c.match?('2019-01-01')  # => true, since (rweek + 1) % 2 == 0
-c.match?('2019-01-08')  # => false, since (rweek + 1) % 2 == 1
+  w = t.rweek
+  t = t.add(24 * 3600)
+  puts if t.rweek != w
 
-# ...
+  if i == 7
+    puts "\n  (...)\n\n"
+    t = EtOrbi.parse('2025-10-04 12:00')
+  end
+end
 ```
 
-`sun%2` matches if Sunday and `current_date.rweek % 2 == 0`
-`tue%3+2` matches if Tuesday and `current_date.rweek + 2 % 3 == 0`
-`tue%x+y` matches if Tuesday and `current_date.rweek + y % x == 0`
+For et-orbi 1.2.11, it yields:
+```
+ * 2018-12-28 Fri / rday:    -3 / rweek:     0
+ * 2018-12-29 Sat / rday:    -2 / rweek:     0
+ * 2018-12-30 Sun / rday:    -1 / rweek:     0
+ * 2018-12-31 Mon / rday:     0 / rweek:     0
 
+ * 2019-01-01 Tue / rday:     1 / rweek:     1
+ * 2019-01-02 Wed / rday:     2 / rweek:     1
+ * 2019-01-03 Thu / rday:     3 / rweek:     1
+ * 2019-01-04 Fri / rday:     4 / rweek:     1
+ * 2019-01-05 Sat / rday:     5 / rweek:     1
 
-### the second extension
+  (...)
 
-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`.
+ * 2025-10-04 Sat / rday:  2469 / rweek:   353
+ * 2025-10-05 Sun / rday:  2470 / rweek:   353
+ * 2025-10-06 Mon / rday:  2471 / rweek:   353
 
-```ruby
-c = Fugit.parse('* * * * *') # every minute
-c = Fugit.parse('5 * * * *') # every hour at minute 5
-c = Fugit.parse('* * * * * *') # every second
-c = Fugit.parse('5 * * * * *') # every minute at second 5
+ * 2025-10-07 Tue / rday:  2472 / rweek:   354
+ * 2025-10-08 Wed / rday:  2473 / rweek:   354
+ * 2025-10-09 Thu / rday:  2474 / rweek:   354
 ```
 
+This [was problematic](https://github.com/floraison/fugit/issues/114), since 
the week started on, well, Tuesday.
 
-## `Fugit::Duration`
 
-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).
+#### et-orbi >= 1.4.0 : reference set on Monday 2018-12-31
 
-Provides duration arithmetic tools.
+Since 1.4.0, et-orbi starts by default on Monday (2018-12-31), as rday 0 with 
rweek 0.
+
+Thus, the above code yields:
+```
+ * 2018-12-28 Fri / rday:    -3 / rweek:    -1
+ * 2018-12-29 Sat / rday:    -2 / rweek:    -1
+ * 2018-12-30 Sun / rday:    -1 / rweek:    -1
+
+ * 2018-12-31 Mon / rday:     0 / rweek:     0
+ * 2019-01-01 Tue / rday:     1 / rweek:     0
+ * 2019-01-02 Wed / rday:     2 / rweek:     0
+ * 2019-01-03 Thu / rday:     3 / rweek:     0
+ * 2019-01-04 Fri / rday:     4 / rweek:     0
+ * 2019-01-05 Sat / rday:     5 / rweek:     0
+
+  (...)
+
+ * 2025-10-04 Sat / rday:  2469 / rweek:   352
+ * 2025-10-05 Sun / rday:  2470 / rweek:   352
+
+ * 2025-10-06 Mon / rday:  2471 / rweek:   353
+ * 2025-10-07 Tue / rday:  2472 / rweek:   353
+ * 2025-10-08 Wed / rday:  2473 / rweek:   353
+ * 2025-10-09 Thu / rday:  2474 / rweek:   353
+```
+
+
+#### modulo and et-orbi >= 1.4.0 sanity check
+
+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.
 
 ```ruby
 require 'fugit'
 
-d = Fugit::Duration.parse('1y2M1d4h')
+c = Fugit.parse_cron('0 12 * * mon%2,wed%3+1')
 
-p d.to_plain_s  # => "1Y2M1D4h"
-p d.to_iso_s    # => "P1Y2M1DT4H" ISO 8601 duration
-p d.to_long_s   # => "1 year, 2 months, 1 day, and 4 hours"
+t = EtOrbi.parse('2025-09-20 12:00')
 
-d += Fugit::Duration.parse('1y1h')
+44.times do
 
-p d.to_long_s  # => "2 years, 2 months, 1 day, and 5 hours"
+  wd = t.strftime('%a')
+  wd = %w[ Mon Wed ].include?(wd) ? '*' + wd.upcase : ' ' + wd.downcase
 
-d += 3600
+  puts "%14s | rweek: %3d | %%2: %d == 0 | %%3: %d == 1 | ? %3s" % [
+    t.strftime('%F') + ' ' + wd,
+    t.rweek,
+    t.rweek % 2, t.rweek % 3,
+    c.match?(t)
+      ].map { |e| e == true ? 'YES' : e == false ? 'no' : e }
 
-p d.to_plain_s  # => "2Y2M1D5h3600s"
+  w = t.rweek
+  t = t.add(24 * 3600)
+  puts if t.rweek != w
+end
+```
 
-p Fugit::Duration.parse('1y2M1d4h').to_sec # => 36820800
+Here's the output:
 ```
+2025-09-20  sat | rweek: 350 | %2: 0 == 0 | %3: 2 == 1 | ?  no
+2025-09-21  sun | rweek: 350 | %2: 0 == 0 | %3: 2 == 1 | ?  no
 
-There is a `#deflate` method
+2025-09-22 *MON | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ?  no
+2025-09-23  tue | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ?  no
+2025-09-24 *WED | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ?  no
+2025-09-25  thu | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ?  no
+2025-09-26  fri | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ?  no
+2025-09-27  sat | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ?  no
+2025-09-28  sun | rweek: 351 | %2: 1 == 0 | %3: 0 == 1 | ?  no
 
-```ruby
-Fugit::Duration.parse(1000).to_plain_s # => "1000s"
-Fugit::Duration.parse(3600).to_plain_s # => "3600s"
-Fugit::Duration.parse(1000).deflate.to_plain_s # => "16m40s"
-Fugit::Duration.parse(3600).deflate.to_plain_s # => "1h"
+2025-09-29 *MON | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? YES
+2025-09-30  tue | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ?  no
+2025-10-01 *WED | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ? YES
+2025-10-02  thu | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ?  no
+2025-10-03  fri | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ?  no
+2025-10-04  sat | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ?  no
+2025-10-05  sun | rweek: 352 | %2: 0 == 0 | %3: 1 == 1 | ?  no
 
-# or event shorter
-Fugit.parse(1000).deflate.to_plain_s # => "16m40s"
-Fugit.parse(3600).deflate.to_plain_s # => "1h"
-```
+2025-10-06 *MON | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ?  no
+2025-10-07  tue | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ?  no
+2025-10-08 *WED | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ?  no
+2025-10-09  thu | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ?  no
+2025-10-10  fri | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ?  no
+2025-10-11  sat | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ?  no
+2025-10-12  sun | rweek: 353 | %2: 1 == 0 | %3: 2 == 1 | ?  no
 
-There is also an `#inflate` method
+2025-10-13 *MON | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ? YES
+2025-10-14  tue | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ?  no
+2025-10-15 *WED | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ?  no
+2025-10-16  thu | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ?  no
+2025-10-17  fri | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ?  no
+2025-10-18  sat | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ?  no
+2025-10-19  sun | rweek: 354 | %2: 0 == 0 | %3: 0 == 1 | ?  no
 
-```ruby
-Fugit::Duration.parse('1h30m12').inflate.to_plain_s # => "5412s"
-Fugit.parse('1h30m12').inflate.to_plain_s # => "5412s"
+2025-10-20 *MON | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ?  no
+2025-10-21  tue | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ?  no
+2025-10-22 *WED | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ? YES
+2025-10-23  thu | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ?  no
+2025-10-24  fri | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ?  no
+2025-10-25  sat | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ?  no
+2025-10-26  sun | rweek: 355 | %2: 1 == 0 | %3: 1 == 1 | ?  no
 
-Fugit.parse('1h30m12').to_sec # => 5412
-Fugit.parse('1h30m12').to_sec.to_s + 's' # => "5412s"
+2025-10-27 *MON | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ? YES
+2025-10-28  tue | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ?  no
+2025-10-29 *WED | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ?  no
+2025-10-30  thu | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ?  no
+2025-10-31  fri | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ?  no
+2025-11-01  sat | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ?  no
+2025-11-02  sun | rweek: 356 | %2: 0 == 0 | %3: 2 == 1 | ?  no
 ```
 
-The `to_*_s` methods are also available as class methods:
-```ruby
-p Fugit::Duration.to_plain_s('1y2M1d4h')
-  # => "1Y2M1D4h"
-p Fugit::Duration.to_iso_s('1y2M1d4h')
-  # => "P1Y2M1DT4H" ISO 8601 duration
-p Fugit::Duration.to_long_s('1y2M1d4h')
-  # => "1 year, 2 months, 1 day, and 4 hours"
-```
 
-## `Fugit::At`
+### the random extension
 
-Points in time are parsed and given back as EtOrbi::EoTime instances.
+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.
 
 ```ruby
-Fugit::At.parse('2017-12-12').to_s
-  # ==> "2017-12-12 00:00:00 +0900" (at least here in Hiroshima)
-
-Fugit::At.parse('2017-12-12 12:00:00 America/New_York').to_s
-  # ==> "2017-12-12 12:00:00 -0500"
+'~ * * * *'    # every hour on a random minute
+'~29 * * * *'  # every hour on a random minute between 0 and 29
+'30~ * * * *'  # every hour on a random minute between 30 and 59
+'~/10 * * * *' # every 10 minutes starting on a random minute between 0 and 9
+'0 12 * * 1~5' # every week at noon on a random workday
 ```
 
-Directly with `Fugit.parse_at(s)` is OK too:
+You may customize how random values are generated by adding the `:random` 
option with a boolean or an object that implements the `#rand` method.
+
 ```ruby
-Fugit.parse_at('2017-12-12 12:00:00 America/New_York').to_s
-  # ==> "2017-12-12 12:00:00 -0500"
+Fugit.parse('~ * * * *', random: false)        # on a non-random minute ==> '0 
* * * *'
+Fugit.parse('~ * * * *', random: true)         # on a random minute (default)
+Fugit.parse('~ * * * *', random: SecureRandom) # on a cryptographically secure 
random minute
 ```
 
-Directly with `Fugit.parse(s)` is OK too:
+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.
+
+
+### the second extension
+
+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`.
+
 ```ruby
-Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s
-  # ==> "2017-12-12 12:00:00 -0500"
+c = Fugit.parse('* * * * *') # every minute
+c = Fugit.parse('5 * * * *') # every hour at minute 5
+c = Fugit.parse('* * * * * *') # every second
+c = Fugit.parse('5 * * * * *') # every minute at second 5
 ```
 
+
 ## `Fugit::Nat`
 
 Fugit understand some kind of "natural" language:
@@ -511,6 +620,96 @@
 ```
 
 
+## `Fugit::Duration`
+
+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).
+
+Provides duration arithmetic tools.
+
+```ruby
+require 'fugit'
+
+d = Fugit::Duration.parse('1y2M1d4h')
+
+p d.to_plain_s  # => "1Y2M1D4h"
+p d.to_iso_s    # => "P1Y2M1DT4H" ISO 8601 duration
+p d.to_long_s   # => "1 year, 2 months, 1 day, and 4 hours"
+
+d += Fugit::Duration.parse('1y1h')
+
+p d.to_long_s  # => "2 years, 2 months, 1 day, and 5 hours"
+
+d += 3600
+
+p d.to_plain_s  # => "2Y2M1D5h3600s"
+
+p Fugit::Duration.parse('1y2M1d4h').to_sec # => 36820800
+```
+
+There is a `#deflate` method
+
+```ruby
+Fugit::Duration.parse(1000).to_plain_s # => "1000s"
+Fugit::Duration.parse(3600).to_plain_s # => "3600s"
+Fugit::Duration.parse(1000).deflate.to_plain_s # => "16m40s"
+Fugit::Duration.parse(3600).deflate.to_plain_s # => "1h"
+
+# or event shorter
+Fugit.parse(1000).deflate.to_plain_s # => "16m40s"
+Fugit.parse(3600).deflate.to_plain_s # => "1h"
+```
+
+There is also an `#inflate` method
+
+```ruby
+Fugit::Duration.parse('1h30m12').inflate.to_plain_s # => "5412s"
+Fugit.parse('1h30m12').inflate.to_plain_s # => "5412s"
+
+Fugit.parse('1h30m12').to_sec # => 5412
+Fugit.parse('1h30m12').to_sec.to_s + 's' # => "5412s"
+```
+
+The `to_*_s` methods are also available as class methods:
+```ruby
+p Fugit::Duration.to_plain_s('1y2M1d4h')
+  # => "1Y2M1D4h"
+p Fugit::Duration.to_iso_s('1y2M1d4h')
+  # => "P1Y2M1DT4H" ISO 8601 duration
+p Fugit::Duration.to_long_s('1y2M1d4h')
+  # => "1 year, 2 months, 1 day, and 4 hours"
+```
+
+
+## `Fugit::At`
+
+Points in time are parsed and given back as EtOrbi::EoTime instances.
+
+```ruby
+Fugit::At.parse('2017-12-12').to_s
+  # ==> "2017-12-12 00:00:00 +0900" (at least here in Hiroshima)
+
+Fugit::At.parse('2017-12-12 12:00:00 America/New_York').to_s
+  # ==> "2017-12-12 12:00:00 -0500"
+```
+
+Directly with `Fugit.parse_at(s)` is OK too:
+```ruby
+Fugit.parse_at('2017-12-12 12:00:00 America/New_York').to_s
+  # ==> "2017-12-12 12:00:00 -0500"
+```
+
+Directly with `Fugit.parse(s)` is OK too:
+```ruby
+Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s
+  # ==> "2017-12-12 12:00:00 -0500"
+```
+
+
+## KNOWN ISSUES
+
+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.
+
+
 ## LICENSE
 
 MIT, see [LICENSE.txt](LICENSE.txt)
Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fugit.gemspec new/fugit.gemspec
--- old/fugit.gemspec   2024-08-15 14:11:09.000000000 +0200
+++ new/fugit.gemspec   2026-07-10 04:25:38.000000000 +0200
@@ -20,18 +20,18 @@
 
   s.metadata = {
     'changelog_uri' => s.homepage + '/blob/master/CHANGELOG.md',
-    'documentation_uri' => s.homepage,
     'bug_tracker_uri' => s.homepage + '/issues',
-    #'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/floraison',
+    'documentation_uri' => s.homepage,
     'homepage_uri' =>  s.homepage,
     'source_code_uri' => s.homepage,
+    #'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/floraison',
     #'wiki_uri' => s.homepage + '/wiki',
+    'rubygems_mfa_required' => 'true',
   }
 
   #s.files = `git ls-files`.split("\n")
   s.files = Dir[
-    'README.{md,txt}',
-    'CHANGELOG.{md,txt}', 'CREDITS.{md,txt}', 'LICENSE.{md,txt}',
+    '{README,CHANGELOG,CREDITS,LICENSE}.{md,txt}',
     #'Makefile',
     'lib/**/*.rb', #'spec/**/*.rb', 'test/**/*.rb',
     "#{s.name}.gemspec",
@@ -41,10 +41,11 @@
     # this dependency appears in 'et-orbi'
 
   s.add_runtime_dependency 'raabro', '~> 1.4'
-  s.add_runtime_dependency 'et-orbi', '~> 1', '>= 1.2.11'
+  s.add_runtime_dependency 'et-orbi', '~> 1.4'
 
   s.add_development_dependency 'rspec', '~> 3.8'
   s.add_development_dependency 'chronic', '~> 0.10'
+  s.add_development_dependency 'ostruct'
 
   s.require_path = 'lib'
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/fugit/at.rb new/lib/fugit/at.rb
--- old/lib/fugit/at.rb 2024-08-15 14:11:09.000000000 +0200
+++ new/lib/fugit/at.rb 2026-07-10 04:25:38.000000000 +0200
@@ -6,14 +6,19 @@
 
     class << self
 
-      def parse(s)
+      def parse(s, opts={})
 
-        ::EtOrbi.make_time(s) rescue nil
+        do_parse(s, opts || {}) rescue nil
       end
 
-      def do_parse(s)
+      def do_parse(s, opts={})
 
-        ::EtOrbi.make_time(s)
+        case s
+        when /^now$/i then ::EtOrbi::EoTime.now
+        when String then ::EtOrbi.parse(s, opts || {})
+        when ::EtOrbi::EoTime then s
+        else ::EtOrbi.make_time(s)
+        end
       end
     end
   end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/fugit/cron.rb new/lib/fugit/cron.rb
--- old/lib/fugit/cron.rb       2024-08-15 14:11:09.000000000 +0200
+++ new/lib/fugit/cron.rb       2026-07-10 04:25:38.000000000 +0200
@@ -29,23 +29,31 @@
         parse(original)
       end
 
-      def parse(s)
+      def parse(s, opts={})
 
         return s if s.is_a?(self)
+        return nil unless s.is_a?(String)
 
-        s = SPECIALS[s] || s
+        s0 = s
+        s = s.strip
 
-        return nil unless s.is_a?(String)
+        s =
+          if s[0, 1] == '@'
+            ss = s.split(/\s+/, 2)
+            [ SPECIALS[ss[0]] || ss, *ss[1..-1] ].join(' ')
+          else
+            s
+          end
 
 #p s; Raabro.pp(Parser.parse(s, debug: 3), colors: true)
-        h = Parser.parse(s.strip)
+        h = Parser.parse(s)
 
-        self.allocate.send(:init, s, h)
+        self.allocate.send(:init, s0, h, opts)
       end
 
-      def do_parse(s)
+      def do_parse(s, opts={})
 
-        parse(s) ||
+        parse(s, opts) ||
         fail(ArgumentError.new("invalid cron string #{trunc(s)}"))
       end
 
@@ -167,34 +175,30 @@
 
     def weekday_hash_match?(nt, hsh)
 
+      return false unless hsh.is_a?(Integer)
+
       phsh, nhsh = nt.wday_in_month
+        #
+        # positive wday, from the beginning of the month
+        # negative wday, from the end of the month, -1 == last
 
-      if hsh > 0
-        hsh == phsh # positive wday, from the beginning of the month
-      else
-        hsh == nhsh # negative wday, from the end of the month, -1 == last
-      end
+      (hsh == phsh) || (hsh == nhsh)
     end
 
     def weekday_modulo_match?(nt, mod)
 
-      (nt.rweek % mod[0]) == (mod[1] % mod[0])
+      mod.is_a?(Array) &&
+      ((nt.rweek % mod[0]) == (mod[1] % mod[0]))
     end
 
     def weekday_match?(nt)
 
-      return true if @weekdays.nil?
-
-      wd, hom = @weekdays.find { |d, _| d == nt.wday }
-
-      return false unless wd
-      return true if hom.nil?
-
-      if hom.is_a?(Array)
-        weekday_modulo_match?(nt, hom)
-      else
-        weekday_hash_match?(nt, hom)
-      end
+      @weekdays.nil? ||
+      @weekdays.find { |wd, hom|
+        (nt.wday == wd) &&
+        (hom.nil? ||
+         weekday_modulo_match?(nt, hom) ||
+         weekday_hash_match?(nt, hom)) }
     end
 
     def monthday_match?(nt)
@@ -555,7 +559,7 @@
 
     FREQUENCY_CACHE = {}
 
-    def init(original, h)
+    def init(original, h, opts)
 
       return nil unless h
 
@@ -564,12 +568,12 @@
       @day_and = h[:&]
 
       valid =
-        determine_seconds(h[:sec]) &&
-        determine_minutes(h[:min]) &&
-        determine_hours(h[:hou]) &&
-        determine_monthdays(h[:dom]) &&
-        determine_months(h[:mon]) &&
-        determine_weekdays(h[:dow]) &&
+        determine_seconds(h[:sec], opts) &&
+        determine_minutes(h[:min], opts) &&
+        determine_hours(h[:hou], opts) &&
+        determine_monthdays(h[:dom], opts) &&
+        determine_months(h[:mon], opts) &&
+        determine_weekdays(h[:dow], opts) &&
         determine_timezone(h[:tz])
 
       return nil unless valid
@@ -578,15 +582,22 @@
       self
     end
 
-    def expand(min, max, r)
+    def expand(min, max, opts, r)
 
-      sta, edn, sla = r
+      sta, edn, sep, sla = r
 
       #return false if sla && sla > max
         #
         # let it go, "* */24 * * *" and "* */27 * * *" are okay
         # gh-86 and gh-103
 
+      return false if sla && sla < 1
+
+      if sep == :tilde
+        sta = random(min, max, sta, edn, sla, opts)
+        edn = nil unless sla
+      end
+
       edn = max if sla && edn.nil?
 
       return nil if sta.nil? && edn.nil? && sla.nil?
@@ -599,6 +610,24 @@
       range(min, max, sta, edn, sla)
     end
 
+    def random(min, max, sta, edn, sla, opts)
+
+      sta = min if sta.nil?
+      edn = max if edn.nil?
+
+      random = opts.fetch(:random, true)
+      random = Random if random == true
+      return sta unless random
+
+      n = edn - sta + 1
+      n = max - min + 1 + n if n <= 0
+      n = sla if sla && sla < n
+
+      r = sta + random.rand(n)
+      r = r + min - max - 1 if r > max
+      r
+    end
+
     def range(min, max, sta, edn, sla)
 
       return [ nil ] if sta == min && edn == max && sla == 1
@@ -641,13 +670,13 @@
         .uniq
     end
 
-    def do_determine(key, arr, min, max)
+    def do_determine(key, arr, min, max, opts)
 
       null = false
 
       r = arr
         .collect { |v|
-          expand(min, max, v) }
+          expand(min, max, opts, v) }
         .flatten(1)
         .collect { |e|
           return false if e == false
@@ -658,36 +687,44 @@
       r.uniq.sort
     end
 
-    def determine_seconds(arr)
-      (@seconds = do_determine(:seconds, arr || [ 0 ], 0, 59)) != false
+    def determine_seconds(arr, opts)
+      (@seconds = do_determine(:seconds, arr || [ 0 ], 0, 59, opts)) != false
     end
 
-    def determine_minutes(arr)
-      (@minutes = do_determine(:minutes, arr, 0, 59)) != false
+    def determine_minutes(arr, opts)
+      (@minutes = do_determine(:minutes, arr, 0, 59, opts)) != false
     end
 
-    def determine_hours(arr)
-      (@hours = do_determine(:hours, arr, 0, 23)) != false
+    def determine_hours(arr, opts)
+      (@hours = do_determine(:hours, arr, 0, 23, opts)) != false
     end
 
-    def determine_monthdays(arr)
-      (@monthdays = do_determine(:monthdays, arr, 1, 31)) != false
+    def determine_monthdays(arr, opts)
+      (@monthdays = do_determine(:monthdays, arr, 1, 31, opts)) != false
     end
 
-    def determine_months(arr)
-      (@months = do_determine(:months, arr, 1, 12)) != false
+    def determine_months(arr, opts)
+      (@months = do_determine(:months, arr, 1, 12, opts)) != false
     end
 
-    def determine_weekdays(arr)
+    def determine_weekdays(arr, opts)
 
       @weekdays = []
 
-      arr.each do |a, z, sl, ha, mo| # a to z, slash, hash, and mod
+      arr.each do |a, z, sp, sl, ha, mo| # a to z, seperator, slash, hash, and 
mod
+        if sp == :tilde
+          a = random(0, 6, a, z, sl, opts)
+          z = nil unless sl
+        end
+
         if ha || mo
           @weekdays << [ a, ha || mo ]
         elsif sl
-          ((a || 0)..(z || (a ? a : 6))).step(sl < 1 ? 1 : sl)
-            .each { |i| @weekdays << [ i ] }
+          z ||= (a ? a : 6)
+          a ||= 0
+          z = z + 7 if a > z
+          (a..z).step(sl < 1 ? 1 : sl)
+            .each { |i| @weekdays << [ (i > 6) ? i - 7 : i ] }
         elsif z
           z = z + 7 if a > z
           (a..z).each { |i| @weekdays << [ (i > 6) ? i - 7 : i ] }
@@ -727,7 +764,8 @@
           else
             a.collect(&:to_s).join('#')
           end }
-        .join(',')
+        .join(',') +
+      (@day_and ? '&' : '')
     end
 
     module Parser include Raabro
@@ -749,7 +787,8 @@
 
       def s(i); rex(nil, i, /[ \t]+/); end
       def star(i); str(nil, i, '*'); end
-      def hyphen(i); str(nil, i, '-'); end
+      def hyphen(i); str(:hyphen, i, '-'); end
+      def tilde(i); str(:tilde, i, '~'); end
       def comma(i); rex(nil, i, /,([ \t]*,)*/); end
       def comma?(i); rex(nil, i, /([ \t]*,)*/); end
       def and?(i); rex(nil, i, /&?/); end
@@ -777,12 +816,19 @@
       def r_mon(i); seq(nil, i, :mon, :_mon, '?'); end
       def r_dow(i); seq(nil, i, :dow, :_dow, '?'); end
 
+      # t: tilde range
+      def t_mos(i); seq(nil, i, :mos, '?', :tilde, :mos, '?'); end
+      def t_hou(i); seq(nil, i, :hou, '?', :tilde, :hou, '?'); end
+      def t_dom(i); seq(nil, i, :dom, '?', :tilde, :dom, '?'); end
+      def t_mon(i); seq(nil, i, :mon, '?', :tilde, :mon, '?'); end
+      def t_dow(i); seq(nil, i, :dow, '?', :tilde, :dow, '?'); end
+
       # sor: star or range
-      def sor_mos(i); alt(nil, i, :star, :r_mos); end
-      def sor_hou(i); alt(nil, i, :star, :r_hou); end
-      def sor_dom(i); alt(nil, i, :star, :r_dom); end
-      def sor_mon(i); alt(nil, i, :star, :r_mon); end
-      def sor_dow(i); alt(nil, i, :star, :r_dow); end
+      def sor_mos(i); alt(nil, i, :star, :t_mos, :r_mos); end
+      def sor_hou(i); alt(nil, i, :star, :t_hou, :r_hou); end
+      def sor_dom(i); alt(nil, i, :star, :t_dom, :r_dom); end
+      def sor_mon(i); alt(nil, i, :star, :t_mon, :r_mon); end
+      def sor_dow(i); alt(nil, i, :star, :t_dow, :r_dow); end
 
       # sorws: star or range with[out] slash
       def sorws_mos(i); seq(nil, i, :sor_mos, :slash, '?'); end
@@ -862,12 +908,16 @@
 
       def rewrite_elt(k, t)
 
-        at, zt, slt, hat, mot = nil; t.subgather(nil).each do |tt|
+        at, zt, spt, slt, hat, mot = nil
+
+        t.subgather(nil).each do |tt|
           case tt.name
           when :slash then slt = tt
           when :hash then hat = tt
           when :mod then mot = tt
-          else if at; zt ||= tt; else; at = tt; end
+          when :hyphen then spt = tt
+          when :tilde then spt = tt
+          else if spt; zt ||= tt; else; at = tt; end
           end
         end
 
@@ -881,11 +931,12 @@
 
         a = at ? rewrite_bound(k, at) : nil
         z = zt ? rewrite_bound(k, zt) : nil
+        sp = spt ? spt.name : nil
 
         #a, z = z, a if a && z && a > z
           # handled downstream since gh-27
 
-        [ a, z, sl, ha, mo ]
+        [ a, z, sp, sl, ha, mo ]
       end
 
       def rewrite_entry(t)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/fugit/duration.rb new/lib/fugit/duration.rb
--- old/lib/fugit/duration.rb   2024-08-15 14:11:09.000000000 +0200
+++ new/lib/fugit/duration.rb   2026-07-10 04:25:38.000000000 +0200
@@ -67,11 +67,20 @@
       day: { a: 'D', r: 'd', i: 'D', s: DAY_S, I: true, l: 'day' },
       hou: { a: 'h', r: 'h', i: 'H', s: 3600, I: true, l: 'hour' },
       min: { a: 'm', r: 'm', i: 'M', s: 60, I: true, l: 'minute' },
-      sec: { a: 's', r: 's', i: 'S', s: 1, I: true, l: 'second' } }.freeze
+      sec: { a: 's', r: 's', i: 'S', s: 1, I: true, l: 'second' }
+        }.freeze
 
-    INFLA_KEYS, NON_INFLA_KEYS = KEYS
-      .partition { |k, v| v[:I] }
-      .collect(&:freeze)
+    #INFLA_KEYS, NON_INFLA_KEYS = KEYS
+    #  .partition { |k, v| v[:I] }
+    #  .collect(&:freeze)
+        #
+        # https://bugs.ruby-lang.org/issues/16252
+        #
+    kes = KEYS.entries
+    INFLA_KEYS,
+    NON_INFLA_KEYS =
+      kes.select { |_, v| v[:I] }.freeze,
+      kes.select { |_, v| ! v[:I] }.freeze
 
     def _to_s(key)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/fugit/nat.rb new/lib/fugit/nat.rb
--- old/lib/fugit/nat.rb        2024-08-15 14:11:09.000000000 +0200
+++ new/lib/fugit/nat.rb        2026-07-10 04:25:38.000000000 +0200
@@ -54,7 +54,7 @@
         %w[ zero ] + one_to_nine +
         %w[ ten eleven twelve thirteen fourteen fifteen sixteen seventeen
             eighteen nineteen ] +
-          %w[ twenty thirty fourty fifty ]
+          %w[ twenty thirty forty fifty ]
             .collect { |a|
               ([ nil ] + one_to_nine)
                 .collect { |b| [ a, b ].compact.join('-') } }
@@ -210,7 +210,7 @@
       end
 
       def ampm(i)
-        rex(:ampm, i, /[ \t]*(am|pm|noon|midday|midnight)/i)
+        rex(:ampm, i, /[ \t]*(am|AM|pm|PM|[Nn]oon|[Mm]idday|[Mm]idnight)/)
       end
       def dark(i)
         rex(:dark, i, /[ \t]*dark/i)
@@ -710,7 +710,7 @@
             e.uniq.sort_by(&letters_last).collect(&:to_s).join(',') }
           .join(' ')
 
-        c = Fugit::Cron.parse(s)
+        c = Fugit::Cron.parse(s, opts)
 
         if opts[:strict]
           restrict(a, c)
@@ -719,6 +719,8 @@
         end
       end
 
+      SLASH_REGEX = /^(\d+|\*)\/(\d+)$/.freeze
+
       # Return nil if the cron is "not strict"
       #
       # For example, "0 0/17 * * *" (gh-86) is a perfectly valid
@@ -729,16 +731,18 @@
       #
       def restrict(a, cron)
 
-        if m = ((a[1] && a[1][0]) || '').match(/^(\d+|\*)\/(\d+)$/)
-#p m
-           sla = m[1].to_i
-          return nil unless [ 1, 2, 3, 4, 5, 6, 8, 12 ].include?(sla)
+        if m = ((a[0] && a[0] != [ 0 ] && a[0][0]) || '').match(SLASH_REGEX)
+          return nil unless (1..60).include?(m[1].to_i)
+        end
+        if m = ((a[1] && a[1][0]) || '').match(SLASH_REGEX)
+          return nil unless [ 1, 2, 3, 4, 5, 6, 8, 12 ].include?(m[1].to_i)
         end
 
         cron
       end
 
       def slot(key, default)
+
         s = @slots[key]
         s ? s.data0 : [ default ]
       end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/fugit/parse.rb new/lib/fugit/parse.rb
--- old/lib/fugit/parse.rb      2024-08-15 14:11:09.000000000 +0200
+++ new/lib/fugit/parse.rb      2026-07-10 04:25:38.000000000 +0200
@@ -2,17 +2,19 @@
 
 module Fugit
 
+  DO_PARSE_ORDER = %i[ cron duration nat at ].freeze
+
   class << self
 
-    def parse_cron(s); ::Fugit::Cron.parse(s); end
+    def parse_cron(s, opts={}); ::Fugit::Cron.parse(s, opts || {}); end
     def parse_duration(s); ::Fugit::Duration.parse(s); end
-    def parse_nat(s, opts={}); ::Fugit::Nat.parse(s, opts); end
-    def parse_at(s); ::Fugit::At.parse(s); end
+    def parse_nat(s, opts={}); ::Fugit::Nat.parse(s, opts || {}); end
+    def parse_at(s, opts={}); ::Fugit::At.parse(s, opts || {}); end
     def parse_in(s); parse_duration(s); end
 
-    def do_parse_cron(s); ::Fugit::Cron.do_parse(s); end
+    def do_parse_cron(s, opts={}); ::Fugit::Cron.do_parse(s, opts || {}); end
     def do_parse_duration(s); ::Fugit::Duration.do_parse(s); end
-    def do_parse_nat(s, opts={}); ::Fugit::Nat.do_parse(s, opts); end
+    def do_parse_nat(s, opts={}); ::Fugit::Nat.do_parse(s, opts || {}); end
     def do_parse_at(s); ::Fugit::At.do_parse(s); end
     def do_parse_in(s); do_parse_duration(s); end
 
@@ -20,10 +22,10 @@
 
       opts[:at] = opts[:in] if opts.has_key?(:in)
 
-      (opts[:cron] != false && parse_cron(s)) || # 542ms 616ms
+      (opts[:cron] != false && parse_cron(s, opts || {})) || # 542ms 616ms
       (opts[:duration] != false && parse_duration(s)) || # 645ms # 534ms
-      (opts[:nat] != false && parse_nat(s, opts)) || # 2s # 35s
-      (opts[:at] != false && parse_at(s)) || # 568ms 622ms
+      (opts[:nat] != false && parse_nat(s, opts || {})) || # 2s # 35s
+      (opts[:at] != false && parse_at(s, opts || {})) || # 568ms 622ms
       nil
     end
 
@@ -34,7 +36,7 @@
       result = nil
       errors = []
 
-      %i[ cron duration nat at ]
+      DO_PARSE_ORDER
         .each { |k|
           begin
             result ||= (opts[k] != false && self.send("do_parse_#{k}", s))
@@ -52,17 +54,35 @@
 
     def parse_cronish(s, opts={})
 
-      r = parse_cron(s) || parse_nat(s, opts)
+      r = parse_cron(s, opts) || parse_nat(s, opts)
 
       r.is_a?(::Fugit::Cron) ? r : nil
     end
 
     def do_parse_cronish(s, opts={})
 
-      parse_cronish(s) ||
+      parse_cronish(s, opts) ||
       fail(ArgumentError.new("not cron or 'natural' cron string: 
#{s.inspect}"))
     end
 
+    def parse_max(s, opts={})
+
+      s0 = s.lines.first
+
+      (0..[ ::Fugit::Nat::MAX_INPUT_LENGTH, s0.length - 1 ].min).each do |i|
+
+        s1 =
+          s0[0, s0.length - i].rstrip
+        f =
+          opts[:cronish] ? parse_cronish(s1, opts) :
+          parse(s1, opts)
+
+        return [ s1, f ] if f
+      end
+
+      nil
+    end
+
     def determine_type(s)
 
       case self.parse(s)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/fugit.rb new/lib/fugit.rb
--- old/lib/fugit.rb    2024-08-15 14:11:09.000000000 +0200
+++ new/lib/fugit.rb    2026-07-10 04:25:38.000000000 +0200
@@ -3,7 +3,7 @@
 
 module Fugit
 
-  VERSION = '1.11.1'
+  VERSION = '1.13.0'
 end
 
 require 'time'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/metadata new/metadata
--- old/metadata        2024-08-15 14:11:09.000000000 +0200
+++ new/metadata        2026-07-10 04:25:38.000000000 +0200
@@ -1,14 +1,14 @@
 --- !ruby/object:Gem::Specification
 name: fugit
 version: !ruby/object:Gem::Version
-  version: 1.11.1
+  version: 1.13.0
 platform: ruby
 authors:
 - John Mettraux
 autorequire:
 bindir: bin
 cert_chain: []
-date: 2024-08-15 00:00:00.000000000 Z
+date: 2026-07-10 00:00:00.000000000 Z
 dependencies:
 - !ruby/object:Gem::Dependency
   name: raabro
@@ -30,20 +30,14 @@
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '1'
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 1.2.11
+        version: '1.4'
   type: :runtime
   prerelease: false
   version_requirements: !ruby/object:Gem::Requirement
     requirements:
     - - "~>"
       - !ruby/object:Gem::Version
-        version: '1'
-    - - ">="
-      - !ruby/object:Gem::Version
-        version: 1.2.11
+        version: '1.4'
 - !ruby/object:Gem::Dependency
   name: rspec
   requirement: !ruby/object:Gem::Requirement
@@ -72,6 +66,20 @@
     - - "~>"
       - !ruby/object:Gem::Version
         version: '0.10'
+- !ruby/object:Gem::Dependency
+  name: ostruct
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
 description: Time tools for flor and the floraison project. Cron parsing and 
occurrence
   computing. Timestamps and more.
 email:
@@ -97,10 +105,11 @@
 - MIT
 metadata:
   changelog_uri: https://github.com/floraison/fugit/blob/master/CHANGELOG.md
-  documentation_uri: https://github.com/floraison/fugit
   bug_tracker_uri: https://github.com/floraison/fugit/issues
+  documentation_uri: https://github.com/floraison/fugit
   homepage_uri: https://github.com/floraison/fugit
   source_code_uri: https://github.com/floraison/fugit
+  rubygems_mfa_required: 'true'
 post_install_message:
 rdoc_options: []
 require_paths:
@@ -116,7 +125,7 @@
     - !ruby/object:Gem::Version
       version: '0'
 requirements: []
-rubygems_version: 3.4.10
+rubygems_version: 3.4.19
 signing_key:
 specification_version: 4
 summary: time tools for flor

Reply via email to