ri_cal 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. data/History.txt +45 -0
  2. data/Manifest.txt +129 -0
  3. data/README.txt +394 -0
  4. data/Rakefile +31 -0
  5. data/bin/ri_cal +8 -0
  6. data/component_attributes/alarm.yml +10 -0
  7. data/component_attributes/calendar.yml +4 -0
  8. data/component_attributes/component_property_defs.yml +180 -0
  9. data/component_attributes/event.yml +45 -0
  10. data/component_attributes/freebusy.yml +16 -0
  11. data/component_attributes/journal.yml +35 -0
  12. data/component_attributes/timezone.yml +3 -0
  13. data/component_attributes/timezone_period.yml +11 -0
  14. data/component_attributes/todo.yml +46 -0
  15. data/copyrights.txt +1 -0
  16. data/docs/draft-ietf-calsify-2446bis-08.txt +7280 -0
  17. data/docs/draft-ietf-calsify-rfc2445bis-09.txt +10416 -0
  18. data/docs/incrementers.txt +7 -0
  19. data/docs/rfc2445.pdf +0 -0
  20. data/lib/ri_cal.rb +144 -0
  21. data/lib/ri_cal/component.rb +247 -0
  22. data/lib/ri_cal/component/alarm.rb +21 -0
  23. data/lib/ri_cal/component/calendar.rb +219 -0
  24. data/lib/ri_cal/component/event.rb +60 -0
  25. data/lib/ri_cal/component/freebusy.rb +18 -0
  26. data/lib/ri_cal/component/journal.rb +30 -0
  27. data/lib/ri_cal/component/t_z_info_timezone.rb +123 -0
  28. data/lib/ri_cal/component/timezone.rb +196 -0
  29. data/lib/ri_cal/component/timezone/daylight_period.rb +25 -0
  30. data/lib/ri_cal/component/timezone/standard_period.rb +23 -0
  31. data/lib/ri_cal/component/timezone/timezone_period.rb +53 -0
  32. data/lib/ri_cal/component/todo.rb +43 -0
  33. data/lib/ri_cal/core_extensions.rb +6 -0
  34. data/lib/ri_cal/core_extensions/array.rb +7 -0
  35. data/lib/ri_cal/core_extensions/array/conversions.rb +15 -0
  36. data/lib/ri_cal/core_extensions/date.rb +13 -0
  37. data/lib/ri_cal/core_extensions/date/conversions.rb +61 -0
  38. data/lib/ri_cal/core_extensions/date_time.rb +15 -0
  39. data/lib/ri_cal/core_extensions/date_time/conversions.rb +50 -0
  40. data/lib/ri_cal/core_extensions/object.rb +8 -0
  41. data/lib/ri_cal/core_extensions/object/conversions.rb +20 -0
  42. data/lib/ri_cal/core_extensions/string.rb +8 -0
  43. data/lib/ri_cal/core_extensions/string/conversions.rb +63 -0
  44. data/lib/ri_cal/core_extensions/time.rb +13 -0
  45. data/lib/ri_cal/core_extensions/time/calculations.rb +153 -0
  46. data/lib/ri_cal/core_extensions/time/conversions.rb +61 -0
  47. data/lib/ri_cal/core_extensions/time/tzid_access.rb +50 -0
  48. data/lib/ri_cal/core_extensions/time/week_day_predicates.rb +88 -0
  49. data/lib/ri_cal/floating_timezone.rb +32 -0
  50. data/lib/ri_cal/invalid_property_value.rb +8 -0
  51. data/lib/ri_cal/invalid_timezone_identifer.rb +20 -0
  52. data/lib/ri_cal/occurrence_enumerator.rb +206 -0
  53. data/lib/ri_cal/occurrence_period.rb +17 -0
  54. data/lib/ri_cal/parser.rb +138 -0
  55. data/lib/ri_cal/properties/alarm.rb +390 -0
  56. data/lib/ri_cal/properties/calendar.rb +164 -0
  57. data/lib/ri_cal/properties/event.rb +1526 -0
  58. data/lib/ri_cal/properties/freebusy.rb +594 -0
  59. data/lib/ri_cal/properties/journal.rb +1240 -0
  60. data/lib/ri_cal/properties/timezone.rb +151 -0
  61. data/lib/ri_cal/properties/timezone_period.rb +416 -0
  62. data/lib/ri_cal/properties/todo.rb +1562 -0
  63. data/lib/ri_cal/property_value.rb +149 -0
  64. data/lib/ri_cal/property_value/array.rb +27 -0
  65. data/lib/ri_cal/property_value/cal_address.rb +11 -0
  66. data/lib/ri_cal/property_value/date.rb +175 -0
  67. data/lib/ri_cal/property_value/date_time.rb +335 -0
  68. data/lib/ri_cal/property_value/date_time/additive_methods.rb +44 -0
  69. data/lib/ri_cal/property_value/date_time/time_machine.rb +181 -0
  70. data/lib/ri_cal/property_value/date_time/timezone_support.rb +96 -0
  71. data/lib/ri_cal/property_value/duration.rb +110 -0
  72. data/lib/ri_cal/property_value/geo.rb +11 -0
  73. data/lib/ri_cal/property_value/integer.rb +12 -0
  74. data/lib/ri_cal/property_value/occurrence_list.rb +144 -0
  75. data/lib/ri_cal/property_value/period.rb +82 -0
  76. data/lib/ri_cal/property_value/recurrence_rule.rb +145 -0
  77. data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +97 -0
  78. data/lib/ri_cal/property_value/recurrence_rule/enumerator.rb +79 -0
  79. data/lib/ri_cal/property_value/recurrence_rule/initialization_methods.rb +148 -0
  80. data/lib/ri_cal/property_value/recurrence_rule/negative_setpos_enumerator.rb +53 -0
  81. data/lib/ri_cal/property_value/recurrence_rule/numbered_span.rb +31 -0
  82. data/lib/ri_cal/property_value/recurrence_rule/occurence_incrementer.rb +793 -0
  83. data/lib/ri_cal/property_value/recurrence_rule/recurring_day.rb +131 -0
  84. data/lib/ri_cal/property_value/recurrence_rule/recurring_month_day.rb +60 -0
  85. data/lib/ri_cal/property_value/recurrence_rule/recurring_numbered_week.rb +33 -0
  86. data/lib/ri_cal/property_value/recurrence_rule/recurring_year_day.rb +49 -0
  87. data/lib/ri_cal/property_value/recurrence_rule/validations.rb +125 -0
  88. data/lib/ri_cal/property_value/text.rb +40 -0
  89. data/lib/ri_cal/property_value/uri.rb +11 -0
  90. data/lib/ri_cal/property_value/utc_offset.rb +33 -0
  91. data/lib/ri_cal/required_timezones.rb +55 -0
  92. data/ri_cal.gemspec +49 -0
  93. data/sample_ical_files/from_ical_dot_app/test1.ics +38 -0
  94. data/script/console +10 -0
  95. data/script/destroy +14 -0
  96. data/script/generate +14 -0
  97. data/script/txt2html +71 -0
  98. data/spec/ri_cal/component/alarm_spec.rb +12 -0
  99. data/spec/ri_cal/component/calendar_spec.rb +54 -0
  100. data/spec/ri_cal/component/event_spec.rb +601 -0
  101. data/spec/ri_cal/component/freebusy_spec.rb +12 -0
  102. data/spec/ri_cal/component/journal_spec.rb +37 -0
  103. data/spec/ri_cal/component/t_z_info_timezone_spec.rb +36 -0
  104. data/spec/ri_cal/component/timezone_spec.rb +218 -0
  105. data/spec/ri_cal/component/todo_spec.rb +112 -0
  106. data/spec/ri_cal/component_spec.rb +224 -0
  107. data/spec/ri_cal/core_extensions/string/conversions_spec.rb +78 -0
  108. data/spec/ri_cal/core_extensions/time/calculations_spec.rb +188 -0
  109. data/spec/ri_cal/core_extensions/time/week_day_predicates_spec.rb +45 -0
  110. data/spec/ri_cal/occurrence_enumerator_spec.rb +573 -0
  111. data/spec/ri_cal/parser_spec.rb +303 -0
  112. data/spec/ri_cal/property_value/date_spec.rb +53 -0
  113. data/spec/ri_cal/property_value/date_time_spec.rb +383 -0
  114. data/spec/ri_cal/property_value/duration_spec.rb +126 -0
  115. data/spec/ri_cal/property_value/occurrence_list_spec.rb +72 -0
  116. data/spec/ri_cal/property_value/period_spec.rb +49 -0
  117. data/spec/ri_cal/property_value/recurrence_rule/recurring_year_day_spec.rb +21 -0
  118. data/spec/ri_cal/property_value/recurrence_rule_spec.rb +1814 -0
  119. data/spec/ri_cal/property_value/text_spec.rb +25 -0
  120. data/spec/ri_cal/property_value/utc_offset_spec.rb +48 -0
  121. data/spec/ri_cal/property_value_spec.rb +125 -0
  122. data/spec/ri_cal/required_timezones_spec.rb +67 -0
  123. data/spec/ri_cal_spec.rb +53 -0
  124. data/spec/spec.opts +4 -0
  125. data/spec/spec_helper.rb +46 -0
  126. data/tasks/gem_loader/load_active_support.rb +3 -0
  127. data/tasks/gem_loader/load_tzinfo_gem.rb +2 -0
  128. data/tasks/ri_cal.rake +410 -0
  129. data/tasks/spec.rake +50 -0
  130. metadata +221 -0
