ri_cal 0.5.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 (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