fugit
Time tools for flor and the floraison group.
It uses et-orbi to represent time instances and raabro as a basis for its parsers.
Fugit is a core dependency of rufus-scheduler >= 3.5.
Related projects
Sister projects
The intersection of those two projects is where fugit is born:
- rufus-scheduler - a cron/at/in/every/interval in-process scheduler, in fact, it's the father project to this fugit project
- flor - a Ruby workflow engine, fugit provides the foundation for its time scheduling capabilities
Similar, sometimes overlapping projects
- chronic - a pure Ruby natural language date parser
- parse-cron - parses cron expressions and calculates the next occurrence after a given date
- ice_cube - Ruby date recurrence library
- ISO8601 - Ruby parser to work with ISO8601 dateTimes and durations
- chrono - a chain of logics about chronology
- CronCalc - calculates cron job occurrences
- Recurrence - a simple library to handle recurring events
- ...
Projects using fugit
- arask - "Automatic RAils taSKs" uses fugit to parse cron strings
- sidekiq-cron - uses fugit to parse cron strings since version 1.0.0, it was using rufus-scheduler previously
- rufus-scheduler - as seen above
- flor - used in the cron procedure
- que-scheduler - a reliable job scheduler for que
- serial_scheduler - ruby task scheduler without threading
- delayed_cron_job - an extension to Delayed::Job that allows you to set cron expressions for your jobs
- GoodJob - a multithreaded, Postgres-based, Active Job backend for Ruby on Rails
- Solid Queue - a DB-based queuing backend for Active Job, designed with simplicity and performance in mind
- ...
Fugit.parse(s)
The simplest way to use fugit is via Fugit.parse(s)
.
require 'fugit'
Fugit.parse('0 0 1 jan *').class
Fugit.parse('12y12M').class
Fugit.parse('2017-12-12').class
Fugit.parse('2017-12-12 UTC').class
Fugit.parse('every day at noon').class
If fugit cannot extract a cron, duration or point in time out of the string, it will return nil.
Fugit.parse('nada')
Fugit.do_parse(s)
Fugit.do_parse(s)
is equivalent to Fugit.parse(s)
, but instead of returning nil, it raises an error if the given string contains no time information.
Fugit.do_parse('nada')
parse_cron, parse_in, parse_at, parse_duration, and parse_nat
require 'fugit'
Fugit.parse_cron('0 0 1 jan *').class
Fugit.parse_duration('12y12M').class
Fugit.parse_at('2017-12-12').class
Fugit.parse_at('2017-12-12 UTC').class
Fugit.parse_nat('every day at noon').class
do_parse_cron, do_parse_in, do_parse_at, do_parse_duration, and do_parse_nat
As Fugit.parse(s)
returns nil when it doesn't grok its input, and Fugit.do_parse(s)
fails when it doesn't grok, each of the parse_
methods has its partner do_parse_
method.
parse_cronish and do_parse_cronish
Sometimes you know a cron expression or an "every" natural expression will come in and you want to discard the rest.
require 'fugit'
Fugit.parse_cronish('0 0 1 jan *').class
Fugit.parse_cronish('every saturday at noon').class
Fugit.parse_cronish('12y12M')
.parse_cronish(s)
will return a Fugit::Cron
instance or else nil.
.do_parse_cronish(s)
will return a Fugit::Cron
instance or else fail with an ArgumentError
.
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.
There is also a #brute_frequency
method which returns an array [ shortest delta, longest delta, occurrence count ]
where delta is the time between two occurrences.
require 'fugit'
c = Fugit::Cron.parse('0 0 * * sun')
c = Fugit::Cron.new('0 0 * * sun')
p Time.now
p c.next_time.to_s
p c.previous_time.to_s
p c.next_time(Time.parse('2024-06-01')).to_s
p c.previous_time(Time.parse('2024-06-01')).to_s
c = Fugit.parse_cron('0 12 * * mon#2')
c.next(Time.parse('2024-02-16 12:00:00'))
.take(3)
.map(&:to_s)
c.prev(Time.parse('2024-02-16 12:00:00'))
.take(3)
.map(&:to_s)
c.within(Time.parse('2024-02-16 12:00')..Time.parse('2024-08-01 12:00'))
.map(&:to_s)
p c.brute_frequency
p c.rough_frequency
p c.match?(Time.parse('2017-08-06'))
p c.match?(Time.parse('2017-08-07'))
p c.match?('2017-08-06')
p c.match?('2017-08-06 12:00')
Example of cron strings understood by fugit:
'5 0 * * *'
'15 14 1 * *'
'0 22 * * 1-5'
'0 22 * * mon-fri'
'23 0-23/2 * * *'
'@yearly'
'@monthly'
'@weekly'
'@daily'
'@midnight'
'@hourly'
'0 0 L * *'
'0 0 last * *'
'0 0 -7-L * *'
Please note that '15/30 * * * *'
is interpreted as '15-59/30 * * * *'
since fugit 1.4.6.
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 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:
Note: The day of a command's execution can be specified by
two fields -- day of month, and day of week. If both fields
are restricted (ie, are not *), the command will be run when
either field matches the current time.
For example, ``30 4 1,15 * 5'' would cause a command to be run
at 4:30 am on the 1st and 15th of each month, plus every Friday.
Fugit follows this specification.
Since fugit 1.7.0, by adding &
right after a day specifier, the day-of-month OR day-of-week
becomes day-of-month AND day-of-week
.
p Fugit.parse_cron('0 0 */2 * 1-5').next_time('2022-08-09').to_s
p Fugit.parse_cron('0 0 */2 * 1-5&').next_time('2022-08-09').to_s
p Fugit.parse_cron('0 0 */2& * 1-5').next_time('2022-08-09').to_s
p Fugit.parse_cron('0 0 */2& * 1-5&').next_time('2022-08-09').to_s
p Fugit.parse_cron('59 6 1-7 * 2').next_time('2020-03-15').to_s
p Fugit.parse_cron('59 6 1-7 * 2&').next_time('2020-03-15').to_s
p Fugit.parse_cron('59 6 1-7& * 2').next_time('2020-03-15').to_s
p Fugit.parse_cron('59 6 1-7& * 2&').next_time('2020-03-15').to_s
the hash extension
Fugit understands 0 5 * * 1#1
or 0 5 * * mon#1
as "each first Monday of the month, at 05:00".
The hash extension can only be used in the day-of-week field.
'0 5 * * 1#1'
'0 5 * * mon#1'
'0 6 * * 5#4,5#5'
'0 6 * * fri#4,fri#5'
'0 7 * * 5#-1'
'0 7 * * fri#-1'
'0 7 * * 5#L'
'0 7 * * fri#L'
'0 7 * * 5#last'
'0 7 * * fri#last'
'0 23 * * mon#2,tue'
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".
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
But what does it reference to? It starts at 1 on 2019-01-01.
require 'et-orbi'
p EtOrbi.parse('2019-01-01').wday
p EtOrbi.parse('2019-01-01').rweek
p EtOrbi.parse('2019-01-01').rweek % 2
p EtOrbi.parse('2019-04-11').wday
p EtOrbi.parse('2019-04-11').rweek
p EtOrbi.parse('2019-04-11').rweek % 2
c = Fugit.parse('* * * * tue%2')
c.match?('2019-01-01')
c.match?('2019-01-08')
c = Fugit.parse('* * * * tue%2+1')
c.match?('2019-01-01')
c.match?('2019-01-08')
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
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
.
c = Fugit.parse('* * * * *')
c = Fugit.parse('5 * * * *')
c = Fugit.parse('* * * * * *')
c = Fugit.parse('5 * * * * *')
Fugit::Duration
A class Fugit::Duration
to parse duration strings (vanilla rufus-scheduler ones and ISO 8601 ones).
Provides duration arithmetic tools.
require 'fugit'
d = Fugit::Duration.parse('1y2M1d4h')
p d.to_plain_s
p d.to_iso_s
p d.to_long_s
d += Fugit::Duration.parse('1y1h')
p d.to_long_s
d += 3600
p d.to_plain_s
p Fugit::Duration.parse('1y2M1d4h').to_sec
There is a #deflate
method
Fugit::Duration.parse(1000).to_plain_s
Fugit::Duration.parse(3600).to_plain_s
Fugit::Duration.parse(1000).deflate.to_plain_s
Fugit::Duration.parse(3600).deflate.to_plain_s
Fugit.parse(1000).deflate.to_plain_s
Fugit.parse(3600).deflate.to_plain_s
There is also an #inflate
method
Fugit::Duration.parse('1h30m12').inflate.to_plain_s
Fugit.parse('1h30m12').inflate.to_plain_s
Fugit.parse('1h30m12').to_sec
Fugit.parse('1h30m12').to_sec.to_s + 's'
The to_*_s
methods are also available as class methods:
p Fugit::Duration.to_plain_s('1y2M1d4h')
p Fugit::Duration.to_iso_s('1y2M1d4h')
p Fugit::Duration.to_long_s('1y2M1d4h')
Fugit::At
Points in time are parsed and given back as EtOrbi::EoTime instances.
Fugit::At.parse('2017-12-12').to_s
Fugit::At.parse('2017-12-12 12:00:00 America/New_York').to_s
Directly with Fugit.parse_at(s)
is OK too:
Fugit.parse_at('2017-12-12 12:00:00 America/New_York').to_s
Directly with Fugit.parse(s)
is OK too:
Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s
Fugit::Nat
Fugit understand some kind of "natural" language:
For example, those "every" get turned into Fugit::Cron
instances:
Fugit::Nat.parse('every day at five')
Fugit::Nat.parse('every weekday at five')
Fugit::Nat.parse('every day at 5 pm')
Fugit::Nat.parse('every tuesday at 5 pm')
Fugit::Nat.parse('every wed at 5 pm')
Fugit::Nat.parse('every day at 16:30')
Fugit::Nat.parse('every day at 16:00 and 18:00')
Fugit::Nat.parse('every day at noon')
Fugit::Nat.parse('every day at midnight')
Fugit::Nat.parse('every tuesday and monday at 5pm')
Fugit::Nat.parse('every wed or Monday at 5pm and 11')
Fugit::Nat.parse('every day at 5 pm on America/Los_Angeles')
Fugit::Nat.parse('every day at 6 pm in Asia/Tokyo')
Fugit::Nat.parse('every 3 hours')
Fugit::Nat.parse('every 4 months')
Fugit::Nat.parse('every 5 minutes')
Fugit::Nat.parse('every 15s')
Directly with Fugit.parse(s)
is OK too:
Fugit.parse('every day at five')
Ambiguous nats
Not all strings result in a clean, single, cron expression. The multi: false|true|:fail
argument to Fugit::Nat.parse
could help.
Fugit::Nat.parse('every day at 16:00 and 18:00')
.to_cron_s
Fugit::Nat.parse('every day at 16:00 and 18:00', multi: true)
.collect(&:to_cron_s)
Fugit::Nat.parse('every day at 16:15 and 18:30')
.to_cron_s
Fugit::Nat.parse('every day at 16:15 and 18:30', multi: true)
.collect(&:to_cron_s)
Fugit::Nat.parse('every day at 16:15 and 18:30', multi: :fail)
Fugit::Nat.parse('every day at 16:15 nada 18:30', multi: true)
multi: true
indicates to Fugit::Nat
that an array of Fugit::Cron
instances is expected as a result.
multi: :fail
tells Fugit::Nat.parse
to fail if the result is more than 1 Fugit::Cron
instances.
multi: false
is the default behaviour, return a single Fugit::Cron
instance or nil when it cannot parse.
Please note that "nat" input is limited to 256 characters (fugit 1.11.1).
Nat Midnight
"Every day at midnight"
is supported, but "Every monday at midnight"
will be interpreted (as of Fugit <= 1.4.x) as "Every monday at 00:00"
. Sorry about that.
12 AM and PM
How does fugit react with "12 am"
, "12 pm"
, "12 midnight"
, etc?
require 'fugit'
p Fugit.parse('every day at 12am').original
p Fugit.parse('every day at 12pm').original
p Fugit.parse('every day at 12:00am').original
p Fugit.parse('every day at 12:00pm').original
p Fugit.parse('every day at 12:00 am').original
p Fugit.parse('every day at 12:00 pm').original
p Fugit.parse('every day at 12:15am').original
p Fugit.parse('every day at 12:15pm').original
p Fugit.parse('every day at 12:15 am').original
p Fugit.parse('every day at 12:15 pm').original
p Fugit.parse('every day at 12 noon').original
p Fugit.parse('every day at 12 midnight').original
p Fugit.parse('every day at 12:00 noon').original
p Fugit.parse('every day at 12:00 midnight').original
p Fugit.parse('every day at 12:15 noon').original
p Fugit.parse('every day at 12:15 midnight').original
LICENSE
MIT, see LICENSE.txt