pax_global_header00006660000000000000000000000064143623630260014517gustar00rootroot0000000000000052 comment=df1e6ada77abe47ede53434899f2473e504b7c4d fugit-1.8.1/000077500000000000000000000000001436236302600126445ustar00rootroot00000000000000fugit-1.8.1/.github/000077500000000000000000000000001436236302600142045ustar00rootroot00000000000000fugit-1.8.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001436236302600163675ustar00rootroot00000000000000fugit-1.8.1/.github/ISSUE_TEMPLATE/issue-report.md000066400000000000000000000046551436236302600213640ustar00rootroot00000000000000--- 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 -e "p [ :env_tz, ENV['TZ'] ]" bundle exec ruby -r et-orbi -e "EtOrbi._make_info" bundle exec ruby -r fugit -e "p [ :fugit, Fugit::VERSION ]" bundle exec ruby -e "p [ :now, Time.now, :zone, Time.now.zone ]" ``` (It's supposed to look like ``` Darwin pollux.local 17.7.0 Darwin Kernel Version 17.7.0: Thu Dec 20 21:47:19 PST 2018; root:xnu-4570.71.22~1/RELEASE_X86_64 x86_64 ruby 2.3.7p456 (2018-03-28 revision 63024) [x86_64-darwin17] [:env_tz, nil] (secs:1553304485.185308,utc~:"2019-03-23 01:28:05.18530797958374023",ltz~:"JST") (etz:nil,tnz:"JST",tziv:"2.0.0",tzidv:"1.2018.9",rv:"2.3.7",rp:"x86_64-darwin17",win:false, rorv:nil,astz:nil,eov:"1.1.7",eotnz:#,eotnfz:"+0900", eotlzn:"Asia/Tokyo",eotnfZ:"JST",debian:nil,centos:nil,osx:"zoneinfo/Asia/Tokyo") "1.3.9" ``` ) ## Additional context Add any other context about the problem here. fugit-1.8.1/.github/dependabot.yml000066400000000000000000000001661436236302600170370ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" fugit-1.8.1/.github/workflows/000077500000000000000000000000001436236302600162415ustar00rootroot00000000000000fugit-1.8.1/.github/workflows/test.yaml000066400000000000000000000015211436236302600201030ustar00rootroot00000000000000 name: test on: [ push, pull_request ] jobs: test: if: " ! (contains(github.event.head_commit.message, 'skip ci') || contains(github.event.head_commit.message, 'ci skip'))" name: ${{matrix.ruby}} on ${{matrix.os}} strategy: matrix: os: [ ubuntu-latest ] ruby: [ '2.3', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', 'jruby-9.2', 'jruby-9.3', 'truffleruby-22.3' ] experimental: [ false ] fail-fast: false runs-on: ${{matrix.os}} continue-on-error: ${{matrix.experimental}} steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - run: ruby --version - run: gem --version - run: bundle --version - run: bundle exec rspec --force-color --format documentation fugit-1.8.1/.gitignore000066400000000000000000000002321436236302600146310ustar00rootroot00000000000000 *.swp .vimrc .viminfo .vimgrep .vimspec .vimmarks pkg/ .ruby-version #.rspec .errors .rspec.out .bxsinfo.yaml .bxsenvs.yaml .todo.md Gemfile.lock fugit-1.8.1/.rspec000066400000000000000000000000401436236302600137530ustar00rootroot00000000000000--colour --format documentation fugit-1.8.1/CHANGELOG.md000066400000000000000000000147251436236302600144660ustar00rootroot00000000000000 # CHANGELOG.md ## fugit 1.8.1 released 2023-01-20 * Fix for month subtraction, gh-84, @mreinsch * Fix duration - time, gh-85, @mreinsch ## fugit 1.8.0 released 2022-12-06 * Introduce Fugit.parse_cronish and .do_parse_cronish, gh-70 ## fugit 1.7.2 released 2022-11-03 * Fix 'every day at 12:15 am', gh-81 * Fix 'every day at 5:00pm', gh-81 ## fugit 1.7.1 released 2022-09-21 * Change behaviour for "0 0/5 * * *", gh-79 go "every 5h start hour 0", previous behaviour only triggered at hour 0 ## fugit 1.7.0 released 2022-09-15 * Introduce the & cron syntax (day-of-month AND day-of-week), gh-78 * Change how cron deals with modulo and offset, gh-76 * Be liberal with extra commas, gh-77 ## fugit 1.6.0 release 2022-08-25 * Ensure input strings are stripped before parsing, gh-74 ## fugit 1.5.3 released 2022-04-02 * Fix Fugit::Cron.to_s vs "0 13 * * wed%2", gh-68 ## fugit 1.5.2 released 2021-09-18 * Simplify inc_day, gh-62 ## fugit 1.5.1 released 2021-08-18 * Fix #next_time break issue for America/Santiago into DST, gh-60 ## fugit 1.5.0 released 2021-06-08 * Accept "at 12 noon" and "at 12 midday" as "* 12 * * *", gh-57 * Accept "at 12pm" as "0 12 * * *", not "0 24 * * *", gh-57 * Accept "15/30 * * * *" as "15-59/30 * * * *", gh-56 ## fugit 1.4.5 released 2021-04-22 * Accept "* * * Mon%2+2", gh-47 ## fugit 1.4.4 released 2021-03-25 * Ensure leaving ZH DST is OK, gh-53 ## fugit 1.4.3 released 2021-03-23 * Fix entering DST issue, gh-53 ## fugit 1.4.2 released 2021-01-12 * Fix Fugit::Cron.previous_time vs last day of month, gh-51 * Let Fugit::Cron.parse('') return nil, gh-49 ## fugit 1.4.1 released 2020-11-25 * Suppress warning, gh-46, thanks @amatsuda ## fugit 1.4.0 released 2020-10-27 * Ensure cron accepts "25-L" for monthday, gh-45 * Allow for "every weekday 8am to 5pm", gh-44 * Allow "every day from the 25th to the last", gh-45 * Rework nat parser ## fugit 1.3.9 released 2020-09-17 * Prevent "New York skip", gh-43, thanks @honglooker ## fugit 1.3.8 released 2020-08-06 * Parse 'every day at 8:30' and ' at 8:30 pm', gh-42 ## fugit 1.3.7 released 2020-08-05 * Parse 'every 12 hours at minute 50', gh-41 ## fugit 1.3.6 released 2020-06-01 * Introduce new nat syntaxed, gh-38 * Rework nat parser ## fugit 1.3.5 released 2020-05-07 * Implement cron @noon, gh-37 * Normalize "every x", gh-37 ## fugit 1.3.4 released 2020-04-06 * Prevent #rough_frequency returning 0, gh-36 ## 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.8.1/CREDITS.md000066400000000000000000000053501436236302600142660ustar00rootroot00000000000000 # fugit credits * Michael Reinsch, https://github.com/mreinsch, gh-84 and gh-85 * Marc Anguera, https://github.com/markets, gh-70 and Sidekiq-Cron * ski-nine, https://github.com/ski-nine, gh-81 * Joseph Halter, https://github.com/JosephHalter, gh-79 * James Healy, https://github.com/yob, gh-76 * John Bachir, https://github.com/jjb, gh-74 * Vivek Miyani, https://github.com/vivekmiyani, gh-71 * Peter Goldstein, https://github.com/petergoldstein infra, gh-65, -72 * Pascal Zumkehr https://github.com/codez gh-62, Santiago into DST vs Time.zone * Ggallardoh https://github.com/Ggallardoh gh-60, America/Santiago into DST * Khaled AbuShqear https://github.com/shqear93 gh-57, "12pm" * John W Higgins https://github.com/wishdev gh-56, 15/30 cron decision * Karen Sawrey https://github.com/karensawrey gh-47, Mon%2+1 rework idea * Olle Jonsson https://github.com/olleolleolle gha Ruby 3.0 * Andy Pfister https://github.com/andyundso gh-53, entering DST * Solteszad https://github.com/solteszad gh-51, fix previous_time vs last day of month * Niklas https://github.com/gr8bit gh-49, Fugit::Cron.parse('') * Matsuda Akira https://github.com/amatsuda gh-46, warning suppression * Honglooker https://github.com/honglooker gh-43, New York cron skip * Jérôme Dalbert https://github.com/jeromedalbert gh-41, gh-42 * Danny Ben Shitrit https://github.com/DannyBen nat variants, gh-38 * Dominik Sander https://github.com/dsander #rough_frequency 0, gh-36 * 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 and gh-47 * Shai Coleman https://github.com/shaicoleman parse_nat enhancements, gh-24, gh-25, gh-28, and gh-37 * 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.8.1/Gemfile000066400000000000000000000006251436236302600141420ustar00rootroot00000000000000 source 'https://rubygems.org' #gem 'tzinfo-data' #gem 'raabro', git: 'https://github.com/floraison/raabro' # temporarily #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.8.1/LICENSE.txt000066400000000000000000000021231436236302600144650ustar00rootroot00000000000000 Copyright (c) 2017-2023, 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.8.1/Makefile000066400000000000000000000024061436236302600143060ustar00rootroot00000000000000 ## gem tasks ## NAME != \ ruby -e "s = eval(File.read(Dir['*.gemspec'][0])); puts s.name" VERSION != \ 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 --otp "$(OTP)" 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.8.1/README.md000066400000000000000000000406321436236302600141300ustar00rootroot00000000000000 # fugit [![tests](https://github.com/floraison/fugit/workflows/test/badge.svg)](https://github.com/floraison/fugit/actions) [![Gem Version](https://badge.fury.io/rb/fugit.svg)](http://badge.fury.io/rb/fugit) [![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. ## 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 * [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) - recent versions of Sidekiq-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) * [serial_scheduler](https://github.com/grosser/serial_scheduler) - ruby task scheduler without threading * [delayed_cron_job](https://github.com/codez/delayed_cron_job) - an extension to Delayed::Job that allows you to set cron expressions for your jobs * ... ## `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. ## parse_cronish and do_parse_cronish Sometimes you know a cron expression or an "every" natural expression will come in and you want to discard the rest. ``` require 'fugit' Fugit.parse_cronish('0 0 1 jan *').class # ==> ::Fugit::Cron Fugit.parse_cronish('every saturday at noon').class # ==> ::Fugit::Cron Fugit.parse_cronish('12y12M') # ==> nil ``` `.parse_cronish(s)` will return a `Fugit::Cron` instance or else nil. `.do_parse_cronish(s)` will return a `Fugit::Cron` instance or else fail with an `ArgumentError`. Introduced in fugit 1.8.0. ## `Fugit::Cron` A class `Fugit::Cron` to parse cron strings and then `#next_time` and `#previous_time` to compute the next or the previous occurrence respectively. There is also a `#brute_frequency` method which returns an array `[ shortest delta, longest delta, occurrence count ]` where delta is the time between two occurrences. ```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... ``` Please note that `'15/30 * * * *'` is interpreted as `'15-59/30 * * * *'` since fugit 1.4.6. ### the first Monday of the month Fugit tries to follow the `man 5 crontab` documentation. There is a surprising thing about this canon, all the columns are joined by ANDs, except for monthday and weekday which are joined together by OR if they are both set (they are not `*`). Many people (me included) [are suprised](https://superuser.com/questions/428807/run-a-cron-job-on-the-first-monday-of-every-month) when they try to specify "at 05:00 on the first Monday of the month" as `0 5 1-7 * 1` or `0 5 1-7 * mon` and the results are off. The man page says: > Note: The day of a command's execution can be specified by > two fields -- day of month, and day of week. If both fields > are restricted (ie, are not *), the command will be run when > either field matches the current time. > For example, ``30 4 1,15 * 5'' would cause a command to be run > at 4:30 am on the 1st and 15th of each month, plus every Friday. Fugit follows this specification. Since fugit 1.7.0, by adding `&` right after a day specifier, the `day-of-month OR day-of-week` becomes `day-of-month AND day-of-week`. ```ruby # standard cron p Fugit.parse_cron('0 0 */2 * 1-5').next_time('2022-08-09').to_s # ==> "2022-08-10 00:00:00 +0900" # with an & p Fugit.parse_cron('0 0 */2 * 1-5&').next_time('2022-08-09').to_s # or p Fugit.parse_cron('0 0 */2& * 1-5').next_time('2022-08-09').to_s p Fugit.parse_cron('0 0 */2& * 1-5&').next_time('2022-08-09').to_s # ==> "2022-08-11 00:00:00 +0900" # standard cron p Fugit.parse_cron('59 6 1-7 * 2').next_time('2020-03-15').to_s # ==> "2020-03-17 06:59:00 +0900" # with an & p Fugit.parse_cron('59 6 1-7 * 2&').next_time('2020-03-15').to_s p Fugit.parse_cron('59 6 1-7& * 2').next_time('2020-03-15').to_s p Fugit.parse_cron('59 6 1-7& * 2&').next_time('2020-03-15').to_s # ==> "2020-04-07 06:59:00 +0900" ``` ### the hash extension Fugit understands `0 5 * * 1#1` or `0 5 * * mon#1` as "each first Monday of the month, at 05:00". ```ruby '0 5 * * 1#1' # '0 5 * * mon#1' # the first Monday of the month at 05:00 '0 6 * * 5#4,5#5' # '0 6 * * fri#4,fri#5' # the 4th and 5th Fridays of the month at 06:00 '0 7 * * 5#-1' # '0 7 * * fri#-1' # the last Friday of the month at 07:00 '0 7 * * 5#L' # '0 7 * * fri#L' # '0 7 * * 5#last' # '0 7 * * fri#last' # the last Friday of the month at 07:00 '0 23 * * mon#2,tue' # the 2nd Monday of the month and every Tuesday, at 23:00 ``` ### the modulo extension Fugit, since 1.1.10, also understands cron strings like "`9 0 * * sun%2`" which can be read as "every other Sunday at 9am". For odd Sundays, one can write `9 0 * * sun%2+1`. It can be combined, as in `9 0 * * sun%2,tue%3+2` But what does it reference to? It starts at 1 on 2019-01-01. ```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 c = Fugit.parse('* * * * tue%2') c.match?('2019-01-01') # => false, since rweek % 2 == 1 c.match?('2019-01-08') # => true, since rweek % 2 == 0 c = Fugit.parse('* * * * tue%2+1') c.match?('2019-01-01') # => true, since (rweek + 1) % 2 == 0 c.match?('2019-01-08') # => false, since (rweek + 1) % 2 == 1 # ... ``` `sun%2` matches if Sunday and `current_date.rweek % 2 == 0` `tue%3+2` matches if Tuesday and `current_date.rweek + 2 % 3 == 0` `tue%x+y` matches if Tuesday and `current_date.rweek + y % x == 0` ## `Fugit::Duration` A class `Fugit::Duration` to parse duration strings (vanilla [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) ones and [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) ones). Provides duration arithmetic tools. ```ruby require 'fugit' d = Fugit::Duration.parse('1y2M1d4h') p d.to_plain_s # => "1Y2M1D4h" p d.to_iso_s # => "P1Y2M1DT4H" ISO 8601 duration p d.to_long_s # => "1 year, 2 months, 1 day, and 4 hours" d += Fugit::Duration.parse('1y1h') p d.to_long_s # => "2 years, 2 months, 1 day, and 5 hours" d += 3600 p d.to_plain_s # => "2Y2M1D5h3600s" p Fugit::Duration.parse('1y2M1d4h').to_sec # => 36820800 ``` There is a `#deflate` method ```ruby Fugit::Duration.parse(1000).to_plain_s # => "1000s" Fugit::Duration.parse(3600).to_plain_s # => "3600s" Fugit::Duration.parse(1000).deflate.to_plain_s # => "16m40s" Fugit::Duration.parse(3600).deflate.to_plain_s # => "1h" # or event shorter Fugit.parse(1000).deflate.to_plain_s # => "16m40s" Fugit.parse(3600).deflate.to_plain_s # => "1h" ``` There is also an `#inflate` method ```ruby Fugit::Duration.parse('1h30m12').inflate.to_plain_s # => "5412s" Fugit.parse('1h30m12').inflate.to_plain_s # => "5412s" Fugit.parse('1h30m12').to_sec # => 5412 Fugit.parse('1h30m12').to_sec.to_s + 's' # => "5412s" ``` The `to_*_s` methods are also available as class methods: ```ruby p Fugit::Duration.to_plain_s('1y2M1d4h') # => "1Y2M1D4h" p Fugit::Duration.to_iso_s('1y2M1d4h') # => "P1Y2M1DT4H" ISO 8601 duration p Fugit::Duration.to_long_s('1y2M1d4h') # => "1 year, 2 months, 1 day, and 4 hours" ``` ## `Fugit::At` Points in time are parsed and given back as EtOrbi::EoTime instances. ```ruby Fugit::At.parse('2017-12-12').to_s # ==> "2017-12-12 00:00:00 +0900" (at least here in Hiroshima) Fugit::At.parse('2017-12-12 12:00:00 America/New_York').to_s # ==> "2017-12-12 12:00:00 -0500" ``` Directly with `Fugit.parse_at(s)` is OK too: ```ruby Fugit.parse_at('2017-12-12 12:00:00 America/New_York').to_s # ==> "2017-12-12 12:00:00 -0500" ``` Directly with `Fugit.parse(s)` is OK too: ```ruby Fugit.parse('2017-12-12 12:00:00 America/New_York').to_s # ==> "2017-12-12 12:00:00 -0500" ``` ## `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. The `multi: false|true|:fail` argument to `Fugit::Nat.parse` could help. ```ruby Fugit::Nat.parse('every day at 16:00 and 18:00') .to_cron_s # ==> '0 16,18 * * *' (a single Fugit::Cron instances) Fugit::Nat.parse('every day at 16:00 and 18:00', multi: true) .collect(&:to_cron_s) # ==> [ '0 16,18 * * *' ] (array of Fugit::Cron instances, here only one) Fugit::Nat.parse('every day at 16:15 and 18:30') .to_cron_s # ==> '15 16 * * *' (a single of Fugit::Cron instances) Fugit::Nat.parse('every day at 16:15 and 18:30', multi: true) .collect(&:to_cron_s) # ==> [ '15 16 * * *', '30 18 * * *' ] (two Fugit::Cron instances) Fugit::Nat.parse('every day at 16:15 and 18:30', multi: :fail) # ==> ArgumentError: multiple crons in "every day at 16:15 and 18:30" (15 16 * * * | 30 18 * * *) Fugit::Nat.parse('every day at 16:15 nada 18:30', multi: true) # ==> nil ``` `multi: true` indicates to `Fugit::Nat` that an array of `Fugit::Cron` instances is expected as a result. `multi: :fail` tells `Fugit::Nat.parse` to fail if the result is more than 1 `Fugit::Cron` instances. `multi: false` is the default behaviour, return a single `Fugit::Cron` instance or nil when it cannot parse. ### Nat Midnight `"Every day at midnight"` is supported, but `"Every monday at midnight"` will be interpreted (as of Fugit <= 1.4.x) as `"Every monday at 00:00"`. Sorry about that. ### 12 AM and PM How does fugit react with `"12 am"`, `"12 pm"`, `"12 midnight"`, etc? ```ruby require 'fugit' p Fugit.parse('every day at 12am').original # ==> "0 0 * * *" p Fugit.parse('every day at 12pm').original # ==> "0 12 * * *" p Fugit.parse('every day at 12:00am').original # ==> "0 0 * * *" p Fugit.parse('every day at 12:00pm').original # ==> "0 12 * * *" p Fugit.parse('every day at 12:00 am').original # ==> "0 0 * * *" p Fugit.parse('every day at 12:00 pm').original # ==> "0 12 * * *" p Fugit.parse('every day at 12:15am').original # ==> "15 0 * * *" p Fugit.parse('every day at 12:15pm').original # ==> "15 12 * * *" p Fugit.parse('every day at 12:15 am').original # ==> "15 0 * * *" p Fugit.parse('every day at 12:15 pm').original # ==> "15 12 * * *" p Fugit.parse('every day at 12 noon').original # ==> "0 12 * * *" p Fugit.parse('every day at 12 midnight').original # ==> "0 24 * * *" p Fugit.parse('every day at 12:00 noon').original # ==> "0 12 * * *" p Fugit.parse('every day at 12:00 midnight').original # ==> "0 24 * * *" p Fugit.parse('every day at 12:15 noon').original # ==> "15 12 * * *" p Fugit.parse('every day at 12:15 midnight').original # ==> "15 24 * * *" # as of fugit 1.7.2 ``` ## LICENSE MIT, see [LICENSE.txt](LICENSE.txt) fugit-1.8.1/doc/000077500000000000000000000000001436236302600134115ustar00rootroot00000000000000fugit-1.8.1/doc/cron.rb000066400000000000000000000010571436236302600147020ustar00rootroot00000000000000 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.8.1/doc/duration.rb000066400000000000000000000005341436236302600155650ustar00rootroot00000000000000 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.8.1/fugit.gemspec000066400000000000000000000026641436236302600153370ustar00rootroot00000000000000 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 = 'https://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.4' s.add_runtime_dependency 'et-orbi', '~> 1', '>= 1.2.7' s.add_development_dependency 'rspec', '~> 3.8' s.add_development_dependency 'chronic', '~> 0.10' s.require_path = 'lib' end fugit-1.8.1/invalid_crons.md000066400000000000000000000010371436236302600160210ustar00rootroot00000000000000 ## 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.8.1/lib/000077500000000000000000000000001436236302600134125ustar00rootroot00000000000000fugit-1.8.1/lib/fugit.rb000066400000000000000000000004171436236302600150570ustar00rootroot00000000000000# frozen_string_literal: true module Fugit VERSION = '1.8.1' 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.8.1/lib/fugit/000077500000000000000000000000001436236302600145305ustar00rootroot00000000000000fugit-1.8.1/lib/fugit/at.rb000066400000000000000000000003471436236302600154650ustar00rootroot00000000000000# frozen_string_literal: true 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.8.1/lib/fugit/cron.rb000066400000000000000000000527421436236302600160300ustar00rootroot00000000000000# frozen_string_literal: true 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 * * *', '@noon' => '0 12 * * *', '@hourly' => '0 * * * *' }.freeze MAXDAYS = [ nil, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ].freeze 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.strip) 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_to_cron_s, @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_t; @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) return if @t.hour == 0 if @t.hour < 12 begin @t = ::EtOrbi.make(@t.year, @t.month, @t.day, @t.zone) rescue ::TZInfo::PeriodNotFound inc((24 - @t.hour) * 3600) end else inc((24 - @t.hour) * 3600) end 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 - 1) * DAY_S + @t.hour * 3600 + @t.min * 60 + @t.sec + 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] % mod[0]) end def weekday_match?(nt) return true if @weekdays.nil? wd, hom = @weekdays.find { |d, _| d == nt.wday } return false unless wd return true if hom.nil? if hom.is_a?(Array) weekday_modulo_match?(nt, hom) else weekday_hash_match?(nt, hom) end 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) if @weekdays && @monthdays return weekday_match?(nt) && monthday_match?(nt) \ if @day_and # # extension for fugit, gh-78 return weekday_match?(nt) || monthday_match?(nt) # # From `man 5 crontab` # # 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. # # as seen in gh-5 and gh-35 end 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 zfrom = t.time.strftime('%z|%Z') 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) tt = t.time st = tt.strftime('%F|%T') zt = tt.strftime('%z|%Z') # if st == sfrom && zt != zfrom from, sfrom, zfrom, ifrom = tt, st, zt, t.to_i next end # # 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, DAY_S, 365 ] ].freeze 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 } .select { |d| d > 0 } # weed out zero deltas .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 / YEAR_S @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 @day_and = h[:&] 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 edn = max if sla && edn.nil? 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 || edn < 0 && sta > 0 range(min, max, sta, edn, sla) end def range(min, max, sta, edn, sla) return [ nil ] if sta == min && edn == max && sla == 1 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) a = [] omin, omax = min, max min, max = -max, -1 if sta < 0 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 def weekdays_to_cron_s return '*' unless @weekdays @weekdays .collect { |a| if a.length == 1 a[0].to_s elsif a[1].is_a?(Array) a11 = a[1][1] off = (a11 < 0) ? a11.to_s : (a11 > 0) ? "+#{a11}" : '' "#{a[0]}%#{a[1][0]}" + off else a.collect(&:to_s).join('#') end } .join(',') end module Parser include Raabro WEEKDAYS = %w[ sunday monday tuesday wednesday thursday friday saturday ].freeze WEEKDS = WEEKDAYS.collect { |d| d[0, 3] }.freeze DOW_REX = /([0-7]|#{WEEKDS.join('|')})/i.freeze MONTHS = %w[ - jan feb mar apr may jun jul aug sep oct nov dec ].freeze MONTH_REX = /(1[0-2]|0?[1-9]|#{MONTHS[1..-1].join('|')})/i.freeze # 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); rex(nil, i, /,([ \t]*,)*/); end def comma?(i); rex(nil, i, /([ \t]*,)*/); end def and?(i); rex(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, MONTH_REX); end def dow(i); rex(:dow, i, DOW_REX); 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, :comma?, :list_sec, :comma?, :s); end def lmin_(i); seq(nil, i, :comma?, :list_min, :comma?, :s); end def lhou_(i); seq(nil, i, :comma?, :list_hou, :comma?, :s); end def ldom_(i); seq(nil, i, :comma?, :list_dom, :comma?, :and?, :s); end def lmon_(i); seq(nil, i, :comma?, :list_mon, :comma?, :s); end def ldow(i); seq(nil, i, :comma?, :list_dow, :comma?, :and?); 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 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.strim z = EtOrbi.get_tzone(s) [ s, z ] end def rewrite_cron(t) st = t .sublookup(nil) # go to :ccron or :scron return nil unless st hcron = st .subgather(nil) # list min, hou, mon, ... .inject({}) { |h, tt| h[tt.name] = tt.name == :tz ? rewrite_tz(tt) : rewrite_entry(tt) h } hcron[:&] = true if t.string.index('&') z, tz = hcron[:tz]; return nil if z && ! tz hcron end end end end fugit-1.8.1/lib/fugit/duration.rb000066400000000000000000000224631436236302600167110ustar00rootroot00000000000000# frozen_string_literal: true 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: YEAR_S, x: 0, l: 'year' }, mon: { a: 'M', r: 'M', i: 'M', s: 30 * DAY_S, x: 1, l: 'month' }, wee: { a: 'W', r: 'w', i: 'W', s: 7 * DAY_S, I: true, l: 'week' }, day: { a: 'D', r: 'd', i: 'D', s: DAY_S, I: true, l: 'day' }, hou: { a: 'h', r: 'h', i: 'H', s: 3600, I: true, l: 'hour' }, min: { a: 'm', r: 'm', i: 'M', s: 60, I: true, l: 'minute' }, sec: { a: 's', r: 's', i: 'S', s: 1, I: true, l: 'second' } }.freeze INFLA_KEYS, NON_INFLA_KEYS = KEYS .partition { |k, v| v[:I] } .collect(&:freeze) 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 + 1, (11+at[1]) % 12 + 1 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 opposite.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.8.1/lib/fugit/misc.rb000066400000000000000000000013461436236302600160140ustar00rootroot00000000000000# frozen_string_literal: true module Fugit DAY_S = (24 * 3600).freeze YEAR_S = (365 * DAY_S).freeze 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.8.1/lib/fugit/nat.rb000066400000000000000000000452041436236302600156440ustar00rootroot00000000000000# frozen_string_literal: true 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) s = s.strip #p s; Raabro.pp(Parser.parse(s, debug: 3), colours: true) #(p s; Raabro.pp(Parser.parse(s, debug: 1), colours: true)) rescue nil if slots = Parser.parse(s) slots.to_crons(opts.merge(_s: s)) else nil end end def do_parse(s, opts={}) parse(s, opts) || fail(ArgumentError.new("could not parse a nat #{s.inspect}")) end end module Parser include Raabro one_to_nine = %w[ one two three four five six seven eight nine ] sixties = %w[ zero ] + one_to_nine + %w[ ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen ] + %w[ twenty thirty fourty fifty ] .collect { |a| ([ nil ] + one_to_nine) .collect { |b| [ a, b ].compact.join('-') } } .flatten NHOURS = sixties[0, 13] .each_with_index .inject({}) { |h, (n, i)| h[n] = i; h } .merge!( 'midnight' => 0, 'oh' => 0, 'noon' => 12) .freeze NMINUTES = sixties .each_with_index .inject({}) { |h, (n, i)| h[n] = i; h } .merge!( "o'clock" => 0, 'hundred' => 0) .freeze WEEKDAYS = ( Fugit::Cron::Parser::WEEKDAYS + Fugit::Cron::Parser::WEEKDS).freeze POINTS = %w[ minutes? mins? seconds? secs? hours? hou h ].freeze INTERVALS = %w[ seconds? minutes? hours? days? months? sec min s m h d M ].freeze oh = { '1st' => 1, '2nd' => 2, '3rd' => 3, '21st' => 21, '22nd' => 22, '23rd' => 23, '31st' => 31, 'last' => 'L' } (4..30) .each { |i| oh["#{i}th"] = i.to_i } %w[ first second third fourth fifth sixth seventh eighth ninth tenth eleventh twelfth thirteenth fourteenth fifteenth sixteenth seventeenth eighteenth nineteenth twentieth twenty-first twenty-second twenty-third twenty-fourth twenty-fifth twenty-sixth twenty-seventh twenty-eighth twenty-ninth thirtieth thirty-first ] .each_with_index { |e, i| oh[e] = i + 1 } OMONTHDAYS = oh.freeze OMONTHDAY_REX = /#{OMONTHDAYS.keys.join('|')}/i.freeze MONTHDAY_REX = /3[0-1]|[0-2]?[0-9]/.freeze WEEKDAY_REX = /(#{WEEKDAYS.join('|')})(?=($|[-, \t]))/i.freeze # prevent "mon" from eating "monday" NAMED_M_REX = /#{NMINUTES.keys.join('|')}/i.freeze NAMED_H_REX = /#{NHOURS.keys.join('|')}/i.freeze POINT_REX = /(#{POINTS.join('|')})[ \t]+/i.freeze INTERVAL_REX = /[ \t]*(#{INTERVALS.join('|')})/.freeze # # parsers bottom to top ################################################# def _every(i); rex(nil, i, /[ \t]*every[ \t]+/i); end def _from(i); rex(nil, i, /[ \t]*from[ \t]+/i); end def _at(i); rex(nil, i, /[ \t]*at[ \t]+/i); end def _on(i); rex(nil, i, /[ \t]*on[ \t]+/i); end def _to(i); rex(nil, i, /[ \t]*to[ \t]+/i); end def _and(i); rex(nil, i, /[ \t]*and[ \t]+/i); end def _and_or_or(i); rex(nil, i, /[ \t]*(and|or)[ \t]+/i); end def _in_or_on(i); rex(nil, i, /(in|on)[ \t]+/i); end def _and_or_or_or_comma(i) rex(nil, i, /[ \t]*(,[ \t]*)?((and|or)[ \t]+|,[ \t]*)/i); end def _to_or_dash(i); rex(nil, i, /[ \t]*-[ \t]*|[ \t]+(to|through)[ \t]+/i); end def _day_s(i); rex(nil, i, /[ \t]*days?[ \t]+/i); end def _the(i); rex(nil, i, /[ \t]*the[ \t]+/i); end def _space(i); rex(nil, i, /[ \t]+/); end def _sep(i); rex(nil, i, /([ \t]+|[ \t]*,[ \t]*)/); end def count(i); rex(:count, i, /\d+/); end def omonthday(i) rex(:omonthday, i, OMONTHDAY_REX) end def monthday(i) rex(:monthday, i, MONTHDAY_REX) end def weekday(i) rex(:weekday, i, WEEKDAY_REX) end def omonthdays(i); jseq(nil, i, :omonthday, :_and_or_or_or_comma); end def monthdays(i); jseq(nil, i, :monthday, :_and_or_or_or_comma); end def weekdays(i); jseq(:weekdays, i, :weekday, :_and_or_or_or_comma); end def on_the(i); seq(nil, i, :_the, :omonthdays); end def _minute(i); rex(nil, i, /[ \t]*minute[ \t]+/i) end def _dmin(i) rex(:dmin, i, /[0-5]?[0-9]/) end def and_dmin(i) seq(nil, i, :_and_or_or_or_comma, :_minute, '?', :_dmin) end def on_minutes(i) seq(:on_minutes, i, :_minute, :_dmin, :and_dmin, '*') end def on_thex(i); rex(:on_thex, i, /[ \t]*the[ \t]+(hour|minute)[ \t]*/i); end def on_thes(i); jseq(:on_thes, i, :on_the, :_and_or_or_or_comma); end def on_days(i); seq(:on_days, i, :_day_s, :monthdays); end def on_weekdays(i); ren(:on_weekdays, i, :weekdays); end def on_object(i) alt(nil, i, :on_days, :on_weekdays, :on_minutes, :on_thes, :on_thex) end def on_objects(i) jseq(nil, i, :on_object, :_and) end #'every month on day 2 at 10:00' => '0 10 2 * *', #'every month on day 2 and 5 at 10:00' => '0 10 2,5 * *', #'every month on days 1,15 at 10:00' => '0 10 1,15 * *', # #'every week on monday 18:23' => '23 18 * * 1', # # every month on the 1st # def on(i) seq(:on, i, :_on, :on_objects) end def city_tz(i) rex(nil, i, /[A-Z][a-zA-Z0-9+\-]+(\/[A-Z][a-zA-Z0-9+\-_]+){0,2}/) end def named_tz(i) rex(nil, i, /Z|UTC/) end def delta_tz(i) rex(nil, i, /[-+]([01][0-9]|2[0-4])(:?(00|15|30|45))?/) end def tz(i) alt(:tz, i, :city_tz, :named_tz, :delta_tz) end def tzone(i) seq(nil, i, :_in_or_on, '?', :tz) end def ampm(i) rex(:ampm, i, /[ \t]*(am|pm|noon|midday|midnight)/i) end def dark(i) rex(:dark, i, /[ \t]*dark/i) end def digital_h(i) rex(:digital_h, i, /(2[0-4]|[0-1]?[0-9]):([0-5][0-9])/i) end def digital_hour(i) seq(:digital_hour, i, :digital_h, :ampm, '?') end def simple_h(i) rex(:simple_h, i, /#{(0..24).to_a.reverse.join('|')}/) end def simple_hour(i) seq(:simple_hour, i, :simple_h, :ampm, '?') end def named_m(i) rex(:named_m, i, NAMED_M_REX) end def named_min(i) seq(nil, i, :_space, :named_m) end def named_h(i) rex(:named_h, i, NAMED_H_REX) end def named_hour(i) seq(:named_hour, i, :named_h, :dark, '?', :named_min, '?', :ampm, '?') end def _point(i); rex(:point, i, POINT_REX); end def counts(i) jseq(nil, i, :count, :_and_or_or_or_comma) end def at_p(i) seq(:at_p, i, :_point, :counts) end def at_point(i) jseq(nil, i, :at_p, :_and_or_or) end # at five # at five pm # at five o'clock # at 16:30 # at noon # at 18:00 UTC <-- ...tz def at_object(i) alt(nil, i, :named_hour, :digital_hour, :simple_hour, :at_point) end def at_objects(i) jseq(nil, i, :at_object, :_and_or_or_or_comma) end def at(i) seq(:at, i, :_at, '?', :at_objects) end def interval(i) rex(:interval, i, INTERVAL_REX) end # every day # every 1 minute def every_interval(i) seq(:every_interval, i, :count, '?', :interval) end def every_single_interval(i) rex(:every_single_interval, i, /(1[ \t]+)?(week|year)/) end def to_weekday(i) seq(:to_weekday, i, :weekday, :_to_or_dash, :weekday) end def weekday_range(i) alt(nil, i, :to_weekday, :weekdays) end def to_omonthday(i) seq(:to_omonthday, i, :_the, '?', :omonthday, :_to, :_the, '?', :omonthday) end def to_hour(i) seq(:to_hour, i, :at_object, :_to, :at_object) end def from_object(i) alt(nil, i, :to_weekday, :to_omonthday, :to_hour) end def from_objects(i) jseq(nil, i, :from_object, :_and_or_or) end def from(i) seq(nil, i, :_from, '?', :from_objects) end # every monday # every Fri-Sun # every Monday and Tuesday def every_weekday(i) jseq(nil, i, :weekday_range, :_and_or_or) end def otm(i) rex(nil, i, /[ \t]+of the month/) end # every 1st of the month # every first of the month # Every 2nd of the month # Every second of the month # every 15th of the month def every_of_the_month(i) seq(nil, i, :omonthdays, :otm) end def every_named(i) rex(:every_named, i, /weekday/i) end def every_object(i) alt( nil, i, :every_weekday, :every_of_the_month, :every_interval, :every_named, :every_single_interval) end def every_objects(i) jseq(nil, i, :every_object, :_and_or_or) end def every(i) seq(:every, i, :_every, :every_objects) end def nat_elt(i) alt(nil, i, :every, :from, :at, :tzone, :on) end def nat(i) jseq(:nat, i, :nat_elt, :_sep) end # # rewrite parsed tree ################################################### def slot(key, data0, data1=nil, opts=nil) Slot.new(key, data0, data1, opts) end def _rewrite_subs(t, key=nil) t.subgather(key).collect { |ct| rewrite(ct) } end def _rewrite_sub(t, key=nil) st = t.sublookup(key) st ? rewrite(st) : nil end def rewrite_dmin(t) t.strinp end def rewrite_on_minutes(t) #Raabro.pp(t, colours: true) mins = t.subgather(:dmin).collect(&:strinp) #slot(:m, mins.join(',')) slot(:hm, '*', mins.join(','), strong: 1) end def rewrite_on_thex(t) case s = t.string #when /hour/i then slot(:h, 0) #else slot(:m, '*') when /hour/i then slot(:hm, 0, '*', strong: 0) else slot(:hm, '*', '*', strong: 1) end end def rewrite_on_thes(t) _rewrite_subs(t, :omonthday) end def rewrite_on_days(t) _rewrite_subs(t, :monthday) end def rewrite_on(t) _rewrite_subs(t) end def rewrite_monthday(t) slot(:monthday, t.string.to_i) end def rewrite_omonthday(t) slot(:monthday, OMONTHDAYS[t.string.downcase]) end def rewrite_at_p(t) pt = t.sublookup(:point).strinpd pt = pt.start_with?('mon') ? 'M' : pt[0, 1] pts = t.subgather(:count).collect { |e| e.string.to_i } #p [ pt, pts ] case pt #when 'm' then slot(:m, pts) when 'm' then slot(:hm, '*', pts, strong: 1) when 's' then slot(:second, pts) else slot(pt.to_sym, pts) end end def rewrite_every_single_interval(t) case t.string when /year/i then [ slot(:month, 1, :weak), slot(:monthday, 1, :weak) ] #when /week/i then xxx... else slot(:weekday, 0, :weak) end end def rewrite_every_interval(t) #Raabro.pp(t, colours: true) ci = t.subgather(nil).collect(&:string) i = ci.pop.strip[0, 3] c = (ci.pop || '1').strip i = (i == 'M' || i.downcase == 'mon') ? 'M' : i[0, 1].downcase cc = c == '1' ? '*' : "*/#{c}" case i when 'M' then slot(:month, cc) when 'd' then slot(:monthday, cc, :weak) #when 'h' then slot(:hm, cc, 0, weak: :minute) when 'h' then slot(:hm, cc, 0, weak: 1) when 'm' then slot(:hm, '*', cc, strong: 1) when 's' then slot(:second, cc) else {} end end def rewrite_every_named(t) case s = t.string when /weekday/i then slot(:weekday, '1-5', :weak) when /week/i then slot(:weekday, '0', :weak) else fail "cannot rewrite #{s.inspect}" end end def rewrite_tz(t) slot(:tz, t.string) end def rewrite_weekday(t) Fugit::Cron::Parser::WEEKDS.index(t.string[0, 3].downcase) end def rewrite_weekdays(t) #Raabro.pp(t, colours: true) slot(:weekday, _rewrite_subs(t, :weekday)) end alias rewrite_on_weekdays rewrite_weekdays def rewrite_to_weekday(t) wd0, wd1 = _rewrite_subs(t, :weekday) #wd1 = 7 if wd1 == 0 slot(:weekday, "#{wd0}-#{wd1}") end def rewrite_to_omonthday(t) md0, md1 = _rewrite_subs(t, :omonthday).collect(&:_data0) slot(:monthday, "#{md0}-#{md1}") end # Try to follow https://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight # def adjust_h(h, m, ap) if ap == 'midnight' && h == 12 24 elsif ap == 'pm' && h < 12 # post meridian h + 12 elsif ap == 'am' && h == 12 # ante meridian 0 else h end end def rewrite_digital_hour(t) h, m = t.sublookup(:digital_h).strinpd.split(':').collect(&:to_i) ap = t.sublookup(:ampm) h, m = adjust_h(h, m, ap && ap.strinpd), m slot(:hm, h, m) end def rewrite_simple_hour(t) h, ap = t.subgather(nil).collect(&:strinpd) h = adjust_h(h.to_i, 0, ap) slot(:hm, h, 0) end def rewrite_named_hour(t) ht = t.sublookup(:named_h) mt = t.sublookup(:named_m) apt = t.sublookup(:ampm) h = ht.strinp m = mt ? mt.strinp : 0 h = NHOURS[h] m = NMINUTES[m] || m h = adjust_h(h, m, apt && apt.strinpd) slot(:hm, h, m) end def rewrite_to_hour(t) #Raabro.pp(t, colours: true) ht0, ht1 = t.subgather(nil) h0, h1 = rewrite(ht0), rewrite(ht1) fail ArgumentError.new( "cannot deal with #{ht0.strinp} to #{ht1.strinp}, minutes diverge" ) if h0.data1 != h1.data1 slot(:hm, "#{h0._data0}-#{h1._data0}", 0, strong: 0) end def rewrite_at(t) _rewrite_subs(t) end def rewrite_every(t) _rewrite_sub(t) end def rewrite_nat(t) #Raabro.pp(t, colours: true) Fugit::Nat::SlotGroup.new(_rewrite_subs(t).flatten) end end class Slot attr_reader :key attr_accessor :_data0, :_data1 def initialize(key, d0, d1=nil, opts=nil) d1, opts = d1.is_a?(Symbol) ? [ nil, d1 ] : [ d1, opts ] @key, @_data0, @_data1 = key, d0, d1 @opts = (opts.is_a?(Symbol) ? { opts => true } : opts) || {} end def data0; @data0 ||= Array(@_data0); end def data1; @data1 ||= Array(@_data1); end def weak; @opts[:weak]; end def strong; @opts[:strong]; end def graded?; weak || strong; end def append(slot) @_data0, @_data1 = conflate(0, slot), conflate(1, slot) @opts.clear self end def inspect a = [ @key, @_data0 ] a << @_data1 if @_data1 != nil a << @opts if @opts && @opts.keys.any? "(slot #{a.collect(&:inspect).join(' ')})" end def a; [ data0, data1 ]; end protected def to_a(x) return [] if x == '*' Array(x) end def conflate(index, slot) a, b = index == 0 ? [ @_data0, slot._data0 ] : [ @_data1, slot._data1 ] return a if b == nil return b if a == nil if ra = (index == 0 && slot.strong == 1 && hour_range) h0, h1 = ra[0], ra[1] - 1; return h0 == h1 ? h0 : "#{h0}-#{h1}" elsif rb = (index == 0 && strong == 1 && slot.hour_range) h0, h1 = rb[0], rb[1] - 1; return h0 == h1 ? h0 : "#{h0}-#{h1}" end return a if strong == index || strong == true return b if slot.strong == index || slot.strong == true return a if slot.weak == index || slot.weak == true return b if weak == index || weak == true return [ '*' ] if a == '*' && b == '*' to_a(a).concat(to_a(b)) end def hour_range m = (key == :hm && @_data1 == 0 && @_data0.match(/\A(\d+)-(\d+)\z/)) m ? [ m[1].to_i, m[2].to_i ] : nil end end class SlotGroup def initialize(slots) #puts "SlotGroup.new " + slots.inspect @slots = {} @hms = [] slots.each do |s| if s.key == :hm #ls = @hms.last; @hms.pop if ls && ls.key == :hm && ls.weak == true @hms << s elsif hs = @slots[s.key] hs.append(s) else @slots[s.key] = s end end if @slots[:monthday] || @slots[:weekday] @hms << make_slot(:hm, 0, 0) if @hms.empty? elsif @slots[:month] @hms << make_slot(:hm, 0, 0) if @hms.empty? @slots[:monthday] ||= make_slot(:monthday, 1) end end def to_crons(opts) multi = opts.has_key?(:multi) ? opts[:multi] : false hms = determine_hms if multi == :fail && hms.count > 1 fail(ArgumentError.new( "multiple crons in #{opts[:_s].inspect} - #{@slots.inspect}")) elsif multi == true hms.collect { |hm| parse_cron(hm) } else parse_cron(hms.first) end end protected def make_slot(key, data0, data1=nil) Fugit::Nat::Slot.new(key, data0, data1) end def determine_hms return [ [ [ '*' ], [ '*' ] ] ] if @hms.empty? hms = @hms.dup # while ig = (hms.count > 1 && hms.index { |hm| hm.graded? }) do sg = hms[ig] so = hms.delete_at(ig == 0 ? 1 : ig - 1) sg.append(so) end hms .collect(&:a) .inject({}) { |r, hm| hm[1].each { |m| (r[m] ||= []).concat(hm[0]) } r } .inject({}) { |r, (m, hs)| (r[hs.sort] ||= []) << m r } .to_a end def parse_cron(hm) a = [ slot(:second, '0'), hm[1], hm[0], slot(:monthday, '*'), slot(:month, '*'), slot(:weekday, '*') ] tz = @slots[:tz] a << tz.data0 if tz a.shift if a.first == [ '0' ] s = a .collect { |e| e.uniq.sort.collect(&:to_s).join(',') } .join(' ') Fugit::Cron.parse(s) end def slot(key, default) s = @slots[key] s ? s.data0 : [ default ] end end end end fugit-1.8.1/lib/fugit/parse.rb000066400000000000000000000030351436236302600161700ustar00rootroot00000000000000# frozen_string_literal: true 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 parse_cronish(s, opts={}) r = parse_cron(s) || parse_nat(s, opts) r.is_a?(::Fugit::Cron) ? r : nil end def do_parse_cronish(s, opts={}) parse_cronish(s) || fail(ArgumentError.new("not cron or 'natural' cron string: #{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.8.1/spec/000077500000000000000000000000001436236302600135765ustar00rootroot00000000000000fugit-1.8.1/spec/at_spec.rb000066400000000000000000000031321436236302600155400ustar00rootroot00000000000000 # # 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 it 'strips before parsing' do t = Fugit.parse_at(" 2017-01-03 11:21:17\n ") expect(Fugit.time_to_plain_s(t, false)).to eq('2017-01-03 11:21:17') 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 ], [ "\n2018-09-04 06:41:34 +11:00", '2018-09-04 06:41:34 +11:00 +1100' ], [ " \n 2018-09-04 06:41:34 Etc/GMT-11", '2018-09-04 06:41:34 +11 +1100' ], ].each do |string, plain| it "parses #{string.inspect}" 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.8.1/spec/cron_spec.rb000066400000000000000000001414301436236302600161010ustar00rootroot00000000000000 # # 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 # # gh-35 '59 6 1-7 * 2', monthdays 1-7 being ignored # # gh-64 # 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. # I need to remember this, else I am going to lose time investigating # those again and again :-( [ '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) [ '59 6 1-7 * 2', '2020-03-17 06:59:00', # not '2020-04-07 06:59:00', tuesday 2 matches '2020-03-15 07:29:00' ], [ '59 6 1-7 * 2', '2020-02-11 06:59:00', # not '2020-03-03 06:59:00', tuesday 2 matches '2020-02-08 07:29:00' ], [ '59 6 1-7 * 2', '2020-03-01 06:59:00', # not '2020-03-03 06:59:00', monthday 1-7 matches '2020-02-29 07:29:00' ], [ '0 0 29 2 0,6', '2022-02-05 00:00:00', '2021-12-27 09:47:00' ], [ '0 0 29 2 sat,sun', '2022-02-05 00:00:00', '2021-12-27 09:47:00' ], [ '0 0 29 2 0,6', '2022-02-05 00:00:00', '2021-12-27 09:47:00', 'UTC' ], # [ '0 0 11 * 3-6', '2021-12-29 00:00:00', '2021-12-27 09:47:00' ], # # 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' ], # gh-52 [ '59 23 * * 2', '2021-02-02 23:59:00', '2021-02-02 00:00:00' ], [ '59 23 * * 2', '2021-02-02 23:59:00', '2021-02-02 00:00:00', 'UTC' ], #[ '59 23 * * 2', '2021-02-02 23:59:00', '2021-02-02 00:00:00', 'utc' ], [ '59 18 * * 2#2', '2021-02-09 18:59:00', '2021-02-09 17:41:10' ], [ '59 18 * * 2#2', '2021-02-09 18:59:00', '2021-02-09 17:41:10', 'UTC' ], #[ '59 18 * * 2#1', '2021-02-09 18:59:00', '2021-02-09 17:41:10', 'utc' ], [ '15/30 * * * *', '2021-02-09 19:15:00', '2021-02-09 19:00:00' ], [ '15/30 * * * *', '2021-02-09 19:45:00', '2021-02-09 19:30:00' ], [ '15-40/30 * * * *', '2021-02-09 20:15:00', '2021-02-09 19:30:00' ], # gh-67 [ '30 18 * * 2#5', '2022-03-29 18:30', '2022-03-08', 'UTC' ], [ '30 18 * * 2#5', '2022-03-29 18:30', '2022-03-08', 'Europe/Paris' ], [ '30 18 * * 2#5 Europe/Paris', '2022-03-29 16:30', '2022-03-08', 'UTC' ], # gh-78 # by way of gh-75 [ '0 0 */2 * 1-5', '2022-08-10 00:00', '2022-08-09', 'UTC' ], [ '0 0 */2 * 1-5&', '2022-08-11 00:00', '2022-08-09', 'UTC' ], [ '0 0 */2& * 1-5', '2022-08-11 00:00', '2022-08-09', 'UTC' ], [ '0 0 */2& * 1-5&', '2022-08-11 00:00', '2022-08-09', 'UTC' ], # by way of gh-35 [ '59 6 1-7 * 2', '2020-03-17 06:59', '2020-03-15', 'UTC' ], [ '59 6 1-7 * 2&', '2020-04-07 06:59', '2020-03-15', 'UTC' ], [ '59 6 1-7& * 2', '2020-04-07 06:59', '2020-03-15', 'UTC' ], [ '59 6 1-7& * 2&', '2020-04-07 06:59', '2020-03-15', 'UTC' ], ] 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 into DST (gh-53 a)' do in_zone 'Europe/Zurich' do c = Fugit::Nat.parse('every monday at midnight') t = EtOrbi::EoTime.parse('2021-03-14 12:00:00') points = 4.times.collect do t = c.next_time(t) tu = t.dup.utc "#{t.strftime('%F_%H:%M_%Z')}__#{tu.strftime('%F_%H:%M_%Z')}" end expect(points.join("\n")).to eq(%w[ 2021-03-15_00:00_CET__2021-03-14_23:00_UTC 2021-03-22_00:00_CET__2021-03-21_23:00_UTC 2021-03-29_00:00_CEST__2021-03-28_22:00_UTC 2021-04-05_00:00_CEST__2021-04-04_22:00_UTC ].join("\n")) end end it 'correctly increments into DST (gh-53 b)' do in_zone 'Europe/Zurich' do #c = Fugit::Nat.parse('every monday at midnight') c = Fugit::Cron.parse('0 0 * * 1') t = EtOrbi::EoTime.parse('2021-03-14 12:00:00') points = 4.times.collect do t = c.next_time(t) tu = t.dup.utc "#{t.strftime('%F_%H:%M_%Z')}__#{tu.strftime('%F_%H:%M_%Z')}" end expect(points.join("\n")).to eq(%w[ 2021-03-15_00:00_CET__2021-03-14_23:00_UTC 2021-03-22_00:00_CET__2021-03-21_23:00_UTC 2021-03-29_00:00_CEST__2021-03-28_22:00_UTC 2021-04-05_00:00_CEST__2021-04-04_22:00_UTC ].join("\n")) end end it 'correctly increments into DST (gh-53 c)' do in_zone 'Europe/Zurich' do #c = Fugit::Nat.parse('every monday at midnight') c = Fugit::Cron.parse('0 0 * * 2') t = EtOrbi::EoTime.parse('2021-03-14 12:00:00') points = 4.times.collect do t = c.next_time(t) tu = t.dup.utc "#{t.strftime('%F_%H:%M_%Z')}__#{tu.strftime('%F_%H:%M_%Z')}" end expect(points.join("\n")).to eq(%w[ 2021-03-16_00:00_CET__2021-03-15_23:00_UTC 2021-03-23_00:00_CET__2021-03-22_23:00_UTC 2021-03-30_00:00_CEST__2021-03-29_22:00_UTC 2021-04-06_00:00_CEST__2021-04-05_22:00_UTC ].join("\n")) end end it 'correctly increments into DST (gh-53 d)' do in_zone 'Europe/Zurich' do #c = Fugit::Nat.parse('every monday at midnight') #c = Fugit::Cron.parse('0 0 * * 2') c = Fugit::Nat.parse('every tuesday at 00:00') t = EtOrbi::EoTime.parse('2021-03-14 12:00:00') points = 4.times.collect do t = c.next_time(t) tu = t.dup.utc "#{t.strftime('%F_%H:%M_%Z')}__#{tu.strftime('%F_%H:%M_%Z')}" end expect(points.join("\n")).to eq(%w[ 2021-03-16_00:00_CET__2021-03-15_23:00_UTC 2021-03-23_00:00_CET__2021-03-22_23:00_UTC 2021-03-30_00:00_CEST__2021-03-29_22:00_UTC 2021-04-06_00:00_CEST__2021-04-05_22:00_UTC ].join("\n")) end end it 'correctly increments into DST (America/New_York) (gh-63)' do in_zone 'America/New_York' do c = Fugit::Cron.parse('30 4 * * *') #t = EtOrbi::EoTime.parse('2021-03-11 12:00:00') t = Time.parse('2021-03-11 12:00:00') points = 6.times.collect do t = c.next_time(t) tu = t.dup.utc "#{t.strftime('%F_%H:%M_%Z')}__#{tu.strftime('%F_%H:%M_%Z')}" end expect(points.join("\n")).to eq(%w[ 2021-03-12_04:30_EST__2021-03-12_09:30_UTC 2021-03-13_04:30_EST__2021-03-13_09:30_UTC 2021-03-14_04:30_EDT__2021-03-14_08:30_UTC 2021-03-15_04:30_EDT__2021-03-15_08:30_UTC 2021-03-16_04:30_EDT__2021-03-16_08:30_UTC 2021-03-17_04:30_EDT__2021-03-17_08:30_UTC ].join("\n")) end end it 'correctly increments out of DST (gh-53 e)' do in_zone 'Europe/Zurich' do #c = Fugit::Nat.parse('every monday at midnight') c = Fugit::Cron.parse('0 0 * * 2') t = EtOrbi::EoTime.parse('2021-10-18 12:00:00') points = 4.times.collect do t = c.next_time(t) tu = t.dup.utc "#{t.strftime('%F_%H:%M_%Z')}__#{tu.strftime('%F_%H:%M_%Z')}" end expect(points.join("\n")).to eq(%w[ 2021-10-19_00:00_CEST__2021-10-18_22:00_UTC 2021-10-26_00:00_CEST__2021-10-25_22:00_UTC 2021-11-02_00:00_CET__2021-11-01_23:00_UTC 2021-11-09_00:00_CET__2021-11-08_23:00_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 it 'correctly increments into DST (America/Santiago) gh-60' do in_zone 'America/Santiago' do c = Fugit.parse('0 8 15 * *') t = EtOrbi::EoTime.parse('2021-06-18 01:00:00') points = 6.times .collect { t = c.next_time(t) t.to_zs + ' // ' + t.to_t.to_s } .join("\n") expect(points).to eq(%{ 2021-07-15 08:00:00 America/Santiago // 2021-07-15 08:00:00 -0400 2021-08-15 08:00:00 America/Santiago // 2021-08-15 08:00:00 -0400 2021-09-15 08:00:00 America/Santiago // 2021-09-15 08:00:00 -0300 2021-10-15 08:00:00 America/Santiago // 2021-10-15 08:00:00 -0300 2021-11-15 08:00:00 America/Santiago // 2021-11-15 08:00:00 -0300 2021-12-15 08:00:00 America/Santiago // 2021-12-15 08:00:00 -0300 }.strip.split("\n").collect(&:strip).join("\n")) expect( c.brute_frequency(2021).occurrences ).to eq(12) end end it 'correctly increments into DST (America/Santiago) Time.zone gh-62' do in_active_support_zone 'America/Santiago' do c = Fugit.parse('0 8 15 * *') t = EtOrbi::EoTime.parse('2021-06-18 01:00:00') points = 6.times .collect { t = c.next_time(t) t.to_zs + ' // ' + t.to_t.dup.utc.to_s } .join("\n") expect(points).to eq(%{ 2021-07-15 08:00:00 America/Santiago // 2021-07-15 12:00:00 UTC 2021-08-15 08:00:00 America/Santiago // 2021-08-15 12:00:00 UTC 2021-09-15 08:00:00 America/Santiago // 2021-09-15 11:00:00 UTC 2021-10-15 08:00:00 America/Santiago // 2021-10-15 11:00:00 UTC 2021-11-15 08:00:00 America/Santiago // 2021-11-15 11:00:00 UTC 2021-12-15 08:00:00 America/Santiago // 2021-12-15 11:00:00 UTC }.strip.split("\n").collect(&:strip).join("\n")) expect( c.brute_frequency(2021).occurrences ).to eq(12) end end it 'correctly increments into DST (America/Santiago) Time.zone gh-62 hourly' do in_active_support_zone 'America/Santiago' do c = Fugit.parse('0 * * * *') t = EtOrbi::EoTime.parse('2021-09-04 21:00:00') points = 6.times .collect { t = c.next_time(t) t.to_zs + ' // ' + t.to_t.dup.utc.to_s } .join("\n") expect(points).to eq( %{ 2021-09-04 22:00:00 America/Santiago // 2021-09-05 02:00:00 UTC 2021-09-04 23:00:00 America/Santiago // 2021-09-05 03:00:00 UTC 2021-09-05 01:00:00 America/Santiago // 2021-09-05 04:00:00 UTC 2021-09-05 02:00:00 America/Santiago // 2021-09-05 05:00:00 UTC 2021-09-05 03:00:00 America/Santiago // 2021-09-05 06:00:00 UTC 2021-09-05 04:00:00 America/Santiago // 2021-09-05 07:00:00 UTC } .strip.split("\n").map(&:strip) .reject { |l| l[0, 1] == '#' }.join("\n")) expect( c.brute_frequency(2021).occurrences ).to eq(8759) end end it 'correctly increments out of DST (America/Santiago) gh-60' do in_zone 'America/Santiago' do c = Fugit.parse('0 8 15 * *') t = EtOrbi::EoTime.parse('2021-01-12 01:00:00') points = 6.times .collect { t = c.next_time(t) t.to_zs + ' // ' + t.to_t.to_s } .join("\n") expect(points).to eq(%{ 2021-01-15 08:00:00 America/Santiago // 2021-01-15 08:00:00 -0300 2021-02-15 08:00:00 America/Santiago // 2021-02-15 08:00:00 -0300 2021-03-15 08:00:00 America/Santiago // 2021-03-15 08:00:00 -0300 2021-04-15 08:00:00 America/Santiago // 2021-04-15 08:00:00 -0400 2021-05-15 08:00:00 America/Santiago // 2021-05-15 08:00:00 -0400 2021-06-15 08:00:00 America/Santiago // 2021-06-15 08:00:00 -0400 }.strip.split("\n").collect(&:strip).join("\n")) expect( c.brute_frequency(2021).occurrences ).to eq(12) end end it 'correctly increments out of DST (America/Santiago) Time.zone gh-62' do in_active_support_zone 'America/Santiago' do c = Fugit.parse('0 8 15 * *') t = EtOrbi::EoTime.parse('2021-01-12 01:00:00') points = 6.times .collect { t = c.next_time(t) t.to_zs + ' // ' + t.to_t.dup.utc.to_s } .join("\n") expect(points).to eq(%{ 2021-01-15 08:00:00 America/Santiago // 2021-01-15 11:00:00 UTC 2021-02-15 08:00:00 America/Santiago // 2021-02-15 11:00:00 UTC 2021-03-15 08:00:00 America/Santiago // 2021-03-15 11:00:00 UTC 2021-04-15 08:00:00 America/Santiago // 2021-04-15 12:00:00 UTC 2021-05-15 08:00:00 America/Santiago // 2021-05-15 12:00:00 UTC 2021-06-15 08:00:00 America/Santiago // 2021-06-15 12:00:00 UTC }.strip.split("\n").collect(&:strip).join("\n")) expect( c.brute_frequency(2021).occurrences ).to eq(12) 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 end after :each do unrequire_chronic end it "doesn't stall or loop ad infinitum" do in_active_support_zone('UTC') do cron = Fugit.do_parse_cron('0 0 1 1 *') expect { cron.next_time }.not_to raise_error end end end context 'New York skip (gh-43)' do it "doesn't skip" do cron = Fugit.parse('0 8-19/4 * * *') st = Time.parse('2020-09-11 12:00:00') nt = cron.next_time(st) #p nt #p nt.to_s #p nt.to_local_time #p nt.utc.to_s expect(nt.to_s).to match(/ 16:00:00 /) end it "doesn't skip (TZ UTC)" do in_zone('UTC') do cron = Fugit.parse('0 8-19/4 * * *') st = Time.parse('2020-09-11 12:00:00') nt = cron.next_time(st) #p nt #p nt.to_s #p nt.to_local_time #p nt.utc.to_s expect(nt.utc.to_s).to match(/ 16:00:00 /) end end it "doesn't skip (ActiveSupport TZ America/New_York)" do in_active_support_zone('America/New_York') do #EtOrbi._make_info #p EtOrbi.determine_local_tzone cron = Fugit.parse('0 8-19/4 * * *') st = Time.parse('2020-09-11 12:00:00 UTC') #p st nt = cron.next_time(st) #p nt #p nt.to_s #p nt.to_local_time #p nt.utc.to_s expect(nt.utc.to_s).to match(/ 16:00:00 /) end end it 'does not break on "* * * * 1%2+2" (gh-47)' do cron0 = Fugit.parse('0 8 * * 1%2+2') cron1 = Fugit.parse('0 8 * * 1%2') expect(cron0.next_time('2021-04-21 07:00:00').to_s ).to match(/^2021-05-03 08:00:00 /) expect(cron0.next_time('2021-04-21 07:00:00').to_s ).to eq(cron1.next_time('2021-04-21 07:00:00').to_s) end it 'does not break on "30 14 * * 4%4+3" (gh-76)' do t0 = Time.utc(2022, 9, 12, 12, 1, 1, 0) nt = Fugit::Cron.do_parse('30 14 * * 4%4+3 Australia/Melbourne') .next_time(t0) #expect(nt.to_s).to eq(/^2022-10-06 03:30:00 (Z|\+0000)$/) # post gh-47 expect(nt.to_s).to match(/^2022-09-22 04:30:00 (Z|\+0000)$/) # post gh-76 in_zone('Australia/Melbourne') do #expect(nt.to_t.to_s).to eq('2022-10-06 14:30:00 +1100') # post gh-47 expect(nt.to_t.to_s).to eq('2022-09-22 14:30:00 +1000') # post gh-76 end end end end describe '#match?' do NEXT_TIMES.each do |cron, next_time, _, zone_name| it "succeeds #{cron.inspect} ? #{next_time.inspect}" do in_zone(zone_name) do c = Fugit::Cron.parse(cron) ent = Time.parse(next_time) expect(c.match?(ent)).to be(true) end 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 20 31 oct *', '2019-10-31 20:00', '2019-11-01' ], # gh-51 [ '0 9 29 feb *', '2016-02-29 09:00', '2019-03-23' ], # gh-18 [ '59 6 1-7 * 2', '2020-03-10 06:59:00', '2020-03-15 07:47' ], # not '2020-03-03 06:59:00', tuesday 2 matches [ '59 6 1-7 * 2', '2020-03-03 06:59:00', '2020-03-04 06:00' ], # yes, either tuesday 2 and monthday 1-7 match [ '59 6 1-7 * 2', '2020-02-25 06:59:00', '2020-03-01 06:00' ], # not '2020-02-04 06:59:00', tuesday 2 matches # # gh-35 # # From `man 5 crontab` # # 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. ] 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', '0 0 */2 * *' => '2d', '0 0 */2 * * Europe/Berlin' => '2d', '0 0 */3 * *' => '3d', '0 0 * * */2' => '1d', }.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 * * *' ], [ '@noon', '0 12 * * *' ], [ '@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/15 * * * *', '0,15,30,45 * * * *' ], # [ '15/30 * * * *', '15,45 * * * *' ], # gh-56 [ '15-40/30 * * * *', '15 * * * *' ], # [ '0 4/12 * * *', '0 4,16 * * *' ], # [ '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 [ '59 6 1-7 * 2', '59 6 1,2,3,4,5,6,7 * 2' ], # # gh-35 [ '0 8-19/4 * * *', '0 8,12,16 * * *' ], # # gh-43 [ '1 /1 * * *', '1 * * * *' ], [ '1 */1 * * *', '1 * * * *' ], [ '1 0/1 * * *', '1 * * * *' ], [ '1 0-23/1 * * *', '1 * * * *' ], # # cronds differ, see https://crontab.guru/#7_0/1_*_*_* # 2022-09-20, gh-79, changing... # [ '0 * * * *', '0 * * * *' ], # gh-79 [ '0 * * * * Asia/Kabul', '0 * * * * Asia/Kabul' ], # [ '0 /1 * * *', '0 * * * *' ], # [ '0 /1 * * * Asia/Kabul', '0 * * * * Asia/Kabul' ], # [ '0 */1 * * *', '0 * * * *' ], # [ '0 */1 * * * Asia/Kabul', '0 * * * * Asia/Kabul' ], # [ '0 0/1 * * *', '0 * * * *' ], # [ '0 0/1 * * * Asia/Kabul', '0 * * * * Asia/Kabul' ], # # [ '0 7/5 * * *', '0 7,12,17,22 * * *' ], [ '0 7/5 * * * Asia/Kabul', '0 7,12,17,22 * * * Asia/Kabul' ], [ '0 7-17/5 * * *', '0 7,12,17 * * *' ], [ '0 7-17/5 * * * Asia/Kabul', '0 7,12,17 * * * Asia/Kabul' ], [ '0 0/4 * * *', '0 0,4,8,12,16,20 * * *' ], [ '0/30 * * * *', '0,30 * * * *' ], [ '0/10 * * * 1-5 Africa/Juba', '0,10,20,30,40,50 * * * 1,2,3,4,5 Africa/Juba' ], [ '0 4/12 * * *', '0 4,16 * * *' ], [ '0 0/12 * * * -09:00', '0 0,12 * * * -09:00' ], [ '30 7/4 * * *', '30 7,11,15,19,23 * * *' ], [ '9,,19 * * * *', '9,19 * * * *' ], [ ',8 * * * *', '8 * * * *' ], [ ',,10,,20, * * * *', '10,20 * * * *' ], [ '10,,20 * 1,,11,,21, * *', '10,20 * 1,11,21 * *' ], [ ',,10,,22, * * * * Asia/Omsk', '10,22 * * * * Asia/Omsk' ], ].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 * *' ], [ '* * 25-l * *', "* * #{(25..31).to_a.map(&:to_s).join(',')} * *" ], [ '* * 25-L * *', "* * #{(25..31).to_a.map(&:to_s).join(',')} * *" ], # not so negative... ].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 nil, '', ' ', '* 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 cron_s = { '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 13 * * wed' => '0 13 * * 3', '0 13 * * wed%2' => '0 13 * * 3%2', '0 13 * * wed%2+1' => '0 13 * * 3%2+1', '0 0 L * *' => '0 0 -1 * *', '23 1-15/2 * * *' => '23 1,3,5,7,9,11,13,15 * * *', '* * * * sun#L' => '* * * * 0#-1', } cron_s.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 cron_s.each do |k, _| it "produces the same cron string if parsing (#{k.inspect} to_cron_s)" do ck = Fugit::Cron.parse(k) cks = Fugit::Cron.parse(ck.to_cron_s) expect(cks).to eq(ck) end 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 describe Fugit do describe '#parse_cron' do [ [ "45 5 * * sun", '45 5 * * 0' ], [ "\n45 5 * * sun \n ", '45 5 * * 0' ], [ "* * last * *", '* * -1 * *' ], [ " * * last * *\n", '* * -1 * *' ], [ "30 18 * * 2#5 Europe/Paris", '30 18 * * 2#5 Europe/Paris' ], [ " 30 18 * * 2#5 Europe/Paris \n", '30 18 * * 2#5 Europe/Paris' ], ].each do |src, cron_s| it "parses #{src.inspect}" do r = Fugit.parse_cron(src) expect(r.class).to eq(Fugit::Cron) expect(r.original).to eq(src) expect(r.to_cron_s).to eq(cron_s) end end end end fugit-1.8.1/spec/duration_spec.rb000066400000000000000000000420461436236302600167700ustar00rootroot00000000000000 # # 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 [ [ "7d7", 7 * 24 * 3600 + 7 ], [ " 7d7\n \n", 7 * 24 * 3600 + 7 ], [ " 7d 7\n \n", 7 * 24 * 3600 + 7 ], ].each do |src, sec| it "strips and parses #{src.inspect}" do d = Fugit::Duration.parse(src) expect(d.original).to eq(src) 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' ], [ '-2M', '2016-02-02', '2015-12-02' ], [ '-15M', '2016-02-02', '2014-11-02' ], [ '-1Y1m17s', '2016-12-30 17:00:00', '2015-12-30 16:58:43' ], [ '10M11m12s', '2016-03-01 16:48:48', '2017-01-01 17:00:00' ], [ '-10M0m0s', '2016-12-30 17:00:00', '2016-03-01 17:00:00' ], [ '-10M11m12s', '2016-12-30 17:00:00', '2016-03-01 16:48:48' ], # # 2016 is a bissextile year ;-) # still "December 30" minus 10 months equals "March 1", ponder... ].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 'subtracts Time instances' do d = Fugit.parse('1Y2h') t = Time.parse('2016-02-02T11:11:11Z') expect(Fugit.time_to_plain_s(d.subtract(t), false)).to eq('2015-02-02 09:11:11') expect(Fugit.time_to_plain_s(d - t, false)).to eq('2015-02-02 09:11:11') 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.8.1/spec/nat_spec.rb000066400000000000000000000264661436236302600157350ustar00rootroot00000000000000 # # 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' => '0 0 * * 1-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 monday' => '0 0 * * 1', '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' => '* * * * * *', # those 3 are moved below for gh-37 'every 12 hours at minute 50' => '50 */12 * * *', 'every 12h at min 50' => '50 */12 * * *', # # gh-41 'every 1st of the month at midnight' => '0 0 1 * *', 'every first of the month at midnight' => '0 0 1 * *', 'Every 2nd of the month at 10:00' => '0 10 2 * *', 'Every second of the month at 10:00' => '0 10 2 * *', 'every month on day 2 at 10:00' => '0 10 2 * *', 'every month on day 2 and 5 at 10:00' => '0 10 2,5 * *', 'every month on days 1,15 at 10:00' => '0 10 1,15 * *', 'every month on the 1st at 11:00' => '0 11 1 * *', 'every 15th of the month' => '0 0 15 * *', # gh-38 title # # gh-38 # 'every month on the 1st and 2nd at 12:00 pm' => '0 12 1,2 * *', 'every month on the 1st and the 2nd at 12:00 pm' => '0 12 1,2 * *', 'every month on the 1st and the second at 12:00 pm' => '0 12 1,2 * *', # # gh-57, 12pm --> noon #'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 8:30' => '30 8 * * *', 'every day at 08:30' => '30 8 * * *', 'every day at 8:30 am' => '30 8 * * *', 'every day at 08:30 am' => '30 8 * * *', 'every day at 8:30 AM' => '30 8 * * *', 'every day at 8:30 pm' => '30 20 * * *', 'every day at 08:30 pm' => '30 20 * * *', 'every day at 08:30 PM' => '30 20 * * *', # # gh-42 'every day at 5pm' => '0 17 * * *', 'every day at 5:00pm' => '0 17 * * *', 'every day at 5:00 pm' => '0 17 * * *', # 'every day at 12am' => '0 0 * * *', 'every day at 12pm' => '0 12 * * *', 'every day at 12:00am' => '0 0 * * *', 'every day at 12:00pm' => '0 12 * * *', 'every day at 12:00 am' => '0 0 * * *', 'every day at 12:00 pm' => '0 12 * * *', 'every day at 12:15am' => '15 0 * * *', 'every day at 12:15pm' => '15 12 * * *', 'every day at 12:15 am' => '15 0 * * *', 'every day at 12:15 pm' => '15 12 * * *', # 'every day at 12 noon' => '0 12 * * *', 'every day at 12 midnight' => '0 24 * * *', 'every day at 12:00 noon' => '0 12 * * *', 'every day at 12:00 midnight' => '0 24 * * *', 'every day at 12:15 noon' => '15 12 * * *', 'every day at 12:15 midnight' => '15 24 * * *', # # gh-81 #'every day at 18:15 and 20:45' => '* * * * *', # # gh-24 see below 'every friday and thursday' => '0 0 * * 4,5', '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 * * 1-4', 'every Mon to Thu, 18:20' => '20 18 * * 1-4', 'every mon-thu at 18:20' => '20 18 * * 1-4', 'every Monday to Thursday at 18:20' => '20 18 * * 1-4', 'every Monday through Friday at 19:20' => '20 19 * * 1-5', 'from Monday through Friday at 19:21' => '21 19 * * 1-5', 'from Monday to Friday at 19:22' => '22 19 * * 1-5', # # 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 'every second' => '* * * * * *', 'every 1 second' => '* * * * * *', 'every minute' => '* * * * *', 'every 1 minute' => '* * * * *', 'every hour' => '0 * * * *', 'every 1 hour' => '0 * * * *', 'every day' => '0 0 * * *', 'every 1 day' => '0 0 * * *', 'every week' => '0 0 * * 0', 'every 1 week' => '0 0 * * 0', 'every month' => '0 0 1 * *', 'every 1 month' => '0 0 1 * *', 'every year' => '0 0 1 1 *', 'every 1 year' => '0 0 1 1 *', # # gh-37 # 'every minute at second 10' => '10 * * * * *', 'every minute at second 10 and 40' => '10,40 * * * * *', 'every minute at secs 10 and 40' => '10,40 * * * * *', 'every hour at min 11' => '11 * * * *', 'every day at 18:22' => '22 18 * * *', 'every week on monday 18:23' => '23 18 * * 1', 'every monday 18:24' => '24 18 * * 1', 'every month at 19:10' => '10 19 1 * *', 'every year at 20:10' => '10 20 1 1 *', 'every day at zero dark twenty' => '20 0 * * *', 'every day at one dark fifty' => '50 1 * * *', #'every day at oh dark fourty' => '40 0 * * *', 'every day at noon' => '0 12 * * *', 'every day at midnight' => '0 0 * * *', 'every 2 days' => '0 0 */2 * *', 'every 2 days at 17:00' => '0 17 */2 * *', 'every 2 months' => '0 0 1 */2 *', 'every day from the 25th to the last' => '0 0 25-L * *', 'every day at noon from the 25th to the last' => '0 12 25-L * *', 'from the 25th to the last' => '0 0 25-L * *', 'from the 25th to the last, at noon and midnight' => '0 0,12 25-L * *', # # gh-45 'every weekday 8am to 5pm' => '0 8-17 * * 1-5', 'every weekday 8am to 5pm on the hour' => '0 8-17 * * 1-5', 'every weekday 8am to 5pm on the minute' => '* 8-16 * * 1-5', 'every weekday 8am to 5pm on minute 10 and 30' => '10,30 8-16 * * 1-5', 'every hour, 8am to 5pm' => '0 8-17 * * *', 'every hour, from 8am to 5pm' => '0 8-17 * * *', 'every minute, 8am to 5pm' => '* 8-16 * * *', 'every minute from 8am to 5pm' => '* 8-16 * * *', # # gh-44 'at 12:00 PM' => '0 12 * * *', 'at 12:00PM' => '0 12 * * *', 'at 12 PM' => '0 12 * * *', 'at 12PM' => '0 12 * * *', 'at 12:00 pm' => '0 12 * * *', 'at 12:00pm' => '0 12 * * *', 'at 12 pm' => '0 12 * * *', 'at 12pm' => '0 12 * * *', 'at noon' => '0 12 * * *', # # gh-57 # 'at 12 noon' => '0 12 * * *', 'at 12 Noon' => '0 12 * * *', 'at 12 NOON' => '0 12 * * *', 'at 12 midday' => '0 12 * * *', 'at 12 midnight' => '0 24 * * *', # minute hour day-of-month month day-of-week }.each do |nat, cron| it "parses #{nat.inspect} into #{cron.inspect}" do c = Fugit::Nat.parse(nat) #File.open('out.rb', 'ab') { |f| f.puts("\n#{nat.inspect}\n #{c.inspect}") } #p c #expect(c).not_to eq(nil) 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 * * 5-0 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) #File.open('out.rb', 'ab') { |f| f.puts("\n#{nat.inspect}\n #{r.inspect}") } #p r #expect(r).not_to eq(nil) 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 it 'rejects strings that cannot be turned into crons' do expect(Fugit::Nat.parse('every 2 years')).to eq(nil) expect(Fugit::Nat.parse('every 2 weeks')).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 describe Fugit do describe '.parse_nat' do { "every day at five" => '0 5 * * *', "every day at 5 pm in Asia/Tokyo" => '0 17 * * * Asia/Tokyo', " \nevery day at five \n" => '0 5 * * *', "\n every day at 5 pm in Asia/Tokyo\n" => '0 17 * * * Asia/Tokyo', }.each do |src, cron_s| it "strips and parses #{src.inspect}" do r = Fugit.parse_nat(src) expect(r.class).to eq(Fugit::Cron) expect(r.to_cron_s).to eq(cron_s) end end end end fugit-1.8.1/spec/parse_spec.rb000066400000000000000000000113041436236302600162460ustar00rootroot00000000000000 # # Specifying fugit # # Tue Jan 3 11:19:52 JST 2017 Ishinomaki # require 'spec_helper' describe Fugit do describe '.parse' do CASES = { '2017-01-03 11:21:17' => [ EtOrbi::EoTime, '2017-01-03 11:21:17 Z' ], '00 00 L 5 *' => [ Fugit::Cron, '0 0 -1 5 *' ], '1Y3M2d' => [ Fugit::Duration, '1Y3M2D' ], '1Y2h' => [ Fugit::Duration, '1Y2h' ], '0 0 1 jan *' => [ Fugit::Cron, '0 0 1 1 *' ], '12y12M' => [ Fugit::Duration, '12Y12M' ], '2017-12-12' => [ EtOrbi::EoTime, '2017-12-12 00:00:00 Z' ], 'every day at noon' => [ Fugit::Cron, '0 12 * * *' ], 'at 12:00 PM' => [ Fugit::Cron, '0 12 * * *' ], 'at 12 PM' => [ Fugit::Cron, '0 12 * * *' ], 'at noon' => [ Fugit::Cron, '0 12 * * *' ], # testing nat: false and cron: false # [ '* * * * 1', { nat: false } ] => [ Fugit::Cron, '* * * * 1' ], [ 'every day at noon', { cron: false } ] => [ Fugit::Cron, '0 12 * * *' ], # [ 'every day at noon', { nat: false } ] => nil, [ '* * * * 1', { cron: false } ] => nil, true => nil, 'I have a pen, I have an apple, pen apple' => nil, 'every day at noon' => [ Fugit::Cron, '0 12 * * *' ], '0 0 1 jan *' => [ Fugit::Cron, '0 0 1 1 *' ], 'at 12 PM' => [ Fugit::Cron, '0 12 * * *' ], 'at noon' => [ Fugit::Cron, '0 12 * * *' ], } CASES.each do |k, (c, s)| k, opts = k t = k.inspect + (opts ? ' ' + opts.inspect : '') opts ||= {} it "parses #{t} into #{c} / #{s.inspect}" do c = c || NilClass x = in_zone('UTC') { Fugit.parse(k, opts) } expect(x.class).to eq(c) expect( case x when EtOrbi::EoTime then Fugit.time_to_plain_s(x) when Fugit::Duration then x.to_plain_s when Fugit::Cron then x.to_cron_s else nil end ).to eq(s) end end CASES.each do |k, (c, s)| k, opts = k t = k.inspect + (opts ? ' ' + opts.inspect : '') t = " \n #{t} \n " opts ||= {} it "parses #{t.inspect} into #{c} / #{s.inspect}" do c = c || NilClass x = in_zone('UTC') { Fugit.parse(k, opts) } expect(x.class).to eq(c) expect( case x when EtOrbi::EoTime then Fugit.time_to_plain_s(x) when Fugit::Duration then x.to_plain_s when Fugit::Cron then x.to_cron_s else nil end ).to eq(s) end 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 [ 'I have a pen, I have an apple, pineapple!', #'0 13 * * 3#2#0', # gh-68 and gh-69 ].each do |k| it "fails when attempting to parse #{k.inspect}" do expect { Fugit.do_parse(k) }.to raise_error(ArgumentError) end end end CRONISHES = { '* * * * *' => '* * * * *', 'every day' => '0 0 * * *', '2022-12-5 11:32' => ArgumentError, 'nada' => ArgumentError, '100 * * * *' => ArgumentError, } describe '.parse_cronish' do CRONISHES.each do |k, v| if v.is_a?(String) it "parses #{k.inspect} to #{v.inspect}" do r = Fugit.parse_cronish(k) expect(r.class).to eq(Fugit::Cron) expect(r.original).to eq(v) end else it "returns nil for #{k.inspect}" do expect(Fugit.parse_cronish(k)).to eq(nil) end end end end describe '.do_parse_cronish' do CRONISHES.each do |k, v| if v.is_a?(String) it "parses #{k.inspect} to #{v.inspect}" do r = Fugit.do_parse_cronish(k) expect(r.class).to eq(Fugit::Cron) expect(r.original).to eq(v) end else it "fails on #{k.inspect}" do expect { Fugit.do_parse_cronish(k) }.to raise_error(v) end end 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.8.1/spec/spec_helper.rb000066400000000000000000000032551436236302600164210ustar00rootroot00000000000000 # # 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 in_active_support_zone(zone_name, &block) prev_tz = ENV['TZ'] ENV['TZ'] = nil # else it takes over Time._zone = zone_name Time.module_eval do class << self def zone; @zone; end end end block.call ensure Time._zone = nil Time.module_eval do class << self undef_method :zone end end 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 # .zone itself is defined/undefined in the #in_active_support_zone # spec helper defined above attr_reader :_zone def _zone=(name) @zone = if name OpenStruct.new(tzinfo: ::TZInfo::Timezone.get(name)) else nil end end end end fugit-1.8.1/tst/000077500000000000000000000000001436236302600134565ustar00rootroot00000000000000fugit-1.8.1/tst/README.md000066400000000000000000000000401436236302600147270ustar00rootroot00000000000000 ## tst/ Various experiments. fugit-1.8.1/tst/gh_63_new_york_dst.rb000066400000000000000000000017731436236302600175100ustar00rootroot00000000000000 #require 'rufus-scheduler' require 'fugit' # https://www.timeanddate.com/time/zone/usa/chicago # https://www.timeanddate.com/time/change/usa/chicago # https://www.timeanddate.com/time/zone/usa/newyork # https://www.timeanddate.com/time/change/usa/newyork ENV['TZ'] = ARGV.find { |a| a.match?(/chi/i) } ? 'America/Chicago' : 'America/New_York' # # using ENV['TZ'] so that Time#to_s stays in ENV['TZ'} c = Fugit.parse_cron( ARGV.find { |a| a.match?(/4/) } ? '30 4 * * *' : '5 0 * * *') p [ :tz, ENV['TZ'] ] p [ :ruby, RUBY_VERSION, RUBY_PLATFORM ] p [ :tzinfo, TZInfo::VERSION ] p [ :fugit, Fugit::VERSION ] # into daylight saving time puts t = Time.parse('2021-03-10') 7.times do t = c.next_time(t) puts "#{t} / #{t.zone}" end # out of daylight saving time puts t = Time.parse('2021-11-05') 7.times do t = c.next_time(t) puts "#{t} / #{t.zone}" end # into daylight saving time puts t = Time.parse('2022-03-10') 7.times do t = c.next_time(t) puts "#{t} / #{t.zone}" end fugit-1.8.1/tst/gh_64_twentyninth.rb000066400000000000000000000021701436236302600173650ustar00rootroot00000000000000 MAX_YEAR = 2030 #matcher = lambda { |t| # if t.month != 2 # false # elsif t.day != 29 # false # elsif t.wday != 0 && t.wday != 6 # false # else # true # end } #matcher = lambda { |t| # if t.month != 11 || t.day != 11 # false # elsif ! [ 0, 1, 2, 3, 4, 5, 6, 7 ].include?(t.wday) # false # else # true # end } # # ["Fri 2022-11-11", 5] # ["Sat 2023-11-11", 6] # ["Mon 2024-11-11", 1] # ["Tue 2025-11-11", 2] # ["Wed 2026-11-11", 3] # ["Thu 2027-11-11", 4] # ["Sat 2028-11-11", 6] # ["Sun 2029-11-11", 0] # '0 0 11 * 3-6' <-------------- NO NO NO it's mday = 11 OR wday in 3-6 # OR, not AND !!!! # matcher = lambda { |t| if t.day != 11 false elsif ! [ 3, 4, 5, 6 ].include?(t.wday) false else true end } # # ["Fri 2022-02-11", 5] # ["Fri 2022-03-11", 5] # ["Wed 2022-05-11", 3] # ["Sat 2022-06-11", 6] # ["Thu 2022-08-11", 4] # ["Fri 2022-11-11", 5] t = Time.now loop do t = t + 24 * 3600 break if t.year >= MAX_YEAR p [ t.strftime('%a %F'), t.wday ] if matcher[t] end fugit-1.8.1/tst/iteration_count.rb000066400000000000000000000022401436236302600172070ustar00rootroot00000000000000 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.8.1/tst/modulo.rb000066400000000000000000000010611436236302600153000ustar00rootroot00000000000000 def show(mod0, mod1) puts "%#{mod0}+#{mod1}" (mod0 + 1).times do |i| print "%3d: " % [ i ] print "%d %% %d --> %d" % [ i, mod0, i % mod0 ] print " | %d %% %d == %d --> %5s" % [ i, mod0, i % mod0, (i % mod0) == mod1 ] print " | (%d + %d) %% %d == 0 --> %5s" % [ i, mod0, mod1, (i + mod1) % mod0 == 0 ] print " | %d %% %d == %d %% %d --> %5s" % [ i, mod0, mod1, mod0, i % mod0 == mod1 % mod0 ] puts end end puts; show(2, 1) puts; show(3, 2) puts; show(2, 2) puts; show(2, 4) puts; show(4, 3) fugit-1.8.1/tst/modulo_cweek_dead_mondays.rb000066400000000000000000000074671436236302600212050ustar00rootroot00000000000000 # 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.