@@ -0,0 +1,79 @@
1
+ module RiCal
2
+ class PropertyValue
3
+ class RecurrenceRule < PropertyValue
4
+ #- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license
5
+ #
6
+ class Enumerator # :nodoc:
7
+ # base_time gets changed everytime the time is updated by the recurrence rule's frequency
8
+ attr_accessor :start_time, :duration, :next_time, :recurrence_rule, :base_time
9
+ def initialize(recurrence_rule, component, setpos_list)
10
+ self.recurrence_rule = recurrence_rule
11
+ self.start_time = component.default_start_time
12
+ self.duration = component.default_duration
13
+ self.next_time = recurrence_rule.adjust_start(self.start_time)
14
+ self.base_time = next_time
15
+ @bounded = recurrence_rule.bounded?
16
+ @count = 0
17
+ @setpos_list = setpos_list
18
+ @setpos = 1
19
+ @next_occurrence_count = 0
20
+ @incrementer = YearlyIncrementer.from_rrule(recurrence_rule, start_time)
21
+ end
22
+
23
+ def self.for(recurrence_rule, component, setpos_list) # :nodoc:
24
+ if !setpos_list || setpos_list.all? {|setpos| setpos > 1}
25
+ self.new(recurrence_rule, component, setpos_list)
26
+ else
27
+ NegativeSetposEnumerator.new(recurrence_rule, component, setpos_list)
28
+ end
29
+ end
30
+
31
+ def empty?
32
+ false
33
+ end
34
+
35
+ def bounded?
36
+ @bounded
37
+ end
38
+
39
+ def result_occurrence_period(date_time_value)
40
+ RiCal::OccurrencePeriod.new(date_time_value, nil)
41
+ end
42
+
43
+ def result_passes_setpos_filter?(result)
44
+ result_setpos = @setpos
45
+ if recurrence_rule.in_same_set?(result, next_time)
46
+ @setpos += 1
47
+ else
48
+ @setpos = 1
49
+ end
50
+ if (result == start_time) || (result > start_time && @setpos_list.include?(result_setpos))
51
+ return true
52
+ else
53
+ return false
54
+ end
55
+ end
56
+
57
+ def result_passes_filters?(result)
58
+ if @setpos_list
59
+ result_passes_setpos_filter?(result)
60
+ else
61
+ result >= start_time
62
+ end
63
+ end
64
+
65
+ def next_occurrence
66
+ while true
67
+ @next_occurrence_count += 1
68
+ result = next_time
69
+ self.next_time = @incrementer.next_time(result)
70
+ if result_passes_filters?(result)
71
+ @count += 1
72
+ return recurrence_rule.exhausted?(@count, result) ? nil : result_occurrence_period(result)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,148 @@
1
+ module RiCal
2
+ class PropertyValue
3
+ class RecurrenceRule < PropertyValue
4
+ #- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license
5
+ #
6
+ module InitializationMethods # :nodoc:
7
+
8
+ attr_reader :by_day_scope
9
+
10
+ def add_to_options_hash(options_hash, key, value)
11
+ options_hash[key] = value if value
12
+ options_hash
13
+ end
14
+
15
+ def add_byrule_strings_to_options_hash(options_hash, key)
16
+ if (rules = by_list[key])
17
+ if rules.length = 1
18
+ options_hash[key] = rules.first.source
19
+ else
20
+ options_hash[key] = rules.map {|rule| rule.source}
21
+ end
22
+ end
23
+ end
24
+
25
+ def to_options_hash
26
+ options_hash = {:freq => freq, :interval => interval}
27
+ options_hash[:params] = params unless params.empty?
28
+ add_to_options_hash(options_hash, :count, @count)
29
+ add_to_options_hash(options_hash, :until, @until)
30
+ add_to_options_hash(options_hash, :interval, @interval)
31
+ [:bysecond, :byminute, :byhour, :bymonth, :bysetpos].each do |bypart|
32
+ add_to_options_hash(options_hash, bypart, by_list[bypart])
33
+ end
34
+ [:byday, :bymonthday, :byyearday, :byweekno].each do |bypart|
35
+ add_byrule_strings_to_options_hash(options_hash, bypart)
36
+ end
37
+ options_hash
38
+ end
39
+
40
+ def initialize_from_value_part(part, dup_hash) # :nodoc:
41
+ part_name, value = part.split("=")
42
+ attribute = part_name.downcase
43
+ errors << "Repeated rule part #{attribute} last occurrence was used" if dup_hash[attribute]
44
+ case attribute
45
+ when "freq"
46
+ self.freq = value
47
+ when "wkst"
48
+ self.wkst = value
49
+ when "until"
50
+ @until = PropertyValue.date_or_date_time(self, :value => value)
51
+ when "count"
52
+ @count = value.to_i
53
+ when "interval"
54
+ self.interval = value.to_i
55
+ when "bysecond", "byminute", "byhour", "bymonthday", "byyearday", "byweekno", "bymonth", "bysetpos"
56
+ send("#{attribute}=", value.split(",").map {|int| int.to_i})
57
+ when "byday"
58
+ self.byday = value.split(",")
59
+ else
60
+ errors << "Invalid rule part #{part}"
61
+ end
62
+ end
63
+
64
+ def by_list
65
+ @by_list ||= {}
66
+ end
67
+
68
+ def calc_by_day_scope
69
+ case freq
70
+ when "YEARLY"
71
+ scope = :yearly
72
+ when "MONTHLY"
73
+ scope = :monthly
74
+ when "WEEKLY"
75
+ scope = :weekly
76
+ else
77
+ scope = :daily
78
+ end
79
+ scope = :monthly if scope != :weekly && @by_list_hash[:bymonth]
80
+ scope = :weekly if scope != :daily && @by_list_hash[:byweekno]
81
+ @by_day_scope = scope
82
+ end
83
+
84
+ def bysecond=(val)
85
+ @by_list_hash[:bysecond] = val
86
+ end
87
+
88
+ def byminute=(val)
89
+ @by_list_hash[:byminute] = val
90
+ end
91
+
92
+ def byhour=(val)
93
+ @by_list_hash[:byhour] = val
94
+ end
95
+
96
+ def bymonth=(val)
97
+ @by_list_hash[:bymonth] = val
98
+ end
99
+
100
+ def bysetpos=(val)
101
+ @by_list_hash[:bysetpos] = val
102
+ end
103
+
104
+ def byday=(val)
105
+ @by_list_hash[:byday] = val
106
+ end
107
+
108
+ def bymonthday=(val)
109
+ @by_list_hash[:bymonthday] = val
110
+ end
111
+
112
+ def byyearday=(val)
113
+ @by_list_hash[:byyearday] = val
114
+ end
115
+
116
+ def byweekno=(val)
117
+ @by_list_hash[:byweekno] = val
118
+ end
119
+
120
+ def init_by_lists
121
+ [:bysecond,
122
+ :byminute,
123
+ :byhour,
124
+ :bymonth,
125
+ :bysetpos
126
+ ].each do |which|
127
+ if val = @by_list_hash[which]
128
+ by_list[which] = [val].flatten.sort
129
+ end
130
+ end
131
+ if val = @by_list_hash[:byday]
132
+ byday_scope = calc_by_day_scope
133
+ by_list[:byday] = [val].flatten.map {|day| RecurringDay.new(day, self, byday_scope)}
134
+ end
135
+ if val = @by_list_hash[:bymonthday]
136
+ by_list[:bymonthday] = [val].flatten.map {|md| RecurringMonthDay.new(md)}
137
+ end
138
+ if val = @by_list_hash[:byyearday]
139
+ by_list[:byyearday] = [val].flatten.map {|yd| RecurringYearDay.new(yd)}
140
+ end
141
+ if val = @by_list_hash[:byweekno]
142
+ by_list[:byweekno] = [val].flatten.map {|wkno| RecurringNumberedWeek.new(wkno, self)}
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,53 @@
1
+ module RiCal
2
+ class PropertyValue
3
+ class RecurrenceRule < PropertyValue
4
+ #- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license
5
+ #
6
+ class NegativeSetposEnumerator < Enumerator # :nodoc:
7
+
8
+ def initialize(recurrence_rule, component, setpos_list)
9
+ super
10
+ @current_set = []
11
+ @valids = []
12
+ end
13
+
14
+ def next_occurrence
15
+ while true
16
+ result = advance
17
+ if result >= start_time
18
+ @count += 1
19
+ return recurrence_rule.exhausted?(@count, result) ? nil : result_occurrence_period(result)
20
+ end
21
+ end
22
+ end
23
+
24
+
25
+
26
+ def advance
27
+ if @valids.empty?
28
+ fill_set
29
+ @valids = @setpos_list.map {|sp| sp < 0 ? @current_set.length + sp : sp - 1}
30
+ current_time_index = @current_set.index(@start_time)
31
+ if current_time_index
32
+ @valids << current_time_index
33
+ end
34
+ @valids = @valids.uniq.sort
35
+ end
36
+ @current_set[@valids.shift]
37
+ end
38
+
39
+ def fill_set
40
+ @current_set = [next_time]
41
+ while true
42
+ self.next_time = @incrementer.next_time(next_time)
43
+ if recurrence_rule.in_same_set?(@current_set.last, next_time)
44
+ @current_set << next_time
45
+ else
46
+ return
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ module RiCal
2
+ class PropertyValue
3
+ class RecurrenceRule < PropertyValue
4
+ #- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license
5
+ #
6
+ class NumberedSpan # :nodoc:
7
+ attr_reader :source
8
+ def initialize(source, rule = nil)
9
+ @source = source
10
+ @rule = rule
11
+ end
12
+
13
+ def valid?
14
+ (1..last).include?(source) || (-last..-1).include?(source)
15
+ end
16
+
17
+ def ==(another)
18
+ self.class == another.class && source == another.source
19
+ end
20
+
21
+ def to_s
22
+ source.to_s
23
+ end
24
+
25
+ def ordinal
26
+ @source
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,793 @@
1
+ module RiCal
2
+ class PropertyValue
3
+ class RecurrenceRule < PropertyValue
4
+ module TimeManipulation #:nodoc:
5
+
6
+ def advance_day(date_time)
7
+ date_time.advance(:days => 1)
8
+ end
9
+
10
+ def first_hour_of_day(date_time)
11
+ date_time.change(:hour => 0)
12
+ end
13
+
14
+ def advance_week(date_time)
15
+ date_time.advance(:days => 7)
16
+ end
17
+
18
+ def first_day_of_week(wkst_day, date_time)
19
+ date_time.at_start_of_week_with_wkst(wkst_day)
20
+ end
21
+
22
+ def advance_month(date_time)
23
+ date_time.advance(:months => 1)
24
+ end
25
+
26
+ def first_day_of_month(date_time)
27
+ date_time.change(:day => 1)
28
+ end
29
+
30
+ def advance_year(date_time)
31
+ date_time.advance(:years => 1)
32
+ end
33
+
34
+ def first_day_of_year(date_time)
35
+ date_time.change(:month => 1, :day => 1)
36
+ end
37
+ end
38
+ #- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license
39
+ #
40
+ class OccurrenceIncrementer # :nodoc:
41
+
42
+ attr_accessor :sub_cycle_incrementer, :current_occurrence, :outer_range
43
+ attr_accessor :outer_incrementers
44
+ attr_accessor :contains_daily_incrementer, :contains_weeknum_incrementer
45
+ attr_reader :leaf_iterator
46
+
47
+ include TimeManipulation
48
+
49
+ class NullSubCycleIncrementer #:nodoc:
50
+ def self.next_time(previous)
51
+ nil
52
+ end
53
+
54
+ def self.add_outer_incrementer(incrementer)
55
+ end
56
+
57
+ def self.first_within_outer_cycle(previous_occurrence, outer_cycle_range)
58
+ outer_cycle_range.first
59
+ end
60
+
61
+ def self.first_sub_occurrence(previous_occurrence, outer_cycle_range)
62
+ nil
63
+ end
64
+
65
+ def self.cycle_adjust(date_time)
66
+ date_time
67
+ end
68
+
69
+ def self.to_s
70
+ "NULL-INCR"
71
+ end
72
+
73
+ def inspect
74
+ to_s
75
+ end
76
+ end
77
+
78
+ def initialize(rrule, sub_cycle_incrementer)
79
+ self.sub_cycle_incrementer = sub_cycle_incrementer
80
+ @outermost = true
81
+ self.outer_incrementers = []
82
+ if sub_cycle_incrementer
83
+ self.contains_daily_incrementer = sub_cycle_incrementer.daily_incrementer? ||
84
+ sub_cycle_incrementer.contains_daily_incrementer?
85
+ self.contains_weeknum_incrementer = sub_cycle_incrementer.weeknum_incrementer?||
86
+ sub_cycle_incrementer.contains_weeknum_incrementer?
87
+ sub_cycle_incrementer.add_outer_incrementer(self)
88
+ else
89
+ self.sub_cycle_incrementer = NullSubCycleIncrementer
90
+ end
91
+ end
92
+
93
+ def add_outer_incrementer(incrementer)
94
+ @outermost = false
95
+ self.outer_incrementers << incrementer
96
+ sub_cycle_incrementer.add_outer_incrementer(incrementer)
97
+ end
98
+
99
+ def outermost?
100
+ @outermost
101
+ end
102
+
103
+ def to_s
104
+ if sub_cycle_incrementer
105
+ "#{self.short_name}->#{sub_cycle_incrementer}"
106
+ else
107
+ self.short_name
108
+ end
109
+ end
110
+
111
+ def short_name
112
+ @short_name ||= self.class.name.split("::").last
113
+ end
114
+
115
+ # Return the next time after previous_occurrence generated by this incrementer
116
+ # But the occurrence is outside the current cycle of any outer incrementer(s) return
117
+ # nil which will cause the outer incrementer to step to its next cycle.
118
+ def next_time(previous_occurrence)
119
+ if current_occurrence
120
+ sub_occurrence = sub_cycle_incrementer.next_time(previous_occurrence)
121
+ else #first time
122
+ sub_occurrence = sub_cycle_incrementer.first_sub_occurrence(previous_occurrence, update_cycle_range(previous_occurrence))
123
+ end
124
+ if sub_occurrence
125
+ candidate = sub_occurrence
126
+ else
127
+ candidate = next_cycle(previous_occurrence)
128
+ end
129
+ if in_outer_cycle?(candidate)
130
+ candidate
131
+ else
132
+ nil
133
+ end
134
+ end
135
+
136
+ def update_cycle_range(date_time)
137
+ self.current_occurrence = date_time
138
+ (date_time..end_of_occurrence(date_time))
139
+ end
140
+
141
+ def in_outer_cycle?(candidate)
142
+ candidate && (outer_range.nil? || (outer_range.first <= candidate && outer_range.last >= candidate))
143
+ end
144
+
145
+ def first_sub_occurrence(previous_occurrence, outer_cycle_range)
146
+ first_within_outer_cycle(previous_occurrence, outer_cycle_range)
147
+ end
148
+
149
+ # Advance to the next cycle, if the result is within the current cycles of all outer incrementers
150
+ def next_cycle(previous_occurrence)
151
+ raise "next_cycle is a subclass responsibility"
152
+ end
153
+
154
+ def contains_daily_incrementer?
155
+ @contains_daily_incrementer
156
+ end
157
+
158
+ def daily_incrementer?
159
+ false
160
+ end
161
+
162
+ def contains_weeknum_incrementer?
163
+ @contains_weeknum_incrementer
164
+ end
165
+
166
+ def weeknum_incrementer?
167
+ false
168
+ end
169
+ end
170
+
171
+ # A ListIncrementer represents a byxxx part of a recurrence rule
172
+ # It contains a list of simple values or recurring values
173
+ # It keeps a collection of occurrences within a given range called a cycle
174
+ # When the collection of occurrences is exhausted it is refreshed if there is no
175
+ # outer incrementer, or if a new cycle would start in the current cycle of the outer incrementers.
176
+ class ListIncrementer < OccurrenceIncrementer #:nodoc:
177
+ attr_accessor :occurrences, :list, :outer_occurrence, :cycle_start
178
+
179
+ def initialize(rrule, list, sub_cycle_incrementer)
180
+ super(rrule, sub_cycle_incrementer)
181
+ self.list = list
182
+ end
183
+
184
+ def self.conditional_incrementer(rrule, by_part, sub_cycle_class)
185
+ sub_cycle_incrementer = sub_cycle_class.for_rrule(rrule)
186
+ list = rrule.by_rule_list(by_part)
187
+ if list
188
+ new(rrule, list, sub_cycle_incrementer)
189
+ else
190
+ sub_cycle_incrementer
191
+ end
192
+ end
193
+
194
+ # Advance to the next occurrence, if the result is within the current cycles of all outer incrementers
195
+ def next_cycle(previous_occurrence)
196
+ unless occurrences
197
+ self.occurrences = occurrences_for(previous_occurrence)
198
+ end
199
+ candidate = next_candidate(previous_occurrence)
200
+ if candidate
201
+ sub_cycle_incrementer.first_within_outer_cycle(previous_occurrence, update_cycle_range(candidate))
202
+ else
203
+ nil
204
+ end
205
+ end
206
+
207
+ def first_within_outer_cycle(previous_occurrence, outer_range)
208
+ self.outer_range = outer_range
209
+ self.occurrences = occurrences_within(outer_range)
210
+ occurrences.each { |occurrence|
211
+ sub = sub_cycle_incrementer.first_within_outer_cycle(previous_occurrence, update_cycle_range(occurrence))
212
+ return sub if sub && sub > previous_occurrence
213
+ }
214
+ nil
215
+ end
216
+
217
+ def next_candidate(date_time)
218
+ candidate = next_in_list(date_time)
219
+ if outermost?
220
+ while candidate.nil?
221
+ get_next_occurrences
222
+ candidate = next_in_list(date_time)
223
+ end
224
+ end
225
+ candidate
226
+ end
227
+
228
+ def next_in_list(date_time)
229
+ occurrences.find {|occurrence| occurrence > date_time}
230
+ end
231
+
232
+ def get_next_occurrences
233
+ adv_cycle = advance_cycle(start_of_cycle(occurrences.first))
234
+ self.occurrences = occurrences_for(adv_cycle)
235
+ end
236
+
237
+ def cycle_adjust(date_time)
238
+ sub_cycle_incrementer.cycle_adjust(start_of_cycle(date_time))
239
+ end
240
+
241
+ def occurrences_for(date_time)
242
+ list.map {|value| date_time.change(varying_time_attribute => value)}
243
+ end
244
+
245
+ def occurrences_within(date_time_range)
246
+ result = []
247
+ date_time = date_time_range.first
248
+ while date_time <= date_time_range.last
249
+ result << occurrences_for(date_time)
250
+ date_time = advance_cycle(date_time)
251
+ end
252
+ result.flatten
253
+ end
254
+ end
255
+
256
+ # A FrequenceIncrementer represents the xxxLY and FREQ parts of a recurrence rule
257
+ # A FrequenceIncrementer has a single occurrence within each cycle.
258
+ class FrequencyIncrementer < OccurrenceIncrementer #:nodoc:
259
+ attr_accessor :interval, :outer_occurrence, :skip_increment
260
+
261
+ alias_method :cycle_start, :current_occurrence
262
+
263
+ def initialize(rrule, sub_cycle_incrementer)
264
+ super(rrule, sub_cycle_incrementer)
265
+ self.interval = rrule.interval
266
+ end
267
+
268
+ def self.conditional_incrementer(rrule, freq_str, sub_cycle_class)
269
+ sub_cycle_incrementer = sub_cycle_class.for_rrule(rrule)
270
+ if rrule.freq == freq_str
271
+ new(rrule, sub_cycle_incrementer)
272
+ else
273
+ sub_cycle_incrementer
274
+ end
275
+ end
276
+
277
+ def multiplier
278
+ 1
279
+ end
280
+
281
+ def step(occurrence)
282
+ occurrence.advance(advance_what => (interval * multiplier))
283
+ end
284
+
285
+ def first_within_outer_cycle(previous_occurrence, outer_cycle_range)
286
+ if outer_range
287
+ first_occurrence = outer_cycle_range.first
288
+ else
289
+ first_occurrence = step(previous_occurrence)
290
+ end
291
+ self.outer_range = outer_cycle_range
292
+ sub_cycle_incrementer.first_within_outer_cycle(previous_occurrence, update_cycle_range(first_occurrence))
293
+ end
294
+
295
+ # Advance to the next occurrence, if the result is within the current cycles of all outer incrementers
296
+ def next_cycle(previous_occurrence)
297
+ if current_occurrence
298
+ candidate = sub_cycle_incrementer.cycle_adjust(step(current_occurrence))
299
+ else
300
+ candidate = step(previous_occurrence)
301
+ end
302
+ if outermost?
303
+ sub_occurrence = sub_cycle_incrementer.first_within_outer_cycle(previous_occurrence, update_cycle_range(candidate))
304
+ until sub_occurrence
305
+ candidate = sub_cycle_incrementer.cycle_adjust(step(candidate))
306
+ sub_occurrence = sub_cycle_incrementer.first_within_outer_cycle(previous_occurrence, update_cycle_range(candidate))
307
+ end
308
+ sub_occurrence
309
+ elsif in_outer_cycle?(candidate)
310
+ sub_cycle_incrementer.first_within_outer_cycle(previous_occurrence, update_cycle_range(candidate))
311
+ else
312
+ nil
313
+ end
314
+ end
315
+ end
316
+
317
+ class SecondlyIncrementer < FrequencyIncrementer #:nodoc:
318
+
319
+ def self.for_rrule(rrule)
320
+ if rrule.freq == "SECONDLY"
321
+ new(rrule, nil)
322
+ else
323
+ nil
324
+ end
325
+ end
326
+
327
+ def advance_what
328
+ :seconds
329
+ end
330
+
331
+ def end_of_occurrence(date_time)
332
+ date_time
333
+ end
334
+ end
335
+
336
+
337
+ class BySecondIncrementer < ListIncrementer #:nodoc:
338
+
339
+ def self.for_rrule(rrule)
340
+ conditional_incrementer(rrule, :bysecond, SecondlyIncrementer)
341
+ end
342
+
343
+ def varying_time_attribute
344
+ :sec
345
+ end
346
+
347
+ def start_of_cycle(date_time)
348
+ date_time.start_of_minute
349
+ end
350
+
351
+ def advance_cycle(date_time)
352
+ date_time.advance(:minutes => 1).start_of_minute
353
+ end
354
+
355
+ def end_of_occurrence(date_time)
356
+ date_time
357
+ end
358
+ end
359
+
360
+ class MinutelyIncrementer < FrequencyIncrementer #:nodoc:
361
+ def self.for_rrule(rrule)
362
+ conditional_incrementer(rrule, "MINUTELY", BySecondIncrementer)
363
+ end
364
+
365
+ def advance_what
366
+ :minutes
367
+ end
368
+
369
+ def end_of_occurrence(date_time)
370
+ date_time.end_of_minute
371
+ end
372
+ end
373
+
374
+ class ByMinuteIncrementer < ListIncrementer #:nodoc:
375
+ def self.for_rrule(rrule)
376
+ conditional_incrementer(rrule, :byminute, MinutelyIncrementer)
377
+ end
378
+
379
+ def advance_cycle(date_time)
380
+ date_time.advance(:hours => 1).start_of_hour
381
+ end
382
+
383
+ def start_of_cycle(date_time)
384
+ date_time.change(:min => 0)
385
+ end
386
+
387
+ def end_of_occurrence(date_time)
388
+ date_time.end_of_minute
389
+ end
390
+
391
+ def varying_time_attribute
392
+ :min
393
+ end
394
+ end
395
+
396
+ class HourlyIncrementer < FrequencyIncrementer #:nodoc:
397
+ def self.for_rrule(rrule)
398
+ conditional_incrementer(rrule, "HOURLY", ByMinuteIncrementer)
399
+ end
400
+
401
+ def advance_what
402
+ :hours
403
+ end
404
+
405
+ def end_of_occurrence(date_time)
406
+ date_time.end_of_hour
407
+ end
408
+ end
409
+
410
+ class ByHourIncrementer < ListIncrementer #:nodoc:
411
+ def self.for_rrule(rrule)
412
+ conditional_incrementer(rrule, :byhour, HourlyIncrementer)
413
+ end
414
+
415
+ def start_of_cycle(date_time)
416
+ date_time.change(:hour => 0)
417
+ end
418
+
419
+ def varying_time_attribute
420
+ :hour
421
+ end
422
+
423
+ def advance_cycle(date_time)
424
+ first_hour_of_day(advance_day(date_time))
425
+ end
426
+
427
+ def end_of_occurrence(date_time)
428
+ date_time.end_of_hour
429
+ end
430
+ end
431
+
432
+ class DailyIncrementer < FrequencyIncrementer #:nodoc:
433
+
434
+ def self.for_rrule(rrule)
435
+ conditional_incrementer(rrule, "DAILY", ByHourIncrementer)
436
+ end
437
+
438
+ def daily_incrementer?
439
+ true
440
+ end
441
+
442
+ def advance_what
443
+ :days
444
+ end
445
+
446
+ def end_of_occurrence(date_time)
447
+ date_time.end_of_day
448
+ end
449
+ end
450
+
451
+ class ByNumberedDayIncrementer < ListIncrementer #:nodoc:
452
+
453
+ def daily_incrementer?
454
+ true
455
+ end
456
+
457
+ def occurrences_for(date_time)
458
+ if occurrences && @scoping_value == scope_of(date_time)
459
+ occurrences
460
+ else
461
+ @scoping_value = scope_of(date_time)
462
+ self.occurrences = list.map {|numbered_day| numbered_day.target_date_time_for(date_time)}.uniq.sort
463
+ occurrences
464
+ end
465
+ end
466
+
467
+ def end_of_occurrence(date_time)
468
+ date_time.end_of_day
469
+ end
470
+
471
+ def candidate_acceptible?(candidate)
472
+ list.any? {|by_part| by_part.include?(candidate)}
473
+ end
474
+ end
475
+
476
+ class ByMonthdayIncrementer < ByNumberedDayIncrementer #:nodoc:
477
+ def self.for_rrule(rrule)
478
+ conditional_incrementer(rrule, :bymonthday, DailyIncrementer)
479
+ end
480
+
481
+ def scope_of(date_time)
482
+ date_time.month
483
+ end
484
+
485
+ def start_of_cycle(date_time)
486
+ date_time.change(:day => 1)
487
+ end
488
+
489
+ def advance_cycle(date_time)
490
+ first_day_of_month(advance_month(date_time))
491
+ end
492
+
493
+ def end_of_occurrence(date_time)
494
+ date_time.end_of_day
495
+ end
496
+ end
497
+
498
+ class ByYeardayIncrementer < ByNumberedDayIncrementer #:nodoc:
499
+ def self.for_rrule(rrule)
500
+ conditional_incrementer(rrule, :byyearday, ByMonthdayIncrementer)
501
+ end
502
+
503
+ def start_of_cycle(date_time)
504
+ date_time.change(:month => 1, :day => 1)
505
+ end
506
+
507
+ def scope_of(date_time)
508
+ date_time.year
509
+ end
510
+
511
+ def advance_cycle(date_time)
512
+ first_day_of_year(advance_year(date_time))
513
+ end
514
+
515
+ def end_of_occurrence(date_time)
516
+ date_time.end_of_day
517
+ end
518
+ end
519
+
520
+ class ByDayIncrementer < ListIncrementer #:nodoc:
521
+
522
+ def initialize(rrule, list, by_monthday_list, by_yearday_list, parent)
523
+ super(rrule, list, parent)
524
+ @monthday_filters = by_monthday_list
525
+ @yearday_filters = by_yearday_list
526
+ @by_day_scope = rrule.by_day_scope
527
+
528
+ case rrule.by_day_scope
529
+ when :yearly
530
+ @cycle_advance_proc = lambda {|date_time| first_day_of_year(advance_year(date_time))}
531
+ @current_proc = lambda {|date_time| same_year?(current, date_time)}
532
+ @first_day_proc = lambda {|date_time| first_day_of_year(date_time)}
533
+ when :monthly
534
+ @cycle_advance_proc = lambda {|date_time| first_day_of_month(advance_month(date_time))}
535
+ @current_proc = lambda {|date_time| same_month?(current, date_time)}
536
+ @first_day_proc = lambda {|date_time| first_day_of_month(date_time)}
537
+ when :weekly
538
+ @cycle_advance_proc = lambda {|date_time| first_day_of_week(rrule.wkst_day, advance_week(date_time))}
539
+ @current_proc = lambda {|date_time| same_week?(rrule.wkst_day, current, date_time)}
540
+ @first_day_proc = lambda {|date_time| first_day_of_week(rrule.wkst_day, date_time)}
541
+ else
542
+ raise "Invalid recurrence rule, byday needs to be scoped by month, week or year"
543
+ end
544
+ end
545
+
546
+ def self.for_rrule(rrule)
547
+ list = rrule.by_rule_list(:byday)
548
+ if list
549
+ sub_cycle_incrementer = DailyIncrementer.for_rrule(rrule)
550
+ new(rrule, list, rrule.by_rule_list(:bymonthday), rrule.by_rule_list(:byyearday), sub_cycle_incrementer)
551
+ else
552
+ ByYeardayIncrementer.for_rrule(rrule)
553
+ end
554
+ end
555
+
556
+ def daily_incrementer?
557
+ true
558
+ end
559
+
560
+ def start_of_cycle(date_time)
561
+ @first_day_proc.call(date_time)
562
+ end
563
+
564
+ def occurrences_for(date_time)
565
+ first_day = start_of_cycle(date_time)
566
+ result = list.map {|recurring_day| recurring_day.matches_for(first_day)}.flatten.uniq.sort
567
+ if @monthday_filters
568
+ result = result.select {|occurrence| @monthday_filters.any? {|recurring_day| recurring_day.include?(occurrence)}}
569
+ end
570
+ if @yearday_filters
571
+ result = result.select {|occurrence| @yearday_filters.any? {|recurring_day| recurring_day.include?(occurrence)}}
572
+ end
573
+ result
574
+ end
575
+
576
+ def candidate_acceptible?(candidate)
577
+ list.any? {|recurring_day| recurring_day.include?(candidate)}
578
+ end
579
+
580
+ def varying_time_attribute
581
+ :day
582
+ end
583
+
584
+ def advance_cycle(date_time)
585
+ @cycle_advance_proc.call(date_time)
586
+ end
587
+
588
+ def end_of_occurrence(date_time)
589
+ date_time.end_of_day
590
+ end
591
+ end
592
+
593
+ class WeeklyIncrementer < FrequencyIncrementer #:nodoc:
594
+
595
+ attr_reader :wkst
596
+
597
+ # include WeeklyBydayMethods
598
+
599
+ def initialize(rrule, parent)
600
+ @wkst = rrule.wkst_day
601
+ super(rrule, parent)
602
+ end
603
+
604
+ def self.for_rrule(rrule)
605
+ conditional_incrementer(rrule, "WEEKLY", ByDayIncrementer)
606
+ end
607
+
608
+ def multiplier
609
+ 7
610
+ end
611
+
612
+ def advance_what
613
+ :days
614
+ end
615
+
616
+ def end_of_occurrence(date_time)
617
+ date_time.end_of_week_with_wkst(wkst)
618
+ end
619
+ end
620
+
621
+ class ByWeekNoIncrementer < ListIncrementer #:nodoc:
622
+ attr_reader :wkst
623
+ # include WeeklyBydayMethods
624
+
625
+ def initialize(rrule, list, sub_cycle_incrementer)
626
+ @wkst = rrule.wkst_day
627
+ super(rrule, list, sub_cycle_incrementer)
628
+ end
629
+
630
+ def self.for_rrule(rrule)
631
+ conditional_incrementer(rrule, :byweekno, WeeklyIncrementer)
632
+ end
633
+
634
+ def weeknum_incrementer?
635
+ true
636
+ end
637
+
638
+ def first_within_outer_cycle(previous_occurrence, outer_range)
639
+ new_range_start = outer_range.first
640
+ new_range_end = new_range_start.end_of_iso_year(wkst)
641
+ super(previous_occurrence, outer_range.first..new_range_end)
642
+ end
643
+
644
+ def start_of_cycle(date_time)
645
+ result = date_time.at_start_of_iso_year(wkst)
646
+ result
647
+ end
648
+
649
+ def occurrences_for(date_time)
650
+ iso_year, year_start = *date_time.iso_year_and_week_one_start(wkst)
651
+ week_one_occurrence = date_time.change(
652
+ :year => year_start.year,
653
+ :month => year_start.month,
654
+ :day => year_start.day
655
+ )
656
+ weeks_in_year_plus_one = week_one_occurrence.iso_weeks_in_year(wkst)
657
+ weeks = list.map {|recurring_weeknum|
658
+ wk_num = recurring_weeknum.ordinal
659
+ (wk_num > 0) ? wk_num : weeks_in_year_plus_one + wk_num
660
+ }.uniq.sort
661
+ weeks.map {|wk_num| week_one_occurrence.advance(:days => (wk_num - 1) * 7)}
662
+ end
663
+
664
+ def candidate_acceptible?(candidate)
665
+ list.include?(candidate.iso_week_num(wkst))
666
+ end
667
+
668
+ def advance_cycle(date_time)
669
+ date_time.at_start_of_next_iso_year(wkst)
670
+ end
671
+
672
+ def end_of_occurrence(date_time)
673
+ date_time.end_of_week_with_wkst(wkst)
674
+ end
675
+ end
676
+
677
+ class MonthlyIncrementer < FrequencyIncrementer #:nodoc:
678
+
679
+ def self.for_rrule(rrule)
680
+ conditional_incrementer(rrule, "MONTHLY", ByWeekNoIncrementer)
681
+ end
682
+
683
+ def advance_what
684
+ :months
685
+ end
686
+
687
+ def step(date_time)
688
+ if contains_daily_incrementer?
689
+ result = super(date_time).change(:day => 1)
690
+ result
691
+ else
692
+ super(date_time)
693
+ end
694
+ end
695
+
696
+ def end_of_occurrence(date_time)
697
+ date_time.end_of_month
698
+ end
699
+ end
700
+
701
+ class ByMonthIncrementer < ListIncrementer #:nodoc:
702
+
703
+ def self.for_rrule(rrule)
704
+ conditional_incrementer(rrule, :bymonth, MonthlyIncrementer)
705
+ end
706
+
707
+ def occurrences_for(date_time)
708
+ if contains_daily_incrementer?
709
+ list.map {|value| date_time.change(:month => value, :day => 1)}
710
+ else
711
+ list.map {|value| date_time.in_month(value)}
712
+ end
713
+ end
714
+
715
+ def range_advance(date_time)
716
+ advance_year(date_time)
717
+ end
718
+
719
+ def start_of_cycle(date_time)
720
+ if contains_daily_incrementer?
721
+ date_time.change(:month => 1, :day => 1)
722
+ else
723
+ date_time.change(:month => 1)
724
+ end
725
+ end
726
+
727
+ def varying_time_attribute
728
+ :month
729
+ end
730
+
731
+ def advance_cycle(date_time)
732
+ if contains_daily_incrementer?
733
+ first_day_of_year(advance_year(date_time))
734
+ else
735
+ advance_year(date_time).change(:month => 1)
736
+ end
737
+ end
738
+
739
+ def end_of_occurrence(date_time)
740
+ date_time.end_of_month
741
+ end
742
+ end
743
+
744
+ class YearlyIncrementer < FrequencyIncrementer #:nodoc:
745
+
746
+ attr_reader :wkst
747
+
748
+ def initialize(rrule, sub_cycle_incrementer)
749
+ @wkst = rrule.wkst_day
750
+ super(rrule, sub_cycle_incrementer)
751
+ end
752
+
753
+ def self.from_rrule(rrule, start_time)
754
+ conditional_incrementer(rrule, "YEARLY", ByMonthIncrementer)
755
+ end
756
+
757
+ def advance_what
758
+ :years
759
+ end
760
+
761
+ def step(date_time)
762
+ if contains_weeknum_incrementer?
763
+ result = date_time
764
+ multiplier.times do
765
+ result = result.at_start_of_next_iso_year(wkst)
766
+ end
767
+ result
768
+ else
769
+ super(date_time)
770
+ end
771
+ end
772
+
773
+ def start_of_cycle(date_time)
774
+ if contains_weeknum_incrementer?
775
+ date_time.at_start_of_iso_year(wkst)
776
+ elsif contains_daily_incrementer?
777
+ date_time.change(:month => 1, :day => 1)
778
+ else
779
+ date_time.change(:month => 1)
780
+ end
781
+ end
782
+
783
+ def end_of_occurrence(date_time)
784
+ if contains_weeknum_incrementer?
785
+ date_time.end_of_iso_year(wkst)
786
+ else
787
+ date_time.end_of_year
788
+ end
789
+ end
790
+ end
791
+ end
792
+ end
793
+ end