Giter Site home page Giter Site logo

fugit's Introduction

fugit

tests Gem Version

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::Cron
Fugit.parse('12y12M').class              # ==> ::Fugit::Duration

Fugit.parse('2017-12-12').class          # ==> ::EtOrbi::EoTime
Fugit.parse('2017-12-12 UTC').class      # ==> ::EtOrbi::EoTime

Fugit.parse('every day at noon').class   # ==> ::Fugit::Cron

If fugit cannot extract a cron, duration or point in time out of the string, it will return nil.

Fugit.parse('nada')
  # ==> nil

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')
  # ==> /home/jmettraux/w/fugit/lib/fugit/parse.rb:32
  #     :in `do_parse': found no time information in "nada" (ArgumentError)

parse_cron, parse_in, parse_at, parse_duration, and parse_nat

require 'fugit'

Fugit.parse_cron('0 0 1 jan *').class       # ==> ::Fugit::Cron
Fugit.parse_duration('12y12M').class        # ==> ::Fugit::Duration

Fugit.parse_at('2017-12-12').class          # ==> ::EtOrbi::EoTime
Fugit.parse_at('2017-12-12 UTC').class      # ==> ::EtOrbi::EoTime

Fugit.parse_nat('every day at noon').class  # ==> ::Fugit::Cron

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::Cron
Fugit.parse_cronish('every saturday at noon').class  # ==> ::Fugit::Cron

Fugit.parse_cronish('12y12M')        # ==> nil

.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')
  # or
c = Fugit::Cron.new('0 0 * *  sun')

p Time.now  # => 2017-01-03 09:53:27 +0900

p c.next_time.to_s      # => 2017-01-08 00:00:00 +0900
p c.previous_time.to_s  # => 2017-01-01 00:00:00 +0900

p c.next_time(Time.parse('2024-06-01')).to_s
  # => "2024-06-02 00:00:00 +0900"
p c.previous_time(Time.parse('2024-06-01')).to_s
  # => "2024-05-26 00:00:00 +0900"
    #
    # `Fugit::Cron#next_time` and `#previous_time` accept a "start time"

c = Fugit.parse_cron('0 12 * * mon#2')

  # `#next` and `#prev` return Enumerable instances
  #
  # These two methods are available since fugit 1.10.0.
  #
c.next(Time.parse('2024-02-16 12:00:00'))
  .take(3)
  .map(&:to_s)
    # => [ '2024-03-11 12:00:00',
    #      '2024-04-08 12:00:00',
    #      '2024-05-13 12:00:00' ]
c.prev(Time.parse('2024-02-16 12:00:00'))
  .take(3)
  .map(&:to_s)
    # => [ '2024-02-12 12:00:00',
    #      '2024-01-08 12:00:00',
    #      '2023-12-11 12:00:00' ]

  # `#within` accepts a time range and returns an array of Eo::EoTime
  # instances that correspond to the occurrences of the cron within
  # the time range
  #
  # This method is available since fugit 1.10.0.
  #
c.within(Time.parse('2024-02-16 12:00')..Time.parse('2024-08-01 12:00'))
  .map(&:to_s)
    # => [ '2024-03-11 12:00:00',
    #      '2024-04-08 12:00:00',
    #      '2024-05-13 12:00:00',
    #      '2024-06-10 12:00:00',
    #      '2024-07-08 12:00:00' ]

p c.brute_frequency  # => [ 604800, 604800, 53 ]
                     #    [ delta min, delta max, occurrence count ]
p c.rough_frequency  # => 7 * 24 * 3600 (7d rough frequency)

p c.match?(Time.parse('2017-08-06'))  # => true
p c.match?(Time.parse('2017-08-07'))  # => false
p c.match?('2017-08-06')              # => true
p c.match?('2017-08-06 12:00')        # => false

Example of cron strings understood by fugit:

'5 0 * * *'         # 5 minutes after midnight, every day
'15 14 1 * *'       # at 1415 on the 1st of every month
'0 22 * * 1-5'      # at 2200 on weekdays
'0 22 * * mon-fri'  # idem
'23 0-23/2 * * *'   # 23 minutes after 00:00, 02:00, 04:00, ...

'@yearly'    # turns into '0 0 1 1 *'
'@monthly'   # turns into '0 0 1 * *'
'@weekly'    # turns into '0 0 * * 0'
'@daily'     # turns into '0 0 * * *'
'@midnight'  # turns into '0 0 * * *'
'@hourly'    # turns into '0 * * * *'

'0 0 L * *'     # last day of month at 00:00
'0 0 last * *'  # idem
'0 0 -7-L * *'  # from the seventh to last to the last day of month at 00:00

# and more...

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.

# standard cron

p Fugit.parse_cron('0 0 */2 * 1-5').next_time('2022-08-09').to_s
  # ==> "2022-08-10 00:00:00 +0900"

# with an &

p Fugit.parse_cron('0 0 */2 * 1-5&').next_time('2022-08-09').to_s # or
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
  # ==> "2022-08-11 00:00:00 +0900"


# standard cron

p Fugit.parse_cron('59 6 1-7 * 2').next_time('2020-03-15').to_s
  # ==> "2020-03-17 06:59:00 +0900"

# with an &

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
  # ==> "2020-04-07 06:59:00 +0900"

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'  # the first Monday of the month at 05:00

'0 6 * * 5#4,5#5'      #
'0 6 * * fri#4,fri#5'  # the 4th and 5th Fridays of the month at 06:00

'0 7 * * 5#-1'    #
'0 7 * * fri#-1'  # the last Friday of the month at 07:00

'0 7 * * 5#L'       #
'0 7 * * fri#L'     #
'0 7 * * 5#last'    #
'0 7 * * fri#last'  # the last Friday of the month at 07:00

'0 23 * * mon#2,tue'  # the 2nd Monday of the month and every Tuesday, at 23:00

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' # >= 1.1.8

# 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

# 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

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

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

# ...

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('* * * * *') # 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::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  # => "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

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

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:

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.

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:

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:

Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s
  # ==> "2017-12-12 12:00:00 -0500"

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')                         # ==> '0 5 * * *'
Fugit::Nat.parse('every weekday at five')                     # ==> '0 5 * * 1,2,3,4,5'
Fugit::Nat.parse('every day at 5 pm')                         # ==> '0 17 * * *'
Fugit::Nat.parse('every tuesday at 5 pm')                     # ==> '0 17 * * 2'
Fugit::Nat.parse('every wed at 5 pm')                         # ==> '0 17 * * 3'
Fugit::Nat.parse('every day at 16:30')                        # ==> '30 16 * * *'
Fugit::Nat.parse('every day at 16:00 and 18:00')              # ==> '0 16,18 * * *'
Fugit::Nat.parse('every day at noon')                         # ==> '0 12 * * *'
Fugit::Nat.parse('every day at midnight')                     # ==> '0 0 * * *'
Fugit::Nat.parse('every tuesday and monday at 5pm')           # ==> '0 17 * * 1,2'
Fugit::Nat.parse('every wed or Monday at 5pm and 11')         # ==> '0 11,17 * * 1,3'
Fugit::Nat.parse('every day at 5 pm on America/Los_Angeles')  # ==> '0 17 * * * America/Los_Angeles'
Fugit::Nat.parse('every day at 6 pm in Asia/Tokyo')           # ==> '0 18 * * * Asia/Tokyo'
Fugit::Nat.parse('every 3 hours')                             # ==> '0 */3 * * *'
Fugit::Nat.parse('every 4 months')                            # ==> '0 0 1 */4 *'
Fugit::Nat.parse('every 5 minutes')                           # ==> '*/5 * * * *'
Fugit::Nat.parse('every 15s')                                 # ==> '*/15 * * * * *'

Directly with Fugit.parse(s) is OK too:

Fugit.parse('every day at five')  # ==> Fugit::Cron instance '0 5 * * *'

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
    # ==> '0 16,18 * * *' (a single Fugit::Cron instances)
Fugit::Nat.parse('every day at 16:00 and 18:00', multi: true)
  .collect(&:to_cron_s)
    # ==> [ '0 16,18 * * *' ] (array of Fugit::Cron instances, here only one)

Fugit::Nat.parse('every day at 16:15 and 18:30')
  .to_cron_s
    # ==> '15 16 * * *' (a single of Fugit::Cron instances)
Fugit::Nat.parse('every day at 16:15 and 18:30', multi: true)
  .collect(&:to_cron_s)
    # ==> [ '15 16 * * *', '30 18 * * *' ] (two Fugit::Cron instances)

Fugit::Nat.parse('every day at 16:15 and 18:30', multi: :fail)
  # ==> ArgumentError: multiple crons in "every day at 16:15 and 18:30"
  #     (15 16 * * * | 30 18 * * *)
Fugit::Nat.parse('every day at 16:15 nada 18:30', multi: true)
  # ==> nil

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.

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  # ==> "0 0 * * *"
p Fugit.parse('every day at 12pm').original  # ==> "0 12 * * *"

