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.
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"