montrose 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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"
|