p Fugit.parse('every day at 12:00am').original   # ==> "0 0 * * *"
p Fugit.parse('every day at 12:00pm').original   # ==> "0 12 * * *"
p Fugit.parse('every day at 12:00 am').original  # ==> "0 0 * * *"
p Fugit.parse('every day at 12:00 pm').original  # ==> "0 12 * * *"
p Fugit.parse('every day at 12:15am').original   # ==> "15 0 * * *"
p Fugit.parse('every day at 12:15pm').original   # ==> "15 12 * * *"
p Fugit.parse('every day at 12:15 am').original  # ==> "15 0 * * *"
p Fugit.parse('every day at 12:15 pm').original  # ==> "15 12 * * *"

p Fugit.parse('every day at 12 noon').original         # ==> "0 12 * * *"
p Fugit.parse('every day at 12 midnight').original     # ==> "0 24 * * *"
p Fugit.parse('every day at 12:00 noon').original      # ==> "0 12 * * *"
p Fugit.parse('every day at 12:00 midnight').original  # ==> "0 24 * * *"
p Fugit.parse('every day at 12:15 noon').original      # ==> "15 12 * * *"
p Fugit.parse('every day at 12:15 midnight').original  # ==> "15 24 * * *"

  # as of fugit 1.7.2

LICENSE

MIT, see LICENSE.txt

fugit's People

Contributors

amatsuda avatar chukwuemekaajah avatar cristianbica avatar delbetu avatar dependabot[bot] avatar gitter-badger avatar grosser avatar hlascelles avatar jmettraux avatar mreinsch avatar olleolleolle avatar petergoldstein avatar solteszad avatar vivekmiyani avatar wishdev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

fugit's Issues

cron #next_time loop breaker issue

Issue description

