fnando-recurrence 0.0.7 → 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.
- data/History.txt +15 -1
- data/README.markdown +19 -6
- data/Rakefile +2 -3
- data/lib/recurrence.rb +17 -6
- data/lib/recurrence/base.rb +115 -133
- data/lib/recurrence/event.rb +57 -109
- data/lib/recurrence/event/daily.rb +10 -0
- data/lib/recurrence/event/monthly.rb +102 -0
- data/lib/recurrence/event/weekly.rb +27 -0
- data/lib/recurrence/event/yearly.rb +58 -0
- data/lib/recurrence/shortcuts.rb +23 -0
- data/recurrence.gemspec +14 -9
- metadata +10 -12
data/History.txt
CHANGED
@@ -38,4 +38,18 @@
|
|
38
38
|
== 0.0.7 2009-01-06
|
39
39
|
|
40
40
|
* 1 major enhancement:
|
41
|
-
* The events method now accepts :starts and :until dates
|
41
|
+
* The events method now accepts :starts and :until dates
|
42
|
+
|
43
|
+
== 0.0.8 2009-01-07
|
44
|
+
|
45
|
+
* 1 small enhancement:
|
46
|
+
* The iteration is stopped when the :until is reached when limiting events
|
47
|
+
|
48
|
+
== 0.1.0 2009-07-18
|
49
|
+
|
50
|
+
* 3 major enhancements:
|
51
|
+
* Allow monthly recurrences to occur by weekday:
|
52
|
+
Recurrence.new(:every => :month, :on => :first, :weekday => :thursday)
|
53
|
+
* Allow several weekdays to be given to weekly recurrence:
|
54
|
+
Recurrence.new(:every => :week, :on => [:monday, :wednesday, :friday])
|
55
|
+
* Added :daily, :weekly, :monthly and :yearly shortcuts to Recurrence.
|
data/README.markdown
CHANGED
@@ -38,18 +38,26 @@ USAGE:
|
|
38
38
|
r = Recurrence.new(:every => :day, :interval => 9)
|
39
39
|
|
40
40
|
# Weekly
|
41
|
-
r = Recurrence.new(:every => :week, :on => :friday)
|
42
41
|
r = Recurrence.new(:every => :week, :on => 5)
|
42
|
+
r = Recurrence.new(:every => :week, :on => :monday)
|
43
|
+
r = Recurrence.new(:every => :week, :on => [:monday, :friday])
|
44
|
+
r = Recurrence.new(:every => :week, :on => [:monday, :wednesday, :friday])
|
43
45
|
r = Recurrence.new(:every => :week, :on => :friday, :interval => 2)
|
44
46
|
|
45
|
-
# Monthly
|
47
|
+
# Monthly by month day
|
46
48
|
r = Recurrence.new(:every => :month, :on => 15)
|
47
49
|
r = Recurrence.new(:every => :month, :on => 31)
|
48
50
|
r = Recurrence.new(:every => :month, :on => 7, :interval => 2)
|
49
51
|
r = Recurrence.new(:every => :month, :on => 7, :interval => :monthly)
|
50
52
|
r = Recurrence.new(:every => :month, :on => 7, :interval => :bimonthly)
|
51
|
-
|
52
|
-
|
53
|
+
|
54
|
+
# Monthly by week day
|
55
|
+
r = Recurrence.new(:every => :month, :on => :first, :weekday => :sunday)
|
56
|
+
r = Recurrence.new(:every => :month, :on => :third, :weekday => :monday)
|
57
|
+
r = Recurrence.new(:every => :month, :on => :last, :weekday => :friday)
|
58
|
+
r = Recurrence.new(:every => :month, :on => :last, :weekday => :friday, :interval => 2)
|
59
|
+
r = Recurrence.new(:every => :month, :on => :last, :weekday => :friday, :interval => :quarterly)
|
60
|
+
r = Recurrence.new(:every => :month, :on => :last, :weekday => :friday, :interval => :semesterly)
|
53
61
|
|
54
62
|
# Yearly
|
55
63
|
r = Recurrence.new(:every => :year, :on => [7, 4]) # => [month, day]
|
@@ -86,7 +94,12 @@ USAGE:
|
|
86
94
|
MAINTAINER
|
87
95
|
----------
|
88
96
|
|
89
|
-
* Nando Vieira (
|
97
|
+
* Nando Vieira (<http://simplesideias.com.br/>)
|
98
|
+
|
99
|
+
CONTRIBUTORS
|
100
|
+
------------
|
101
|
+
|
102
|
+
* José Valim (<http://josevalim.blogspot.com/>)
|
90
103
|
|
91
104
|
LICENSE:
|
92
105
|
--------
|
@@ -110,4 +123,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
110
123
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
111
124
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
112
125
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
113
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
126
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ PKG_FILES = %w(init.rb Rakefile recurrence.gemspec History.txt License.txt READM
|
|
5
5
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
7
7
|
s.name = "recurrence"
|
8
|
-
s.version = "0.0
|
8
|
+
s.version = "0.1.0"
|
9
9
|
s.summary = "A simple library to handle recurring events"
|
10
10
|
s.authors = ["Nando Vieira"]
|
11
11
|
s.email = ["fnando.vieira@gmail.com"]
|
@@ -14,7 +14,6 @@ spec = Gem::Specification.new do |s|
|
|
14
14
|
s.has_rdoc = false
|
15
15
|
s.files = PKG_FILES
|
16
16
|
|
17
|
-
s.add_dependency "rubigen"
|
18
17
|
s.add_dependency "activesupport", ">=2.1.1"
|
19
18
|
end
|
20
19
|
|
@@ -74,4 +73,4 @@ end
|
|
74
73
|
desc "Execute specs"
|
75
74
|
task :spec do
|
76
75
|
system "spec spec/recurrence_spec.rb -c -f s"
|
77
|
-
end
|
76
|
+
end
|
data/lib/recurrence.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
require
|
1
|
+
require 'rubygems'
|
2
|
+
require 'date'
|
3
|
+
require 'activesupport'
|
4
|
+
|
5
|
+
dirname = File.dirname(__FILE__)
|
6
|
+
require dirname + '/recurrence/base'
|
7
|
+
require dirname + '/recurrence/shortcuts'
|
8
|
+
require dirname + '/recurrence/event'
|
9
|
+
require dirname + '/recurrence/event/daily'
|
10
|
+
require dirname + '/recurrence/event/weekly'
|
11
|
+
require dirname + '/recurrence/event/monthly'
|
12
|
+
require dirname + '/recurrence/event/yearly'
|
13
|
+
|
14
|
+
class Recurrence
|
15
|
+
include Base
|
16
|
+
extend Shortcuts
|
17
|
+
end
|
data/lib/recurrence/base.rb
CHANGED
@@ -1,145 +1,127 @@
|
|
1
1
|
class Recurrence
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
}
|
25
|
-
|
26
|
-
attr_reader :event
|
27
|
-
|
28
|
-
def initialize(options)
|
29
|
-
raise ArgumentError, ':every option is required' unless options.key?(:every)
|
30
|
-
raise ArgumentError, 'invalid :every option' unless FREQUENCY.include?(options[:every].to_s)
|
31
|
-
|
32
|
-
if options.key?(:interval)
|
33
|
-
if options[:every].to_sym == :month && options[:interval].is_a?(Symbol) && !INTERVALS.key?(options[:interval])
|
34
|
-
raise ArgumentError, 'interval symbol is not valid'
|
35
|
-
elsif options[:interval].to_i == 0
|
36
|
-
raise ArgumentError, 'interval should be greater than zero'
|
2
|
+
module Base
|
3
|
+
FREQUENCY = %w(day week month year)
|
4
|
+
|
5
|
+
attr_reader :event
|
6
|
+
|
7
|
+
def self.included(base) #:nodoc:
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
# Holds default_until_date configuration.
|
12
|
+
#
|
13
|
+
module ClassMethods #:nodoc:
|
14
|
+
def default_until_date
|
15
|
+
@default_until_date ||= Date.new(2037, 12, 31)
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_until_date=(date)
|
19
|
+
@default_until_date = if date.is_a?(String)
|
20
|
+
Date.parse(date)
|
21
|
+
else
|
22
|
+
date
|
23
|
+
end
|
37
24
|
end
|
38
25
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@options.merge!(:on => [MONTHS[@options[:on].first.to_s], @options.last]) unless @options[:on].first.kind_of?(Numeric)
|
58
|
-
@event = Recurrence::Event.new(:year, @options)
|
26
|
+
|
27
|
+
def initialize(options)
|
28
|
+
raise ArgumentError, ':every option is required' unless options.key?(:every)
|
29
|
+
raise ArgumentError, 'invalid :every option' unless FREQUENCY.include?(options[:every].to_s)
|
30
|
+
|
31
|
+
@options = initialize_dates(options)
|
32
|
+
@options[:interval] ||= 1
|
33
|
+
|
34
|
+
@event = case @options[:every].to_sym
|
35
|
+
when :day
|
36
|
+
Recurrence::Event::Daily.new(@options)
|
37
|
+
when :week
|
38
|
+
Recurrence::Event::Weekly.new(@options)
|
39
|
+
when :month
|
40
|
+
Recurrence::Event::Monthly.new(@options)
|
41
|
+
when :year
|
42
|
+
Recurrence::Event::Yearly.new(@options)
|
43
|
+
end
|
59
44
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
45
|
+
|
46
|
+
def reset!
|
47
|
+
@event.reset!
|
48
|
+
@events = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def include?(required_date)
|
52
|
+
required_date = Date.parse(required_date) if required_date.is_a?(String)
|
53
|
+
|
54
|
+
if required_date < @options[:starts] || required_date > @options[:until]
|
55
|
+
false
|
56
|
+
else
|
57
|
+
each do |date|
|
58
|
+
return true if date == required_date
|
59
|
+
end
|
75
60
|
end
|
61
|
+
|
62
|
+
return false
|
76
63
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
elsif options[:starts] && options[:until].nil? && date >= options[:starts]
|
106
|
-
_events << date
|
107
|
-
elsif options[:until] && options[:starts].nil? && date <= options[:until]
|
108
|
-
_events << date
|
109
|
-
elsif options[:starts].nil? && options[:until].nil?
|
110
|
-
_events << date
|
64
|
+
|
65
|
+
def next
|
66
|
+
@event.next
|
67
|
+
end
|
68
|
+
|
69
|
+
def next!
|
70
|
+
@event.next!
|
71
|
+
end
|
72
|
+
|
73
|
+
def events(options={})
|
74
|
+
options[:starts] = Date.parse(options[:starts]) if options[:starts].is_a?(String)
|
75
|
+
options[:until] = Date.parse(options[:until]) if options[:until].is_a?(String)
|
76
|
+
|
77
|
+
reset! if options[:starts] || options[:until]
|
78
|
+
|
79
|
+
@events ||= begin
|
80
|
+
_events = []
|
81
|
+
|
82
|
+
loop do
|
83
|
+
date = @event.next!
|
84
|
+
|
85
|
+
break if date.nil?
|
86
|
+
|
87
|
+
valid_start = options[:starts].nil? || date >= options[:starts]
|
88
|
+
valid_until = options[:until].nil? || date <= options[:until]
|
89
|
+
_events << date if valid_start && valid_until
|
90
|
+
|
91
|
+
break if options[:until] && options[:until] <= date
|
111
92
|
end
|
93
|
+
|
94
|
+
_events
|
112
95
|
end
|
113
|
-
|
114
|
-
_events
|
115
96
|
end
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
events(options)
|
121
|
-
end
|
122
|
-
|
123
|
-
def each!(&block)
|
124
|
-
reset!
|
125
|
-
each(&block)
|
126
|
-
end
|
127
|
-
|
128
|
-
def each(&block)
|
129
|
-
events.each do |item|
|
130
|
-
yield item
|
97
|
+
|
98
|
+
def events!(options={})
|
99
|
+
reset!
|
100
|
+
events(options)
|
131
101
|
end
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
102
|
+
|
103
|
+
def each!(&block)
|
104
|
+
reset!
|
105
|
+
each(&block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def each(&block)
|
109
|
+
events.each do |item|
|
110
|
+
yield item
|
138
111
|
end
|
139
|
-
|
140
|
-
options[:starts] ||= Date.today
|
141
|
-
options[:until] ||= Date.parse('2037-12-31')
|
142
|
-
|
143
|
-
options
|
144
112
|
end
|
145
|
-
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def initialize_dates(options) #:nodoc:
|
117
|
+
[:starts, :until].each do |name|
|
118
|
+
options[name] = Date.parse(options[name]) if options[name].is_a?(String)
|
119
|
+
end
|
120
|
+
|
121
|
+
options[:starts] ||= Date.today
|
122
|
+
options[:until] ||= self.class.default_until_date
|
123
|
+
|
124
|
+
options
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/recurrence/event.rb
CHANGED
@@ -1,130 +1,78 @@
|
|
1
1
|
class Recurrence::Event
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
2
|
+
CARDINALS = %w(first second third fourth fifth)
|
3
|
+
DAYS = %w(sunday monday tuesday wednesday thursday friday saturday)
|
4
|
+
|
5
|
+
attr_accessor :start_date
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
every, options = nil, every if every.is_a?(Hash)
|
9
|
+
|
10
|
+
@options = options
|
11
|
+
@date = options[:starts]
|
12
|
+
@finished = false
|
13
|
+
|
14
|
+
validate
|
15
|
+
raise ArgumentError, 'interval should be greater than zero' if @options[:interval] <= 0
|
16
|
+
|
9
17
|
prepare!
|
10
18
|
end
|
11
|
-
|
12
|
-
def prepare!
|
13
|
-
self.next!
|
14
|
-
@start_date = @date
|
15
|
-
end
|
16
|
-
|
17
|
-
def next?
|
18
|
-
!!@next
|
19
|
-
end
|
20
|
-
|
19
|
+
|
21
20
|
def next!
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
if inited? && !next?
|
29
|
-
@next = true
|
30
|
-
return @date
|
31
|
-
end
|
32
|
-
|
33
|
-
case @every
|
34
|
-
when :day
|
35
|
-
@date = next_day
|
36
|
-
when :week
|
37
|
-
@date = next_week
|
38
|
-
when :month
|
39
|
-
@date = next_month
|
40
|
-
when :year
|
41
|
-
@date = next_year
|
42
|
-
end
|
43
|
-
|
44
|
-
# if limit date has been reached just set the date
|
45
|
-
# object to nil
|
46
|
-
@date = nil unless @date.to_date <= @options[:until].to_date
|
47
|
-
|
21
|
+
return nil if finished?
|
22
|
+
return @date = @start_date if @start_date && @date.nil?
|
23
|
+
|
24
|
+
@date = next_in_recurrence
|
25
|
+
|
26
|
+
@finished, @date = true, nil if @date > @options[:until]
|
48
27
|
@date
|
49
28
|
end
|
50
|
-
|
29
|
+
|
51
30
|
def next
|
52
|
-
|
31
|
+
return nil if finished?
|
32
|
+
@date || @start_date
|
53
33
|
end
|
54
|
-
|
34
|
+
|
55
35
|
def reset!
|
56
|
-
@date =
|
36
|
+
@date = nil
|
57
37
|
end
|
58
|
-
|
38
|
+
|
39
|
+
def finished?
|
40
|
+
@finished
|
41
|
+
end
|
42
|
+
|
59
43
|
private
|
60
|
-
|
44
|
+
|
45
|
+
def initialized?
|
61
46
|
!!@start_date
|
62
47
|
end
|
63
|
-
|
64
|
-
def
|
65
|
-
|
66
|
-
date
|
67
|
-
|
68
|
-
date.to_date
|
69
|
-
end
|
70
|
-
|
71
|
-
def next_week
|
72
|
-
date = @date
|
73
|
-
|
74
|
-
if inited?
|
75
|
-
date = date + @options[:interval].weeks
|
76
|
-
elsif date.wday != @options[:on]
|
77
|
-
date = date.next until @options[:on] == date.wday && date > @options[:starts]
|
78
|
-
date = date + (@options[:interval] - 1).weeks
|
79
|
-
end
|
80
|
-
|
81
|
-
date.to_date
|
48
|
+
|
49
|
+
def prepare!
|
50
|
+
@start_date = next!
|
51
|
+
@date = nil
|
82
52
|
end
|
83
|
-
|
84
|
-
def next_month
|
85
|
-
date = @date
|
86
|
-
|
87
|
-
if inited?
|
88
|
-
date = advance_to_month(date)
|
89
|
-
else
|
90
|
-
day = [options[:on], Time.days_in_month(date.month, date.year)].min
|
91
|
-
date = Date.new(date.year, date.month, day)
|
92
|
-
date = advance_to_month(date) if @date.day > day
|
93
|
-
end
|
94
53
|
|
95
|
-
|
54
|
+
def validate
|
55
|
+
# Inject custom validations
|
96
56
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
if date.day != day && date.day < day
|
103
|
-
date = date.to_date.next until date.day == day
|
104
|
-
end
|
105
|
-
|
106
|
-
date
|
57
|
+
|
58
|
+
# Common validation for inherited classes.
|
59
|
+
#
|
60
|
+
def valid_month_day?(day) #:nodoc:
|
61
|
+
raise ArgumentError, "invalid day #{day}" unless (1..31).include?(day)
|
107
62
|
end
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
63
|
+
|
64
|
+
# Check if the given key has a valid weekday (0 upto 6) or a valid weekday
|
65
|
+
# name (defined in the DAYS constant). If a weekday name (String) is given,
|
66
|
+
# convert it to a weekday (Integer).
|
67
|
+
#
|
68
|
+
def valid_weekday_or_weekday_name?(value)
|
69
|
+
if value.kind_of?(Numeric)
|
70
|
+
raise ArgumentError, "invalid day #{value}" unless (0..6).include?(value)
|
71
|
+
value
|
114
72
|
else
|
115
|
-
|
116
|
-
|
117
|
-
date = advance_to_year(date) if @date.month > date.month || @options[:on].last < @date.day
|
73
|
+
raise ArgumentError, "invalid weekday #{value}" unless DAYS.include?(value.to_s)
|
74
|
+
DAYS.index(value.to_s)
|
118
75
|
end
|
119
|
-
|
120
|
-
date.to_date
|
121
|
-
end
|
122
|
-
|
123
|
-
def advance_to_year(date)
|
124
|
-
date = date.beginning_of_month + @options[:interval].years
|
125
|
-
day = [Time.days_in_month(@options[:on].first, date.year), @options[:on].last].min
|
126
|
-
year = date.year
|
127
|
-
|
128
|
-
date = Date.new(year, @options[:on].first, day)
|
129
76
|
end
|
77
|
+
|
130
78
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
class Recurrence::Event::Monthly < Recurrence::Event
|
2
|
+
INTERVALS = {
|
3
|
+
:monthly => 1,
|
4
|
+
:bimonthly => 2,
|
5
|
+
:quarterly => 3,
|
6
|
+
:semesterly => 6
|
7
|
+
}
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def validate
|
12
|
+
if @options.key?(:weekday)
|
13
|
+
|
14
|
+
# Allow :on => :last, :weekday => :thursday contruction.
|
15
|
+
if @options[:on].to_s == 'last'
|
16
|
+
@options[:on] = 5
|
17
|
+
elsif @options[:on].kind_of?(Numeric)
|
18
|
+
valid_week?(@options[:on])
|
19
|
+
else
|
20
|
+
valid_cardinal?(@options[:on])
|
21
|
+
@options[:on] = CARDINALS.index(@options[:on].to_s) + 1
|
22
|
+
end
|
23
|
+
|
24
|
+
@options[:weekday] = valid_weekday_or_weekday_name?(@options[:weekday])
|
25
|
+
else
|
26
|
+
valid_month_day?(@options[:on])
|
27
|
+
end
|
28
|
+
|
29
|
+
if @options[:interval].is_a?(Symbol)
|
30
|
+
valid_interval?(@options[:interval])
|
31
|
+
@options[:interval] = INTERVALS[@options[:interval]]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def next_in_recurrence
|
36
|
+
return next_month if self.respond_to?(:next_month)
|
37
|
+
type = @options.key?(:weekday) ? :weekday : :monthday
|
38
|
+
|
39
|
+
class_eval <<-METHOD
|
40
|
+
def next_month
|
41
|
+
if initialized?
|
42
|
+
advance_to_month_by_#{type}(@date)
|
43
|
+
else
|
44
|
+
new_date = advance_to_month_by_#{type}(@date, 0)
|
45
|
+
new_date = advance_to_month_by_#{type}(new_date) if @date > new_date
|
46
|
+
new_date
|
47
|
+
end
|
48
|
+
end
|
49
|
+
METHOD
|
50
|
+
|
51
|
+
next_month
|
52
|
+
end
|
53
|
+
|
54
|
+
def advance_to_month_by_monthday(date, interval=@options[:interval])
|
55
|
+
# Have a raw month from 0 to 11 interval
|
56
|
+
raw_month = date.month + interval - 1
|
57
|
+
|
58
|
+
next_year = date.year + raw_month / 12
|
59
|
+
next_month = (raw_month % 12) + 1 # change back to ruby interval
|
60
|
+
next_day = [ @options[:on], Time.days_in_month(next_month, next_year) ].min
|
61
|
+
|
62
|
+
Date.new(next_year, next_month, next_day)
|
63
|
+
end
|
64
|
+
|
65
|
+
def advance_to_month_by_weekday(date, interval=@options[:interval])
|
66
|
+
raw_month = date.month + interval - 1
|
67
|
+
next_year = date.year + raw_month / 12
|
68
|
+
next_month = (raw_month % 12) + 1 # change back to ruby interval
|
69
|
+
date = Date.new(next_year, next_month, 1)
|
70
|
+
|
71
|
+
weekday, month = @options[:weekday], date.month
|
72
|
+
|
73
|
+
# Adjust week day
|
74
|
+
to_add = weekday - date.wday
|
75
|
+
to_add += 7 if to_add < 0
|
76
|
+
to_add += (@options[:on] - 1) * 7
|
77
|
+
date += to_add
|
78
|
+
|
79
|
+
# Go to the previous month if we lost it
|
80
|
+
if date.month != month
|
81
|
+
weeks = (date.day - 1) / 7 + 1
|
82
|
+
date -= weeks * 7
|
83
|
+
end
|
84
|
+
|
85
|
+
date
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def valid_cardinal?(cardinal) #:nodoc:
|
91
|
+
raise ArgumentError, "invalid cardinal #{cardinal}" unless CARDINALS.include?(cardinal.to_s)
|
92
|
+
end
|
93
|
+
|
94
|
+
def valid_interval?(interval) #:nodoc:
|
95
|
+
raise ArgumentError, "invalid cardinal #{interval}" unless INTERVALS.key?(interval)
|
96
|
+
end
|
97
|
+
|
98
|
+
def valid_week?(week) #:nodoc:
|
99
|
+
raise ArgumentError, "invalid week #{week}" unless (1..5).include?(week)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Recurrence::Event::Weekly < Recurrence::Event
|
2
|
+
|
3
|
+
protected
|
4
|
+
|
5
|
+
def validate
|
6
|
+
@options[:on] = Array.wrap(@options[:on]).inject([]) do |days, value|
|
7
|
+
days << valid_weekday_or_weekday_name?(value)
|
8
|
+
end
|
9
|
+
|
10
|
+
@options[:on].sort!
|
11
|
+
end
|
12
|
+
|
13
|
+
def next_in_recurrence
|
14
|
+
return @date if !initialized? && @options[:on].include?(@date.wday)
|
15
|
+
|
16
|
+
if next_day = @options[:on].find { |day| day > @date.wday }
|
17
|
+
to_add = next_day - @date.wday
|
18
|
+
else
|
19
|
+
to_add = (7 - @date.wday) # Move to next week
|
20
|
+
to_add += (@options[:interval] - 1) * 7 # Add extra intervals
|
21
|
+
to_add += @options[:on].first # Go to first required day
|
22
|
+
end
|
23
|
+
|
24
|
+
@date.to_date + to_add
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class Recurrence::Event::Yearly < Recurrence::Event
|
2
|
+
MONTHS = {
|
3
|
+
"jan" => 1, "january" => 1,
|
4
|
+
"feb" => 2, "february" => 2,
|
5
|
+
"mar" => 3, "march" => 3,
|
6
|
+
"apr" => 4, "april" => 4,
|
7
|
+
"may" => 5,
|
8
|
+
"jun" => 6, "june" => 6,
|
9
|
+
"jul" => 7, "july" => 7,
|
10
|
+
"aug" => 8, "august" => 8,
|
11
|
+
"sep" => 9, "september" => 9,
|
12
|
+
"oct" => 10, "october" => 10,
|
13
|
+
"nov" => 11, "november" => 11,
|
14
|
+
"dec" => 12, "december" => 12
|
15
|
+
}
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def validate
|
20
|
+
valid_month_day?(@options[:on].last)
|
21
|
+
|
22
|
+
if @options[:on].first.kind_of?(Numeric)
|
23
|
+
valid_month?(@options[:on].first)
|
24
|
+
else
|
25
|
+
valid_month_name?(@options[:on].first)
|
26
|
+
@options[:on] = [ MONTHS[@options[:on].first.to_s], @options.last ]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def next_in_recurrence
|
31
|
+
if initialized?
|
32
|
+
advance_to_year(@date)
|
33
|
+
else
|
34
|
+
new_date = advance_to_year(@date, 0)
|
35
|
+
new_date = advance_to_year(new_date) if @date > new_date
|
36
|
+
new_date
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def advance_to_year(date, interval=@options[:interval])
|
41
|
+
next_year = date.year + interval
|
42
|
+
next_month = @options[:on].first
|
43
|
+
next_day = [ @options[:on].last, Time.days_in_month(next_month, next_year) ].min
|
44
|
+
|
45
|
+
Date.new(next_year, next_month, next_day)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def valid_month?(month) #:nodoc:
|
51
|
+
raise ArgumentError, "invalid month #{month}" unless (1..12).include?(month)
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid_month_name?(month) #:nodoc:
|
55
|
+
raise ArgumentError, "invalid month #{month}" unless MONTHS.keys.include?(month.to_s)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Recurrence
|
2
|
+
module Shortcuts
|
3
|
+
def daily(options={})
|
4
|
+
options[:every] = :day
|
5
|
+
new(options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def weekly(options)
|
9
|
+
options[:every] = :week
|
10
|
+
new(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def monthly(options)
|
14
|
+
options[:every] = :month
|
15
|
+
new(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def yearly(options)
|
19
|
+
options[:every] = :year
|
20
|
+
new(options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/recurrence.gemspec
CHANGED
@@ -2,26 +2,31 @@
|
|
2
2
|
# RUN : 'rake gem:update_gemspec'
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
|
-
s.
|
5
|
+
s.add_dependency "activesupport", ">= 2.1.1"
|
6
6
|
s.require_paths = ["lib"]
|
7
|
-
s.required_rubygems_version = ">= 0"
|
8
|
-
s.has_rdoc = false
|
9
7
|
s.files = ["init.rb",
|
10
8
|
"Rakefile",
|
11
9
|
"recurrence.gemspec",
|
12
10
|
"History.txt",
|
13
11
|
"License.txt",
|
14
12
|
"README.markdown",
|
13
|
+
"lib/recurrence.rb",
|
15
14
|
"lib/recurrence",
|
16
15
|
"lib/recurrence/base.rb",
|
17
16
|
"lib/recurrence/event.rb",
|
18
|
-
"lib/recurrence.rb"
|
17
|
+
"lib/recurrence/shortcuts.rb",
|
18
|
+
"lib/recurrence/event",
|
19
|
+
"lib/recurrence/event/monthly.rb",
|
20
|
+
"lib/recurrence/event/daily.rb",
|
21
|
+
"lib/recurrence/event/weekly.rb",
|
22
|
+
"lib/recurrence/event/yearly.rb"]
|
23
|
+
s.authors = ["Nando Vieira"]
|
24
|
+
s.name = "recurrence"
|
25
|
+
s.required_rubygems_version = ">= 0"
|
26
|
+
s.has_rdoc = false
|
19
27
|
s.email = ["fnando.vieira@gmail.com"]
|
20
|
-
s.version = "0.0.7"
|
21
28
|
s.homepage = "http://github.com/fnando/recurrence"
|
22
|
-
s.name = "recurrence"
|
23
|
-
s.summary = "A simple library to handle recurring events"
|
24
|
-
s.add_dependency "rubigen", ">= 0"
|
25
|
-
s.add_dependency "activesupport", ">= 2.1.1"
|
26
29
|
s.bindir = "bin"
|
30
|
+
s.version = "0.1.0"
|
31
|
+
s.summary = "A simple library to handle recurring events"
|
27
32
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fnando-recurrence
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nando Vieira
|
@@ -9,20 +9,12 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2009-05-16 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
|
-
- !ruby/object:Gem::Dependency
|
16
|
-
name: rubigen
|
17
|
-
version_requirement:
|
18
|
-
version_requirements: !ruby/object:Gem::Requirement
|
19
|
-
requirements:
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: "0"
|
23
|
-
version:
|
24
15
|
- !ruby/object:Gem::Dependency
|
25
16
|
name: activesupport
|
17
|
+
type: :runtime
|
26
18
|
version_requirement:
|
27
19
|
version_requirements: !ruby/object:Gem::Requirement
|
28
20
|
requirements:
|
@@ -46,10 +38,16 @@ files:
|
|
46
38
|
- History.txt
|
47
39
|
- License.txt
|
48
40
|
- README.markdown
|
41
|
+
- lib/recurrence.rb
|
49
42
|
- lib/recurrence
|
50
43
|
- lib/recurrence/base.rb
|
51
44
|
- lib/recurrence/event.rb
|
52
|
-
- lib/recurrence.rb
|
45
|
+
- lib/recurrence/shortcuts.rb
|
46
|
+
- lib/recurrence/event
|
47
|
+
- lib/recurrence/event/monthly.rb
|
48
|
+
- lib/recurrence/event/daily.rb
|
49
|
+
- lib/recurrence/event/weekly.rb
|
50
|
+
- lib/recurrence/event/yearly.rb
|
53
51
|
has_rdoc: false
|
54
52
|
homepage: http://github.com/fnando/recurrence
|
55
53
|
post_install_message:
|