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 + +--- + [](https://github.com/floraison/fugit/actions) [](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