A clear and concise description of what the issue is. (There is an example of a carefully filled issue at #18)

How to reproduce

The simplest piece of code that reproduces the issue, for example:

heroku local

Or else, please describe carefully what to do to see a live example of the issue.

Error and error backtrace (if any)

(This should look like:

/home/ggallardo/.rvm/gems/ruby-3.0.2/bin/sidekiq:23:in `load'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/bin/sidekiq:23:in `<main>'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/bin/ruby_executable_hooks:22:in `eval'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/bin/ruby_executable_hooks:22:in `<main>'
10:04:01 AM worker.1 |  2021-08-17T14:04:01.193Z pid=11350 tid=b42 WARN: RuntimeError: too many loops for "0 8 15 * *" #next_time, breaking, cron expression most likely invalid (Feb 30th like?), please fill an issue at https://git.io/fjJC9
10:04:01 AM worker.1 |  2021-08-17T14:04:01.193Z pid=11350 tid=b42 WARN: /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/fugit-1.5.0/lib/fugit/cron.rb:238:in `block in next_time'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/fugit-1.5.0/lib/fugit/cron.rb:236:in `loop'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/fugit-1.5.0/lib/fugit/cron.rb:236:in `next_time'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/rufus-scheduler-3.7.0/lib/rufus/scheduler/jobs_repeat.rb:329:in `set_next_time'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/rufus-scheduler-3.7.0/lib/rufus/scheduler/jobs_repeat.rb:281:in `initialize'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/rufus-scheduler-3.7.0/lib/rufus/scheduler.rb:711:in `new'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/rufus-scheduler-3.7.0/lib/rufus/scheduler.rb:711:in `do_schedule'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/rufus-scheduler-3.7.0/lib/rufus/scheduler.rb:226:in `cron'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/scheduler.rb:244:in `new_job'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/scheduler.rb:118:in `block in load_schedule_job'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/scheduler.rb:111:in `each'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/scheduler.rb:111:in `load_schedule_job'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/scheduler.rb:89:in `block in load_schedule!'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/scheduler.rb:87:in `each'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/scheduler.rb:87:in `load_schedule!'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/manager.rb:37:in `start'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler.rb:18:in `block (2 levels) in <main>'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-6.2.1/lib/sidekiq/util.rb:87:in `block in fire_event'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-6.2.1/lib/sidekiq/util.rb:86:in `each'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-6.2.1/lib/sidekiq/util.rb:86:in `fire_event'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-6.2.1/lib/sidekiq/cli.rb:92:in `run'
10:04:01 AM worker.1 |  /home/ggallardo/.rvm/gems/ruby-3.0.2/gems/sidekiq-6.2.1/bin/sidekiq:31:in `<top (required)>'

)

Expected behaviour

A clear and concise description of what you expected to happen.

Context

ggallardo@ggallardo-Z390-UD:~/tango/api$ uname -a
Linux ggallardo-Z390-UD 5.11.0-25-generic #27~20.04.1-Ubuntu SMP Tue Jul 13 17:41:23 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
ggallardo@ggallardo-Z390-UD:~/tango/api$ bundle exec ruby -v
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
ggallardo@ggallardo-Z390-UD:~/tango/api$ bundle exec ruby -e "p [ :env_tz, ENV['TZ'] ]"
[:env_tz, nil]
ggallardo@ggallardo-Z390-UD:~/tango/api$ bundle exec ruby -r et-orbi -e "EtOrbi._make_info"
(secs:1629209891.538592,utc~:"2021-08-17 14:18:11.5385921001434326",ltz~:"-04")
(etz:nil,tnz:"-04",tziv:"2.0.4",tzidv:nil,rv:"3.0.2",rp:"x86_64-linux",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::DataTimezone: America/Santiago>,eotnfz:"-0400",eotlzn:"America/Santiago",eotnfZ:"-04",debian:"America/Santiago",centos:nil,osx:"America/Santiago")
ggallardo@ggallardo-Z390-UD:~/tango/api$ bundle exec ruby -r fugit -e "p Fugit::VERSION"
"1.5.0"
ggallardo@ggallardo-Z390-UD:~/tango/api$ bundle exec ruby -e "p [ :now, Time.now, :zone, Time.now.zone ]
> 

Additional context

Add any other context about the problem here.

TZInfo::PeriodNotFound exception when using Time.zone

Issue description

When using Time.zone instead of ENV['TZ'], Cron#next_time throws a TZInfo::PeriodNotFound exception.

How to reproduce

require 'fugit'
require 'active_support/core_ext/time/zones'
Time.zone = 'America/Santiago'  # it works with ENV['TZ'] = 'America/Santiago'
t = EtOrbi::EoTime.parse('2021-08-18 01:00:00')
puts Fugit::Cron.parse('0 8 15 * *').next_time(t).to_s

Error and error backtrace (if any)

 11: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/fugit-1.5.1/lib/fugit/cron.rb:251:in `next_time'
 10: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/fugit-1.5.1/lib/fugit/cron.rb:251:in `loop'
  9: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/fugit-1.5.1/lib/fugit/cron.rb:261:in `block in next_time'
  8: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/fugit-1.5.1/lib/fugit/cron.rb:107:in `inc_day'
  7: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/et-orbi-1.2.4/lib/et-orbi/make.rb:59:in `make_time'
  6: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/et-orbi-1.2.4/lib/et-orbi/make.rb:108:in `make_from_array'
  5: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/et-orbi-1.2.4/lib/et-orbi/make.rb:44:in `parse'
  4: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/tzinfo-2.0.3/lib/tzinfo/timezone.rb:648:in `local_to_utc'
  3: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/tzinfo-2.0.3/lib/tzinfo/timestamp.rb:142:in `for'
  2: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/tzinfo-2.0.3/lib/tzinfo/timezone.rb:652:in `block in local_to_utc'
  1: from /Users/pascal/.rvm/gems/ruby-2.7.2/gems/tzinfo-2.0.3/lib/tzinfo/timezone.rb:500:in `period_for_local'
     TZInfo::PeriodNotFound (2021-09-05 00:00:00 is an invalid local time.)

Expected behaviour

The timezone setting in Time.zone should be used as fallback when ENV['TZ'] is nil and the correct output ("2021-09-15 08:00:00 -0300") should be returned.

Context

Darwin Pascals-BigPro.fritz.box 20.6.0 Darwin Kernel Version 20.6.0: Wed Jun 23 00:26:31 PDT 2021; root:xnu-7195.141.2~5/RELEASE_X86_64 x86_64
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin20]
[:env_tz, nil]
(secs:1631862880.412856,utc~:"2021-09-17 07:14:40.4128561019897461",ltz~:"CEST")
(etz:nil,tnz:"CEST",tziv:"2.0.4",tzidv:nil,rv:"2.7.2",rp:"x86_64-darwin20",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::TimezoneProxy: Africa/Ceuta>,eotnfz:"+0200",eotlzn:"Africa/Ceuta",eotnfZ:"CEST",debian:nil,centos:nil,osx:"zoneinfo/Europe/Zurich")
"1.5.1"
[:now, 2021-09-17 09:15:13.611169 +0200, :zone, "CEST"]

)

cron time + DST trouble

I'm using Fugit.parse('55 6 * * 2#1 America/Los_Angeles') for a monthly alert.
This has been alerting fine at 6:55 all the time, but 2020-12-01 it alerted 1 hour early (5:55).
Last time it alerted Nov 3 at 6:55 AM which was in DST already, so I'd have expected that to fail and not the December one, but there is a chance it was scheduled before DST kicked in.
I reproduced the issue using Timecop.travel('2020-11-30') and Fugit.parse('55 6 * * 2#1 America/Los_Angeles').next_time.to_debug_s and it looks like the timezone is wrong:

Timecop.travel('2020-11-30')
# => 2020-11-30 00:00:00 -0800
Fugit.parse('55 6 * * 2#1 America/Los_Angeles').next_time.to_s
# => "2020-12-01 07:55:00 -0700" <-- wrong ?
Time.now
# => 2020-11-30 00:09:15 -0800

cron: mon-thu not respected when L (last day of month)

As reported by @mitnal in jmettraux/rufus-scheduler#270

require 'rufus-scheduler'

#       June 2018
#  Su Mo Tu We Th Fr Sa
#                  1  2
#   3  4  5  6  7  8  9
#  10 11 12 13 14 15 16
#  17 18 19 20 21 22 23
#  24 25 26 27 28 29 30
#
#      July 2018
# Su Mo Tu We Th Fr Sa
#  1  2  3  4  5  6  7
#  8  9 10 11 12 13 14
# 15 16 17 18 19 20 21
# 22 23 24 25 26 27 28
# 29 30 31

p RUBY_VERSION
p RUBY_PLATFORM

ENV['TZ'] = 'Europe/Berlin'

puts
#now = EtOrbi.parse('2018-06-28 18:00:00 Europe/Berlin')
now = Rufus::Scheduler.parse('2018-06-28 18:00:00')
puts "now: " + now.to_s
puts
puts "  '0 9 -2 * *'  9am the second to last day of the month"
puts "3.4.2:  2018-07-29 09:00:00 +0200"
puts "3.5.0:  " + Rufus::Scheduler.parse('0 9 -2 * * Europe/Berlin').next_time(now).to_s
puts
puts "  '0 8 L * mon-thu'  8am the last day of the month (must be a mon or a thu)"
puts "3.4.2:  2018-07-31 08:00:00 +0200"
puts "3.5.0:  " + Rufus::Scheduler.parse('0 8 L * mon-thu Europe/Berlin').next_time(now).to_s
puts
puts "  '0 0 -5 * *' 0am fifth to last day of the month"
puts "3.4.2:  2018-07-26 00:00:00 +0200"
puts "3.5.0:  " + Rufus::Scheduler.parse('0 0 -5 * *').next_time(now).to_s
puts

gives:

"2.4.4"
"x86_64-darwin17"

now: 2018-06-28 18:00:00 +0200

  '0 9 -2 * *'  9am the second to last day of the month
3.4.2:  2018-07-29 09:00:00 +0200
3.5.0:  2018-06-29 09:00:00 +0200

  '0 8 L * * mon-thu'  8am the last day of the month (must be a mon or a thu)
3.4.2:  2018-07-31 08:00:00 +0200
3.5.0:  2018-06-30 08:00:00 +0200

  '0 0 -5 * *' 0am fifth to last day of the month
3.4.2:  2018-07-26 00:00:00 +0200
3.5.0:  2018-07-27 00:00:00 +0200

The second result is wrong. It should give 2018-07-31 08:00:00 +0200 instead (like rufus-scheduler 3.4.2 did).

[feature request] cron syntax validation

Issue description

It would be great to have a method like .validate that can validate and check if the syntax of the cron string is correct.

How to reproduce

N/A

Expected behaviour

N/A

Context

I want to store the cron string into the database and use it later for checking purposes.
So it would be great if I can do some validation with the string before saving it.

Additional context

N/A

cron #next_time loop breaker issue

Issue description

A clear and concise description of what the issue is. (There is an example of a carefully filled issue at #18)

How to reproduce

The simplest piece of code that reproduces the issue, for example:

require 'fugit'
c = Fugit.parse('0 9 29 feb *')
p c.previous_time

Or else, please describe carefully what to do to see a live example of the issue.

Error and error backtrace (if any)

(This should look like:

ArgumentError: found no time information in "0-65 * * * *"
  from /home/john/w/fugit/lib/fugit/parse.rb:32:in `do_parse'
  from ...
  from /home/john/.gem/ruby/2.3.7/gems/bundler-1.16.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start'
  from /home/john/.gem/ruby/2.3.7/gems/bundler-1.16.2/lib/bundler/cli.rb:18:in `start'
  from /home/john/.gem/ruby/2.3.7/gems/bundler-1.16.2/exe/bundle:30:in `block in <top (required)>'
  from /home/john/.gem/ruby/2.3.7/gems/bundler-1.16.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'
  from /home/john/.gem/ruby/2.3.7/gems/bundler-1.16.2/exe/bundle:22:in `<top (required)>'
  from /home/john/.gem/ruby/2.3.7/bin/bundle:22:in `load'
  from /home/john/.gem/ruby/2.3.7/bin/bundle:22:in `<main>'

)

Expected behaviour

A clear and concise description of what you expected to happen.

Context

Please replace the content of this section with the output of the following commands:

uname -a
bundle exec ruby -v
bundle exec ruby -e "p [ :env_tz, ENV['TZ'] ]"
bundle exec ruby -r et-orbi -e "EtOrbi._make_info"
bundle exec ruby -r fugit -e "p Fugit::VERSION"
bundle exec ruby -e "p [ :now, Time.now, :zone, Time.now.zone ]

(It's supposed to look like

Darwin pollux.local 17.7.0 Darwin Kernel Version 17.7.0: Thu Dec 20 21:47:19 PST 2018;
  root:xnu-4570.71.22~1/RELEASE_X86_64 x86_64
ruby 2.3.7p456 (2018-03-28 revision 63024) [x86_64-darwin17]
[:env_tz, nil]
(secs:1553304485.185308,utc~:"2019-03-23 01:28:05.18530797958374023",ltz~:"JST")
(etz:nil,tnz:"JST",tziv:"2.0.0",tzidv:"1.2018.9",rv:"2.3.7",rp:"x86_64-darwin17",win:false,
  rorv:nil,astz:nil,eov:"1.1.7",eotnz:#<TZInfo::TimezoneProxy: Asia/Tokyo>,eotnfz:"+0900",
  eotlzn:"Asia/Tokyo",eotnfZ:"JST",debian:nil,centos:nil,osx:"zoneinfo/Asia/Tokyo")
"1.3.9"

)

Additional context

Add any other context about the problem here.

cron #previous_time loop breaker issue: modulo plus offset

Issue description

I am not fully confident in what the combination of these two syntaxes is supposed to mean, and the +2 syntax isnโ€™t documented fully in the README, but I believe this user input would be interpreted as every odd Tuesday but only after the first two odd Tuesdays of a month? Which seems to mean โ€œnever,โ€ and thus warrant an out of range error.

How to reproduce

require 'fugit'
c = Fugit.parse('0 8 * * 1%2+2')
p c.next_time

Error and error backtrace (if any)

(This should look like:

Traceback (most recent call last):
       16: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/base.rb:476:in `start'
       15: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/cli.rb:30:in `dispatch'
       14: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor.rb:399:in `dispatch'
       13: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
       12: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
       11: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/cli.rb:476:in `exec'
       10: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:28:in `run'
        9: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `kernel_load'
        8: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `load'
        7: from /Users/ticky/.rbenv/versions/2.6.5/bin/irb:23:in `<top (required)>'
        6: from /Users/ticky/.rbenv/versions/2.6.5/bin/irb:23:in `load'
        5: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/irb-1.2.3/exe/irb:11:in `<top (required)>'
        4: from (irb):4
        3: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/fugit-1.4.1/lib/fugit/cron.rb:242:in `next_time'
        2: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/fugit-1.4.1/lib/fugit/cron.rb:242:in `loop'
        1: from /Users/ticky/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/fugit-1.4.1/lib/fugit/cron.rb:244:in `block in next_time'
RuntimeError (too many loops for "0 8 * * 1%2+2" #next_time, breaking, cron expression most likely invalid (Feb 30th like?), please fill an issue at https://git.io/fjJC9)

)

Expected behaviour

It ought to either return a valid value, or give a descriptive error rather than the loop error.

Context

Darwin Kinzie.home.drac.at 20.1.0 Darwin Kernel Version 20.1.0: Sat Oct 31 00:07:11 PDT 2020; root:xnu-7195.50.7~2/RELEASE_X86_64 x86_64
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin18]
[:env_tz, nil]
(secs:1606760366.633137,utc~:"2020-11-30 18:19:26.6331369876861572",ltz~:"MST")
(etz:nil,tnz:"PST",tziv:"1.2.8",tzidv:nil,rv:"2.6.5",rp:"x86_64-darwin18",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::TimezoneProxy: America/Dawson>,eotnfz:"-0700",eotlzn:"America/Dawson",eotnfZ:"MST",debian:nil,centos:nil,osx:"zoneinfo/America/Vancouver")
"1.4.1"

