recurrence 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +28 -29
- data/README.markdown +15 -26
- data/Rakefile +23 -73
- data/init.rb +2 -1
- data/lib/recurrence.rb +138 -14
- data/lib/recurrence/event.rb +7 -76
- data/lib/recurrence/event/base.rb +80 -0
- data/lib/recurrence/event/daily.rb +10 -8
- data/lib/recurrence/event/monthly.rb +83 -82
- data/lib/recurrence/event/weekly.rb +20 -19
- data/lib/recurrence/event/yearly.rb +50 -49
- data/lib/recurrence/version.rb +8 -0
- data/recurrence.gemspec +57 -29
- data/spec/recurrence_spec.rb +523 -0
- metadata +58 -26
- data/lib/recurrence/base.rb +0 -127
- data/lib/recurrence/shortcuts.rb +0 -23
data/lib/recurrence/event.rb
CHANGED
@@ -1,78 +1,9 @@
|
|
1
|
-
class Recurrence
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
17
|
-
prepare!
|
1
|
+
class Recurrence
|
2
|
+
module Event
|
3
|
+
autoload :Base, "recurrence/event/base"
|
4
|
+
autoload :Daily, "recurrence/event/daily"
|
5
|
+
autoload :Monthly, "recurrence/event/monthly"
|
6
|
+
autoload :Weekly, "recurrence/event/weekly"
|
7
|
+
autoload :Yearly, "recurrence/event/yearly"
|
18
8
|
end
|
19
|
-
|
20
|
-
def next!
|
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]
|
27
|
-
@date
|
28
|
-
end
|
29
|
-
|
30
|
-
def next
|
31
|
-
return nil if finished?
|
32
|
-
@date || @start_date
|
33
|
-
end
|
34
|
-
|
35
|
-
def reset!
|
36
|
-
@date = nil
|
37
|
-
end
|
38
|
-
|
39
|
-
def finished?
|
40
|
-
@finished
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def initialized?
|
46
|
-
!!@start_date
|
47
|
-
end
|
48
|
-
|
49
|
-
def prepare!
|
50
|
-
@start_date = next!
|
51
|
-
@date = nil
|
52
|
-
end
|
53
|
-
|
54
|
-
def validate
|
55
|
-
# Inject custom validations
|
56
|
-
end
|
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)
|
62
|
-
end
|
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
|
72
|
-
else
|
73
|
-
raise ArgumentError, "invalid weekday #{value}" unless DAYS.include?(value.to_s)
|
74
|
-
DAYS.index(value.to_s)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
9
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class Recurrence
|
2
|
+
module Event
|
3
|
+
class Base
|
4
|
+
CARDINALS = %w(first second third fourth fifth)
|
5
|
+
DAYS = %w(sunday monday tuesday wednesday thursday friday saturday)
|
6
|
+
|
7
|
+
attr_accessor :start_date
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
every, options = nil, every if every.is_a?(Hash)
|
11
|
+
|
12
|
+
@options = options
|
13
|
+
@date = options[:starts]
|
14
|
+
@finished = false
|
15
|
+
|
16
|
+
validate
|
17
|
+
raise ArgumentError, "interval should be greater than zero" if @options[:interval] <= 0
|
18
|
+
|
19
|
+
prepare!
|
20
|
+
end
|
21
|
+
|
22
|
+
def next!
|
23
|
+
return nil if finished?
|
24
|
+
return @date = @start_date if @start_date && @date.nil?
|
25
|
+
|
26
|
+
@date = next_in_recurrence
|
27
|
+
|
28
|
+
@finished, @date = true, nil if @date > @options[:until]
|
29
|
+
@date
|
30
|
+
end
|
31
|
+
|
32
|
+
def next
|
33
|
+
return nil if finished?
|
34
|
+
@date || @start_date
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset!
|
38
|
+
@date = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def finished?
|
42
|
+
@finished
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def initialized?
|
47
|
+
!!@start_date
|
48
|
+
end
|
49
|
+
|
50
|
+
def prepare!
|
51
|
+
@start_date = next!
|
52
|
+
@date = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate
|
56
|
+
# Inject custom validations
|
57
|
+
end
|
58
|
+
|
59
|
+
# Common validation for inherited classes.
|
60
|
+
#
|
61
|
+
def valid_month_day?(day) #:nodoc:
|
62
|
+
raise ArgumentError, "invalid day #{day}" unless (1..31).include?(day)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check if the given key has a valid weekday (0 upto 6) or a valid weekday
|
66
|
+
# name (defined in the DAYS constant). If a weekday name (String) is given,
|
67
|
+
# convert it to a weekday (Integer).
|
68
|
+
#
|
69
|
+
def valid_weekday_or_weekday_name?(value)
|
70
|
+
if value.kind_of?(Numeric)
|
71
|
+
raise ArgumentError, "invalid day #{value}" unless (0..6).include?(value)
|
72
|
+
value
|
73
|
+
else
|
74
|
+
raise ArgumentError, "invalid weekday #{value}" unless DAYS.include?(value.to_s)
|
75
|
+
DAYS.index(value.to_s)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -1,10 +1,12 @@
|
|
1
|
-
class Recurrence
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
1
|
+
class Recurrence
|
2
|
+
module Event
|
3
|
+
class Daily < Base
|
4
|
+
protected
|
5
|
+
def next_in_recurrence
|
6
|
+
date = @date.to_date
|
7
|
+
date += @options[:interval] if initialized?
|
8
|
+
date
|
9
|
+
end
|
8
10
|
end
|
9
|
-
|
11
|
+
end
|
10
12
|
end
|
@@ -1,102 +1,103 @@
|
|
1
|
-
class Recurrence
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@options[:on]
|
17
|
-
|
18
|
-
|
1
|
+
class Recurrence
|
2
|
+
module Event
|
3
|
+
class Monthly < Base
|
4
|
+
INTERVALS = {
|
5
|
+
:monthly => 1,
|
6
|
+
:bimonthly => 2,
|
7
|
+
:quarterly => 3,
|
8
|
+
:semesterly => 6
|
9
|
+
}
|
10
|
+
|
11
|
+
protected
|
12
|
+
def validate
|
13
|
+
if @options.key?(:weekday)
|
14
|
+
|
15
|
+
# Allow :on => :last, :weekday => :thursday contruction.
|
16
|
+
if @options[:on].to_s == "last"
|
17
|
+
@options[:on] = 5
|
18
|
+
elsif @options[:on].kind_of?(Numeric)
|
19
|
+
valid_week?(@options[:on])
|
20
|
+
else
|
21
|
+
valid_cardinal?(@options[:on])
|
22
|
+
@options[:on] = CARDINALS.index(@options[:on].to_s) + 1
|
23
|
+
end
|
24
|
+
|
25
|
+
@options[:weekday] = valid_weekday_or_weekday_name?(@options[:weekday])
|
19
26
|
else
|
20
|
-
|
21
|
-
@options[:on] = CARDINALS.index(@options[:on].to_s) + 1
|
27
|
+
valid_month_day?(@options[:on])
|
22
28
|
end
|
23
29
|
|
24
|
-
@options[:
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
if @options[:interval].is_a?(Symbol)
|
30
|
-
valid_interval?(@options[:interval])
|
31
|
-
@options[:interval] = INTERVALS[@options[:interval]]
|
30
|
+
if @options[:interval].is_a?(Symbol)
|
31
|
+
valid_interval?(@options[:interval])
|
32
|
+
@options[:interval] = INTERVALS[@options[:interval]]
|
33
|
+
end
|
32
34
|
end
|
33
|
-
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
36
|
+
def next_in_recurrence
|
37
|
+
return next_month if self.respond_to?(:next_month)
|
38
|
+
type = @options.key?(:weekday) ? :weekday : :monthday
|
39
|
+
|
40
|
+
class_eval <<-METHOD
|
41
|
+
def next_month
|
42
|
+
if initialized?
|
43
|
+
advance_to_month_by_#{type}(@date)
|
44
|
+
else
|
45
|
+
new_date = advance_to_month_by_#{type}(@date, 0)
|
46
|
+
new_date = advance_to_month_by_#{type}(new_date) if @date > new_date
|
47
|
+
new_date
|
48
|
+
end
|
47
49
|
end
|
48
|
-
|
49
|
-
METHOD
|
50
|
+
METHOD
|
50
51
|
|
51
|
-
|
52
|
-
|
52
|
+
next_month
|
53
|
+
end
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
55
|
+
def advance_to_month_by_monthday(date, interval=@options[:interval])
|
56
|
+
# Have a raw month from 0 to 11 interval
|
57
|
+
raw_month = date.month + interval - 1
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
next_year = date.year + raw_month / 12
|
60
|
+
next_month = (raw_month % 12) + 1 # change back to ruby interval
|
61
|
+
next_day = [ @options[:on], Time.days_in_month(next_month, next_year) ].min
|
61
62
|
|
62
|
-
|
63
|
-
|
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)
|
63
|
+
Date.new(next_year, next_month, next_day)
|
64
|
+
end
|
70
65
|
|
71
|
-
|
66
|
+
def advance_to_month_by_weekday(date, interval=@options[:interval])
|
67
|
+
raw_month = date.month + interval - 1
|
68
|
+
next_year = date.year + raw_month / 12
|
69
|
+
next_month = (raw_month % 12) + 1 # change back to ruby interval
|
70
|
+
date = Date.new(next_year, next_month, 1)
|
72
71
|
|
73
|
-
|
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
|
72
|
+
weekday, month = @options[:weekday], date.month
|
78
73
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
74
|
+
# Adjust week day
|
75
|
+
to_add = weekday - date.wday
|
76
|
+
to_add += 7 if to_add < 0
|
77
|
+
to_add += (@options[:on] - 1) * 7
|
78
|
+
date += to_add
|
84
79
|
|
85
|
-
|
86
|
-
|
80
|
+
# Go to the previous month if we lost it
|
81
|
+
if date.month != month
|
82
|
+
weeks = (date.day - 1) / 7 + 1
|
83
|
+
date -= weeks * 7
|
84
|
+
end
|
87
85
|
|
88
|
-
|
86
|
+
date
|
87
|
+
end
|
89
88
|
|
90
|
-
|
91
|
-
|
92
|
-
|
89
|
+
private
|
90
|
+
def valid_cardinal?(cardinal) #:nodoc:
|
91
|
+
raise ArgumentError, "invalid cardinal #{cardinal}" unless CARDINALS.include?(cardinal.to_s)
|
92
|
+
end
|
93
93
|
|
94
|
-
|
95
|
-
|
96
|
-
|
94
|
+
def valid_interval?(interval) #:nodoc:
|
95
|
+
raise ArgumentError, "invalid cardinal #{interval}" unless INTERVALS.key?(interval)
|
96
|
+
end
|
97
97
|
|
98
|
-
|
99
|
-
|
98
|
+
def valid_week?(week) #:nodoc:
|
99
|
+
raise ArgumentError, "invalid week #{week}" unless (1..5).include?(week)
|
100
|
+
end
|
100
101
|
end
|
101
|
-
|
102
|
+
end
|
102
103
|
end
|
@@ -1,27 +1,28 @@
|
|
1
|
-
class Recurrence
|
1
|
+
class Recurrence
|
2
|
+
module Event
|
3
|
+
class Weekly < Base
|
4
|
+
protected
|
5
|
+
def validate
|
6
|
+
@options[:on] = Array.wrap(@options[:on]).inject([]) do |days, value|
|
7
|
+
days << valid_weekday_or_weekday_name?(value)
|
8
|
+
end
|
2
9
|
|
3
|
-
|
4
|
-
|
5
|
-
def validate
|
6
|
-
@options[:on] = Array.wrap(@options[:on]).inject([]) do |days, value|
|
7
|
-
days << valid_weekday_or_weekday_name?(value)
|
10
|
+
@options[:on].sort!
|
8
11
|
end
|
9
12
|
|
10
|
-
|
11
|
-
|
13
|
+
def next_in_recurrence
|
14
|
+
return @date if !initialized? && @options[:on].include?(@date.wday)
|
12
15
|
|
13
|
-
|
14
|
-
|
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
|
15
23
|
|
16
|
-
|
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
|
24
|
+
@date.to_date + to_add
|
22
25
|
end
|
23
|
-
|
24
|
-
@date.to_date + to_add
|
25
26
|
end
|
26
|
-
|
27
|
+
end
|
27
28
|
end
|
@@ -1,58 +1,59 @@
|
|
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
|
-
|
1
|
+
class Recurrence
|
2
|
+
module Event
|
3
|
+
class Yearly < Base
|
4
|
+
MONTHS = {
|
5
|
+
"jan" => 1, "january" => 1,
|
6
|
+
"feb" => 2, "february" => 2,
|
7
|
+
"mar" => 3, "march" => 3,
|
8
|
+
"apr" => 4, "april" => 4,
|
9
|
+
"may" => 5,
|
10
|
+
"jun" => 6, "june" => 6,
|
11
|
+
"jul" => 7, "july" => 7,
|
12
|
+
"aug" => 8, "august" => 8,
|
13
|
+
"sep" => 9, "september" => 9,
|
14
|
+
"oct" => 10, "october" => 10,
|
15
|
+
"nov" => 11, "november" => 11,
|
16
|
+
"dec" => 12, "december" => 12
|
17
|
+
}
|
18
|
+
|
19
|
+
protected
|
20
|
+
def validate
|
21
|
+
valid_month_day?(@options[:on].last)
|
22
|
+
|
23
|
+
if @options[:on].first.kind_of?(Numeric)
|
24
|
+
valid_month?(@options[:on].first)
|
25
|
+
else
|
26
|
+
valid_month_name?(@options[:on].first)
|
27
|
+
@options[:on] = [ MONTHS[@options[:on].first.to_s], @options.last ]
|
28
|
+
end
|
27
29
|
end
|
28
|
-
end
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
31
|
+
def next_in_recurrence
|
32
|
+
if initialized?
|
33
|
+
advance_to_year(@date)
|
34
|
+
else
|
35
|
+
new_date = advance_to_year(@date, 0)
|
36
|
+
new_date = advance_to_year(new_date) if @date > new_date
|
37
|
+
new_date
|
38
|
+
end
|
37
39
|
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
40
|
|
45
|
-
|
46
|
-
|
41
|
+
def advance_to_year(date, interval=@options[:interval])
|
42
|
+
next_year = date.year + interval
|
43
|
+
next_month = @options[:on].first
|
44
|
+
next_day = [ @options[:on].last, Time.days_in_month(next_month, next_year) ].min
|
47
45
|
|
48
|
-
|
46
|
+
Date.new(next_year, next_month, next_day)
|
47
|
+
end
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
private
|
50
|
+
def valid_month?(month) #:nodoc:
|
51
|
+
raise ArgumentError, "invalid month #{month}" unless (1..12).include?(month)
|
52
|
+
end
|
53
53
|
|
54
|
-
|
55
|
-
|
54
|
+
def valid_month_name?(month) #:nodoc:
|
55
|
+
raise ArgumentError, "invalid month #{month}" unless MONTHS.keys.include?(month.to_s)
|
56
|
+
end
|
56
57
|
end
|
57
|
-
|
58
|
+
end
|
58
59
|
end
|