montrose 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +113 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +13 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +191 -0
- data/Rakefile +21 -0
- data/bin/_guard-core +16 -0
- data/bin/console +7 -0
- data/bin/guard +16 -0
- data/bin/m +16 -0
- data/bin/rake +16 -0
- data/bin/rubocop +16 -0
- data/bin/setup +7 -0
- data/lib/montrose.rb +20 -0
- data/lib/montrose/chainable.rb +210 -0
- data/lib/montrose/clock.rb +77 -0
- data/lib/montrose/errors.rb +5 -0
- data/lib/montrose/frequency.rb +63 -0
- data/lib/montrose/frequency/daily.rb +9 -0
- data/lib/montrose/frequency/hourly.rb +9 -0
- data/lib/montrose/frequency/minutely.rb +9 -0
- data/lib/montrose/frequency/monthly.rb +9 -0
- data/lib/montrose/frequency/weekly.rb +19 -0
- data/lib/montrose/frequency/yearly.rb +9 -0
- data/lib/montrose/options.rb +293 -0
- data/lib/montrose/recurrence.rb +67 -0
- data/lib/montrose/rule.rb +47 -0
- data/lib/montrose/rule/after.rb +27 -0
- data/lib/montrose/rule/before.rb +23 -0
- data/lib/montrose/rule/day_of_month.rb +31 -0
- data/lib/montrose/rule/day_of_week.rb +23 -0
- data/lib/montrose/rule/day_of_year.rb +37 -0
- data/lib/montrose/rule/hour_of_day.rb +23 -0
- data/lib/montrose/rule/month_of_year.rb +23 -0
- data/lib/montrose/rule/nth_day_matcher.rb +32 -0
- data/lib/montrose/rule/nth_day_of_month.rb +63 -0
- data/lib/montrose/rule/nth_day_of_year.rb +63 -0
- data/lib/montrose/rule/time_of_day.rb +33 -0
- data/lib/montrose/rule/total.rb +29 -0
- data/lib/montrose/rule/week_of_year.rb +23 -0
- data/lib/montrose/schedule.rb +42 -0
- data/lib/montrose/stack.rb +51 -0
- data/lib/montrose/utils.rb +32 -0
- data/lib/montrose/version.rb +3 -0
- data/montrose.gemspec +32 -0
- metadata +192 -0
data/bin/console
ADDED
data/bin/guard
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'guard' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('guard', 'guard')
|
data/bin/m
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'm' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('m', 'm')
|
data/bin/rake
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rake' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rake', 'rake')
|
data/bin/rubocop
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rubocop' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rubocop', 'rubocop')
|
data/bin/setup
ADDED
data/lib/montrose.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_support/core_ext/object"
|
3
|
+
require "active_support/core_ext/numeric"
|
4
|
+
require "active_support/core_ext/date"
|
5
|
+
require "active_support/core_ext/time"
|
6
|
+
require "active_support/core_ext/date_time"
|
7
|
+
|
8
|
+
require "montrose/utils"
|
9
|
+
require "montrose/rule"
|
10
|
+
require "montrose/clock"
|
11
|
+
require "montrose/chainable"
|
12
|
+
require "montrose/recurrence"
|
13
|
+
require "montrose/frequency"
|
14
|
+
require "montrose/schedule"
|
15
|
+
require "montrose/stack"
|
16
|
+
require "montrose/version"
|
17
|
+
|
18
|
+
module Montrose
|
19
|
+
extend Chainable
|
20
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require "montrose/options"
|
2
|
+
|
3
|
+
module Montrose
|
4
|
+
module Chainable
|
5
|
+
# Create a recurrence from the given frequency
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# Montrose.every(:hour)
|
9
|
+
# Montrose.every(:hour, interval: 2) #=> every 2 hours
|
10
|
+
# Montrose.every(3.days, starts: 2.days.from_now) #=> every 3 days
|
11
|
+
# Montrose.every(1.year, until: 10.days.from_now)
|
12
|
+
#
|
13
|
+
def every(frequency, options = {})
|
14
|
+
branch options.merge(every: frequency)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a minutely recurrence.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
#
|
21
|
+
# Montrose.minutely
|
22
|
+
# Montrose.minutely(interval: 2) #=> every 2 minutes
|
23
|
+
# Montrose.minutely(starts: 3.days.from_now)
|
24
|
+
# Montrose.minutely(until: 10.days.from_now)
|
25
|
+
# Montrose.minutely(total: 5)
|
26
|
+
# Montrose.minutely(except: Date.tomorrow)
|
27
|
+
#
|
28
|
+
def minutely(options = {})
|
29
|
+
branch options.merge(every: :minute)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create a hourly recurrence.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
#
|
36
|
+
# Montrose.hourly
|
37
|
+
# Montrose.hourly(interval: 2) #=> every 2 hours
|
38
|
+
# Montrose.hourly(starts: 3.days.from_now)
|
39
|
+
# Montrose.hourly(until: 10.days.from_now)
|
40
|
+
# Montrose.hourly(total: 5)
|
41
|
+
# Montrose.hourly(except: Date.tomorrow)
|
42
|
+
#
|
43
|
+
def hourly(options = {})
|
44
|
+
branch options.merge(every: :hour)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a daily recurrence.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
#
|
51
|
+
# Montrose.daily
|
52
|
+
# Montrose.daily(interval: 2) #=> every 2 days
|
53
|
+
# Montrose.daily(starts: 3.days.from_now)
|
54
|
+
# Montrose.daily(until: 10.days.from_now)
|
55
|
+
# Montrose.daily(total: 5)
|
56
|
+
# Montrose.daily(except: Date.tomorrow)
|
57
|
+
#
|
58
|
+
def daily(options = {})
|
59
|
+
branch options.merge(every: :day)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create a weekly recurrence.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# Montrose.weekly(on: 5) #=> 0 = sunday, 1 = monday, ...
|
66
|
+
# Montrose.weekly(on: :saturday)
|
67
|
+
# Montrose.weekly(on: [sunday, :saturday])
|
68
|
+
# Montrose.weekly(on: :saturday, interval: 2)
|
69
|
+
# Montrose.weekly(on: :saturday, total: 5)
|
70
|
+
#
|
71
|
+
def weekly(options = {})
|
72
|
+
branch options.merge(every: :week)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Create a monthly recurrence.
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# Montrose.monthly(on: 15) #=> every 15th day
|
79
|
+
# Montrose.monthly(on: :first, weekday: :sunday)
|
80
|
+
# Montrose.monthly(on: :second, weekday: :sunday)
|
81
|
+
# Montrose.monthly(on: :third, weekday: :sunday)
|
82
|
+
# Montrose.monthly(on: :fourth, weekday: :sunday)
|
83
|
+
# Montrose.monthly(on: :fifth, weekday: :sunday)
|
84
|
+
# Montrose.monthly(on: :last, weekday: :sunday)
|
85
|
+
# Montrose.monthly(on: 15, interval: 2)
|
86
|
+
# Montrose.monthly(on: 15, interval: :monthly)
|
87
|
+
# Montrose.monthly(on: 15, interval: :bimonthly)
|
88
|
+
# Montrose.monthly(on: 15, interval: :quarterly)
|
89
|
+
# Montrose.monthly(on: 15, interval: :semesterly)
|
90
|
+
# Montrose.monthly(on: 15, total: 5)
|
91
|
+
#
|
92
|
+
# The <tt>:on</tt> option can be one of the following:
|
93
|
+
#
|
94
|
+
# * :sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday
|
95
|
+
#
|
96
|
+
def monthly(options = {})
|
97
|
+
branch options.merge(every: :month)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create a yearly recurrence.
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
#
|
104
|
+
# Montrose.yearly(on: [7, 14]) #=> every Jul 14
|
105
|
+
# Montrose.yearly(on: [7, 14], interval: 2) #=> every 2 years on Jul 14
|
106
|
+
# Montrose.yearly(on: [:jan, 14], interval: 2)
|
107
|
+
# Montrose.yearly(on: [:january, 14], interval: 2)
|
108
|
+
# Montrose.yearly(on: [:january, 14], total: 5)
|
109
|
+
#
|
110
|
+
def yearly(options = {})
|
111
|
+
branch options.merge(every: :year)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create a recurrence starting at given timestamp.
|
115
|
+
#
|
116
|
+
# @param [Time, Date] starts_at
|
117
|
+
#
|
118
|
+
def starting(starts_at)
|
119
|
+
merge(starts: starts_at)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Create a recurrence ending at given timestamp.
|
123
|
+
#
|
124
|
+
# @param [Time, Date] ends_at
|
125
|
+
#
|
126
|
+
def ending(ends_at)
|
127
|
+
merge(until: ends_at)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Create a recurrence occurring during date range.
|
131
|
+
#
|
132
|
+
# @param [Range<Date>] date_range
|
133
|
+
#
|
134
|
+
def between(date_range)
|
135
|
+
merge(between: date_range)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Create a recurrence for given days of month
|
139
|
+
#
|
140
|
+
# @param [Fixnum] days (1, 2, -1, ...)
|
141
|
+
#
|
142
|
+
def day_of_month(*days)
|
143
|
+
merge(mday: days)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Create a recurrence for given days of week
|
147
|
+
#
|
148
|
+
# @param [Symbol] weekdays (:sunday, :monday, ...)
|
149
|
+
#
|
150
|
+
def day_of_week(*weekdays)
|
151
|
+
merge(day: weekdays)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Create a recurrence for given days of year
|
155
|
+
#
|
156
|
+
# @param [Fixnum] days (1, 10, 100, ...)
|
157
|
+
#
|
158
|
+
def day_of_year(*days)
|
159
|
+
merge(yday: days)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Create a recurrence for given hours of day
|
163
|
+
#
|
164
|
+
# @param [Fixnum, Range] days (1, 10, 100, ...)
|
165
|
+
#
|
166
|
+
def hour_of_day(*hours)
|
167
|
+
merge(hour: hours)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Create a recurrence for given months of year
|
171
|
+
#
|
172
|
+
# @param [Fixnum, Symbol] months (:january, :april, ...)
|
173
|
+
#
|
174
|
+
def month_of_year(*months)
|
175
|
+
merge(month: months)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Create a recurrence that ends after given number
|
179
|
+
# of occurrences
|
180
|
+
#
|
181
|
+
# @param [Fixnum] total
|
182
|
+
#
|
183
|
+
def total(total)
|
184
|
+
merge(total: total)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Create a recurrence for given weeks of year
|
188
|
+
#
|
189
|
+
# @param [Fixnum] weeks (1, 20, 50)
|
190
|
+
#
|
191
|
+
def week_of_year(*weeks)
|
192
|
+
merge(week: weeks)
|
193
|
+
end
|
194
|
+
|
195
|
+
# @private
|
196
|
+
def merge(opts = {})
|
197
|
+
branch default_options.merge(opts)
|
198
|
+
end
|
199
|
+
|
200
|
+
# @private
|
201
|
+
def default_options
|
202
|
+
@default_options ||= Montrose::Options.new
|
203
|
+
end
|
204
|
+
|
205
|
+
# @private
|
206
|
+
def branch(options)
|
207
|
+
Montrose::Recurrence.new(options)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "montrose/errors"
|
2
|
+
|
3
|
+
module Montrose
|
4
|
+
class Clock
|
5
|
+
def initialize(opts = {})
|
6
|
+
@options = Montrose::Options.new(opts)
|
7
|
+
@time = nil
|
8
|
+
@every = @options.fetch(:every) { fail ConfigurationError, "Required option :every not provided" }
|
9
|
+
@starts = @options.fetch(:starts)
|
10
|
+
@interval = @options.fetch(:interval)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Advances time to new unit by increment and sets
|
14
|
+
# new time as "current" time for next tick
|
15
|
+
#
|
16
|
+
def tick
|
17
|
+
@time = peek
|
18
|
+
end
|
19
|
+
|
20
|
+
def peek
|
21
|
+
return @starts if @time.nil?
|
22
|
+
|
23
|
+
@time.advance(step)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def step
|
29
|
+
@step ||= smallest_step or fail ConfigurationError, "No step for #{@options.inspect}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def smallest_step
|
33
|
+
unit_step(:minute) ||
|
34
|
+
unit_step(:hour) ||
|
35
|
+
unit_step(:day, :mday, :yday) ||
|
36
|
+
unit_step(:week) ||
|
37
|
+
unit_step(:month) ||
|
38
|
+
unit_step(:year)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @private
|
42
|
+
#
|
43
|
+
# Returns hash representing unit and amount to advance time
|
44
|
+
# when options contain given unit as a key or as a value of
|
45
|
+
# the key :every in options
|
46
|
+
#
|
47
|
+
# @options = { every: :day, hour: 8.12 }
|
48
|
+
# unit_step(:minute)
|
49
|
+
# => nil
|
50
|
+
# unit_step(:hour)
|
51
|
+
# => { hour: 1 }
|
52
|
+
#
|
53
|
+
# @options = { every: :hour, interval: 6 }
|
54
|
+
# unit_step(:minute)
|
55
|
+
# => nil
|
56
|
+
# unit_step(:hour)
|
57
|
+
# => { hour: 6 }
|
58
|
+
#
|
59
|
+
def unit_step(unit, *alternates)
|
60
|
+
is_frequency = @every == unit
|
61
|
+
if ([unit] + alternates).any? { |u| @options.key?(u) } && !is_frequency
|
62
|
+
# smallest unit, increment by 1
|
63
|
+
{ step_key(unit) => 1 }
|
64
|
+
elsif is_frequency
|
65
|
+
{ step_key(unit) => @interval }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @private
|
70
|
+
#
|
71
|
+
# Change 'unit' to :units
|
72
|
+
#
|
73
|
+
def step_key(unit)
|
74
|
+
"#{unit}s".to_sym
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "montrose/errors"
|
2
|
+
require "montrose/options"
|
3
|
+
|
4
|
+
module Montrose
|
5
|
+
# Abstract class for special recurrence rule required
|
6
|
+
# in all instances of Recurrence. Frequency describes
|
7
|
+
# the base recurrence interval.
|
8
|
+
#
|
9
|
+
class Frequency
|
10
|
+
include Montrose::Rule
|
11
|
+
|
12
|
+
FREQUENCY_TERMS = {
|
13
|
+
"minute" => "Minutely",
|
14
|
+
"hour" => "Hourly",
|
15
|
+
"day" => "Daily",
|
16
|
+
"week" => "Weekly",
|
17
|
+
"month" => "Monthly",
|
18
|
+
"year" => "Yearly"
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
FREQUENCY_KEYS = FREQUENCY_TERMS.keys.freeze
|
22
|
+
|
23
|
+
attr_reader :time, :starts
|
24
|
+
|
25
|
+
# Factory method for instantiating the appropriate Frequency
|
26
|
+
# subclass.
|
27
|
+
#
|
28
|
+
def self.from_options(opts)
|
29
|
+
frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" }
|
30
|
+
class_name = FREQUENCY_TERMS.fetch(frequency.to_s) do
|
31
|
+
fail "Don't know how to enumerate every: #{frequency}"
|
32
|
+
end
|
33
|
+
|
34
|
+
Montrose::Frequency.const_get(class_name).new(opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @private
|
38
|
+
def self.assert(frequency)
|
39
|
+
FREQUENCY_TERMS.key?(frequency.to_s) or fail ConfigurationError,
|
40
|
+
"Don't know how to enumerate every: #{frequency}"
|
41
|
+
|
42
|
+
frequency.to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(opts = {})
|
46
|
+
opts = Montrose::Options.new(opts)
|
47
|
+
@time = nil
|
48
|
+
@starts = opts.fetch(:starts)
|
49
|
+
@interval = opts.fetch(:interval)
|
50
|
+
end
|
51
|
+
|
52
|
+
def matches_interval?(time_diff)
|
53
|
+
(time_diff % @interval).zero?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
require "montrose/frequency/daily"
|
59
|
+
require "montrose/frequency/hourly"
|
60
|
+
require "montrose/frequency/minutely"
|
61
|
+
require "montrose/frequency/monthly"
|
62
|
+
require "montrose/frequency/weekly"
|
63
|
+
require "montrose/frequency/yearly"
|