Additional context

Like #30, this is from in-the-wild user input. ๐Ÿ˜ƒ

NoMethodError when parse'ing empty string

Issue description

Parsing an empty string leads to NoMethodError instead of ArgumentError (when using do_parse) or nil (when using parse).

How to reproduce

require 'fugit'
Fugit::Cron.parse(' ')
 => nil 
Fugit::Cron.parse(nil)
 => nil 
Fugit::Cron.parse('')
Traceback (most recent call last):
        1: from (irb):20
NoMethodError (undefined method `subgather' for nil:NilClass)

Expected behaviour

Empty string should parse as invalid cron string, just like " " or nil.

Context

Linux 242fdceb1492 5.4.39-linuxkit #1 SMP Fri May 8 23:03:06 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-linux]
[:env_tz, nil]
(secs:1606866124.7550213,utc~:"2020-12-01 23:42:04.755021333694458",ltz~:"UTC")
(etz:nil,tnz:"UTC",tziv:"1.2.8",tzidv:nil,rv:"2.6.6",rp:"x86_64-linux",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::TimezoneProxy: Etc/UCT>,eotnfz:"+0000",eotlzn:"Etc/UCT",eotnfZ:"UTC",debian:"/UTC",centos:nil,osx:"/UTC")
"1.4.1"

Additional context

I'm using Fugit in Rails context.

too many loops in #next_time/previous_time for invalid dates

Issue description

This is again a known issue reporting as per error message. Although the input date is incorrect (e.g. Feb 31st) the :parse method returns a valid object and the loop :next_time reaches the max number of iterations.

How to reproduce

require 'fugit'
c = Fugit.parse('0 12 31 2 *')
p c.next_time

Same issue occurs with all inputs that have month within 1-12 and day within 1-31 but are not valid dates:

  • Feb 30th
  • Feb 31st
  • Apr 31st
  • Jun 31st
  • Sep 31st
  • Nov 31st

Suggestions

  • Could the parse method return nil directly when invalid combination of month-day is detected?

Endless loop after call Fugit::Cron#next_time with time zone 'CST'

environment:

  • ruby 2.3.3p222
  • fugit 1.1.6
  • Time.now.zone is CST(+08:00)

code:

Time.now.utc # "2019-01-02T05:25:53.960Z"
cron = Fugit.do_parse_cron('0 0 1 1 *')
cron.next_time.to_s

expected:

"2019-12-01 00:00:00 +0000"

got:

Endless loop

What I found:

After some debugging, I thought the method TimeCursor#inc_mont doesn't work right in timezone CST (maybe all positive east of UTC timezones).

@t = ::EtOrbi.make(y, m)

This line will parse the year and month as time zone CST, and then transfer to UTC.

For example:

::EtOrbi.make(2019, 2).to_s

expected: 2019-02-01 00:00:00 +0000
but got: 2019-01-31 16:00:00 +0000

The month isn't increased, and the loop in next_time will last forever.

Thank you for your work.

Natural parsing for specifying days in the month up to the last day

Issue description

Apologies for the issue spamming...

I also need a way to do natural parsing of days of the month up to the last day of the month. I need this because the system I'm building will have users that wish to make themselves unavailable during the last n days of a month where they're closing their financial books off or whatever other things businesses tend to do at the end of the month that makes their people unavailable.

How to reproduce

require 'fugit'

f = Fugit::Nat.parse('every 25th to the last day of the month') # => nil

Expected behaviour

I was expecting to get a cron represenation back that looks a little something like this?

* * 25-L * *

Context

Please replace the content of this section with the output of the following commands:

Linux a716f734f0ee 4.19.76-linuxkit #1 SMP Tue May 26 11:42:35 UTC 2020 x86_64 GNU/Linux

ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-linux]

[:env_tz, nil]

(secs:1601231894.3672342,utc~:"2020-09-27 18:38:14.3672342300415039",ltz~:"UTC")
(etz:nil,tnz:"UTC",tziv:"1.2.7",tzidv:nil,rv:"2.6.6",rp:"x86_64-linux",win:false,rorv:"6.0.3.3",astz:[ActiveSupport::TimeZone, "Etc/UTC"],eov:"1.2.4",eotnz:#<TZInfo::DataTimezone: Etc/UTC>,eotnfz:"+0000",eotlzn:"Etc/UTC",eotnfZ:"UTC",debian:"Etc/UTC",centos:nil,osx:"Etc/UTC")

Additional context

None that I can think of right now...

Fugit::Nat.parse('every hour') returns nil

Fugit::Nat.parse('every hour') returns nil
Fugit::Nat.parse('every day') incorrectly returns '0 * * * *' which is every hour
It would be great if it could handle some common cases without an explicit number.

require 'fugit'
['second', 'minute', 'hour', 'day', 'week', 'month', 'year'].each do |period|
  puts "every #{period}: #{Fugit::Nat.parse("every #{period}")&.to_cron_s}"
  puts "every 1 #{period}: #{Fugit::Nat.parse("every 1 #{period}")&.to_cron_s}"
end
every second: [MISSING]
every 1 second: * * * * * * [OK]
every minute: [MISSING]
every 1 minute: * * * * * [OK]
every hour: [MISSING]
every 1 hour: 0 * * * * [OK]
every day: 0 * * * * [INCORRECT]
every 1 day: 0 0 * * * [OK]
every week: [MISSING]
every 1 week: [MISSING]
every month: [MISSING]
every 1 month: 0 0 1 * * [OK]
every year: [MISSING]
every 1 year: [MISSING]

For every day/every week/every month/every year, it should mirror the behaviour of cron, i.e.

@yearly        Run once a year, "0 0 1 1 *".
@monthly       Run once a month, "0 0 1 * *".
@weekly        Run once a week, "0 0 * * 0".
@daily         Run once a day, "0 0 * * *".
@hourly        Run once an hour, "0 * * * *".

(above taken from man 5 crontab)

Thanks!

Incorrect transition into DST for America/New_York

As pointed at by @harsha-flipp in jmettraux/rufus-scheduler#329

Given the script

#require 'rufus-scheduler'
require 'fugit'

# https://www.timeanddate.com/time/zone/usa/chicago
# https://www.timeanddate.com/time/change/usa/chicago

# https://www.timeanddate.com/time/zone/usa/newyork
# https://www.timeanddate.com/time/change/usa/newyork

#ENV['TZ'] = 'America/Chicago' # so that Time#to_s below stays in Chicago
ENV['TZ'] = 'America/New_York' # so that Time#to_s below stays in New York

c = Fugit.parse_cron('5 0 * * *')
#c = Fugit.parse_cron('30 4 * * *')


p [ :tz, ENV['TZ'] ]
p [ :ruby, RUBY_VERSION, RUBY_PLATFORM ]
p [ :tzinfo, TZInfo::VERSION ]
p [ :fugit, Fugit::VERSION ]

# into daylight saving time

puts

t = Time.parse('2021-03-10')
10.times do
  t = c.next_time(t)
  p t.to_s
end

# out of daylight saving time

puts

t = Time.parse('2021-11-05')
10.times do
  t = c.next_time(t)
  p t.to_s
end

America/Chicago yields the result below, which correctly goes into DST and out of it.

[:tz, "America/Chicago"]
[:ruby, "2.7.1", "x86_64-openbsd6.7"]
[:tzinfo, "2.0.4"]
[:fugit, "1.5.2"]

"2021-03-10 00:05:00 -0600"
"2021-03-11 00:05:00 -0600"
"2021-03-12 00:05:00 -0600"
"2021-03-13 00:05:00 -0600"
"2021-03-14 00:05:00 -0600"
"2021-03-15 00:05:00 -0500"
"2021-03-16 00:05:00 -0500"
"2021-03-17 00:05:00 -0500"
"2021-03-18 00:05:00 -0500"
"2021-03-19 00:05:00 -0500"

"2021-11-05 00:05:00 -0500"
"2021-11-06 00:05:00 -0500"
"2021-11-07 00:05:00 -0500"
"2021-11-08 00:05:00 -0600"
"2021-11-09 00:05:00 -0600"
"2021-11-10 00:05:00 -0600"
"2021-11-11 00:05:00 -0600"
"2021-11-12 00:05:00 -0600"
"2021-11-13 00:05:00 -0600"
"2021-11-14 00:05:00 -0600"

But for America/New_York

[:tz, "America/New_York"]
[:ruby, "2.7.1", "x86_64-openbsd6.7"]
[:tzinfo, "2.0.4"]
[:fugit, "1.5.2"]

"2021-03-10 00:05:00 -0500"
"2021-03-11 00:05:00 -0500"
"2021-03-12 00:05:00 -0500"
"2021-03-13 00:05:00 -0500"
"2021-03-14 00:05:00 -0500"
"2021-03-14 23:05:00 -0500" # the change happens but the TZ is still the same?
"2021-03-15 23:05:00 -0500"
"2021-03-16 23:05:00 -0500"
"2021-03-17 23:05:00 -0500"
"2021-03-18 23:05:00 -0500"

"2021-11-05 00:05:00 -0400"
"2021-11-06 00:05:00 -0400"
"2021-11-07 00:05:00 -0400"
"2021-11-08 00:05:00 -0500"
"2021-11-09 00:05:00 -0500"
"2021-11-10 00:05:00 -0500"
"2021-11-11 00:05:00 -0500"
"2021-11-12 00:05:00 -0500"
"2021-11-13 00:05:00 -0500"
"2021-11-14 00:05:00 -0500"

Also have to try with "30 4 * * *" as OP did.

"at 12:00 PM" gives the wrong crontab pattern

Issue description

When I parse the string "at 12:00 PM", the result is "0 24 * * *" which is wrong

How to reproduce

require 'fugit'
c = Fugit.parse('at 12:00 PM')
p c.original # "0 24 * * *"

Expected behaviour

It should return "0 12 * * *"

Context

Please replace the content of this section with the output of the following commands:

Darwin Khaleds-MBP 20.5.0 Darwin Kernel Version 20.5.0: Sat May  8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin20]
[:env_tz, nil]
(secs:1623078834.395216,utc~:"2021-06-07 15:13:54.3952159881591797",ltz~:"EEST")
(etz:nil,tnz:"EEST",tziv:"1.2.9",tzidv:nil,rv:"2.7.1",rp:"x86_64-darwin20",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::TimezoneProxy: Asia/Amman>,eotnfz:"+0300",eotlzn:"Asia/Amman",eotnfZ:"EEST",debian:nil,centos:nil,osx:"zoneinfo/Asia/Amman")
"1.4.4"

Problem parsing 'every 12 hours at minute 50'

Issue description

Fugit::Nat.parse('every 12 hours at minute 50').to_cron_s returns "50 * * * *" when it should return "50 0,12 * * *". The hours are getting ignored.

How to reproduce

require 'fugit'
Fugit::Nat.parse('every 12 hours at minute 50').to_cron_s

#=> "50 * * * *"

Expected behaviour

require 'fugit'
Fugit::Nat.parse('every 12 hours at minute 50').to_cron_s

#=> "50 0,12 * * *" (or "50 */12 * * *")

Context

Darwin Jeromes-2017-MBP.local 19.6.0 Darwin Kernel Version 19.6.0: Sun Jul  5 00:43:10 PDT 2020; root:xnu-6153.141.1~9/RELEASE_X86_64 x86_64
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]
(secs:1596594797.9150012,utc~:"2020-08-05 02:33:17.9150011539459229",ltz~:"PDT")
(etz:nil,tnz:"PDT",tziv:"2.0.2",tzidv:nil,rv:"2.7.1",rp:"x86_64-darwin19",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::TimezoneProxy: America/Ensenada>,eotnfz:"-0700",eotlzn:"America/Ensenada",eotnfZ:"PDT",debian:nil,centos:nil,osx:"zoneinfo/America/Los_Angeles")

weekday cron does not run today when started before execution time

fugit 1.4.2

the second Tuesday of the week was today, we started the cron earlier today, but it never executed, because it thinks the next execution is next month and not today ... so seems like the weekday calculation needs to take time of day into account

TZ=utc bundle exec ruby -r bundler/setup -r fugit -e 'puts Time.now'
2021-02-09 17:41:10 +0000
TZ=utc bundle exec ruby -r bundler/setup -r fugit -e 'puts Fugit.do_parse_cron("55 18 * * 2#1").next_time.to_s'
2021-03-02 18:55:00 +0000
  it "calculates weekdays correctly when starting at the same day" do
    Time.stubs(:now).returns(Time.parse("2021-02-02 00:00:00")) # tuesday morning
    Fugit.do_parse_cron("59 23 * * 1").next_time.to_s.must_equal "2021-02-02 23:59:00 -0000"
  end

cron #previous_time loop breaker issue

Issue description

A clear and concise description of what the issue is.

How to reproduce

require 'fugit'
c = Fugit.parse('0 9 29 feb *')
p c.previous_time

produces:

/Users/jmettraux/w/fugit/lib/fugit/cron.rb:225:in `block in previous_time': too many loops for "0 9 29 feb *" #previous_time, breaking, please fill an issue at https://git.io/fjJCQ (RuntimeError)
	from /Users/jmettraux/w/fugit/lib/fugit/cron.rb:223:in `loop'
	from /Users/jmettraux/w/fugit/lib/fugit/cron.rb:223:in `previous_time'
	from tst/iteration_count.rb:15:in `<main>'

Expected behaviour

Expected something like:

"2016-2-29 09:00:00 +09:00"

Context

111644 pollux ไป ~/w/fugit (master) ใƒ€ uname -a
Darwin pollux.local 17.7.0 Darwin Kernel Version 17.7.0: Thu Dec 20 21:47:19 PST 2018; root:xnu-4570.71.22~1/RELEASE_X86_64 x86_64
111752 pollux ไป ~/w/fugit (master) ใƒ€ bundle exec ruby -v
ruby 2.3.7p456 (2018-03-28 revision 63024) [x86_64-darwin17]
111752 pollux ไป ~/w/fugit (master) ใƒ€ bundle exec ruby -r et-orbi -e "EtOrbi._make_info"
(secs:1553307474.76292,utc~:"2019-03-23 02:17:54.7629199028015137",ltz~:"JST")
(etz:nil,tnz:"JST",tziv:"2.0.0",tzidv:"1.2018.9",rv:"2.3.7",rp:"x86_64-darwin17",win:false,rorv:nil,astz:nil,eov:"1.1.7",eotnz:#<TZInfo::TimezoneProxy: Asia/Tokyo>,eotnfz:"+0900",eotlzn:"Asia/Tokyo",eotnfZ:"JST",debian:nil,centos:nil,osx:"zoneinfo/Asia/Tokyo")

Additional context

None.

Invalid โ€œhoursโ€ in range cause cron #next_time loop breaker issue

Issue description

Values outside 0-23 in an hour range cause an infinite loop error.

How to reproduce

require 'fugit'
c = Fugit.parse('* 0-24 * * *')
RuntimeError (too many loops for {:min=>0, :max=>23, :sta=>0, :edn=>24, :sla=>1} #range, breaking, please fill an issue at https://git.io/fjJC9)

Expected behaviour

The โ€œ24โ€ being out of range should either result in a parsing error or being treated as 0.

Context

Please replace the content of this section with the output of the following commands:

Darwin Kinzie 18.7.0 Darwin Kernel Version 18.7.0: Thu Jun 20 18:42:21 PDT 2019; root:xnu-4903.270.47~4/RELEASE_X86_64 x86_64
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-darwin18]
(secs:1565722428.8614998,utc~:"2019-08-13 18:53:48.8614997863769531",ltz~:"PDT")
(etz:nil,tnz:"PDT",tziv:"1.2.5",tzidv:nil,rv:"2.6.2",rp:"x86_64-darwin18",win:false,rorv:nil,astz:nil,eov:"1.2.1",eotnz:#<TZInfo::TimezoneProxy: America/Dawson>,eotnfz:"-0700",eotlzn:"America/Dawson",eotnfZ:"PDT",debian:nil,centos:nil,osx:"zoneinfo/America/Vancouver")

Additional context

Encountered via in-the-wild user input!

Fugit::Nat.parse with multi: :fail returns an array

When specifying multi: :fail, the expected behaviour is not to return an array

puts Fugit::Nat.parse('every 1 hour').inspect
#<Fugit::Cron:0x0000560b30bce300 @original="0 * * * *", @cron_s=nil, @seconds=[0], @minutes=[0], @hours=nil, @monthdays=nil, @months=nil, @weekdays=nil, @zone=nil, @timezone=nil>

puts Fugit::Nat.parse('every 1 hour', multi: :fail).inspect
[#<Fugit::Cron:0x0000560b30c028a8 @original="0 * * * *", @cron_s=nil, @seconds=[0], @minutes=[0], @hours=nil, @monthdays=nil, @months=nil, @weekdays=nil, @zone=nil, @timezone=nil>]

Thanks again for everything

[Documentation] Regex for crontab notation

Hi,

I'm working on a frontend which lets the user input a crontime to generate a cronjob for sidekiq-cron in the backend.

I'm validating the string backend-wise with your validate method but do you maybe have a regex for doing the validation in the frontend, too? A good documentation which cron string is allowed would also work, of course.

Typical cron regexes didn't work because of your optional 6 digit syntax and so on..

Thank you in advance!

Cron#rough_frequency returns 0 for some cronlines

Hi ๐Ÿ‘‹ !

Issue description

Given cronlines that are supposed to run every x days, rough_frequency returns 0 in some cases while others work as expected (see the examples below).

How to reproduce

require 'fugit'
Fugit.parse('0 0 */2 * * Europe/Berlin').rough_frequency
# => 0
Fugit.parse('0 0 */2 * * Europe/Berlin').brute_frequency
# => #<Fugit::Cron::Frequency:0x00007ff5c5bc0da0 @delta_max=342000, @delta_min=86400, @occurrences=185, @span=31536000.0, @span_years=1.0, @yearly_occurrences=185.0>
[32] pry(main)> Fugit.parse('0 0 */1 * *').rough_frequency
=> 86400
[33] pry(main)> Fugit.parse('0 0 */2 * *').rough_frequency
=> 0
[34] pry(main)> Fugit.parse('0 0 */3 * *').rough_frequency
=> 0
[35] pry(main)> Fugit.parse('0 0 */4 * *').rough_frequency
=> 172800
[36] pry(main)> Fugit.parse('0 0 */5 * *').rough_frequency
=> 0
[37] pry(main)> Fugit.parse('0 0 */6 * *').rough_frequency
=> 0
[38] pry(main)> Fugit.parse('0 0 */7 * *').rough_frequency
=> 172800
[39] pry(main)> Fugit.parse('0 0 */8 * *').rough_frequency
=> 518400
[40] pry(main)> Fugit.parse('0 0 */9 * *').rough_frequency
=> 259200
[41] pry(main)> Fugit.parse('0 0 */10 * *').rough_frequency
=> 0
[42] pry(main)> Fugit.parse('0 0 */11 * *').rough_frequency
=> 691200
[43] pry(main)> Fugit.parse('0 0 */12 * *').rough_frequency
=> 518400
[44] pry(main)> Fugit.parse('0 0 */13 * *').rough_frequency
=> 345600
[45] pry(main)> Fugit.parse('0 0 */14 * *').rough_frequency
=> 172800
[46] pry(main)> Fugit.parse('0 0 */15 * *').rough_frequency
=> 0
[47] pry(main)> Fugit.parse('0 0 */16 * *').rough_frequency
=> 1209600
[48] pry(main)> Fugit.parse('0 0 */17 * *').rough_frequency
=> 1123200
[49] pry(main)> Fugit.parse('0 0 */18 * *').rough_frequency
=> 1036800
[50] pry(main)> Fugit.parse('0 0 */19 * *').rough_frequency
=> 950400
[51] pry(main)> Fugit.parse('0 0 */20 * *').rough_frequency
=> 864000
[52] pry(main)> Fugit.parse('0 0 */21 * *').rough_frequency
=> 777600
[53] pry(main)> Fugit.parse('0 0 */22 * *').rough_frequency
=> 691200
[54] pry(main)> Fugit.parse('0 0 */23 * *').rough_frequency
=> 604800
[55] pry(main)> Fugit.parse('0 0 */24 * *').rough_frequency
=> 518400
[56] pry(main)> Fugit.parse('0 0 */25 * *').rough_frequency
=> 432000
[57] pry(main)> Fugit.parse('0 0 */26 * *').rough_frequency
=> 345600
[58] pry(main)> Fugit.parse('0 0 */27 * *').rough_frequency
=> 259200
[59] pry(main)> Fugit.parse('0 0 */28 * *').rough_frequency
=> 172800
[60] pry(main)> Fugit.parse('0 0 */29 * *').rough_frequency
=> 86400
[61] pry(main)> Fugit.parse('0 0 */30 * *').rough_frequency
=> 0

Context

Please replace the content of this section with the output of the following commands:

Darwin local 18.7.0 Darwin Kernel Version 18.7.0: Tue Aug 20 16:57:14 PDT 2019; root:xnu-4903.271.2~2/RELEASE_X86_64 x86_64
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin18]
(secs:1586083793.553639,utc~:"2020-04-05 10:49:53.5536389350891113",ltz~:"CEST")
(etz:"Europe/Berlin",tnz:"CEST",tziv:"1.2.7",tzidv:nil,rv:"2.6.1",rp:"x86_64-darwin18",win:false,rorv:"6.0.2.2",astz:[ActiveSupport::TimeZone, "Europe/Berlin"],eov:"1.2.2",eotnz:#<TZInfo::DataTimezone: Europe/Berlin>,eotnfz:"+0200",eotlzn:"Europe/Berlin",eotnfZ:"CEST",debian:nil,centos:nil,osx:"zoneinfo/Europe/Berlin")

Skips four hour increment with UTC on some occasions

Issue description

Hi, thanks for making this library, it's been really useful.

We're running into an issue with this cron: 0 8-19/4 * * *.
We basically want it to run every day from 8am to 7pm with a 4-hour increment, which boils down to 8am, 12pm, and 4pm.

We're having issues when we set the timezone to America/New_York, but have the system time in UTC.
If we query for the next time at 8am, fugit yields 20:00 UTC (4pm EST) when we expect 16:00 UTC (12pm EST). It seems to skip a 4-hour block when requesting next right on the edge.

How to reproduce

Gemfile

source 'https://rubygems.org'

platforms :jruby do
  gem 'activesupport', '4.2.11.1'
  gem 'fugit', '1.3.6'
end

utc_repro.rb

require 'active_support/time'
require 'fugit'

cron = Fugit.parse('0 8-19/4 * * *')
# note that Time.parse only cares about sys tz, so this is 12:00 UTC <=> 08:00 EST
puts Time.use_zone('America/New_York') { cron.next_time(Time.parse('2020-09-11 12:00:00')).to_local_time }
  1. set computer timezone to UTC
  2. bundle install
  3. ruby utc_repro.rb

Error

Running ruby utc_repro.rb yields
2020-09-11 20:00:00 UTC
when we would expect
2020-09-11 16:00:00 UTC.

Context

We're using jruby 9.2.13.0, but MRI should yield the same results.

Lastly, if we update the time and add a few minutes (e.g. try 12:10 UTC), we get the expected time:
puts Time.use_zone('America/New_York') { cron.next_time(Time.parse('2020-09-11 12:10:00')).to_local_time }
yields
2020-09-11 16:00:00 UTC.

Feel free to let me know if anything is unclear, happy to clarify.

Don't double cron matches when getting out of DST

Fugit::Cron does not handle the edge cases of daylight saving time.

Fugit::Cron.new("0 0 * * * America/New_York").brute_frequency shows 365 occurrences, which makes sense at one per day for a year. However, Fugit::Cron.new("1 1 * * * America/New_York").brute_frequency shows 366 occurrences (since 1:01 a.m. occurs twice when moving the clock backwards) and Fugit::Cron.new("1 2 * * * America/New_York").brute_frequency shows 364 occurrences (since 2:01 a.m. is skipped when moving the clock forward).

As I quickly researched around the internet, I see that this support is dependent on the version of cron used but I think this is a good feature to have. What are your thoughts on adding in this support?

Parse::Nat with 24h times doesn't parse correctly

# Parses correctly with am/pm times
puts Fugit.parse_nat('every day at 6pm and 8pm UTC').hours.inspect
=> [18, 20]

# Parses incorrectly with 24h times
puts Fugit.parse_nat('every day at 18:00 and 20:00 UTC').hours.inspect
=> [20]

Running Fugit 1.2.2 / Ruby 2.6.3 / Ubuntu 18.04

endless loop dealing with cron "seconds" string

@conet hijacked the closed issue gh-11, challenging the fix there, but provided nothing that supports his accusation.

In fact, there is a source issue report at:
https://gitlab.com/gitlab-org/gitlab-ce/issues/59273

we had:

gitlab_rails['ldap_sync_worker_cron'] = "5 * * * * *"
gitlab_rails['ldap_group_sync_worker_cron'] = "10 * * * * *"

instead of:

gitlab_rails['ldap_sync_worker_cron'] = "5 * * * *"
gitlab_rails['ldap_group_sync_worker_cron'] = "10 * * * *"

Once the cron expression is fixed cpu usage goes back to normal

OP adds:

invalid cron expressions should not cause such behavior.

irb(main):001:0> require 'Fugit'
=> true
irb(main):002:0> Fugit.parse('5 * * * * *')
=> #<Fugit::Cron:0x00007f91ba8d54c0
  @original="5 * * * * *", @cron_s=nil, @seconds=[5],
  @minutes=nil, @hours=nil, @monthdays=nil, @months=nil, @weekdays=nil,
  @zone=nil, @timezone=nil>
irb(main):003:0> Fugit.parse('10 * * * * *')
=> #<Fugit::Cron:0x00007f91ba844650 @original="10 * * * * *", @cron_s=nil, @seconds=[10],
  @minutes=nil, @hours=nil, @monthdays=nil, @months=nil, @weekdays=nil,
  @zone=nil, @timezone=nil>

In other words: "5 * * * * *" means "every time the second hand hits the 5".

Sidekiq-cron allows for second crons (see sidekiq-cron/sidekiq-cron#240). These are NOT invalid cron expressions.

Questions to @conet :

  • you mention
2019-03-20_18:36:54.79856 2019-03-20T18:36:54.798Z 2977 TID-ot8g7o9m9 WARN: Thread TID-ot89u8ea9 scheduler
2019-03-20_18:36:54.79857 2019-03-20T18:36:54.798Z 2977 TID-ot8g7o9m9 WARN: /opt/gitlab/embedded/lib/ruby/gems/2.5.0/gems/tzinfo-1.2.5/lib/tzinfo/time_or_datetime.rb:321:in `wrap'
...

as an endless loop. Do you mean that this warn trace repeats endlessly in your log?

  • Does the LDAP sync work your settings seem to imply happen or not? Does the LDAP sync work happens every 5 / 10 seconds ? Does it happen but with a different schedule ? Or is it NOT happening at all?

Thanks in advance and best regards.

Fugit::Cron previous_time method bug

Hi, there:

I was in +8 timezone, I exec the following statement in rails console:
Fugit::Cron.parse('0 16 30 9 *').previous_time.to_local_time

and I got the following error:
RuntimeError: too many loops for "0 16 30 9 *" #previous_time, breaking

Also, the same error occurred when I exec:
Fugit::Cron.parse('0 16 31 10 *').previous_time.to_local_time

I guess that when the cron is across the month, this error occurred. So boundary conditions need
to be fully considered

Problem parsing 'every day at 8:30'

Issue description

  • Fugit::Nat.parse('every day at 8:30')&.to_cron_s returns nil when I think it should return "30 8 * * *".

    It works with 'every day at 08:30' but not when there is no leading 0.

  • Fugit::Nat.parse('every day at 8:30 pm')&.to_cron_s returns nil when it should return "30 20 * * *".

    Here the problem is that even if you provide a leading zero with 'every day at 08:30 pm', it does not work when there is am or pm.

How to reproduce

require 'fugit'

Fugit::Nat.parse('every day at 8:30')&.to_cron_s
#=> nil

Fugit::Nat.parse('every day at 8:30 pm')&.to_cron_s
#=> nil

Fugit::Nat.parse('every day at 08:30 pm')&.to_cron_s
#=> nil

Expected behaviour

require 'fugit'

Fugit::Nat.parse('every day at 8:30')&.to_cron_s
#=> "30 8 * * *"

Fugit::Nat.parse('every day at 8:30 pm')&.to_cron_s
#=> "30 20 * * *"

Fugit::Nat.parse('every day at 08:30 pm')&.to_cron_s
#=> "30 20 * * *"

Context

Darwin Jeromes-2017-MBP.local 19.6.0 Darwin Kernel Version 19.6.0: Sun Jul  5 00:43:10 PDT 2020; root:xnu-6153.141.1~9/RELEASE_X86_64 x86_64
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]
(secs:1596594797.9150012,utc~:"2020-08-05 02:33:17.9150011539459229",ltz~:"PDT")
(etz:nil,tnz:"PDT",tziv:"2.0.2",tzidv:nil,rv:"2.7.1",rp:"x86_64-darwin19",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::TimezoneProxy: America/Ensenada>,eotnfz:"-0700",eotlzn:"America/Ensenada",eotnfZ:"PDT",debian:nil,centos:nil,osx:"zoneinfo/America/Los_Angeles")

