montrose 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rubocop.yml +113 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +13 -0
  7. data/Guardfile +34 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +191 -0
  10. data/Rakefile +21 -0
  11. data/bin/_guard-core +16 -0
  12. data/bin/console +7 -0
  13. data/bin/guard +16 -0
  14. data/bin/m +16 -0
  15. data/bin/rake +16 -0
  16. data/bin/rubocop +16 -0
  17. data/bin/setup +7 -0
  18. data/lib/montrose.rb +20 -0
  19. data/lib/montrose/chainable.rb +210 -0
  20. data/lib/montrose/clock.rb +77 -0
  21. data/lib/montrose/errors.rb +5 -0
  22. data/lib/montrose/frequency.rb +63 -0
  23. data/lib/montrose/frequency/daily.rb +9 -0
  24. data/lib/montrose/frequency/hourly.rb +9 -0
  25. data/lib/montrose/frequency/minutely.rb +9 -0
  26. data/lib/montrose/frequency/monthly.rb +9 -0
  27. data/lib/montrose/frequency/weekly.rb +19 -0
  28. data/lib/montrose/frequency/yearly.rb +9 -0
  29. data/lib/montrose/options.rb +293 -0
  30. data/lib/montrose/recurrence.rb +67 -0
  31. data/lib/montrose/rule.rb +47 -0
  32. data/lib/montrose/rule/after.rb +27 -0
  33. data/lib/montrose/rule/before.rb +23 -0
  34. data/lib/montrose/rule/day_of_month.rb +31 -0
  35. data/lib/montrose/rule/day_of_week.rb +23 -0
  36. data/lib/montrose/rule/day_of_year.rb +37 -0
  37. data/lib/montrose/rule/hour_of_day.rb +23 -0
  38. data/lib/montrose/rule/month_of_year.rb +23 -0
  39. data/lib/montrose/rule/nth_day_matcher.rb +32 -0
  40. data/lib/montrose/rule/nth_day_of_month.rb +63 -0
  41. data/lib/montrose/rule/nth_day_of_year.rb +63 -0
  42. data/lib/montrose/rule/time_of_day.rb +33 -0
  43. data/lib/montrose/rule/total.rb +29 -0
  44. data/lib/montrose/rule/week_of_year.rb +23 -0
  45. data/lib/montrose/schedule.rb +42 -0
  46. data/lib/montrose/stack.rb +51 -0
  47. data/lib/montrose/utils.rb +32 -0
  48. data/lib/montrose/version.rb +3 -0
  49. data/montrose.gemspec +32 -0
  50. metadata +192 -0
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "montrose"
5
+
6
+ require "pry"
7
+ Pry.start
@@ -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')
@@ -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')
@@ -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')
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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,5 @@
1
+ module Montrose
2
+ Error = Class.new(StandardError)
3
+ ConfigurationError = Class.new(Error)
4
+ SerializationError = Class.new(Error)
5
+ 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"