pax_global_header00006660000000000000000000000064135317360340014517gustar00rootroot0000000000000052 comment=b6dea6a4ff1e9f43f45ee80d6acb0a825b507f71 fugit-1.3.3/000077500000000000000000000000001353173603400126415ustar00rootroot00000000000000fugit-1.3.3/.github/000077500000000000000000000000001353173603400142015ustar00rootroot00000000000000fugit-1.3.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001353173603400163645ustar00rootroot00000000000000fugit-1.3.3/.github/ISSUE_TEMPLATE/issue-report.md000066400000000000000000000043521353173603400213530ustar00rootroot00000000000000--- name: Issue Report about: Create a report to help us help you title: '' labels: '' assignees: '' --- ## Issue description A clear and concise description of what the issue is. (There is an example of a carefully filled issue at https://github.com/floraison/fugit/issues/18) ## How to reproduce The simplest piece of code that reproduces the issue, for example: ```ruby 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 ' 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 `' 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 `
' ``` ) ## 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 -r et-orbi -e "EtOrbi._make_info" ``` (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] (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:#,eotnfz:"+0900", eotlzn:"Asia/Tokyo",eotnfZ:"JST",debian:nil,centos:nil,osx:"zoneinfo/Asia/Tokyo") ``` ) ## Additional context Add any other context about the problem here. fugit-1.3.3/.gitignore000066400000000000000000000002321353173603400146260ustar00rootroot00000000000000 *.swp .vimrc .viminfo .vimgrep .vimspec .vimmarks pkg/ .ruby-version #.rspec .errors .rspec.out .bxsinfo.yaml .bxsenvs.yaml .todo.md Gemfile.lock fugit-1.3.3/.rspec000066400000000000000000000000401353173603400137500ustar00rootroot00000000000000--colour --format documentation fugit-1.3.3/.travis.yml000066400000000000000000000010611353173603400147500ustar00rootroot00000000000000 language: ruby rvm: #- 1.8.7 # no, since it doesn't get fun(a, *b, c) or fun0\n.fun1 #- 1.9.3 # Travis broken September 2017 #- 2.1.1 - 2.2.2 - 2.3.1 - 2.4.2 - 2.5.1 #- jruby-19mode # Travis broken September 2017 #- jruby-20mode # Travis broken September 2017 #- jruby-9.1.13.0 - jruby-9.2.5.0 #matrix: # include: # - rvm: jruby-9.1.13.0 # #- env: JRUBY_OPTS="--profile.api" #before_install: gem install bundler script: bundle exec rspec branches: only: - master #except: # - master cache: bundler #env: # - XX=0 fugit-1.3.3/CHANGELOG.md000066400000000000000000000101141353173603400144470ustar00rootroot00000000000000 # CHANGELOG.md ## fugit 1.3.3 released 2019-08-29 * Fix Cron#match?(t) with respect to the cron's timezone, gh-31 ## fugit 1.3.2 released 2019-08-14 * Allow for "* 0-24 * * *", gh-30 ## fugit 1.3.1 released 2019-07-27 * Fix nat parsing for 'every day at 18:00 and 18:15', gh-29 * and for 'every day at 18:00, 18:15, 20:00, and 20:15', gh-29 * Ensure multi: :fail doesn't force into multi, gh-28 * Fix nat parsing for 'every Fri-Sun at 18:00', gh-27 ## fugit 1.3.0 released 2019-07-21 * Introduce Fugit.parse_nat('every day at 18:00 and 19:15', multi: true) * Rework AM/PM parsing ## fugit 1.2.3 released 2019-07-16 * Allow for "from Monday to Friday at 19:22", gh-25 * Allow for "every Monday to Friday at 18:20", gh-25 * Allow for "every day at 18:00 and 20:00", gh-24 ## fugit 1.2.2 released 2019-06-21 * Fix Fugit.parse vs "every 15 minutes", gh-22 ## fugit 1.2.1 released 2019-05-04 * Return nil when parsing a cron with February 30 and friend, gh-21 ## fugit 1.2.0 released 2019-04-22 * Accept "/15 * * * *" et al, gh-19 and resque/resque-scheduler#649 * Stop fooling around and stick to https://semver.org ## fugit 1.1.10 released 2019-04-12 * Implement `"0 9 * * sun%2+1"` * Simplify cron parser ## fugit 1.1.9 released 2019-03-26 * Fix cron `"0 9 29 feb *"` endless loop, gh-18 * Fix cron endless loop when #previous_time(t) and t matches, gh-15 * Simplify Cron #next_time / #previous_time breaker system, gh-15 Thanks @godfat and @conet ## fugit 1.1.8 released 2019-01-17 * Ensure Cron#next_time happens in cron's time zone, gh-12 ## fugit 1.1.7 released 2019-01-15 * Add breaker to Cron #next_time / #previous_time, gh-13 * Prevent 0 as a month in crons, gh-10 * Prevent 0 as a day of month in crons, gh-10 ## fugit 1.1.6 released 2018-09-05 * Ensure `Etc/GMT-11` and all Olson timezone names are recognized in cron and nat strings, gh-9 ## fugit 1.1.5 released 2018-07-30 * Add Fugit::Cron#rough_frequency (for https://github.com/jmettraux/rufus-scheduler/pull/276) ## fugit 1.1.4 released 2018-07-20 * Add duration support for Fugit::Nat (@cristianbica gh-7) * Fix Duration not correctly parsing minutes and seconds long format (@cristianbica gh-7) * Add timezone support for Fugit::Nat (@cristianbica gh-7) * Use timezone name when converting a Fugit::Cron to cron string (@cristianbica gh-7) ## fugit 1.1.3 released 2018-06-21 * Silenced Ruby warnings (Utilum in gh-4) ## fugit 1.1.2 released 2018-06-20 * Added Fugit::Cron#seconds (Tero Marttila in gh-3) ## fugit 1.1.1 released 2018-05-04 * Depend on et-orbi 1.1.1 and better ## fugit 1.1.0 released 2018-03-27 * Travel in Cron zone in #next_time and #previous_time, return from zone * Parse and store timezone in Fugit::Cron * Introduce Fugit::Duration#deflate month: d / year: d * Introduce Fugit::Duration#drop_seconds * Alias Fugit::Duration#to_h to Fugit::Duration#h * Introduce to_rufus_s (1y2M3d) vs to_plain_s (1Y2M3D) * Ensure Duration#deflate preserves at least `{ sec: 0 }` * Stringify 0 seconds as "0s" * Ignore "-5" and "-5.", only accept "-5s" and "-5.s" * Introduce "signed durations", "-1Y+2Y-3m" * Ensure `1.0d1.0w1.0d` gets parsed correctly * Ensure Fugit::Cron.next_time returns plain seconds (.0, not .1234...) * Introduce Fugit::Frequency for cron ## fugit 1.0.0 released 2017-06-23 * Introduce et-orbi dependency (1.0.5 or better) * Wire #deflate into Duration.to_long_s / .to_iso_s / .to_plain_s ## fugit 0.9.6 released 2017-05-24 * Provide Duration.to_long_s / .to_iso_s / .to_plain_s at class level ## fugit 0.9.5 released 2017-01-07 * Implement Fugit.determine_type(s) * Rename core.rb to parse.rb ## fugit 0.9.4 released 2017-01-06 * Accept cron strings with seconds ## fugit 0.9.3 released 2017-01-05 * First version of Fugit::Nat ## fugit 0.9.2 released 2017-01-04 * Accept decimal places for duration seconds * Alias Fugit .parse_in to .parse_duration ## fugit 0.9.1 released 2017-01-03 * Implement Fugit::Duration #inflate and #deflate * Bring in Fugit::Duration * Implement Fugit .parse, .parse_at and .parse_cron ## fugit 0.9.0 released 2017-01-03 * Initial release fugit-1.3.3/CREDITS.md000066400000000000000000000025031353173603400142600ustar00rootroot00000000000000 # fugit credits * Milovan Zogovic https://github.com/assembler Cron#match? vs TZ, gh-31 * Jessica Stokes https://github.com/ticky 0-24 issue with cron, gh-30 * Shai Coleman https://github.com/shaicoleman parse_nat enhancements, gh-24, gh-25, and gh-28 * Jan Stevens https://github.com/JanStevens Fugit.parse('every 15 minutes') gh-22 * Fabio Pitino https://github.com/hspazio nil on February 30 gh-21 * Cristian Oneț https://github.com/conet #previous_time vs 1/-1 endless loop gh-15 * Wenhui Wang https://github.com/w11th #next_time vs Chronic+ActiveSupport gh-11 * Lin-Jen Shin https://github.com/godfat #next_time untamed loop gh-13 * Nils Mueller https://github.com/Tolsto missing Olson timezone names gh-9 * jakemack https://github.com/jakemack issue when going out of DST gh-6 * Cristian Bica https://github.com/cristianbica Nat improvements gh-7 * Utilum https://github.com/utilum silenced Ruby warnings * Tero Marttila https://github.com/SpComb added missing Cron#seconds * Harry Lascelles https://github.com/hlascelles timezone reminder and more * John Mettraux https://github.com/jmettraux author and maintainer ## rufus-scheduler credits As fugit originates in rufus-scheduler, many thanks to all the rufus-scheduler contributors and people who gave feedback. https://github.com/jmettraux/rufus-scheduler/blob/master/CREDITS.md fugit-1.3.3/Gemfile000066400000000000000000000004641353173603400141400ustar00rootroot00000000000000 source 'https://rubygems.org' #gem 'tzinfo-data' #gem 'raabro', path: '../raabro/' # temporarily #gem 'et-orbi', git: 'https://github.com/floraison/et-orbi' # temporarily #gem 'et-orbi', path: '../et-orbi/' # temporarily #gem 'activesupport' # temporarily for gh-11 gemspec fugit-1.3.3/LICENSE.txt000066400000000000000000000021231353173603400144620ustar00rootroot00000000000000 Copyright (c) 2017-2019, John Mettraux, jmettraux+flor@gmail.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Made in Japan fugit-1.3.3/Makefile000066400000000000000000000024071353173603400143040ustar00rootroot00000000000000 ## gem tasks ## NAME = \ $(shell ruby -e "s = eval(File.read(Dir['*.gemspec'][0])); puts s.name") VERSION = \ $(shell ruby -e "s = eval(File.read(Dir['*.gemspec'][0])); puts s.version") count_lines: find lib -name "*.rb" | xargs cat | ruby -e "p STDIN.readlines.count { |l| l = l.strip; l[0, 1] != '#' && l != '' }" find spec -name "*_spec.rb" | xargs cat | ruby -e "p STDIN.readlines.count { |l| l = l.strip; l[0, 1] != '#' && l != '' }" cl: count_lines scan: scan lib/**/*.rb gemspec_validate: @echo "---" ruby -e "s = eval(File.read(Dir['*.gemspec'].first)); p s.validate" @echo "---" name: gemspec_validate @echo "$(NAME) $(VERSION)" cw: find lib -name "*.rb" -exec ruby -cw {} \; build: gemspec_validate gem build $(NAME).gemspec mkdir -p pkg mv $(NAME)-$(VERSION).gem pkg/ push: build gem push pkg/$(NAME)-$(VERSION).gem spec: bundle exec rspec test: spec ## specific to project ## info: uname -a bundle exec ruby -v bundle exec ruby -Ilib -r et-orbi -e "EtOrbi._make_info" tzones: bundle exec ruby -r tzinfo -e "TZInfo::Timezone.all.each { |tz| p tz.name }" #tzonesd: # bundle exec ruby -r tzinfo -r tzinfo-data -e "::TZInfo::Timezone.all.each { |tz| p tz.name }" .PHONY: count_lines scan gemspec_validate name cw build push spec info tzones fugit-1.3.3/README.md000066400000000000000000000235021353173603400141220ustar00rootroot00000000000000 # fugit [![Build Status](https://secure.travis-ci.org/floraison/fugit.svg)](http://travis-ci.org/floraison/fugit) [![Gem Version](https://badge.fury.io/rb/fugit.svg)](http://badge.fury.io/rb/fugit) [![Join the chat at https://gitter.im/floraison/fugit](https://badges.gitter.im/floraison/fugit.svg)](https://gitter.im/floraison/fugit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Time tools for [flor](https://github.com/floraison/flor) and the floraison group. It uses [et-orbi](https://github.com/floraison/et-orbi) to represent time instances and [raabro](https://github.com/floraison/raabro) as a basis for its parsers. Fugit is a core dependency of [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) 3.5.x. ## Related projects ### Sister projects 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 ### 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 * ... ### Projects using fugit * [arask](https://github.com/Ebbe/arask) - "Automatic RAils taSKs" uses fugit to parse cron strings * [sideqik-cron](https://github.com/ondrejbartas/sidekiq-cron) - recent versions of Sideqik-Cron use fugit to parse cron strings * [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) - * [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) * ... ## `Fugit.parse(s)` The simplest way to use fugit is via `Fugit.parse(s)`. ```ruby 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. ```ruby 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. ```ruby 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 ```ruby 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. ## `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. ```ruby 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 # => 2017-01-08 00:00:00 +0900 p c.previous_time # => 2017-01-01 00:00:00 +0900 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: ```ruby '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... ``` ### 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". 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 references to? It starts at 1 on 2019-01-01. ```ruby 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 ``` ## `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 ``` 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" ``` ## `Fugit::Nat` Fugit understand some kind of "natural" language: For example, those "every" get turned into `Fugit::Cron` instances: ```ruby 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: ```ruby Fugit.parse('every day at five') # ==> Fugit::Cron instance '0 5 * * *' ``` ### Ambiguous nats Not all strings result in a clean, single, cron expression. ```ruby Fugit::Nat.parse('every day at 16:00 and 18:00', multi: true) # ==> [ '0 16,18 * * *' ] Fugit::Nat.parse('every day at 16:15 and 18:30') # ==> [ '15 16 * * *' ] Fugit::Nat.parse('every day at 16:15 and 18:30', multi: true) # ==> [ '15 16 * * *', '30 18 * * *' ] 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 * * *) ``` ## LICENSE MIT, see [LICENSE.txt](LICENSE.txt) fugit-1.3.3/doc/000077500000000000000000000000001353173603400134065ustar00rootroot00000000000000fugit-1.3.3/doc/cron.rb000066400000000000000000000010571353173603400146770ustar00rootroot00000000000000 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 # => 2017-01-08 00:00:00 +0900 p c.previous_time # => 2017-01-01 00:00:00 +0900 p c.brute_frequency # => [ 604800, 604800, 53 ] # [ delta min, delta max, occurrence count ] 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 fugit-1.3.3/doc/duration.rb000066400000000000000000000005341353173603400155620ustar00rootroot00000000000000 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" fugit-1.3.3/fugit.gemspec000066400000000000000000000026641353173603400153340ustar00rootroot00000000000000 Gem::Specification.new do |s| s.name = 'fugit' s.version = File.read( File.expand_path('../lib/fugit.rb', __FILE__) ).match(/ VERSION *= *['"]([^'"]+)/)[1] s.platform = Gem::Platform::RUBY s.authors = [ 'John Mettraux' ] s.email = [ 'jmettraux+flor@gmail.com' ] s.homepage = 'http://github.com/floraison/fugit' s.license = 'MIT' s.summary = 'time tools for flor' s.description = %{ Time tools for flor and the floraison project. Cron parsing and occurrence computing. Timestamps and more. }.strip 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', 'homepage_uri' => s.homepage, 'source_code_uri' => s.homepage, #'wiki_uri' => s.homepage + '/wiki', } #s.files = `git ls-files`.split("\n") s.files = Dir[ 'README.{md,txt}', 'CHANGELOG.{md,txt}', 'CREDITS.{md,txt}', 'LICENSE.{md,txt}', 'Makefile', 'lib/**/*.rb', #'spec/**/*.rb', 'test/**/*.rb', "#{s.name}.gemspec", ] #s.add_runtime_dependency 'tzinfo' # this dependency appears in 'et-orbi' s.add_runtime_dependency 'raabro', '~> 1.1' s.add_runtime_dependency 'et-orbi', '~> 1.1', '>= 1.1.8' s.add_development_dependency 'rspec', '~> 3.8' s.add_development_dependency 'chronic', '~> 0.10' s.require_path = 'lib' end fugit-1.3.3/invalid_crons.md000066400000000000000000000010371353173603400160160ustar00rootroot00000000000000 ## invalid_crons.md * gh-20 @nulian `"* * 31 11 *"` aka "every 31th of November" The easy way out would be to run `#next_time` right after instantiation to determine if the cron is valid. But that has a cost (around 0.2s for those worst cases). `#parse_and_validate('* * 31 11 *')` maybe? Another way would be to have some smart validation. But that's almost like duplicating `#next_time`. Note that https://crontab.gury does consider `"* * 31 11 *"`. Crond probably never schedules it, silently. (Up to the tools that use fugit then). fugit-1.3.3/lib/000077500000000000000000000000001353173603400134075ustar00rootroot00000000000000fugit-1.3.3/lib/fugit.rb000066400000000000000000000003601353173603400150510ustar00rootroot00000000000000 module Fugit VERSION = '1.3.3' end require 'time' require 'stringio' require 'raabro' require 'et-orbi' require 'fugit/misc' require 'fugit/cron' require 'fugit/duration' require 'fugit/nat' require 'fugit/at' require 'fugit/parse' fugit-1.3.3/lib/fugit/000077500000000000000000000000001353173603400145255ustar00rootroot00000000000000fugit-1.3.3/lib/fugit/at.rb000066400000000000000000000003111353173603400154510ustar00rootroot00000000000000 module Fugit module At class << self def parse(s) ::EtOrbi.make_time(s) rescue nil end def do_parse(s) ::EtOrbi.make_time(s) end end end end fugit-1.3.3/lib/fugit/cron.rb000066400000000000000000000470211353173603400160170ustar00rootroot00000000000000 module Fugit class Cron SPECIALS = { '@reboot' => :reboot, '@yearly' => '0 0 1 1 *', '@annually' => '0 0 1 1 *', '@monthly' => '0 0 1 * *', '@weekly' => '0 0 * * 0', '@daily' => '0 0 * * *', '@midnight' => '0 0 * * *', '@hourly' => '0 * * * *' } MAXDAYS = [ nil, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ] attr_reader( :original, :zone) attr_reader( :seconds, :minutes, :hours, :monthdays, :months, :weekdays, :timezone) class << self def new(original) parse(original) end def parse(s) return s if s.is_a?(self) s = SPECIALS[s] || s return nil unless s.is_a?(String) #p s; Raabro.pp(Parser.parse(s, debug: 3), colors: true) h = Parser.parse(s) return nil unless h self.allocate.send(:init, s, h) end def do_parse(s) parse(s) || fail(ArgumentError.new("invalid cron string #{s.inspect}")) end end def to_cron_s @cron_s ||= [ @seconds == [ 0 ] ? nil : (@seconds || [ '*' ]).join(','), (@minutes || [ '*' ]).join(','), (@hours || [ '*' ]).join(','), (@monthdays || [ '*' ]).join(','), (@months || [ '*' ]).join(','), (@weekdays || [ [ '*' ] ]).map { |d| d.compact.join('#') }.join(','), @timezone ? @timezone.name : nil ].compact.join(' ') end class TimeCursor def initialize(cron, t) @cron = cron @t = t.is_a?(TimeCursor) ? t.time : t @t.seconds = @t.seconds.to_i end def time; @t; end def to_i; @t.to_i; end %w[ year month day wday hour min sec wday_in_month rweek rday ] .collect(&:to_sym).each { |k| define_method(k) { @t.send(k) } } def inc(i) @t = @t + i self end def dec(i); inc(-i); end def inc_month y = @t.year m = @t.month + 1 if m == 13; m = 1; y += 1; end @t = ::EtOrbi.make(y, m, @t.zone) self end def inc_day inc((24 - @t.hour) * 3600 - @t.min * 60 - @t.sec) end def inc_hour inc((60 - @t.min) * 60 - @t.sec) end def inc_min inc(60 - @t.sec) end def inc_sec if sec = @cron.seconds.find { |s| s > @t.sec } inc(sec - @t.sec) else inc(60 - @t.sec + @cron.seconds.first) end end def dec_month #dec(@t.day * 24 * 3600 + @t.hour * 3600 + @t.min * 60 + @t.sec + 1) # # gh-18, so that '0 9 29 feb *' doesn't get skipped (over and over) # dec(@t.day * 24 * 3600 + 1) end def dec_day dec(@t.hour * 3600 + @t.min * 60 + @t.sec + 1) end def dec_hour dec(@t.min * 60 + @t.sec + 1) end def dec_min dec(@t.sec + 1) end def dec_sec target = @cron.seconds.reverse.find { |s| s < @t.sec } || @cron.seconds.last inc(target - @t.sec - (@t.sec > target ? 0 : 60)) end end def month_match?(nt); ( ! @months) || @months.include?(nt.month); end def hour_match?(nt); ( ! @hours) || @hours.include?(nt.hour); end def min_match?(nt); ( ! @minutes) || @minutes.include?(nt.min); end def sec_match?(nt); ( ! @seconds) || @seconds.include?(nt.sec); end def weekday_hash_match?(nt, hsh) phsh, nhsh = nt.wday_in_month 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 end def weekday_modulo_match?(nt, mod) nt.rweek % mod[0] == mod[1] 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 end def monthday_match?(nt) return true if @monthdays.nil? last = (TimeCursor.new(self, nt).inc_month.time - 24 * 3600).day + 1 @monthdays .collect { |d| d < 1 ? last + d : d } .include?(nt.day) end def day_match?(nt) return weekday_match?(nt) || monthday_match?(nt) \ if @weekdays && @monthdays return false unless weekday_match?(nt) return false unless monthday_match?(nt) true end def match?(t) t = Fugit.do_parse_at(t).translate(@timezone) month_match?(t) && day_match?(t) && hour_match?(t) && min_match?(t) && sec_match?(t) end MAX_ITERATION_COUNT = 2048 # # See gh-15 and tst/iteration_count.rb # # Initially set to 1024 after seeing the worst case for #next_time # at 167 iterations, I placed it at 2048 after experimenting with # gh-18 and noticing some > 1024 for some experiments. 2048 should # be ok. def next_time(from=::EtOrbi::EoTime.now) from = ::EtOrbi.make_time(from) sfrom = from.strftime('%F|%T') ifrom = from.to_i i = 0 t = TimeCursor.new(self, from.translate(@timezone)) # # the translation occurs in the timezone of # this Fugit::Cron instance loop do fail RuntimeError.new( "too many loops for #{@original.inspect} #next_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJC9" ) if (i += 1) > MAX_ITERATION_COUNT (ifrom == t.to_i) && (t.inc(1); next) month_match?(t) || (t.inc_month; next) day_match?(t) || (t.inc_day; next) hour_match?(t) || (t.inc_hour; next) min_match?(t) || (t.inc_min; next) sec_match?(t) || (t.inc_sec; next) st = t.time.strftime('%F|%T') (from, sfrom, ifrom = t.time, st, t.to_i; next) if st == sfrom # # when transitioning out of DST, this prevents #next_time from # yielding the same literal time twice in a row, see gh-6 break end t.time.translate(from.zone) # # the answer time is in the same timezone as the `from` # starting point end def previous_time(from=::EtOrbi::EoTime.now) from = ::EtOrbi.make_time(from) i = 0 t = TimeCursor.new(self, (from - 1).translate(@timezone)) loop do fail RuntimeError.new( "too many loops for #{@original.inspect} #previous_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJCQ" ) if (i += 1) > MAX_ITERATION_COUNT month_match?(t) || (t.dec_month; next) day_match?(t) || (t.dec_day; next) hour_match?(t) || (t.dec_hour; next) min_match?(t) || (t.dec_min; next) sec_match?(t) || (t.dec_sec; next) break end t.time.translate(from.zone) end # Mostly used as a #next_time sanity check. # Avoid for "business" use, it's slow. # # 2017 is a non leap year (though it is preceded by # a leap second on 2016-12-31) # # Nota bene: cron with seconds are not supported. # def brute_frequency(year=2017) FREQUENCY_CACHE["#{to_cron_s}|#{year}"] ||= begin deltas = [] t = EtOrbi.make_time("#{year}-01-01") - 1 t0 = nil t1 = nil loop do t1 = next_time(t) deltas << (t1 - t).to_i if t0 t0 ||= t1 break if deltas.any? && t1.year > year break if t1.year - t0.year > 7 t = t1 end Frequency.new(deltas, t1 - t0) end end SLOTS = [ [ :seconds, 1, 60 ], [ :minutes, 60, 60 ], [ :hours, 3600, 24 ], [ :days, 24 * 3600, 365 ] ] def rough_frequency slots = SLOTS .collect { |k, v0, v1| a = (k == :days) ? rough_days : instance_variable_get("@#{k}") [ k, v0, v1, a ] } slots.each do |k, v0, _, a| next if a == [ 0 ] break if a != nil return v0 if a == nil end slots.each do |k, v0, v1, a| next unless a && a.length > 1 return (a + [ a.first + v1 ]) .each_cons(2) .collect { |a0, a1| a1 - a0 } .min * v0 end slots.reverse.each do |k, v0, v1, a| return v0 * v1 if a && a.length == 1 end 1 # second end class Frequency attr_reader :span, :delta_min, :delta_max, :occurrences attr_reader :span_years, :yearly_occurrences def initialize(deltas, span) @span = span @delta_min = deltas.min; @delta_max = deltas.max @occurrences = deltas.size @span_years = span / (365 * 24 * 3600) @yearly_occurrences = @occurrences.to_f / @span_years end def to_debug_s { dmin: Fugit::Duration.new(delta_min).deflate.to_plain_s, dmax: Fugit::Duration.new(delta_max).deflate.to_plain_s, ocs: occurrences, spn: Fugit::Duration.new(span.to_i).deflate.to_plain_s, spnys: span_years.to_i, yocs: yearly_occurrences.to_i }.collect { |k, v| "#{k}: #{v}" }.join(', ') end end def to_a [ @seconds, @minutes, @hours, @monthdays, @months, @weekdays ] end def to_h { seconds: @seconds, minutes: @minutes, hours: @hours, monthdays: @monthdays, months: @months, weekdays: @weekdays } end def ==(o) o.is_a?(::Fugit::Cron) && o.to_a == to_a end alias eql? == def hash to_a.hash end protected def compact_month_days return true if @months == nil || @monthdays == nil ms, ds = @months.inject([ [], [] ]) { |a, m| @monthdays.each { |d| next if d > MAXDAYS[m] a[0] << m; a[1] << d } a } @months = ms.uniq @monthdays = ds.uniq @months.any? && @monthdays.any? end def rough_days return nil if @weekdays == nil && @monthdays == nil months = (@months || (1..12).to_a) monthdays = months .product(@monthdays || []) .collect { |m, d| d = 31 + d if d < 0 (m - 1) * 30 + d } # rough weekdays = (@weekdays || []) .collect { |d, w| w ? d + (w - 1) * 7 : (0..3).collect { |ww| d + ww * 7 } } .flatten weekdays = months .product(weekdays) .collect { |m, d| (m - 1) * 30 + d } # rough (monthdays + weekdays).sort end FREQUENCY_CACHE = {} def init(original, h) @original = original @cron_s = nil # just to be sure 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_timezone(h[:tz]) return nil unless compact_month_days self end def expand(min, max, r) sta, edn, sla = r sla = nil if sla == 1 # don't get fooled by /1 return [ nil ] if sta.nil? && edn.nil? && sla.nil? return [ sta ] if sta && edn.nil? sla = 1 if sla == nil sta = min if sta == nil edn = max if edn == nil range(min, max, sta, edn, sla) end def range(min, max, sta, edn, sla) fail ArgumentError.new( 'both start and end must be negative in ' + { min: min, max: max, sta: sta, edn: edn, sla: sla }.inspect ) if (sta < 0 && edn > 0) || (edn < 0 && sta > 0) #p({ min: min, max: max, sta: sta, edn: edn, sla: sla }) a = [] omin, omax = min, max min, max = -max, -1 if sta < 0 #p({ min: min, max: max }) cur = sta loop do a << cur break if cur == edn cur += 1 if cur > max cur = min edn = edn - max - 1 if edn > max end fail RuntimeError.new( "too many loops for " + { min: omin, max: omax, sta: sta, edn: edn, sla: sla }.inspect + " #range, breaking, " + "please fill an issue at https://git.io/fjJC9" ) if a.length > 2 * omax # there is a #uniq afterwards, hence the 2* for 0-24 and friends end a.each_with_index .select { |e, i| i % sla == 0 } .collect(&:first) .uniq end def compact(key) arr = instance_variable_get(key) return instance_variable_set(key, nil) if arr.include?(nil) # reductio ad astrum arr.uniq! arr.sort! end def determine_seconds(arr) @seconds = (arr || [ 0 ]).inject([]) { |a, s| a.concat(expand(0, 59, s)) } compact(:@seconds) end def determine_minutes(arr) @minutes = arr.inject([]) { |a, m| a.concat(expand(0, 59, m)) } compact(:@minutes) end def determine_hours(arr) @hours = arr .inject([]) { |a, h| a.concat(expand(0, 23, h)) } .collect { |h| h == 24 ? 0 : h } compact(:@hours) end def determine_monthdays(arr) @monthdays = arr.inject([]) { |a, d| a.concat(expand(1, 31, d)) } compact(:@monthdays) end def determine_months(arr) @months = arr.inject([]) { |a, m| a.concat(expand(1, 12, m)) } compact(:@months) end def determine_weekdays(arr) @weekdays = [] arr.each do |a, z, sl, ha, mo| # a to z, slash, hash, and mod if ha || mo @weekdays << [ a, ha || mo ] elsif sl ((a || 0)..(z || (a ? a : 6))).step(sl < 1 ? 1 : sl) .each { |i| @weekdays << [ i ] } elsif z z = z + 7 if a > z (a..z).each { |i| @weekdays << [ (i > 6) ? i - 7 : i ] } elsif a @weekdays << [ a ] #else end end @weekdays.each { |wd| wd[0] = 0 if wd[0] == 7 } # turn sun7 into sun0 @weekdays.uniq! @weekdays.sort! @weekdays = nil if @weekdays.empty? end def determine_timezone(z) @zone, @timezone = z end module Parser include Raabro WEEKDAYS = %w[ sunday monday tuesday wednesday thursday friday saturday ] WEEKDS = WEEKDAYS.collect { |d| d[0, 3] } MONTHS = %w[ - jan feb mar apr may jun jul aug sep oct nov dec ] # piece parsers bottom to top def s(i); rex(nil, i, /[ \t]+/); end def star(i); str(nil, i, '*'); end def hyphen(i); str(nil, i, '-'); end def comma(i); str(nil, i, ','); end def slash(i); rex(:slash, i, /\/\d\d?/); end def mos(i); rex(:mos, i, /[0-5]?\d/); end # min or sec def hou(i); rex(:hou, i, /(2[0-4]|[01]?[0-9])/); end def dom(i); rex(:dom, i, /(-?(3[01]|[12][0-9]|0?[1-9])|last|l)/i); end def mon(i); rex(:mon, i, /(1[0-2]|0?[1-9]|#{MONTHS[1..-1].join('|')})/i); end def dow(i); rex(:dow, i, /([0-7]|#{WEEKDS.join('|')})/i); end def dow_hash(i); rex(:hash, i, /#(-?[1-5]|last|l)/i); end def _mos(i); seq(nil, i, :hyphen, :mos); end def _hou(i); seq(nil, i, :hyphen, :hou); end def _dom(i); seq(nil, i, :hyphen, :dom); end def _mon(i); seq(nil, i, :hyphen, :mon); end def _dow(i); seq(nil, i, :hyphen, :dow); end # r: range def r_mos(i); seq(nil, i, :mos, :_mos, '?'); end def r_hou(i); seq(nil, i, :hou, :_hou, '?'); end def r_dom(i); seq(nil, i, :dom, :_dom, '?'); end def r_mon(i); seq(nil, i, :mon, :_mon, '?'); end def r_dow(i); seq(nil, i, :dow, :_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 # sorws: star or range with[out] slash def sorws_mos(i); seq(nil, i, :sor_mos, :slash, '?'); end def sorws_hou(i); seq(nil, i, :sor_hou, :slash, '?'); end def sorws_dom(i); seq(nil, i, :sor_dom, :slash, '?'); end def sorws_mon(i); seq(nil, i, :sor_mon, :slash, '?'); end def sorws_dow(i); seq(nil, i, :sor_dow, :slash, '?'); end # ssws: slash or sorws def mos_elt(i); alt(:elt, i, :slash, :sorws_mos); end def hou_elt(i); alt(:elt, i, :slash, :sorws_hou); end def dom_elt(i); alt(:elt, i, :slash, :sorws_dom); end def mon_elt(i); alt(:elt, i, :slash, :sorws_mon); end def dow_elt(i); alt(:elt, i, :slash, :sorws_dow); end def mod(i); rex(:mod, i, /%\d+(\+\d+)?/); end def mod_dow(i); seq(:elt, i, :dow, :mod); end def h_dow(i); seq(:elt, i, :dow, :dow_hash); end def dow_elt_(i); alt(nil, i, :h_dow, :mod_dow, :dow_elt); end def list_sec(i); jseq(:sec, i, :mos_elt, :comma); end def list_min(i); jseq(:min, i, :mos_elt, :comma); end def list_hou(i); jseq(:hou, i, :hou_elt, :comma); end def list_dom(i); jseq(:dom, i, :dom_elt, :comma); end def list_mon(i); jseq(:mon, i, :mon_elt, :comma); end def list_dow(i); jseq(:dow, i, :dow_elt_, :comma); end def lsec_(i); seq(nil, i, :list_sec, :s); end def lmin_(i); seq(nil, i, :list_min, :s); end def lhou_(i); seq(nil, i, :list_hou, :s); end def ldom_(i); seq(nil, i, :list_dom, :s); end def lmon_(i); seq(nil, i, :list_mon, :s); end alias ldow list_dow def _tz_name(i) rex(nil, i, / +[A-Z][a-zA-Z0-9+\-]+(\/[A-Z][a-zA-Z0-9+\-_]+){0,2}/) end def _tz_delta(i) rex(nil, i, / +[-+]([01][0-9]|2[0-4]):?(00|15|30|45)/) end def _tz(i); alt(:tz, i, :_tz_delta, :_tz_name); end def classic_cron(i) seq(:ccron, i, :lmin_, :lhou_, :ldom_, :lmon_, :ldow, :_tz, '?') end def second_cron(i) seq(:scron, i, :lsec_, :lmin_, :lhou_, :ldom_, :lmon_, :ldow, :_tz, '?') end def cron(i) alt(:cron, i, :second_cron, :classic_cron) end # rewriting the parsed tree def rewrite_bound(k, t) s = t.string.downcase (k == :mon && MONTHS.index(s)) || (k == :dow && WEEKDS.index(s)) || ((k == :dom) && s[0, 1] == 'l' && -1) || # L, l, last s.to_i end def rewrite_mod(k, t) mod, plus = t.string .split(/[%+]/).reject(&:empty?).collect(&:to_i) [ mod, plus || 0 ] end def rewrite_elt(k, t) at, zt, 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 end end sl = slt ? slt.string[1..-1].to_i : nil ha = hat ? hat.string[1..-1] : nil ha = -1 if ha && ha.upcase[0, 1] == 'L' ha = ha.to_i if ha mo = mot ? rewrite_mod(k, mot) : nil a = at ? rewrite_bound(k, at) : nil z = zt ? rewrite_bound(k, zt) : nil #a, z = z, a if a && z && a > z # handled downstream since gh-27 [ a, z, sl, ha, mo ] end def rewrite_entry(t) t .subgather(:elt) .collect { |et| rewrite_elt(t.name, et) } end def rewrite_tz(t) s = t.string.strip z = EtOrbi.get_tzone(s) [ s, z ] end def rewrite_cron(t) hcron = t .sublookup(nil) # go to :ccron or :scron .subgather(nil) # list min, hou, mon, ... .inject({}) { |h, tt| h[tt.name] = tt.name == :tz ? rewrite_tz(tt) : rewrite_entry(tt) h } z, tz = hcron[:tz]; return nil if z && ! tz hcron end end end end fugit-1.3.3/lib/fugit/duration.rb000066400000000000000000000223661353173603400167100ustar00rootroot00000000000000 module Fugit class Duration attr_reader :original, :h, :options class << self def new(s) parse(s) end def parse(s, opts={}) return s if s.is_a?(self) original = s s = "#{s}s" if s.is_a?(Numeric) return nil unless s.is_a?(String) s = s.strip #p [ original, s ]; Raabro.pp(Parser.parse(s, debug: 3), colours: true) h = if opts[:iso] IsoParser.parse(opts[:stricter] ? s : s.upcase) elsif opts[:plain] Parser.parse(s) else Parser.parse(s) || IsoParser.parse(opts[:stricter] ? s : s.upcase) end h ? self.allocate.send(:init, original, opts, h) : nil end def do_parse(s, opts={}) parse(s, opts) || fail(ArgumentError.new("not a duration #{s.inspect}")) end def to_plain_s(o); do_parse(o).deflate.to_plain_s; end def to_iso_s(o); do_parse(o).deflate.to_iso_s; end def to_long_s(o, opts={}); do_parse(o).deflate.to_long_s(opts); end def common_rewrite_dur(t) t .subgather(nil) .inject({}) { |h, tt| v = tt.string; v = v.index('.') ? v.to_f : v.to_i # drops ending ("y", "m", ...) by itself h[tt.name] = (h[tt.name] || 0) + v h } end end KEYS = { yea: { a: 'Y', r: 'y', i: 'Y', s: 365 * 24 * 3600, x: 0, l: 'year' }, mon: { a: 'M', r: 'M', i: 'M', s: 30 * 24 * 3600, x: 1, l: 'month' }, wee: { a: 'W', r: 'w', i: 'W', s: 7 * 24 * 3600, I: true, l: 'week' }, day: { a: 'D', r: 'd', i: 'D', s: 24 * 3600, 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' }, } INFLA_KEYS, NON_INFLA_KEYS = KEYS.partition { |k, v| v[:I] } def _to_s(key) KEYS.inject([ StringIO.new, '+' ]) { |(s, sign), (k, a)| v = @h[k] next [ s, sign ] unless v sign1 = v < 0 ? '-' : '+' s << (sign1 != sign ? sign1 : '') << v.abs.to_s << a[key] [ s, sign1 ] }[0].string end; protected :_to_s def to_plain_s; _to_s(:a); end def to_rufus_s; _to_s(:r); end def to_iso_s t = false s = StringIO.new s << 'P' KEYS.each_with_index do |(k, a), i| v = @h[k]; next unless v if i > 3 && t == false t = true s << 'T' end s << v.to_s; s << a[:i] end s.string end def to_long_s(opts={}) s = StringIO.new adn = [ false, 'no' ].include?(opts[:oxford]) ? ' and ' : ', and ' a = @h.to_a while kv = a.shift k, v = kv aa = KEYS[k] s << v.to_i s << ' '; s << aa[:l]; s << 's' if v > 1 s << (a.size == 1 ? adn : ', ') if a.size > 0 end s.string end # For now, let's alias to #h # def to_h; h; end def to_rufus_h KEYS.inject({}) { |h, (ks, kh)| v = @h[ks]; h[kh[:r].to_sym] = v if v; h } end # Warning: this is an "approximation", months are 30 days and years are # 365 days, ... # def to_sec KEYS.inject(0) { |s, (k, a)| v = @h[k]; next s unless v; s += v * a[:s] } end def inflate params = @h.inject({ sec: 0 }) { |h, (k, v)| a = KEYS[k] if a[:I] h[:sec] += (v * a[:s]) else h[k] = v end h } self.class.allocate.init(@original, {}, params) end # Round float seconds to 9 decimals when deflating # SECOND_ROUND = 9 def deflate(options={}) id = inflate h = id.h.dup s = h.delete(:sec) || 0 keys = INFLA_KEYS mon = options[:month] yea = options[:year] keys = keys.dup if mon || yea if mon mon = 30 if mon == true mon = "#{mon}d" if mon.is_a?(Integer) keys.unshift([ :mon, { s: Fugit::Duration.parse(mon).to_sec } ]) end if yea yea = 365 if yea == true yea = "#{yea}d" if yea.is_a?(Integer) keys.unshift([ :yea, { s: Fugit::Duration.parse(yea).to_sec } ]) end keys[0..-2].each do |k, v| vs = v[:s]; next if s < vs h[k] = (h[k] || 0) + s.to_i / vs s = s % vs end h[:sec] = s.is_a?(Integer) ? s : s.round(SECOND_ROUND) self.class.allocate.init(@original, {}, h) end def opposite params = @h.inject({}) { |h, (k, v)| h[k] = -v; h } self.class.allocate.init(nil, {}, params) end alias -@ opposite def add_numeric(n) h = @h.dup h[:sec] = (h[:sec] || 0) + n.to_i self.class.allocate.init(nil,{}, h) end def add_duration(d) params = d.h.inject(@h.dup) { |h, (k, v)| h[k] = (h[k] || 0) + v; h } self.class.allocate.init(nil, {}, params) end def add_to_time(t) t = ::EtOrbi.make_time(t) INFLA_KEYS.each do |k, a| v = @h[k]; next unless v t = t + v * a[:s] end NON_INFLA_KEYS.each do |k, a| v = @h[k]; next unless v at = [ t.year, t.month, t.day, t.hour, t.min, t.sec ] at[a[:x]] += v if at[1] > 12 n, m = at[1] / 12, at[1] % 12 at[0], at[1] = at[0] + n, m elsif at[1] < 1 n, m = -at[1] / 12, -at[1] % 12 at[0], at[1] = at[0] - n, m end t = ::EtOrbi.make_time(at, t.zone) end t end def add(a) case a when Numeric then add_numeric(a) when Fugit::Duration then add_duration(a) when String then add_duration(self.class.parse(a)) when ::Time, EtOrbi::EoTime then add_to_time(a) else fail ArgumentError.new( "cannot add #{a.class} instance to a Fugit::Duration") end end alias + add def subtract(a) case a when Numeric then add_numeric(-a) when Fugit::Duration then add_duration(-a) when String then add_duration(-self.class.parse(a)) when ::Time, ::EtOrbi::EoTime then add_to_time(a) else fail ArgumentError.new( "cannot subtract #{a.class} instance to a Fugit::Duration") end end alias - subtract def ==(o) o.is_a?(Fugit::Duration) && o.h == @h end alias eql? == def hash @h.hash end def next_time(from=::EtOrbi::EoTime.now) add(from) end # Returns a copy of this duration, omitting its seconds. # def drop_seconds h = @h.dup h.delete(:sec) h[:min] = 0 if h.empty? self.class.allocate.init(nil, { literal: true }, h) end protected def init(original, options, h) @original = original @options = options if options[:literal] @h = h else @h = h.reject { |k, v| v == 0 } @h[:sec] = 0 if @h.empty? end self end module Parser include Raabro # piece parsers bottom to top def sep(i); rex(nil, i, /([ \t,]+|and)*/i); end def yea(i); rex(:yea, i, /(\d+\.\d*|(\d*\.)?\d+) *y(ears?)?/i); end def mon(i); rex(:mon, i, /(\d+\.\d*|(\d*\.)?\d+) *(M|months?)/); end def wee(i); rex(:wee, i, /(\d+\.\d*|(\d*\.)?\d+) *w(eeks?)?/i); end def day(i); rex(:day, i, /(\d+\.\d*|(\d*\.)?\d+) *d(ays?)?/i); end def hou(i); rex(:hou, i, /(\d+\.\d*|(\d*\.)?\d+) *h(ours?)?/i); end def min(i); rex(:min, i, /(\d+\.\d*|(\d*\.)?\d+) *m(in(ute)?s?)?/); end def sec(i); rex(:sec, i, /(\d+\.\d*|(\d*\.)?\d+) *s(ec(ond)?)?s?/i); end def sek(i); rex(:sec, i, /(\d+\.\d*|\.\d+|\d+)$/); end def elt(i); alt(nil, i, :yea, :mon, :wee, :day, :hou, :min, :sec, :sek); end def sign(i); rex(:sign, i, /[-+]?/); end def sdur(i); seq(:sdur, i, :sign, '?', :elt, '+'); end def dur(i); jseq(:dur, i, :sdur, :sep); end # rewrite parsed tree def merge(h0, h1) sign = h1.delete(:sign) || 1 h1.inject(h0) { |h, (k, v)| h.merge(k => (h[k] || 0) + sign * v) } end def rewrite_sdur(t) h = Fugit::Duration.common_rewrite_dur(t) sign = t.sublookup(:sign) sign = (sign && sign.string == '-') ? -1 : 1 h.merge(sign: sign) end def rewrite_dur(t) #Raabro.pp(t, colours: true) t.children.inject({}) { |h, ct| merge(h, ct.name ? rewrite(ct) : {}) } end end module IsoParser include Raabro # piece parsers bottom to top def p(i); rex(nil, i, /P/); end def t(i); rex(nil, i, /T/); end def yea(i); rex(:yea, i, /-?\d+Y/); end def mon(i); rex(:mon, i, /-?\d+M/); end def wee(i); rex(:wee, i, /-?\d+W/); end def day(i); rex(:day, i, /-?\d+D/); end def hou(i); rex(:hou, i, /-?\d+H/); end def min(i); rex(:min, i, /-?\d+M/); end def sec(i); rex(:sec, i, /-?(\d*\.)?\d+S/); end def delt(i); alt(nil, i, :yea, :mon, :wee, :day); end def telt(i); alt(nil, i, :hou, :min, :sec); end def date(i); rep(nil, i, :delt, 1); end def time(i); rep(nil, i, :telt, 1); end def t_time(i); seq(nil, i, :t, :time); end def dur(i); seq(:dur, i, :p, :date, '?', :t_time, '?'); end # rewrite parsed tree def rewrite_dur(t); Fugit::Duration.common_rewrite_dur(t); end end end end fugit-1.3.3/lib/fugit/misc.rb000066400000000000000000000012121353173603400160010ustar00rootroot00000000000000 module Fugit class << self def isostamp(show_date, show_time, show_usec, time) t = time || Time.now s = StringIO.new s << t.strftime('%Y-%m-%d') if show_date s << t.strftime('T%H:%M:%S') if show_time s << sprintf('.%06d', t.usec) if show_time && show_usec s << 'Z' if show_time && time.utc? s.string end def time_to_s(t) isostamp(true, true, false, t) end def time_to_plain_s(t=Time.now, z=true) t.strftime('%Y-%m-%d %H:%M:%S') + (z && t.utc? ? ' Z' : '') end def time_to_zone_s(t=Time.now) t.strftime('%Y-%m-%d %H:%M:%S %Z %z') end end end fugit-1.3.3/lib/fugit/nat.rb000066400000000000000000000154261353173603400156440ustar00rootroot00000000000000 module Fugit # A natural language set of parsers for fugit. # Focuses on cron expressions. The rest is better left to Chronic and friends. # module Nat class << self def parse(s, opts={}) return s if s.is_a?(Fugit::Cron) || s.is_a?(Fugit::Duration) return nil unless s.is_a?(String) #p s; Raabro.pp(Parser.parse(s, debug: 3), colours: true) a = Parser.parse(s) return nil unless a return parse_crons(s, a, opts) \ if a.include?([ :flag, 'every' ]) return parse_crons(s, a, opts) \ if a.include?([ :flag, 'from' ]) && a.find { |e| e[0] == :day_range } nil end def do_parse(s, opts={}) parse(s, opts) || fail(ArgumentError.new("could not parse a nat #{s.inspect}")) end protected def parse_crons(s, a, opts) dhs, aa = a .partition { |e| e[0] == :digital_hour } ms = dhs .inject({}) { |h, dh| (h[dh[1][0]] ||= []) << dh[1][1]; h } .values .uniq crons = #if ms.size <= 1 || hs.size <= 1 if ms.size <= 1 [ parse_cron(a, opts) ] else dhs.collect { |dh| parse_cron([ dh ] + aa, opts) } end fail ArgumentError.new( "multiple crons in #{s.inspect} " + "(#{crons.collect(&:original).join(' | ')})" ) if opts[:multi] == :fail && crons.size > 1 if opts[:multi] == true || (opts[:multi] && opts[:multi] != :fail) crons else crons.first end end def parse_cron(a, opts) h = { min: nil, hou: [], dom: nil, mon: nil, dow: nil } hkeys = h.keys a.each do |key, val| if key == :biz_day (h[:dow] ||= []) << '1-5' elsif key == :simple_hour || key == :numeral_hour h[:hou] << val elsif key == :digital_hour (h[:hou] ||= []) << val[0].to_i (h[:min] ||= []) << val[1].to_i elsif key == :name_day (h[:dow] ||= []) << val elsif key == :day_range (h[:dow] ||= []) << val.collect { |v| v.to_s[0, 3] }.join('-') elsif key == :tz h[:tz] = val elsif key == :duration process_duration(h, *val[0].to_h.first) end end h[:min] ||= [ 0 ] h[:min].uniq! h[:hou].uniq!; h[:hou].sort! h[:dow].sort! if h[:dow] a = hkeys .collect { |k| v = h[k] (v && v.any?) ? v.collect(&:to_s).join(',') : '*' } a.insert(0, h[:sec]) if h[:sec] a << h[:tz].first if h[:tz] s = a.join(' ') Fugit::Cron.parse(s) end def process_duration(h, interval, value) send("process_duration_#{interval}", h, value) end def process_duration_mon(h, value) h[:hou] = [ 0 ] h[:dom] = [ 1 ] h[:mon] = [ value == 1 ? '*' : "*/#{value}" ] end def process_duration_day(h, value) h[:hou] = [ 0 ] h[:dom] = [ value == 1 ? '*' : "*/#{value}" ] end def process_duration_hou(h, value) h[:hou] = [ value == 1 ? '*' : "*/#{value}" ] end def process_duration_min(h, value) h[:hou] = [ '*' ] h[:min] = [ value == 1 ? '*' : "*/#{value}" ] end def process_duration_sec(h, value) h[:hou] = [ '*' ] h[:min] = [ '*' ] h[:sec] = [ value == 1 ? '*' : "*/#{value}" ] end end module Parser include Raabro NUMS = %w[ zero one two three four five six seven eight nine ten eleven twelve ] WEEKDAYS = Fugit::Cron::Parser::WEEKDS + Fugit::Cron::Parser::WEEKDAYS NHOURS = { 'noon' => [ 12, 0 ], 'midnight' => [ 0, 0 ] } # piece parsers bottom to top def am_pm(i) rex(:am_pm, i, / *(am|pm)/i) end def digital_hour(i) rex(:digital_hour, i, /(2[0-4]|[01][0-9]):?[0-5]\d/) end def _simple_hour(i) rex(:sh, i, /(2[0-4]|[01]?[0-9])/) end def simple_hour(i) seq(:simple_hour, i, :_simple_hour, :am_pm, '?') end def _numeral_hour(i) rex(:nh, i, /(#{NUMS.join('|')})/i) end def numeral_hour(i) seq(:numeral_hour, i, :_numeral_hour, :am_pm, '?') end def name_hour(i) rex(:name_hour, i, /(#{NHOURS.keys.join('|')})/i) end def plain_day(i); rex(:plain_day, i, /day/i); end def biz_day(i); rex(:biz_day, i, /(biz|business|week) *day/i); end def name_day(i); rex(:name_day, i, /#{WEEKDAYS.reverse.join('|')}/i); end def range_sep(i); rex(nil, i, / *- *| +(to|through) +/); end def day_range(i) seq(:day_range, i, :name_day, :range_sep, :name_day) end def _tz_name(i) rex(nil, i, /[A-Z][a-zA-Z0-9+\-]+(\/[A-Z][a-zA-Z0-9+\-_]+){0,2}/) end def _tz_delta(i) rex(nil, i, /[-+]([01][0-9]|2[0-4]):?(00|15|30|45)/) end def _tz(i); alt(:tz, i, :_tz_delta, :_tz_name); end def duration(i) rex( :duration, i, / \d+ \s? (mon(ths?)?|d(ays?)?|h(ours?)?|m(in(ute)?s?)?|s(ec(ond)?s?)?) /ix) end def flag(i); rex(:flag, i, /(every|from|at|after|on|in)/i); end def datum(i) alt(nil, i, :day_range, :plain_day, :biz_day, :name_day, :_tz, :flag, :duration, :name_hour, :numeral_hour, :digital_hour, :simple_hour) end def sugar(i); rex(nil, i, /(and|or|[, \t]+)/i); end def elt(i); alt(nil, i, :sugar, :datum); end def nat(i); rep(:nat, i, :elt, 1); end # rewrite parsed tree def rewrite_nat(t) #Raabro.pp(t, colours: true) t .subgather(nil) .collect { |tt| k = tt.name v = tt.string.downcase case k when :tz [ k, [ tt.string.strip, EtOrbi.get_tzone(tt.string.strip) ] ] when :duration [ k, [ Fugit::Duration.parse(tt.string.strip) ] ] when :digital_hour v = v.gsub(/:/, '') [ k, [ v[0, 2], v[2, 2] ] ] when :name_hour [ :digital_hour, NHOURS[v] ] when :name_day [ k, WEEKDAYS.index(v[0, 3]) ] when :day_range [ k, tt.subgather(nil).collect { |st| st.string.downcase } ] when :numeral_hour, :simple_hour vs = tt.subgather(nil).collect { |ttt| ttt.string.downcase.strip } v = k == :simple_hour ? vs[0].to_i : NUMS.index(vs[0]) v += 12 if vs[1] == 'pm' [ k, v ] else [ k, v ] end } end end end end fugit-1.3.3/lib/fugit/parse.rb000066400000000000000000000023431353173603400161660ustar00rootroot00000000000000 module Fugit class << self def parse_cron(s); ::Fugit::Cron.parse(s); 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_in(s); parse_duration(s); end def do_parse_cron(s); ::Fugit::Cron.do_parse(s); 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_at(s); ::Fugit::At.do_parse(s); end def do_parse_in(s); do_parse_duration(s); end def parse(s, opts={}) opts[:at] = opts[:in] if opts.has_key?(:in) (opts[:cron] != false && parse_cron(s)) || (opts[:duration] != false && parse_duration(s)) || (opts[:nat] != false && parse_nat(s, opts)) || (opts[:at] != false && parse_at(s)) || nil end def do_parse(s, opts={}) parse(s, opts) || fail(ArgumentError.new("found no time information in #{s.inspect}")) end def determine_type(s) case self.parse(s) when ::Fugit::Cron then 'cron' when ::Fugit::Duration then 'in' when ::Time, ::EtOrbi::EoTime then 'at' else nil end end end end fugit-1.3.3/spec/000077500000000000000000000000001353173603400135735ustar00rootroot00000000000000fugit-1.3.3/spec/at_spec.rb000066400000000000000000000023551353173603400155430ustar00rootroot00000000000000 # # Specifying fugit # # Sat Jun 10 07:44:42 JST 2017 圓さんの家 # require 'spec_helper' describe Fugit do describe '.parse_at' do it 'parses time points' do t = Fugit.parse_at('2017-01-03 11:21:17') expect(t.class).to eq(::EtOrbi::EoTime) expect(Fugit.time_to_plain_s(t, false)).to eq('2017-01-03 11:21:17') end it 'returns an EoTime instance as is' do eot = ::EtOrbi::EoTime.new('2017-01-03 11:21:17', 'America/Los_Angeles') t = Fugit.parse_at(eot) expect(t.class).to eq(::EtOrbi::EoTime) expect(t.object_id).to eq(eot.object_id) end context 'with timezones' do [ [ '2018-09-04 06:41:34 +11', '2018-09-04 06:41:34 +11 +1100' ], [ '2018-09-04 06:41:34 +1100', '2018-09-04 06:41:34 +1100 +1100' ], [ '2018-09-04 06:41:34 +11:00', '2018-09-04 06:41:34 +11:00 +1100' ], [ '2018-09-04 06:41:34 Etc/GMT-11', '2018-09-04 06:41:34 +11 +1100' ], #[ '2018-09-04 06:41:34 UTC+11', nil ], ].each do |string, plain| it "parses #{string}" do t = Fugit.parse_at(string) expect(t.class).to eq(::EtOrbi::EoTime) expect(Fugit.time_to_zone_s(t)).to eq(plain) end end end end end fugit-1.3.3/spec/cron_spec.rb000066400000000000000000000747301353173603400161060ustar00rootroot00000000000000 # # Specifying fugit # # Mon Jan 2 11:17:40 JST 2017 Ishinomaki # require 'spec_helper' describe Fugit::Cron do NOW = Time.parse('2017-01-02 12:00:00') NEXT_TIMES = [ # min hou dom mon dow, expected next time[, now] [ '* * * * *', '2017-01-02 12:01:00' ], [ '5 0 * * *', '2017-01-03 00:05:00' ], [ '15 14 1 * *', '2017-02-01 14:15:00' ], [ '0 0 1 1 *', '2018-01-01 00:00:00' ], [ '* * 29 * *', '2017-01-29 00:00:00' ], [ '* * 29 * *', '2016-02-29 00:00:00', '2016-02-01' ], [ '* * L * *', '2016-02-29 00:00:00', '2016-02-01' ], [ '* * last * *', '2016-02-29 00:00:00', '2016-02-01' ], [ '* * -1 * *', '2016-02-29 00:00:00', '2016-02-01' ], [ '* * L * *', '2016-02-29 00:00:00', '2016-02-01' ], [ '0 0 -4,-3 * *', '2016-02-26 00:00:00', '2016-02-01' ], [ '0 0 -4,-3 * *', '2016-02-27 00:00:00', '2016-02-26 12:00' ], [ '* * * * sun', '2017-01-8' ], [ '* * -2 * *', '2017-01-30' ], [ '* * -1 * *', '2017-01-31' ], [ '* * L * *', '2017-01-31' ], [ '* * * * mon#2', '2017-01-09' ], [ '* * * * mon#-1', '2017-01-30' ], [ '* * * * tue#L', '2017-01-31' ], [ '* * * * tue#last', '2017-01-31' ], [ '* * * * mon#2,tue', '2016-12-06', '2016-12-01' ], [ '* * * * mon#2,tue', '2016-12-12', '2016-12-07' ], [ '0 0 * * mon#2,tue', '2017-01-09', '2017-01-06' ], [ '0 0 * * mon#2,tue', '2017-01-31', '2017-01-30' ], [ '00 24 * * *', '2017-01-02 00:00:00', '2017-01-01 12:00' ], # 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. # # Thanks to Dominik Sander for pointing to that in # https://github.com/jmettraux/rufus-scheduler/pull/226 [ '30 04 1,15 * 5', '2017-01-06 04:30:00', '2017-01-03' ], [ '30 04 1,15 * 5', '2017-01-15 04:30:00', '2017-01-14' ], [ '30 04 1,15 * 5', '2017-01-20 04:30:00', '2017-01-16' ], # # gh-5 '0 8 L * * mon-thu', last day of month on Saturday # 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. [ '0 8 L * mon-thu', '2018-06-30 08:00:00', '2018-06-28 18:00:00', 'Europe/Berlin' ], # [ '0 9 -2 * *', '2018-06-29 09:00:00', '2018-06-28 18:00:00', 'Europe/Berlin' ], [ '0 0 -5 * *', '2018-07-27 00:00:00', '2018-06-28 18:00:00', 'Europe/Berlin' ], # [ '0 8 L * *', '2018-06-30 08:00:00', '2018-06-28 18:00:00', 'Europe/Berlin' ], [ '0 9 29 feb *', '2016-02-29 09:00', '2016-01-23' ], # gh-18 (mirror #prev) # # gh-1 '0 9 * * sun%2' and '* * * * sun%2+1' # every other Sunday [ '0 9 * * sat%2', '2019-01-12 09:00:00', '2019-01-01 09:00:00' ], [ '0 10 * * sun%2', '2019-04-21 10:00:00', '2019-04-11 09:00:00', 'Europe/Berlin' ], [ '0 10 * * sun%2+1', '2019-04-14 10:00:00', '2019-04-11 09:00:00', 'Europe/Berlin' ], ] describe '#next_time' do NEXT_TIMES.each do |cron, next_time, now, zone_name| d = "succeeds #{cron.inspect} -> #{next_time.inspect}" d += " in #{zone_name}" if zone_name it(d) do in_zone(zone_name) do c = Fugit::Cron.parse(cron) expect(c.class).to eq(Fugit::Cron) ent = Time.parse(next_time) now = Time.parse(now) if now nt = c.next_time(now || NOW) expect( Fugit.time_to_plain_s(nt, false) ).to eq( Fugit.time_to_plain_s(ent, false) ) expect(nt.zone.name).to eq(zone_name) if zone_name end end end context 'implicit tz DST transition' do [ [ 'America/Los_Angeles', '* * * * *', '2015-03-08 09:59:00 UTC', '2015-03-08 03:00:00 PDT -0700' ], ].each do |tz, cron, from, target| it "correctly transit in or out of DST for #{tz.inspect}" do in_zone(tz) do c = Fugit::Cron.parse(cron) f = ::EtOrbi.make_time(from) nt = c.next_time(f).localtime expect(Fugit.time_to_zone_s(nt)).to eq(target) end end end it 'correctly increments every minute into DST' do in_zone 'America/Los_Angeles' do c = Fugit::Cron.parse('* * * * *') t = Time.parse('2015-03-08 01:57:00') points = 4.times.collect do t = c.next_time(t) t.strftime("%H:%M_%Z") + '__' + t.dup.utc.strftime("%H:%M_%Z") end expect(points.join("\n")).to eq(%w[ 01:58_PST__09:58_UTC 01:59_PST__09:59_UTC 03:00_PDT__10:00_UTC 03:01_PDT__10:01_UTC ].join("\n")) end end it 'correctly increments every minute into DST (explicit TZ)' do in_zone 'America/Los_Angeles' do c = Fugit::Cron.parse('* * * * * Europe/Berlin') t = EtOrbi::EoTime.parse('2015-03-08 01:57:00') points = 4.times.collect do t = c.next_time(t) t.strftime('%H:%M_%Z') + '__' + t.dup.utc.strftime('%H:%M_%Z') end expect(points.join("\n")).to eq(%w[ 01:58_PST__09:58_UTC 01:59_PST__09:59_UTC 03:00_PDT__10:00_UTC 03:01_PDT__10:01_UTC ].join("\n")) end end it 'correctly increments out of DST' do in_zone 'America/Los_Angeles' do c = Fugit::Cron.parse('15 * * * *') t = Time.parse('2015-11-01 00:14:00') points = 5.times .collect { t = c.next_time(t) t.to_zs + ' // ' + t.to_t.to_s } .join("\n") expect(points).to eq(%{ 2015-11-01 00:15:00 America/Los_Angeles // 2015-11-01 00:15:00 -0700 2015-11-01 01:15:00 America/Los_Angeles // 2015-11-01 01:15:00 -0700 2015-11-01 02:15:00 America/Los_Angeles // 2015-11-01 02:15:00 -0800 2015-11-01 03:15:00 America/Los_Angeles // 2015-11-01 03:15:00 -0800 2015-11-01 04:15:00 America/Los_Angeles // 2015-11-01 04:15:00 -0800 }.strip.split("\n").collect(&:strip).join("\n")) expect( c.brute_frequency(2015).occurrences ).to eq(8759) end end it 'correctly increments out of DST (America/New_York)' do in_zone 'America/New_York' do c = Fugit::Cron.parse('59 1 * * *') t = EtOrbi::EoTime.parse('2018-11-03 00:00:00') points = 4.times .collect { t = c.next_time(t) t.to_zs + ' // ' + t.to_t.to_s } .join("\n") expect(points).to eq(%{ 2018-11-03 01:59:00 America/New_York // 2018-11-03 01:59:00 -0400 2018-11-04 01:59:00 America/New_York // 2018-11-04 01:59:00 -0400 2018-11-05 01:59:00 America/New_York // 2018-11-05 01:59:00 -0500 2018-11-06 01:59:00 America/New_York // 2018-11-06 01:59:00 -0500 }.strip.split("\n").collect(&:strip).join("\n")) expect( c.brute_frequency(2018).occurrences ).to eq(365) end end end it 'returns a plain second' do c = Fugit::Cron.parse('* * * * *') nt = c.next_time expect(nt.seconds.to_s).to eq(nt.seconds.to_i.to_s + '.0') end context 'explicit timezone' do it 'computes in the cron zone but returns in the from zone' do c = Fugit::Cron.parse('* * * * * Europe/Rome') f = EtOrbi.parse('2017-03-25 21:59 Asia/Tbilisi') t = c.next_time(f) expect(t.class).to eq(EtOrbi::EoTime) expect(t.iso8601).to eq('2017-03-25T22:00:00+04:00') end it 'returns the right result' do c = Fugit::Cron.parse('0 0 1 1 * Europe/Rome') f = EtOrbi.parse('2017-03-25 21:59 Asia/Tbilisi') t = c.next_time(f) expect(t.class) .to eq(EtOrbi::EoTime) expect(t.iso8601) .to eq('2018-01-01T03:00:00+04:00') expect(t.translate('Europe/Rome').iso8601) .to eq('2018-01-01T00:00:00+01:00') end end it 'breaks if its loop takes too long' do c = Fugit::Cron.parse('* * 1 * *') c.instance_eval { @monthdays = [ 0 ] } # # forge an invalid cron expect { c.next_time }.to raise_error( RuntimeError, "too many loops for \"* * 1 * *\" #next_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJC9" ) end context '(defective et-orbi)' do before :each do class Fugit::Cron::TimeCursor alias inc _bad_inc end end after :each do class Fugit::Cron::TimeCursor alias inc _original_inc end end it 'breaks if its loop stalls' do c = Fugit::Cron.parse('* * 1 * *') expect { c.next_time }.to raise_error( RuntimeError, "too many loops for \"* * 1 * *\" #next_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJC9" ) end end context '(Chronic and ActiveSupport, gh-11)' do before :each do require_chronic class ::Time class << self def zone; @zone; end end end end after :each do unrequire_chronic class ::Time class << self undef_method :zone end end end it "doesn't stall or loop ad infinitum" do Time._zone = 'UTC' cron = Fugit.do_parse_cron('0 0 1 1 *') expect { cron.next_time }.not_to raise_error end end end describe '#match?' do NEXT_TIMES.each do |cron, next_time, _| it "succeeds #{cron.inspect} ? #{next_time.inspect}" do c = Fugit::Cron.parse(cron) ent = Time.parse(next_time) expect(c.match?(ent)).to be(true) end end context '"0 0 * * * Europe/Berlin" (gh-31)' do # in Changi before :each do @cron = Fugit::Cron.parse('0 0 * * * Europe/Berlin') end it "doesn't match midnight in London" do in_zone('Europe/London') do expect(@cron.match?(Time.new(2019, 1, 1)) ).to eq(false) end end it "matches midnight in Berlin" do in_zone('Europe/London') do expect(@cron.match?(Fugit.parse('2019-1-1 00:00:00 Europe/Berlin')) ).to eq(true) end end end end PREVIOUS_TIMES = [ [ '5 0 * * *', '2016-12-31 00:05:00', '2017-01-01' ], [ '5 0 * * *', '2017-01-13 00:05:00', '2017-01-14' ], [ '0 0 1 1 *', '2017-01-01 00:00:00', '2017-03-15' ], [ '0 12 1 1 *', '2016-01-01 12:00:00', '2017-01-01' ], [ '* * 29 * *', '2017-01-29 23:59:00', '2017-03-15' ], [ '* * 29 * *', '2016-02-29 23:59:00', '2016-03-15' ], [ '* * L * *', '2017-02-28 23:59:00', '2017-03-15' ], [ '* * L * *', '2016-02-29 23:59:00', '2016-03-15' ], [ '* * last * *', '2016-02-29 23:59:00', '2016-03-15' ], [ '* * -1 * *', '2016-02-29 23:59:00', '2016-03-15' ], [ '0 0 -4,-3 * *', '2017-02-26 00:00:00', '2017-03-15' ], [ '0 0 -4,-3 * *', '2017-02-25 00:00:00', '2017-02-25 23:00' ], [ '* * * * sun', '2017-01-29 23:59:00', '2017-01-31' ], [ '* * * * mon#2', '2017-01-09 23:59:00', '2017-01-31' ], [ '* * * * mon#-1', '2017-01-30 23:59:00', '2017-01-31' ], [ '* * * * wed#L', '2017-01-25 23:59:00', '2017-01-31' ], [ '* * * * wed#last', '2017-01-25 23:59:00', '2017-01-31' ], [ '* * * * mon#2,tue', '2017-01-24 23:59:00', '2017-01-30' ], [ '* * * * mon#2,wed', '2017-01-09 23:59:00', '2017-01-10' ], [ '30 04 1,15 * 5', '2017-01-15 04:30:00', '2017-01-16' ], [ '30 04 1,15 * 5', '2017-01-13 04:30:00', '2017-01-15' ], [ '00 24 * * *', '2017-01-02', '2017-01-02 12:00' ], [ '0 0 * * mon#2,tue', '2017-01-09', '2017-01-09 12:00' ], [ '0 0 * * mon#2,tue', '2017-01-03', '2017-01-04' ], [ '0 9 29 feb *', '2016-02-29 09:00', '2019-03-23' ], # gh-18 ] describe '#previous_time' do PREVIOUS_TIMES.each do |cron, previous_time, now| now = now ? Time.parse(now) : NOW it "succeeds #{cron.inspect} #{now} -> #{previous_time.inspect}" do c = Fugit::Cron.parse(cron) ept = Time.parse(previous_time) pt = c.previous_time(now) expect( Fugit.time_to_plain_s(pt, false) ).to eq( Fugit.time_to_plain_s(ept, false) ) expect(c.match?(ept)).to eq(true) # quick check end end it 'breaks if its loop takes too long' do c = Fugit::Cron.parse('* * 1 * *') c.instance_eval { @monthdays = [ 0 ] } # # forge an invalid cron expect { c.previous_time }.to raise_error( RuntimeError, "too many loops for \"* * 1 * *\" #previous_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJCQ" ) end it 'does not go into an endless loop over time == previous_time (gh-15)' do c = Fugit.parse('10 * * * * *') t = c.previous_time.to_f# + 0.123 #(some float x so that 0.0 <= x < 1.0) expect( c.previous_time(Time.at(t)).to_i ).to eq( t.to_i - 60 ) end context '(defective et-orbi)' do before :each do class Fugit::Cron::TimeCursor alias inc _bad_inc end end after :each do class Fugit::Cron::TimeCursor alias inc _original_inc end end it 'breaks if its loop stalls' do c = Fugit::Cron.parse('* * 1 * *') expect { c.previous_time }.to raise_error( RuntimeError, "too many loops for \"* * 1 * *\" #previous_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJCQ" ) end end end describe '#brute_frequency' do [ [ '* * * * *', 'dmin: 1m, dmax: 1m, ocs: 525600, spn: 52W1D, spnys: 1, yocs: 525600' ], [ '0 0 * * *', 'dmin: 1D, dmax: 1D, ocs: 365, spn: 52W1D, spnys: 1, yocs: 365' ], [ '0 0 * * sun', 'dmin: 1W, dmax: 1W, ocs: 53, spn: 53W, spnys: 1, yocs: 52' ], [ '0 0 1 1 *', 'dmin: 52W1D, dmax: 52W1D, ocs: 1, spn: 52W1D, spnys: 1, yocs: 1' ], [ '0 0 29 2 *', 'dmin: 208W5D, dmax: 208W5D, ocs: 1, spn: 208W5D, spnys: 4, yocs: 0' ] ].each do |cron, freq| it "computes #{freq.inspect} for #{cron.inspect}" do f = Fugit::Cron.parse(cron).brute_frequency.to_debug_s expect(f).to eq(freq) end end it 'accepts a year argument' do expect( Fugit::Cron.parse('0 0 * * sun').brute_frequency(2016).to_debug_s ).to eq( 'dmin: 1W, dmax: 1W, ocs: 52, spn: 52W, spnys: 0, yocs: 52' ) end end describe '#rough_frequency' do # (seconds 0-59) # minute 0-59 # hour 0-23 # day of month 1-31 # month 1-12 (or names, see below) # day of week 0-7 (0 or 7 is Sun, or use names) { '* * * * *' => '1m', '* * * * * *' => 1, '0 0 * * *' => '1d', '10,15 0 * * *' => '5m', '0 0 * * sun' => '7d', '0 0 1 1 *' => '1Y', '0 0 29 2 *' => '1Y', # ! rough frequency ! '0 0 28 2,3 *' => '1M', '0 0 28 2,4 *' => '2M', '*/15 * * * * *' => 15, '*/15 * * * *' => '15m', '5 0 * * *' => '1d', '15 14 1 * *' => '1M', '* * 29 * *' => '1m', '* * L * *' => '1m', '* * last * *' => '1m', '* * -1 * *' => '1m', '0 0 -4,-3 * *' => '1d', '* * * * sun' => '1m', '* * -2 * *' => '1m', '* * * * mon' => '1m', '* * * * * mon' => 1, '* * * * mon,tue' => '1m', '* * * * mon#2' => '1m', '* * * * mon#-1' => '1m', '* * * * tue#L' => '1m', '* * * * tue#last' => '1m', '* * * * mon#2,tue' => '1m', '0 0 * * mon' => '1W', '0 0 * * mon,tue' => '1d', '0 0 * * mon#2' => '1M', '0 0 * * mon#-1' => '1M', '0 0 * * tue#L' => '1M', '0 0 * * tue#last' => '1M', '0 0 * * mon#2,tue' => '1d', '00 24 * * *' => '1d', '30 04 1,15 * 5' => '3d', # rough '0 8 L * mon-thu' => '1d', # last day of month OR monday to thursday '0 9 -2 * *' => '1M', '0 0 -5 * *' => '1M', '0 8 L * *' => '1M', }.each do |cron, freq| it "gets #{cron.inspect} and outputs #{freq.inspect}" do f = freq.is_a?(String) ? Fugit.parse(freq).to_sec : freq rf = Fugit::Cron.parse(cron).rough_frequency #p Fugit::Duration.parse(rf).deflate.to_plain_s expect(rf).to eq(f) end end end describe '.parse' do it "returns the input immediately if it's a cron" do c = Fugit.parse('* * * * *'); expect(c.class).to eq(Fugit::Cron) c1 = Fugit::Cron.parse(c) expect(c1.class).to eq(Fugit::Cron) expect(c1.object_id).to eq(c.object_id) end it 'returns nil if it cannot parse' do expect(Fugit::Cron.parse(true)).to eq(nil) expect(Fugit::Cron.parse('nada')).to eq(nil) end it 'parses @reboot' context 'success' do [ [ '@yearly', '0 0 1 1 *' ], [ '@annually', '0 0 1 1 *' ], [ '@monthly', '0 0 1 * *' ], [ '@weekly', '0 0 * * 0' ], [ '@daily', '0 0 * * *' ], [ '@midnight', '0 0 * * *' ], [ '@hourly', '0 * * * *' ], # min hou dom mon dow [ '5 0 * * *', '5 0 * * *' ], # 5 minutes after midnight, every day [ '15 14 1 * *', '15 14 1 * *' ], # at 1415 on the 1st of every month [ '0 22 * * 1-5', '0 22 * * 1,2,3,4,5' ], # at 2200 on weekdays [ '0 22 * * 0', '0 22 * * 0' ], [ '0 22 * * 7', '0 22 * * 0' ], # at 2200 on sunday [ '0 23 * * 7-1', '0 23 * * 0,1' ], # at 2300 sunday to monday [ '0 23 * * 6-1', '0 23 * * 0,1,6' ], # at 2300 saturday to monday [ '23 0-23/2 * * *', '23 0,2,4,6,8,10,12,14,16,18,20,22 * * *' ], # 23 minutes after midnight, 0200, 0400, ... #[ '5 4 * * sun', :xxx ], # 0405 every sunday [ '14,24 8-12,14-19/2 * * *', '14,24 8,9,10,11,12,14,16,18 * * *' ], [ '24,14 14-19/2,8-12 * * *', '14,24 8,9,10,11,12,14,16,18 * * *' ], [ '*/1 1-3/1 * * *', '* 1,2,3 * * *' ], [ '0 22 * * 5-1', '0 22 * * 0,1,5,6' ], [ '0 9-17/2 * * *', '0 9,11,13,15,17 * * *' ], [ '0 */2 * * *', '0 0,2,4,6,8,10,12,14,16,18,20,22 * * *' ], [ '0 0 * * */2', '0 0 * * 0,2,4,6' ], [ '0 0 * * 1-5/2', '0 0 * * 1,3,5' ], [ '0 0 * * 3/2', '0 0 * * 3' ], [ '* * 1 * *', '* * 1 * *' ], # gh-10, double-check that 01 is a dom [ '* * 01 * *', '* * 1 * *' ], # [ '* * * 1 *', '* * * 1 *' ], # and that 01 is a month [ '* * * 01 *', '* * * 1 *' ], # [ '*/15 * * * *', '0,15,30,45 * * * *' ], # gh-19 [ '/15 * * * *', '0,15,30,45 * * * *' ], # [ '/15 * * * * *', '0,15,30,45 * * * * *' ], # [ '/15 /4 * * *', '0,15,30,45 0,4,8,12,16,20 * * *' ], # [ '0 18 * * fri-sun UTC', '0 18 * * 0,5,6 UTC' ], # gh-27 [ '0 19 * 7-8 0', '0 19 * 7,8 0' ], [ '0 19 * nov-dec 0', '0 19 * 11,12 0' ], [ '0 19 * 11-2 0', '0 19 * 1,2,11,12 0' ], [ '0 19 * nov-mar 0', '0 19 * 1,2,3,11,12 0' ], # gh-27 on month [ '10-15 7 * * *', '10,11,12,13,14,15 7 * * *' ], [ '55-5 7 * * *', '0,1,2,3,4,5,55,56,57,58,59 7 * * *' ], [ '10 18-20 * * *', '10 18,19,20 * * *' ], [ '10 23-04 * * *', '10 0,1,2,3,4,23 * * *' ], [ '0 23 10-15 * *', '0 23 10,11,12,13,14,15 * *' ], [ '0 23 30-3 * *', '0 23 1,2,3,30,31 * *' ], [ '0 23 1 10-12 *', '0 23 1 10,11,12 *' ], [ '0 23 1 11-2 *', '0 23 1 1,2,11,12 *' ], [ '0 23 * * fri-sun', '0 23 * * 0,5,6' ], [ '0 23 * * 5-0', '0 23 * * 0,5,6' ], [ '0 23 * * sat-mon', '0 23 * * 0,1,6' ], [ '0 23 * * 6-1', '0 23 * * 0,1,6' ], [ '10-15 0 23 * * *', '10,11,12,13,14,15 0 23 * * *' ], [ '58-2 0 23 * * *', '0,1,2,58,59 0 23 * * *' ], [ '* 0-24 * * *', "* #{(0..23).to_a.collect(&:to_s).join(',')} * * *" ], [ '* 22-24 * * *', '* 0,22,23 * * *' ], [ '* * * 1-13 *', nil ], # month 13 isn't allowed at parsing # # gh-30 ].each { |c, e| it "parses #{c}" do c = Fugit::Cron.parse(c) expect(c ? c.to_cron_s : c).to eq(e) end } context 'negative monthdays' do [ [ '* * -1 * *', '* * -1 * *' ], [ '* * -7--1 * *', '* * -7,-6,-5,-4,-3,-2,-1 * *' ], [ '* * -1--27 * *', '* * -31,-30,-29,-28,-27,-1 * *' ], [ '* * -7--1/2 * *', '* * -7,-5,-3,-1 * *' ], [ '* * L * *', '* * -1 * *' ], [ '* * -7-L * *', '* * -7,-6,-5,-4,-3,-2,-1 * *' ], [ '* * last * *', '* * -1 * *' ], ].each { |c, e| it("parses #{c}") { expect(Fugit::Cron.parse(c).to_cron_s).to eq(e) } } end context 'months' do [ [ '* * * jan-mar *', '* * * 1,2,3 *' ], [ '* * * Jan-Aug/2 *', '* * * 1,3,5,7 *' ], ].each { |c, e| it("parses #{c}") { expect(Fugit::Cron.parse(c).to_cron_s).to eq(e) } } end context 'weekdays' do [ [ '* * * * sun,mon', '* * * * 0,1' ], [ '* * * * Sun,mOn', '* * * * 0,1' ], [ '* * * * mon-wed', '* * * * 1,2,3' ], [ '* * * * sun,2-4', '* * * * 0,2,3,4' ], [ '* * * * sun,mon-tue', '* * * * 0,1,2' ], [ '* * * * sun,Sun,0,7', '* * * * 0' ], #a_e q '0 0 * * mon#1,tue', [[0], [0], [0], nil, nil, [2], ["1#1"]] ].each { |c, e| it("parses #{c}") { expect(Fugit::Cron.parse(c).to_cron_s).to eq(e) } } end context 'weekdays #' do [ [ '0 0 * * mon#1,tue', '0 0 * * 1#1,2' ], [ '0 0 * * mon#-1,tue', '0 0 * * 1#-1,2' ], [ '0 0 * * mon#L,tue', '0 0 * * 1#-1,2' ], [ '0 0 * * mon#last,tue', '0 0 * * 1#-1,2' ], ].each { |c, e| it("parses #{c}") { expect(Fugit::Cron.parse(c).to_cron_s).to eq(e) } } end context 'timezone' do ( ::TZInfo::Timezone.all.collect { |tz| [ "* * * * * #{tz.name}", tz.name ] } + [ [ '* * * * * +09:00', '+09:00' ], [ '* * * * * +0900', '+0900' ], ] ).each do |c, z| it "parses #{c}" do c = Fugit::Cron.parse(c) tz = EtOrbi.get_tzone(z) || fail("unknown tz #{z.inspect}") expect(c.class).to eq(Fugit::Cron) expect(c.zone).to eq(z) expect(c.timezone).to eq(tz) end end [ '* * * * * America/SaoPaulo', '* * * * * America/Los Angeles', '* * * * * Issy_Les_Moulineaux', ].each { |c| it "returns nil for #{c.inspect}" do expect(Fugit::Cron.parse(c)).to eq(nil) end } end end context 'failure' do [ # min hou dom mon dow '* 25 * * *', '* * -32 * *', '* * 0 * *', # gh-10, 0 is not a valid day of month '* * 00 * *', # '* * * 0 *', # and 0 is not a valid month '* * * 00 *', # ].each do |cron| it "returns nil for #{cron}" do expect(Fugit::Cron.parse(cron)).to eq(nil) end end end context 'impossible days' do { '* * 32 1 *' => nil, '* * 30 2 *' => nil, '* * 30,31 2 *' => nil, '* * 31 4 *' => nil, '* * 31 6 *' => nil, '* * 31 9 *' => nil, '* * 31 11 *' => nil, '* * 31 2,4 *' => nil, '* * 30,31 2,3 *' => [ [ 3 ], [ 30, 31 ] ], # not how February gets dropped '* * 30,31 4 *' => [ [ 4 ], [ 30 ] ], # not how the 31st gets dropped '* * 30,31 3,4 *' => [ [ 3, 4 ], [ 30, 31 ] ], '* * 31 3,4 *' => [ [ 3 ], [ 31 ] ], }.each do |cron, modays| if modays it "parses #{cron.inspect} months/monthdays to #{modays.inspect}" do c = Fugit::Cron.parse(cron) expect([ c.months, c.monthdays ]).to eq(modays) end else it "returns nil for #{cron.inspect}" do expect(Fugit::Cron.parse(cron)).to eq(nil) end end end end context 'weekdays' do { '* * * * sun#L' => [ [ 0, -1 ] ], '* * * * sun%2' => [ [ 0, [ 2, 0 ] ] ], '* * * * sun%2+1' => [ [ 0, [ 2, 1 ] ] ], }.each do |cron, weekdays| it "parses #{cron.inspect} weekdays to #{weekdays.inspect}" do c = Fugit::Cron.parse(cron) expect(c.weekdays).to eq(weekdays) end end end end describe '.do_parse' do [ # min hou dom mon dow '* 25 * * *', '* * -32 * *', '* * 0 * *', # gh-10, 0 is not a valid day of month '* * 00 * *', # '* * * 0 *', # and 0 is not a valid month '* * * 00 *', # ].each do |cron| it "raises for #{cron}" do expect { Fugit::Cron.do_parse(cron) }.to raise_error( ArgumentError, "invalid cron string #{cron.inspect}" ) end end end describe '#==' do it 'returns true when equal' do expect( Fugit::Cron.parse('* * * * *') == Fugit::Cron.parse('* * * * *') ).to eq(true) expect( Fugit::Cron.parse('* * * * *') == Fugit::Cron.parse('* * */1 * *') ).to eq(true) end it 'returns false else' do expect( Fugit::Cron.parse('* * * * *') == Fugit::Cron.parse('* * * * 1') ).to eq(false) expect( Fugit::Cron.parse('* * * * *') != Fugit::Cron.parse('* * * * 1') ).to eq(true) expect(Fugit::Cron.parse('* * * * *') == 1).to eq(false) end end describe '#to_cron_s' do [ [ '0 */3 * * 1,2', '0 0,3,6,9,12,15,18,21 * * 1,2' ], [ '0 5 * * 1,2,3,4,5', '0 5 * * 1,2,3,4,5' ], [ '0 5 * * 1-4,fri#3', '0 5 * * 1,2,3,4,5#3' ], [ '* * * * * America/Los_Angeles', '* * * * * America/Los_Angeles' ], #[ '0 */3 * * 1,2', '0 */3 * * 1-2' ], #[ '0 5 * * 1,2,3,4,5', '0 5 * * 1-5' ], #[ '0 5 * * 1,2,3,4,fri#3', '0 5 * * 1-4,5#3' ], ].each do |source, target| it "represents #{source.inspect} into #{target.inspect}" do sc = Fugit::Cron.parse(source) tc = Fugit::Cron.parse(target) expect(sc).to eq(tc) expect(sc.to_cron_s).to eq(target) end end it "produces the same cron if parsing again the to_cron_s" do c1 = Fugit::Cron.parse('* * * * * America/Los_Angeles') c2 = Fugit::Cron.parse(c1.to_cron_s) expect(c1).to eq(c2) end end describe '#seconds' do { '* * * * *' => [ 0 ], '5 * * * * *' => [ 5 ], '5,10 * * * * *' => [ 5, 10 ], '*/10 * * * * *' => [ 0, 10, 20, 30, 40, 50 ], }.each do |string, expected| it "returns #{expected.inspect} for #{string}" do expect(Fugit::Cron.parse(string).seconds).to eq(expected) end end end describe '#range (protected)' do { { min: 1, max: 12, sta: 2, edn: 4, sla: 1 } => [ 2, 3, 4 ], { min: 1, max: 12, sta: 2, edn: 4, sla: 2 } => [ 2, 4 ], { min: 1, max: 12, sta: 11, edn: 2, sla: 1 } => [ 11, 12, 1, 2 ], { min: 1, max: 12, sta: 11, edn: 2, sla: 2 } => [ 11, 1 ], { min: 1, max: 31, sta: -5, edn: -1, sla: 1 } => [ -5, -4, -3, -2, -1 ], { min: 1, max: 31, sta: -1, edn: -29, sla: 1 } => [ -1, -31, -30, -29 ], { min: 0, max: 23, sta: 0, edn: 24, sla: 1 } => (0..23).to_a, #{ min: 1, max: 12, sta: 0, edn: 12, sla: 1 } => ... #{ min: 1, max: 12, sta: 1, edn: 13, sla: 1 } => ... # month 13 no parse # # gh-30 }.each do |args, result| it "returns #{result.inspect} for #{args.inspect}" do as = [ :min, :max, :sta, :edn, :sla ].collect { |k| args[k] } expect(Fugit::Cron.allocate.send(:range, *as)).to eq(result) end end end end describe Fugit::Cron do context 'sec6' do [ [ '* 5 0 * * *', '* 5 0 * * *' ], ].each do |s0, s1| it "parses #{s0.inspect} and renders it as #{s1.inspect}" do c = Fugit::Cron.parse(s0) expect(c.to_cron_s).to eq(s1) end end [ [ '0 5 0 * * *', [ [ 0 ], [ 5 ], [ 0 ], nil, nil, nil ] ], [ '5 0 * * *', [ [ 0 ], [ 5 ], [ 0 ], nil, nil, nil ] ], [ '* 5 0 * * *', [ nil, [ 5 ], [ 0 ], nil, nil, nil ] ], ].each do |s, a| it "parses #{s.inspect} and stores it as #{a.inspect}" do c = Fugit::Cron.parse(s) expect(c.to_a).to eq(a) end end [ # c: cron, f: from, nt: next_time, pt: previous_time { c: '15 5 0 * * *', f: '2017-01-01', nt: '2017-01-01 00:05:15' }, { c: '15 5 0 * * *', f: '2017-01-01', pt: '2016-12-31 00:05:15' }, { c: '15,30 5 0 * * *', f: '2017-01-01', nt: '2017-01-01 00:05:15' }, { c: '15,30 5 0 * * *', f: '2017-01-01 00:05:15', nt: '2017-01-01 00:05:30' }, { c: '15,30 5 0 * * *', f: '2017-01-01 00:05:31', nt: '2017-01-02 00:05:15' }, { c: '15,30 5 0 * * *', f: '2017-01-01', pt: '2016-12-31 00:05:30' }, { c: '15,30 5 0 * * *', f: '2017-01-01 00:05:30', pt: '2017-01-01 00:05:15' }, ].each do |h| cron = h[:c] from = Time.parse(h[:f]) || Time.now nt = h[:nt] pt = h[:pt] it "computes the #{nt ? 'next' : 'previous'} time correctly for #{cron.inspect}" do c = Fugit::Cron.parse(cron) if nt expect(Fugit.time_to_plain_s(c.next_time(from), false)).to eq(nt) else expect(Fugit.time_to_plain_s(c.previous_time(from), false)).to eq(pt) end end end end end fugit-1.3.3/spec/duration_spec.rb000066400000000000000000000377071353173603400167750ustar00rootroot00000000000000 # # Specifying fugit # # Tue Jan 3 11:31:29 JST 2017 Ishinomaki # require 'spec_helper' describe Fugit::Duration do describe '.parse' do it 'returns nil when it cannot parse' do expect(Fugit::Duration.parse('NADA')).to eq(nil) end [ [ 0, '0s' ], [ 7, '7s' ], [ 0.3, '0.3s' ], [ 1000, '1000s' ], [ 1001.05, '1001.05s' ], ].each do |source, target| it "turns numeric #{source.inspect} into #{target.inspect}" do expect(Fugit::Duration.parse(source).to_plain_s).to eq(target) end end [ [ 0, 'PT0S' ], [ 1000, 'PT1000S' ], [ 1001.05, 'PT1001.05S' ], ].each do |source, target| it "turns numeric #{source.inspect} into ISO #{target.inspect}" do expect(Fugit::Duration.parse(source).to_iso_s).to eq(target) end end it "returns the input immediately if it's a duration" do d = Fugit::Duration.parse('1s'); expect(d.class).to eq(Fugit::Duration) d1 = Fugit::Duration.parse(d) expect(d1.class).to eq(Fugit::Duration) expect(d1.object_id).to eq(d.object_id) end it 'returns nil if it cannot parse' do expect(Fugit::Duration.parse(true)).to eq(nil) expect(Fugit::Duration.parse('nada')).to eq(nil) end DAY_S = 24 * 3600 [ [ '1y2M', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ '1M1y1M', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ '10d10h', '10D10h', '10d10h', 'P10DT10H', 10 * DAY_S + 10 * 3600 ], [ '100s', '100s', '100s', 'PT100S', 100 ], [ '-1y-2M', '-1Y2M', '-1y2M', 'P-1Y-2M', - 365 * DAY_S - 60 * DAY_S ], [ '1M-1y-1M', '-1Y', '-1y', 'P-1Y', - 365 * DAY_S ], [ '-1y+2M', '-1Y+2M', '-1y+2M', 'P-1Y2M', - 365 * DAY_S + 60 * DAY_S ], [ '1M+1y-1M', '1Y', '1y', 'P1Y', 365 * DAY_S ], [ '1y 2M', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ '1M 1y 1M', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ ' 1M1y1M ', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ '1 year and 2 months', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ '1 y, 2 M, and 2 months', '1Y4M', '1y4M', 'P1Y4M', 41904000 ], [ '1 y, 2 M and 2 m', '1Y2M2m', '1y2M2m', 'P1Y2MT2M', 36720120 ], [ 'P1Y2M', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ 'P1Y2M', '1Y2M', '1y2M', 'P1Y2M', 365 * DAY_S + 60 * DAY_S ], [ 'P10DT10H', '10D10h', '10d10h', 'P10DT10H', 10 * DAY_S + 10 * 3600 ], [ 'PT100S', '100s', '100s', 'PT100S', 100 ], [ 'P-1Y-2M', '-1Y2M', '-1y2M', 'P-1Y-2M', - 365 * DAY_S - 60 * DAY_S ], [ 'p1M-1y-1Mt-1M', '-1Y1m', '-1y1m', 'P-1YT-1M', -31536060 ], [ '1min', '1m', '1m', 'PT1M', 60 ], [ '1 min', '1m', '1m', 'PT1M', 60 ], [ '1m', '1m', '1m', 'PT1M', 60 ], [ '1 m', '1m', '1m', 'PT1M', 60 ], [ '1minute', '1m', '1m', 'PT1M', 60 ], [ '1 minute', '1m', '1m', 'PT1M', 60 ], [ '3mins', '3m', '3m', 'PT3M', 180 ], [ '3 mins', '3m', '3m', 'PT3M', 180 ], [ '3m', '3m', '3m', 'PT3M', 180 ], [ '3 m', '3m', '3m', 'PT3M', 180 ], [ '3minutes', '3m', '3m', 'PT3M', 180 ], [ '3 minutes', '3m', '3m', 'PT3M', 180 ], [ '3secs', '3s', '3s', 'PT3S', 3 ], [ '3 secs', '3s', '3s', 'PT3S', 3 ], [ '3s', '3s', '3s', 'PT3S', 3 ], [ '3 s', '3s', '3s', 'PT3S', 3 ], [ '3seconds', '3s', '3s', 'PT3S', 3 ], [ '3 seconds', '3s', '3s', 'PT3S', 3 ], [ '1.4s', '1.4s', '1.4s', 'PT1.4S', 1.4 ], [ 'PT1.5S', '1.5s', '1.5s', 'PT1.5S', 1.5 ], [ '.4s', '0.4s', '0.4s', 'PT0.4S', 0.4 ], [ 'PT.5S', '0.5s', '0.5s', 'PT0.5S', 0.5 ], [ '1.0d1.0w1.0d', '1.0W2.0D', '1.0w2.0d', 'P1.0W2.0D', 777_600.0 ], [ '-5.s', '-5.0s', '-5.0s', 'PT-5.0S', -5.0 ], [ '7d7', '7D7s', '7d7s', 'P7DT7S', 7 * 24 * 3600 + 7 ], [ '7', '7s', '7s', 'PT7S', 7 ], [ '0.3', '0.3s', '0.3s', 'PT0.3S', 0.3 ], [ '0.1s0.3', '0.4s', '0.4s', 'PT0.4S', 0.4 ], ].each do |source, target, rufus_target, iso_target, sec| it "parses #{source.inspect}" do d = Fugit::Duration.parse(source) expect(d.class).to eq(::Fugit::Duration) expect(d.to_plain_s).to eq(target) expect(d.to_rufus_s).to eq(rufus_target) expect(d.to_iso_s).to eq(iso_target) expect(d.to_sec).to eq(sec) end end it 'rejects lower case when ISO and :stricter' do expect( Fugit::Duration.parse('p1y', stricter: true) ).to eq(nil) end it 'rejects when :iso and not ISO' do expect( Fugit::Duration.parse('1y', iso: true) ).to eq(nil) end end describe '.do_parse' do it 'raises an ArgumentError when it cannot parse' do expect { Fugit::Duration.do_parse('NADA') }.to raise_error( ArgumentError, 'not a duration "NADA"' ) end end describe '#deflate' do [ %w[ 3600s 3600s 1h ], %w[ 1y3600s 1Y3600s 1Y1h ], %w[ 1d60s 86460s 1D1m ], %w[ 3d-3h 248400s 2D21h ], %w[ 0s 0s 0s ], %w[ 0.1s 0.1s 0.1s ], %w[ 1.1s 1.1s 1.1s ], [ 61.127, '61.127s', '1m1.127s' ], ].each do |source, step, target| it( "deflates #{source.inspect} via #{step.inspect} into #{target.inspect}" ) do d = Fugit::Duration.new(source) id = d.inflate expect(id.class).to eq(::Fugit::Duration) expect(id.to_plain_s).to eq(step) cd = d.deflate expect(cd.class).to eq(::Fugit::Duration) expect(cd.to_plain_s).to eq(target) end end context 'month: true' do [ [ '1M4W3s', { mon: 1, wee: 4, sec: 3 }, { mon: 1, wee: 4, sec: 3 } ], [ '5w3s', { wee: 5, sec: 3 }, { mon: 1, day: 5, sec: 3 } ], [ '40d', { day: 40 }, { mon: 1, wee: 1, day: 3 } ], [ 40 * 24 * 3600, { sec: 40 * 24 * 3600 }, { mon: 1, wee: 1, day: 3 } ], ].each do |src, h0, h1| it "returns a copy of the duration without its seconds (#{src})" do d = Fugit::Duration.parse(src) d1 = d.deflate(:month => true) expect(d.h).to eq(h0) expect(d1.h).to eq(h1) end end end context 'month: 30' do [ %w[ 3600s 1h ], [ "#{30 * 24 * 3600}s", '1M' ], [ "#{1 + 30 * 24 * 3600}s", '1M1s' ], ].each do |source, target| it "deflates #{source.inspect} into #{target.inspect}" do d = Fugit::Duration.new(source).deflate(month: 30) expect(d.to_plain_s).to eq(target) end end end context 'month: "29d"' do [ %w[ 3600s 1h ], [ "#{30 * 24 * 3600}s", '1M1D' ], [ "#{1 + 30 * 24 * 3600}s", '1M1D1s' ], ].each do |source, target| it "deflates #{source.inspect} into #{target.inspect}" do d = Fugit::Duration.new(source).deflate(month: '29d') expect(d.to_plain_s).to eq(target) end end end context 'year: 365' do [ %w[ 3600s 1h ], [ '366d', '1Y1D' ], [ '53w', '1Y6D' ], ].each do |source, target| it "deflates #{source.inspect} into #{target.inspect}" do d = Fugit::Duration.new(source).deflate(year: 365) expect(d.to_plain_s).to eq(target) end end end context 'year: "52w"' do [ %w[ 3600s 1h ], [ '366d', '1Y2D' ], [ '53w', '1Y1W' ], ].each do |source, target| it "deflates #{source.inspect} into #{target.inspect}" do d = Fugit::Duration.new(source).deflate(year: '52w') expect(d.to_plain_s).to eq(target) end end end end describe '#opposite' do it 'returns the additive inverse' do d = Fugit::Duration.new('1y2m-3h') od = d.opposite expect(od.to_plain_s).to eq('-1Y+3h-2m') expect(od.to_iso_s).to eq('P-1YT3H-2M') end end describe '#-@' do it 'returns the additive inverse' do d = Fugit::Duration.new('1y2m-3h') od = - d expect(od.to_plain_s).to eq('-1Y+3h-2m') expect(od.to_iso_s).to eq('P-1YT3H-2M') end end describe '#add' do it 'adds Numeric instances' do d = Fugit.parse('1Y2h') expect(d.add(1).to_plain_s).to eq('1Y2h1s') expect((d + 1).to_plain_s).to eq('1Y2h1s') end it 'adds Duration instances' do d0 = Fugit.parse('1Y2h') d1 = Fugit.parse('1Y2h1s') expect(d0.add(d1).to_plain_s).to eq('2Y4h1s') expect((d0 + d1).to_plain_s).to eq('2Y4h1s') end it 'adds String instances (parses them as Duration)' do d = Fugit.parse('1Y2h') s = '1Y-1h1s' expect(d.add(s).to_plain_s).to eq('2Y1h-1s') expect((d + s).to_plain_s).to eq('2Y1h-1s') end it 'yields a Time instance when adding a Time instance' do d = Fugit.parse('1Y1m17s') t = Time.parse('2017-01-03 17:02:00') t1 = d.add(t) expect(Fugit.time_to_plain_s(t1, false)).to eq('2018-01-03 17:03:17') t1 = d + t expect(Fugit.time_to_plain_s(t1, false)).to eq('2018-01-03 17:03:17') end [ [ '1Y1m17s', '2016-12-30 17:00:00', '2017-12-30 17:01:17' ], [ '1Y1M17s', '2016-12-30 17:00:00', '2018-01-30 17:00:17' ], [ '1M', '2016-02-02', '2016-03-02' ], ].each do |d, t, tt| it "adding #{t.inspect} to #{d.inspect} yields #{tt.inspect}" do d = Fugit.parse(d) t = Fugit.parse(t) t1 = d.add(t) expect( Fugit.time_to_plain_s(t1, false) ).to eq( Fugit.time_to_plain_s(Time.parse(tt), false) ) end end it 'fails else' do d = Fugit.parse('1Y2h') x = false expect { d.add(x) }.to raise_error( ArgumentError, 'cannot add FalseClass instance to a Fugit::Duration' ) expect { d + x }.to raise_error( ArgumentError, 'cannot add FalseClass instance to a Fugit::Duration' ) end it 'preserves the zone of an EoTime instance (local)' do t = ::EtOrbi::EoTime.now t1 = Fugit.parse('1Y2M3m') + t expect(t1.zone).to eq(t.zone) end it 'preserves the zone of an EoTime instance (UTC)' do t = ::EtOrbi::EoTime.parse('2017-06-22 00:00:00 UTC') t1 = Fugit.parse('1Y2M3m') + t expect(t1.zone.canonical_identifier).to eq('UTC') end end describe '#subtract' do it 'subtracts Numeric instances' do d = Fugit.parse('1Y2h') expect(d.add(-1).to_plain_s).to eq('1Y2h-1s') expect((d + -1).to_plain_s).to eq('1Y2h-1s') expect((d - 1).to_plain_s).to eq('1Y2h-1s') expect((d - 1).deflate.to_plain_s).to eq('1Y1h59m59s') end it 'subtracts Duration instances' do d0 = Fugit.parse('1Y2h') d1 = Fugit.parse('1Y2h1s') expect(d0.subtract(d1).to_plain_s).to eq('-1s') expect((d0 + -d1).to_plain_s).to eq('-1s') expect((d0 - d1).to_plain_s).to eq('-1s') end it 'subtracts String instances (parses them as Duration)' do d = Fugit.parse('1Y2h') s = '1Y-1h1s' expect(d.subtract(s).to_plain_s).to eq('3h1s') expect((d - s).to_plain_s).to eq('3h1s') end it 'fails else' do d = Fugit.parse('1Y2h') x = false expect { d.subtract(x) }.to raise_error( ArgumentError, 'cannot subtract FalseClass instance to a Fugit::Duration' ) expect { d - x }.to raise_error( ArgumentError, 'cannot subtract FalseClass instance to a Fugit::Duration' ) end end describe '#==' do it 'returns true when equal' do expect( Fugit::Duration.new('1Y2m') == Fugit::Duration.new('1Y2m') ).to eq(true) expect( Fugit::Duration.new('1Y2m') == Fugit::Duration.new('2m1Y') ).to eq(true) end it 'returns false else' do expect( Fugit::Duration.new('1Y2m') == Fugit::Duration.new('1Y3m') ).to eq(false) expect( Fugit::Duration.new('1Y2m') != Fugit::Duration.new('1Y3m') ).to eq(true) expect( Fugit::Duration.new('1Y2m') == 1 ).to eq(false) expect( Fugit::Duration.new('1Y2m') != 1 ).to eq(true) end end describe '#to_long_s' do [ [ '1M1Y1M3h', '2 months, 1 year, and 3 hours' ], [ '1Y1M3h', '1 year, 1 month, and 3 hours' ], ].each do |duration, long| it "renders #{duration.inspect} as #{long.inspect}" do expect(Fugit::Duration.parse(duration).to_long_s).to eq(long) end end it 'understands the oxford: false option' do expect( Fugit::Duration.parse('1Y1M3h').to_long_s(oxford: false) ).to eq( '1 year, 1 month and 3 hours' ) end end describe '#to_rufus_h' do [ [ '1y2M', { :y => 1, :M => 2 } ], [ '1M1y1M', { :M => 2, :y => 1 } ], [ '10d10h', { :d => 10, :h => 10 } ], [ '100s', { :s => 100 } ], [ '-1y-2M', { :y => -1, :M => -2 } ], [ '1M-1y-1M', { :y => -1 } ], [ '-1y+2M', { :y => -1, :M => 2 } ], [ '1M+1y-1M', { :y => 1 } ], [ '1y 2M', { :y => 1, :M => 2 } ], [ '1M 1y 1M', { :y => 1, :M => 2 } ], [ ' 1M1y1M ', { :y => 1, :M => 2 } ], [ '1 year and 2 months', { :y => 1, :M => 2 } ], [ '1 y, 2 M, and 2 months', { :y => 1, :M => 4 } ], [ '1 y, 2 M and 2 m', { :y => 1, :M => 2, :m => 2 } ], [ 'P1Y2M', { :y => 1, :M => 2} ], [ 'P10DT10H', { :d => 10, :h => 10 } ], [ 'PT100S', { :s => 100 } ], [ 'P-1Y-2M', { :y => -1, :M => -2 } ], [ 'p1M-1y-1Mt-1M', { :y => -1, :m => -1 } ], [ '1.4s', { :s => 1.4 } ], [ 'PT1.5S', { :s => 1.5 } ], [ '.4s', { :s => 0.4 } ], [ 'PT.5S', { :s => 0.5 } ], [ '1.0d1.0w1.0d', { :w => 1.0, :d => 2.0 } ], [ '-5.s', { :s => -5.0 } ], [ '7d7', { :d => 7, :s => 7 } ], [ '7', { :s => 7 } ], [ '0.3', { :s => 0.3 } ], [ '0.1s0.3', { :s => 0.4 } ], ].each do |d, h| it "renders #{d.inspect} as #{h.inspect}" do expect(Fugit::Duration.parse(d).to_rufus_h).to eq(h) end end end describe '#next_time' do it 'returns now + this duration if no argument' do d = Fugit::Duration.new('1Y') t = d.next_time expect(t.class).to eq(::EtOrbi::EoTime) expect( t.strftime('%Y-%m-%d') ).to eq( "#{Time.now.year + 1}-#{Time.now.strftime('%m-%d')}" ) end it 'returns arg + this duration' do d = Fugit::Duration.new('1Y') t = d.next_time(Time.parse('2016-12-31')) expect(t.class).to eq(::EtOrbi::EoTime) expect(Fugit.time_to_plain_s(t, false)).to eq('2017-12-31 00:00:00') end end describe '#drop_seconds' do [ [ '1M10s', { mon: 1, sec: 10 }, { mon: 1 } ], [ '1M', { mon: 1 }, { mon: 1 } ], [ 0, { sec: 0 }, { min: 0 } ], ].each do |src, h0, h1| it "returns a copy of the duration without its seconds (#{src})" do d = Fugit::Duration.parse(src) d1 = d.drop_seconds expect(d.h).to eq(h0) expect(d1.h).to eq(h1) end end end describe '.to_plain_s(o)' do it 'works' do expect(Fugit::Duration.to_plain_s(1000)).to eq('16m40s') expect(Fugit::Duration.to_plain_s('100d')).to eq('14W2D') end end describe '.to_iso_s(o)' do it 'works' do expect(Fugit::Duration.to_iso_s(1000)).to eq('PT16M40S') expect(Fugit::Duration.to_iso_s('100d')).to eq('P14W2D') expect(Fugit::Duration.to_iso_s('77d88s')).to eq('P11WT1M28S') end it 'may fail with an ArgumentError' do expect { Fugit::Duration.to_iso_s('77d88k') }.to raise_error(ArgumentError, 'not a duration "77d88k"') end end describe '.to_long_s(o)' do it 'works' do expect( Fugit::Duration.to_long_s(1000)).to eq('16 minutes, and 40 seconds') expect( Fugit::Duration.to_long_s('100d')).to eq('14 weeks, and 2 days') end end end fugit-1.3.3/spec/nat_spec.rb000066400000000000000000000120501353173603400157120ustar00rootroot00000000000000 # # Specifying fugit # # Wed Jan 4 07:23:09 JST 2017 Ishinomaki # require 'spec_helper' describe Fugit::Nat do describe '.parse' do context '(simple crons)' do { 'every day at five' => '0 5 * * *', 'every weekday at five' => '0 5 * * 1-5', 'every weekday at five pm' => '0 17 * * 1-5', 'every day at 5 pm' => '0 17 * * *', 'every tuesday at 5 pm' => '0 17 * * 2', 'every wed at 5 pm' => '0 17 * * 3', 'every day at 16:30' => '30 16 * * *', 'every day at noon' => '0 12 * * *', 'every day at midnight' => '0 0 * * *', 'every day at 5 pm on America/Bogota' => '0 17 * * * America/Bogota', 'every day at 5 pm in Asia/Tokyo' => '0 17 * * * Asia/Tokyo', 'every day at 5 pm in Etc/GMT-11' => '0 17 * * * Etc/GMT-11', 'every day at 5 pm in Etc/GMT+5' => '0 17 * * * Etc/GMT+5', 'every 3h' => '0 */3 * * *', 'every 3 hours' => '0 */3 * * *', 'every 4M' => '0 0 1 */4 *', 'every 4 months' => '0 0 1 */4 *', 'every 5m' => '*/5 * * * *', 'every 5 min' => '*/5 * * * *', 'every 5 minutes' => '*/5 * * * *', 'every 15s' => '*/15 * * * * *', 'every 15 sec' => '*/15 * * * * *', 'every 15 seconds' => '*/15 * * * * *', 'every 1 h' => '0 * * * *', 'every 1 hour' => '0 * * * *', 'every 1 month' => '0 0 1 * *', 'every 1 second' => '* * * * * *', #'every 1st of the month at midnight' => '', #'at 5 after 4, everyday' => '', 'every day at 6pm and 8pm' => '0 18,20 * * *', 'every day at 6pm and 8pm UTC' => '0 18,20 * * * UTC', 'every day at 18:00 and 20:00' => '0 18,20 * * *', 'every day at 18:00 and 20:00 UTC' => '0 18,20 * * * UTC', # # gh-24 #'every day at 18:15 and 20:45' => '* * * * *', # # gh-24 see below 'every tuesday and monday at 5pm' => '0 17 * * 1,2', 'every wed or Monday at 5pm and 11' => '0 11,17 * * 1,3', 'every Mon,Tue,Wed,Thu,Fri at 18:00' => '0 18 * * 1,2,3,4,5', 'every Mon, Tue, and Wed at 18:15' => '15 18 * * 1,2,3', 'every Mon to Thu at 18:20' => '20 18 * * mon-thu', 'every Mon to Thu, 18:20' => '20 18 * * mon-thu', 'every mon-thu at 18:20' => '20 18 * * mon-thu', 'every Monday to Thursday at 18:20' => '20 18 * * mon-thu', 'every Monday through Friday at 19:20' => '20 19 * * mon-fri', 'from Monday through Friday at 19:21' => '21 19 * * mon-fri', 'from Monday to Friday at 19:22' => '22 19 * * mon-fri', # # gh-25 'every day at 18:00 and 18:15' => '0,15 18 * * *', 'every day at 18:00, 18:15' => '0,15 18 * * *', 'every day at 18:00, 18:15, 20:00, and 20:15' => '0,15 18,20 * * *', # # gh-29 }.each do |nat, cron| it "parses #{nat.inspect} into #{cron.inspect}" do c = Fugit::Nat.parse(nat) expect(c.class).to eq(Fugit::Cron) expect(c.original).to eq(cron) #expect(c.to_cron_s).to eq(cron) end end end it 'parses "every Fri-Sun at 18:00 UTC" (gh-27)' do c = Fugit::Nat.parse('every Fri-Sun at 18:00 UTC') expect(c.original).to eq('0 18 * * fri-sun UTC') expect(c.weekdays).to eq([ [ 0 ], [ 5 ], [ 6 ] ]) end context 'multi:' do { # mostly for gh-24 and `multi: true` [ 'every day at 18:15 and 20:45', {} ] => '15 18 * * *', [ 'every day at 18:15 and 20:45', { multi: true } ] => [ '15 18 * * *', '45 20 * * *' ], [ 'every day at 18:15', { multi: true } ] => [ '15 18 * * *' ], [ 'every day at 18:15 and 20:45', { multi: :fail } ] => [ ArgumentError, /\Amultiple crons in / ], [ 'every 1 hour', { multi: :fail } ] => # gh-28 '0 * * * *', }.each do |(nat, opts), result| if ( result.is_a?(Array) && result[0].is_a?(Class) && result[0].ancestors.include?(Exception) ) then it "fails for #{nat.inspect} (#{opts.inspect})" do expect { Fugit::Nat.parse(nat, opts) }.to raise_error(*result) end else it "parses #{nat.inspect} (#{opts.inspect}) into #{result.inspect}" do r = Fugit::Nat.parse(nat, opts) if opts[:multi] == true expect(r.collect(&:class).uniq).to eq([ Fugit::Cron ]) expect(r.collect(&:original)).to eq(result) else expect(r.class).to eq(Fugit::Cron) expect(r.original).to eq(result) end end end end end it 'returns nil if it cannot parse' do expect(Fugit::Nat.parse(true)).to eq(nil) expect(Fugit::Nat.parse('nada')).to eq(nil) end end describe '.do_parse' do it 'fails if it cannot parse' do expect { Fugit::Nat.do_parse(true) }.to raise_error(ArgumentError) expect { Fugit::Nat.do_parse('nada') }.to raise_error(ArgumentError) end end end fugit-1.3.3/spec/parse_spec.rb000066400000000000000000000061601353173603400162470ustar00rootroot00000000000000 # # Specifying fugit # # Tue Jan 3 11:19:52 JST 2017 Ishinomaki # require 'spec_helper' describe Fugit do describe '.parse' do it 'parses time points' do t = Fugit.parse('2017-01-03 11:21:17') expect(t.class).to eq(::EtOrbi::EoTime) expect(Fugit.time_to_plain_s(t, false)).to eq('2017-01-03 11:21:17') end it 'parses cron strings' do c = Fugit.parse('00 00 L 5 *') expect(c.class).to eq(Fugit::Cron) expect(c.to_cron_s).to eq('0 0 -1 5 *') end it 'parses durations' do d = Fugit.parse('1Y3M2d') expect(d.class).to eq(Fugit::Duration) expect(d.to_plain_s).to eq('1Y3M2D') end it 'parses durations' do d = Fugit.parse('1Y2h') expect(d.class).to eq(Fugit::Duration) expect(d.to_plain_s).to eq('1Y2h') end [ [ '0 0 1 jan *', ::Fugit::Cron ], [ '12y12M', ::Fugit::Duration ], [ '2017-12-12', ::EtOrbi::EoTime ], #[ 'every day at noon', ::Fugit::Cron ], ].each do |str, kla| it "parses #{str.inspect} into a #{kla} instance" do r = Fugit.parse(str) expect(r.class).to eq(kla) end end it 'parses "nats"' do c = Fugit.parse('every day at noon') expect(c.class).to eq(Fugit::Cron) expect(c.to_cron_s).to eq('0 12 * * *') end it 'disables nat parsing when nat: false' do x = Fugit.parse('* * * * 1', nat: false) expect(x.class).to eq(Fugit::Cron) x = Fugit.parse('every day at noon', nat: false) expect(x).to eq(nil) x = Fugit.parse('* * * * 1', cron: false) expect(x).to eq(nil) x = Fugit.parse('every day at noon', cron: false) expect(x.class).to eq(Fugit::Cron) end it 'returns nil when it cannot parse' do expect(Fugit.parse(true)).to eq(nil) expect(Fugit.parse('I have a pen, I have an apple, pen apple')).to eq(nil) end [ 'every 5 minutes', 'every 15 minutes', 'every 30 minutes', 'every 40 minutes', ].each do |s| it "uses #parse_nat for #{s.inspect}" do o = Fugit.parse(s) n = Fugit.parse_nat(s) expect(o).to eq(n) end end end describe '.do_parse' do it 'parses' do c = Fugit.do_parse('every day at midnight') expect(c.class).to eq(Fugit::Cron) expect(c.to_cron_s).to eq('0 0 * * *') end it 'fails when it cannot parse' do expect { Fugit.do_parse('I have a pen, I have an apple, pineapple!') }.to raise_error( ArgumentError, 'found no time information in ' + '"I have a pen, I have an apple, pineapple!"' ) end end describe '.determine_type' do it 'returns nil if it cannot determine' do expect(Fugit.determine_type('nada')).to eq(nil) expect(Fugit.determine_type(true)).to eq(nil) end it 'returns the right type' do expect(Fugit.determine_type('* * * * *')).to eq('cron') expect(Fugit.determine_type('* * * * * *')).to eq('cron') expect(Fugit.determine_type('1s')).to eq('in') expect(Fugit.determine_type('2017-01-01')).to eq('at') end end end fugit-1.3.3/spec/spec_helper.rb000066400000000000000000000021571353173603400164160ustar00rootroot00000000000000 # # Specifying fugit # # Sun Jan 1 12:09:21 JST 2017 Ishinomaki # require 'pp' require 'ostruct' require 'chronic' ::Khronic = ::Chronic Object.send(:remove_const, :Chronic) require 'fugit' module Helpers def jruby?; !! RUBY_PLATFORM.match(/java/); end def windows?; Gem.win_platform?; end def in_zone(zone_name, &block) prev_tz = ENV['TZ'] ENV['TZ'] = zone_name if zone_name block.call ensure ENV['TZ'] = prev_tz end def require_chronic Object.const_set(:Chronic, Khronic) end def unrequire_chronic Object.send(:remove_const, :Chronic) end end # Helpers RSpec.configure do |c| c.alias_example_to(:they) c.alias_example_to(:so) c.include(Helpers) end # A _bad_inc that doesn't progress, to test #next_time and # #previous_time loop breakers... # class Fugit::Cron::TimeCursor def _bad_inc(i) @t = @t + 0 self end alias _original_inc inc end # Simulating ActiveSupport Time.zone # class Time class << self attr_accessor :_zone def _zone=(name) @zone = OpenStruct.new(tzinfo: ::TZInfo::Timezone.get(name)) end end end fugit-1.3.3/tst/000077500000000000000000000000001353173603400134535ustar00rootroot00000000000000fugit-1.3.3/tst/README.md000066400000000000000000000000401353173603400147240ustar00rootroot00000000000000 ## tst/ Various experiments. fugit-1.3.3/tst/iteration_count.rb000066400000000000000000000022401353173603400172040ustar00rootroot00000000000000 require 'fugit' # For gh-15 # # Conjuring up "worst-case" crons and determining how many iteration # to compute #previous_time / #next_time to get an idea # for a max iteration count that is minimal and does not prevent # computing the worst-case crons. # min hou dom mon dow # sec min hou dom mon dow c = Fugit.parse('0 9 29 feb *') p c.next_time(Time.parse('2016-03-01')).iso8601 # 167 iterations are necessary c = Fugit.parse('*/10 0 9 29 feb *') p c.next_time(Time.parse('2016-03-01')).iso8601 # 167 iterations are necessary #c = Fugit.parse('0 9 29 feb sun') #c.next_time # # is tempting, but # # > 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. # # it's thus no "next time the 29th of February falls on a Sunday", # it's "next 29th of February or next Sunday of February" # 167 iterations? Let's put the breaker at 1024 :-) fugit-1.3.3/tst/modulo_cweek_dead_mondays.rb000066400000000000000000000074671353173603400212020ustar00rootroot00000000000000 # https://github.com/jmettraux/rufus-scheduler/issues/259 # https://github.com/floraison/fugit/issues/1 # Are there transitions for which the canonical week goes from odd to odd # (or even even to even)? require 'time' require 'date' (1970..2050).each do |y| (25..31).each do |md| t0 = Time.parse("#{y}-12-#{md}") t1 = t0 + 7 * 24 * 3600 st0 = t0.strftime('%F') st1 = t1.strftime('%F') d0 = Date.parse(st0) d1 = Date.parse(st1) next unless d0.cweek % 2 == d1.cweek % 2 p [ st0, t0.strftime('%a'), d0.cweek, st1, t1.strftime('%a'), d1.cweek ] end end # result: # # ["1970-12-28", "Mon", 53, "1971-01-04", "Mon", 1] # ["1970-12-29", "Tue", 53, "1971-01-05", "Tue", 1] # ["1970-12-30", "Wed", 53, "1971-01-06", "Wed", 1] # ["1970-12-31", "Thu", 53, "1971-01-07", "Thu", 1] # ["1976-12-27", "Mon", 53, "1977-01-03", "Mon", 1] # ["1976-12-28", "Tue", 53, "1977-01-04", "Tue", 1] # ["1976-12-29", "Wed", 53, "1977-01-05", "Wed", 1] # ["1976-12-30", "Thu", 53, "1977-01-06", "Thu", 1] # ["1976-12-31", "Fri", 53, "1977-01-07", "Fri", 1] # ["1981-12-28", "Mon", 53, "1982-01-04", "Mon", 1] # ["1981-12-29", "Tue", 53, "1982-01-05", "Tue", 1] # ["1981-12-30", "Wed", 53, "1982-01-06", "Wed", 1] # ["1981-12-31", "Thu", 53, "1982-01-07", "Thu", 1] # ["1987-12-28", "Mon", 53, "1988-01-04", "Mon", 1] # ["1987-12-29", "Tue", 53, "1988-01-05", "Tue", 1] # ["1987-12-30", "Wed", 53, "1988-01-06", "Wed", 1] # ["1987-12-31", "Thu", 53, "1988-01-07", "Thu", 1] # ["1992-12-28", "Mon", 53, "1993-01-04", "Mon", 1] # ["1992-12-29", "Tue", 53, "1993-01-05", "Tue", 1] # ["1992-12-30", "Wed", 53, "1993-01-06", "Wed", 1] # ["1992-12-31", "Thu", 53, "1993-01-07", "Thu", 1] # ["1998-12-28", "Mon", 53, "1999-01-04", "Mon", 1] # ["1998-12-29", "Tue", 53, "1999-01-05", "Tue", 1] # ["1998-12-30", "Wed", 53, "1999-01-06", "Wed", 1] # ["1998-12-31", "Thu", 53, "1999-01-07", "Thu", 1] # ["2004-12-27", "Mon", 53, "2005-01-03", "Mon", 1] # ["2004-12-28", "Tue", 53, "2005-01-04", "Tue", 1] # ["2004-12-29", "Wed", 53, "2005-01-05", "Wed", 1] # ["2004-12-30", "Thu", 53, "2005-01-06", "Thu", 1] # ["2004-12-31", "Fri", 53, "2005-01-07", "Fri", 1] # ["2009-12-28", "Mon", 53, "2010-01-04", "Mon", 1] # ["2009-12-29", "Tue", 53, "2010-01-05", "Tue", 1] # ["2009-12-30", "Wed", 53, "2010-01-06", "Wed", 1] # ["2009-12-31", "Thu", 53, "2010-01-07", "Thu", 1] # ["2015-12-28", "Mon", 53, "2016-01-04", "Mon", 1] # ["2015-12-29", "Tue", 53, "2016-01-05", "Tue", 1] # ["2015-12-30", "Wed", 53, "2016-01-06", "Wed", 1] # ["2015-12-31", "Thu", 53, "2016-01-07", "Thu", 1] # ["2020-12-28", "Mon", 53, "2021-01-04", "Mon", 1] # ["2020-12-29", "Tue", 53, "2021-01-05", "Tue", 1] # ["2020-12-30", "Wed", 53, "2021-01-06", "Wed", 1] # ["2020-12-31", "Thu", 53, "2021-01-07", "Thu", 1] # ["2026-12-28", "Mon", 53, "2027-01-04", "Mon", 1] # ["2026-12-29", "Tue", 53, "2027-01-05", "Tue", 1] # ["2026-12-30", "Wed", 53, "2027-01-06", "Wed", 1] # ["2026-12-31", "Thu", 53, "2027-01-07", "Thu", 1] # ["2032-12-27", "Mon", 53, "2033-01-03", "Mon", 1] # ["2032-12-28", "Tue", 53, "2033-01-04", "Tue", 1] # ["2032-12-29", "Wed", 53, "2033-01-05", "Wed", 1] # ["2032-12-30", "Thu", 53, "2033-01-06", "Thu", 1] # ["2032-12-31", "Fri", 53, "2033-01-07", "Fri", 1] # ["2037-12-28", "Mon", 53, "2038-01-04", "Mon", 1] # ["2037-12-29", "Tue", 53, "2038-01-05", "Tue", 1] # ["2037-12-30", "Wed", 53, "2038-01-06", "Wed", 1] # ["2037-12-31", "Thu", 53, "2038-01-07", "Thu", 1] # ["2043-12-28", "Mon", 53, "2044-01-04", "Mon", 1] # ["2043-12-29", "Tue", 53, "2044-01-05", "Tue", 1] # ["2043-12-30", "Wed", 53, "2044-01-06", "Wed", 1] # ["2043-12-31", "Thu", 53, "2044-01-07", "Thu", 1] # ["2048-12-28", "Mon", 53, "2049-01-04", "Mon", 1] # ["2048-12-29", "Tue", 53, "2049-01-05", "Tue", 1] # ["2048-12-30", "Wed", 53, "2049-01-06", "Wed", 1] # ["2048-12-31", "Thu", 53, "2049-01-07", "Thu", 1] # Yes.