Question: how to find the end of the current cron "validity interval"?

Challenge description

I'm using the cron syntax not only as an indicator for several points in time but as a duration as well.
E.g. I interpret (and check via match?(Time.now)) the following as:
'10-40 22 * * mon-fri' indicates "every Monday till Friday between 22:10 and 22:40".
'* 0 * * mon' indicates "every Monday between 00:00 and 00:59".
'* 8 * * *' indicates "every day between 08:00 and 08:59".
...you get the idea. ;)

Works fine so far, but instead of finding the time when the next "interval" will start I need to find (if in an active interval) when it will end.
E.g. if current time was 2020-11-01 08:32:02, this interval
'* 8 * * *' should return a time of 2020-11-01 08:59:59 (or even 2020-11-01 09:00:00).

Do you think that's possible? Any ideas appreciated! :)

Natural parsing for ranges of hours in ranges of days

Issue description

Hello there, I'm wondering if fugit supports natural parsing of both days and weeks? We're looking for a way to define blocks of time where people are or aren't available. Below is a simple example.

How to reproduce

require 'fugit'

f = Fugit::Nat.parse('every weekday 8am to 5pm') # => nil

Expected behaviour

I was expecting to get a fugit cron object back that looks a little something like this?

#<Fugit::Cron:0x000055f7859a1ca8 @original="* 8-17 * * 1-5", @cron_s=nil, @seconds=[0], @minutes=[0..59], @hours=[8..17], @monthdays=nil, @months=nil, @weekdays=[[1], [2], [3], [4], [5]], @zone=nil, @timezone=nil>

It might not be feasible to include every minute of every hour in the 8am-5pm range, but any help getting a string like this to parse would be very helpful. Many thanks for this gem, it truly is wonderful.

Context

Please replace the content of this section with the output of the following commands:

Linux a716f734f0ee 4.19.76-linuxkit #1 SMP Tue May 26 11:42:35 UTC 2020 x86_64 GNU/Linux

ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-linux]

[:env_tz, nil]

(secs:1601231894.3672342,utc~:"2020-09-27 18:38:14.3672342300415039",ltz~:"UTC")
(etz:nil,tnz:"UTC",tziv:"1.2.7",tzidv:nil,rv:"2.6.6",rp:"x86_64-linux",win:false,rorv:"6.0.3.3",astz:[ActiveSupport::TimeZone, "Etc/UTC"],eov:"1.2.4",eotnz:#<TZInfo::DataTimezone: Etc/UTC>,eotnfz:"+0000",eotlzn:"Etc/UTC",eotnfZ:"UTC",debian:"Etc/UTC",centos:nil,osx:"Etc/UTC")

Additional context

It turns out that parsing every monday to friday 8am and 5pm will correctly parse the day range supplied, but change the hours from a list; 8am and 5pm to a range; 8am to 5pm returns a nil.

I tried with multi: true and still get a nil returned.

parse_nat with weekday ranges doesn't parse correctly

# Parses correctly with days specified individually
puts Fugit.parse_nat('every Fri,Sat,Sun at 18:00 UTC').weekdays.inspect
=> [[0], [5], [6]]

# Parses incorrectly with day ranges spanning the end of week
puts Fugit.parse_nat('every Fri-Sun at 18:00 UTC').weekdays.inspect
=> [[0], [1], [2], [3], [4], [5]]

Invalid cron (zero day of month) results in unpredictable behaviour

Trying to create an invalid cron with a 0th day of month "works".

Fugit::Cron.parse('0 2 1 0 *')                                                                                                                                                                       
=> #<Fugit::Cron:0x00007f90279df648 @cron_s=nil, @hours=[2], @minutes=[0], @monthdays=[1], @months=[0], @original="0 2 1 0 *", @seconds=[0], @timezone=nil, @weekdays=nil, @zone=nil>

The resulting object is unusable (especially for next_time), but there was no error, and it did not return nil.

Should(n't) it do so?

UTC cron next_time should be a UTC time, not a local time

require 'fugit'

#ENV['TZ'] = 'Asia/Shanghai'
#ENV['TZ'] = 'CST'

EtOrbi._make_info

puts "---"

p Time.now
p Time.now.utc
cron = Fugit.do_parse_cron('0 0 1 1 *')
p cron
p cron.next_time.to_s
p cron.next_time.utc.to_s

puts "---"

p Time.now
p Time.now.utc
cron = Fugit.do_parse_cron('0 0 1 1 * UTC')
p cron
p cron.next_time.to_s
p cron.next_time.utc.to_s
(secs:1546413051.166213,utc~:"2019-01-02 07:10:51.1662130355834961",ltz~:"JST")
(etz:nil,tnz:"JST",tzid:nil,rv:"2.3.7",rp:"x86_64-darwin17",
  win:false,rorv:nil,astz:nil,eov:"1.1.6",eotnz:#<TZInfo::TimezoneProxy: Asia/Tokyo>,
  eotnfz:"+0900",eotlzn:"Asia/Tokyo",debian:nil,centos:nil,osx:"zoneinfo/Asia/Tokyo")
---
2019-01-02 16:10:51 +0900
2019-01-02 07:10:51 UTC
#<Fugit::Cron:0x00007f97d51fb750 @original="0 0 1 1 *", @cron_s=nil,
  @seconds=[0], @minutes=[0], @hours=[0], @monthdays=[1], @months=[1], @weekdays=nil,
  @zone=nil, @timezone=nil>
"2020-01-01 00:00:00 +0900"
"2019-12-31 15:00:00 UTC"
---
2019-01-02 16:10:51 +0900
2019-01-02 07:10:51 UTC
#<Fugit::Cron:0x00007f97d3c665e8 @original="0 0 1 1 * UTC", @cron_s=nil,
  @seconds=[0], @minutes=[0], @hours=[0], @monthdays=[1], @months=[1], @weekdays=nil,
  @zone="UTC", @timezone=#<TZInfo::DataTimezone: UTC>>
"2020-01-01 00:00:00 +0900"
"2019-12-31 15:00:00 UTC"

The second p cron.next_time.to_s should be in the UTC timezone.

As seen in gh-11.

Handling timezones in cron lines

Is the handling of timezones in fugit cron lines planned? (ie, will rufus-scheduler 4.x be compatible with rufus-scheduler 3.x?).

Background: que-scheduler uses fugit to parse cron lines. It was hoped the config file it would mirror the one for resque-scheduler. However fugit doesn't seem to handle timezones. This means the config for que-scheduler throws errors.

I could move to using rufus-scheduler 3.x in que-scheduler for now, but didn't want to add features which would not be forwardly compatible.

Support for timezone offsets

Do you plan to support timezone offsets in the future?
Currently, the following examples do not work:

Fugit::Cron.parse("29 9 16 * * UTC/+11")
Fugit::Cron.parse("29 9 16 * * Etc/GMT-11")

Fugit::Nat.parse with different times in the same hour not parsed as single cron

# When there are different hours, it works correctly
Fugit::Nat.parse('every day at 18:00 and 19:00 UTC').original
=> "0 18,19 * * * UTC"

# When there are different minutes in the same hour, it doesn't
Fugit::Nat.parse('every day at 18:00 and 18:15 UTC').original
=> "0 18 * * * UTC" # should be: "0,15 18 * * * UTC"

# More complex example (but possibly difficult to implement)
Fugit::Nat.parse('every day at 18:00 and 18:15 and 19:00 and 19:15 UTC').original
=> "0 18 * * * UTC" # should be: "0,15 18,19 * * * UTC"

Thanks again

Natural cron "every 15th of the month"

Issue description

Trying to convert natural language to a cron expression that happens every specific day of the month.

Looking at the specs, I see that it was intended, but currently not working?

#'every 1st of the month at midnight' => '',

Is it planned?

I don't even mind if it will be a little less natural language, like:

  • every month on day 2 at 10:00
  • every month on days 1,15 at 10:00 # twice a month 1st and 15th

if that makes it easier to implement.

How to reproduce

require 'fugit'
p Fugit::Nat.parse('Every 2nd of the month at 10:00')   #=> nil

Parse::Nat does not accept every 15 minutes

Issue description

Attempt to parse every 15 minutes

How to reproduce

Nat seems to be able to parse the simplest cases and also 40 minutes, but throw in 30 minutes or 15 minutes and it fails to parse it (which seems a bit unexpected)

require 'fugit'
puts Fugit.parse('every 15 minutes')
puts Fugit.parse('every 30 minutes')
puts Fugit.parse("every 5 minutes")
puts Fugit.parse("every 40 minutes")

Expected behaviour

I expect that it is able to parse every case

Context

Please replace the content of this section with the output of the following commands:

Darwin fritz-macbook 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64 i386 MacBookPro14,3 Darwin
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]
bundle exec ruby -r et-orbi -e "EtOrbi._make_info"

Additional context

I'm attempting to convert an iso8601 interval to cron output (I'm using sidekiq cron for my databased custom cron jobs but they all use iso8601 interval)

Timezone seems ignored in cron.match?

As originally reported by @assembler in floraison/et-orbi#21

require 'fugit'

cron = Fugit::Cron.parse("0 0 * * * PST8PDT")
p cron
  # => #<Fugit::Cron:0x00007fb3b8bdf880
  #      @original="0 0 * * * PST8PDT", @cron_s=nil, @seconds=[0],
  #      @minutes=[0], @hours=[0], @monthdays=nil, @months=nil, @weekdays=nil,
  #      @zone="PST8PDT", @timezone=#<TZInfo::DataTimezone: PST8PDT>>


#p cron.match?(Time.utc(2019, 1, 1, 0, 0, 0))
p cron.match?(EtOrbi.parse('2019-1-1'))
  # => true

#p cron.match?(Time.new(2019, 1, 1, 0, 0, 0, TZInfo::Timezone.get('UTC')))
p cron.match?(EtOrbi.parse('2019-1-1 UTC'))
  # => true

#p cron.match?(Time.new(2019, 1, 1, 0, 0, 0, TZInfo::Timezone.get('CET')))
p cron.match?(EtOrbi.parse('2019-1-1 ETC'))
  # => true

#p cron.match?(Time.new(2019, 1, 1, 0, 0, 0, TZInfo::Timezone.get('PST8PDT')))
p cron.match?(EtOrbi.parse('2019-1-1 PST8PDT'))
  # => true

undefined method `inject' for nil:NilClass

Issue description

undefined method `inject' for nil:NilClass

How to reproduce

c = Fugit::Cron.parse('0 0 * *  sun')
Traceback (most recent call last):
        2: from (irb):10
        1: from (irb):11:in `rescue in irb_binding'
NoMethodError (undefined method `inject' for nil:NilClass)

Feature request: parse_nat weekday ranges

Would be handy to have weekday ranges, e.g. instead of:
every Mon,Tue,Wed,Thu,Fri at 18:00, have the option to write
every Mon-Fri at 18:00 or
every Mon to Fri at 18:00.

Strange behaviour with DST?

Issue description

First of all, I'm based in Switzerland. Our clock will change next week to summer time. We use Fugit in a project to allow users schedule background jobs. We have a test suite that has a test where a job should run each week with the instruction every monday at midnight. We compare the result of Fugit with an expected value to make sure our scheduler works.

Now, instructing Fugit to give the next date for said schedule strangely gives a date in two weeks instead of next week. Other hours seem to work fine:

irb(main):003:0> Fugit.parse("every monday at 3am").next_time.to_s
=> "2021-03-29 03:00:00 +0200"
irb(main):004:0> Fugit.parse("every monday at 2am").next_time.to_s
=> "2021-03-29 02:00:00 +0200"
irb(main):005:0> Fugit.parse("every monday at 1am").next_time.to_s
=> "2021-03-29 01:00:00 +0200"
irb(main):006:0> Fugit.parse("every monday at midnight").next_time.to_s
=> "2021-04-05 00:00:00 +0200"

I'm not sure if it is related to DST and if this is supposed to happen (I don't know cron too well).

How to reproduce

See above.

Error and error backtrace (if any)

Not needed, as no error will be printed.

Expected behaviour

next_time should print Mon, 29 Mar 2021 00:00:00 CEST +02:00.

Context

Please replace the content of this section with the output of the following commands:

uname -a
bundle exec ruby -v
bundle exec ruby -e "p [ :env_tz, ENV['TZ'] ]"
bundle exec ruby -r et-orbi -e "EtOrbi._make_info"
bundle exec ruby -r fugit -e "p Fugit::VERSION"

(It's supposed to look like

Linux apf-work-home 5.4.0-67-generic #75-Ubuntu SMP Fri Feb 19 18:03:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
ruby 2.5.8p224 (2020-03-31 revision 67882) [x86_64-linux]
[:env_tz, nil]
(secs:1616405592.3763082,utc~:"2021-03-22 09:33:12.3763082027435303",ltz~:"CET")
(etz:nil,tnz:"CET",tziv:"1.2.9",tzidv:nil,rv:"2.5.8",rp:"x86_64-linux",win:false,rorv:nil,astz:nil,eov:"1.2.4",eotnz:#<TZInfo::DataTimezone: Europe/Zurich>,eotnfz:"+0100",eotlzn:"Europe/Zurich",eotnfZ:"CET",debian:"Europe/Zurich",centos:nil,osx:"Europe/Zurich")
"1.4.2"

)

Incorrect #rough_frequency and #brute_frequency for '0 */5 * * *'

Issue description

Parsing of the above cron schedule can't seem to be parsed properly:

Rufus::Scheduler.parse('0 */5 * * *').rough_frequency
=> 14400
# even if we try `brute_schedule` it's off:
Rufus::Scheduler.parse('0 */5 * * *').brute_frequency
=> #<Fugit::Cron::Frequency:0x0000000124f5f440 @delta_max=21600, @delta_min=14400, @occurrences=1822, @span=31482000.0, @span_years=0.9982876712328768, @yearly_occurrences=1825.1252144082332>

value should be: 18000

Parsing of 0 */3 * * *, 0 */4 * * * and 0 */6 * * * work fine it seems.

How to reproduce

See above

Expected behaviour

Seems like a simple cron schedule like this, should be able to be parsed properly? If neither of these methods work, what other way is there to get an accurate frequency?

Every x minute is parsed incorrectly for cron

image

*/55 * * * * should run once every 55 minutes, but fugit runs it at x:55 and x:00, where x is 0-23. In the example above, it's ran at 12:00, 12:55, 13:00, 13:55, etc. when it should run at 12:00, 12:55, 13:50, 14:45, etc.

cron #next_time loop breaker issue

I know it's an invalid date and I'm happy in latest version it no longer hangs infinitely but your response did say report it.

So minimum reproduce case.

cron = Fugit::Cron.new("* * 31 11 *")
cron.next_time

RuntimeError: too many loops for "* * 31 11 *" #next_time, breaking, please fill an issue at https://git.io/fjJC9
from /Users/petera/.rvm/gems/ruby-2.2.10/gems/fugit-1.2.0/lib/fugit/cron.rb:225:in `block in next_time'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.