ri_cal 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,6 @@
1
+ === 0.5.3 - 1 June, 2009
2
+ * Improved performance of time zone enumeration, TimeZonePeriod now caches occurrences
3
+ * Added a profiling directory which contains ruby programs which benchmark and/or profile performance
1
4
  === 0.5.2
2
5
  * Fixed http://rick_denatale.lighthouseapp.com/projects/30941/tickets/11
3
6
  Export folding is not UTF-8 Safe
data/Manifest.txt CHANGED
@@ -89,6 +89,10 @@ lib/ri_cal/property_value/text.rb
89
89
  lib/ri_cal/property_value/uri.rb
90
90
  lib/ri_cal/property_value/utc_offset.rb
91
91
  lib/ri_cal/required_timezones.rb
92
+ profiling/ical_files/profile3.ics
93
+ profiling/profile1.rb
94
+ profiling/profile2.rb
95
+ profiling/profile3.rb
92
96
  ri_cal.gemspec
93
97
  sample_ical_files/from_ical_dot_app/test1.ics
94
98
  script/console
data/Rakefile CHANGED
@@ -10,12 +10,13 @@ $hoe = Hoe.new('ri_cal', RiCal::VERSION) do |p|
10
10
  p.developer('author=Rick DeNatale', 'rick.denatale@gmail.com')
11
11
  p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
12
12
  # p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
13
- p.rubyforge_name = 'rical'
13
+ p.rubyforge_name = 'ri-cal'
14
14
  # p.extra_deps = [
15
15
  # ['tzinfo','>= 2.0.2'],
16
16
  # ]
17
17
  p.extra_dev_deps = [
18
- ['newgem', ">= #{::Newgem::VERSION}"]
18
+ ['newgem', ">= #{::Newgem::VERSION}"],
19
+ 'ruby-prof'
19
20
  ]
20
21
 
21
22
  p.clean_globs |= %w[**/.DS_Store tmp *.log]
data/lib/ri_cal.rb CHANGED
@@ -4,6 +4,9 @@
4
4
  # and building calendars and calendar components.
5
5
  module RiCal
6
6
 
7
+ require 'stringio'
8
+ require 'rational'
9
+
7
10
  my_dir = File.dirname(__FILE__)
8
11
 
9
12
  autoload :Component, "#{my_dir}/ri_cal/component.rb"
@@ -11,7 +14,7 @@ module RiCal
11
14
  autoload :OccurrenceEnumerator, "#{my_dir}/ri_cal/occurrence_enumerator.rb"
12
15
 
13
16
  # :stopdoc:
14
- VERSION = '0.5.2'
17
+ VERSION = '0.5.3'
15
18
  LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
16
19
  PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
17
20
 
@@ -137,8 +140,36 @@ module RiCal
137
140
  def self.Todo(&init_block)
138
141
  Component::Todo.new(&init_block)
139
142
  end
143
+
144
+ def self.ro_calls=(value)
145
+ @ro_calls = value
146
+ end
147
+
148
+ def self.ro_calls
149
+ @ro_calls ||= 0
150
+ end
151
+
152
+ def self.ro_misses=(value)
153
+ @ro_misses = value
154
+ end
155
+
156
+ def self.ro_misses
157
+ @ro_misses ||= 0
158
+ end
159
+
160
+ def self.RationalOffset
161
+ self.ro_calls += 1
162
+ @rational_offset ||= Hash.new {|h, seconds|
163
+ self.ro_misses += 1
164
+ h[seconds] = Rational(seconds, 86400)}
165
+ end
166
+
140
167
  end # module RiCal
141
168
 
142
- RiCal.require_all_libs_relative_to(__FILE__)
169
+ (-12..12).each do |hour_offset|
170
+ RiCal.RationalOffset[hour_offset * 86400]
171
+ end
172
+
143
173
 
174
+ RiCal.require_all_libs_relative_to(__FILE__)
144
175
  # EOF
@@ -140,7 +140,8 @@ module RiCal
140
140
 
141
141
 
142
142
  def rational_utc_offset(local)
143
- Rational(tzinfo.period_for_local(local, true).utc_total_offset, 3600) / 24
143
+ # 86400 is the number of seconds in a day
144
+ RiCal.RationalOffset[tzinfo.period_for_local(local, true).utc_total_offset]
144
145
  end
145
146
 
146
147
  end
@@ -24,7 +24,8 @@ module RiCal
24
24
  end
25
25
 
26
26
  def rational_utc_offset(local) #:nodoc:
27
- Rational(period_for_local(local, true).utc_total_offset, 3600) / 24
27
+ # 86400 is the number of seconds in a day
28
+ RiCal.RationalOffset[period_for_local(local, true).utc_total_offset]
28
29
  end
29
30
 
30
31
  # Returns the TimezonePeriod for the given UTC time. utc can either be a DateTime,
@@ -14,6 +14,10 @@ module RiCal
14
14
 
15
15
  include OccurrenceEnumerator
16
16
 
17
+ def occurrence_cache #:nodoc:
18
+ @occurrence_cache ||= []
19
+ end
20
+
17
21
  def zone_identifier #:nodoc:
18
22
  tzname.first
19
23
  end
@@ -25,7 +29,7 @@ module RiCal
25
29
  def exdate_property #:nodoc:
26
30
  nil
27
31
  end
28
-
32
+
29
33
  def utc_total_offset #:nodoc:
30
34
  tzoffsetto_property.to_seconds
31
35
  end
@@ -38,13 +42,34 @@ module RiCal
38
42
  last_before_local(utc_time + tzoffsetfrom_property)
39
43
  end
40
44
 
45
+ def fill_cache(local_time)
46
+ if occurrence_cache.empty? || occurrence_cache.last.dtstart_property <= local_time
47
+ while true
48
+ occurrence = enumeration_instance.next_occurrence
49
+ break unless occurrence
50
+ occurrence = recurrence(occurrence)
51
+ occurrence_cache << occurrence
52
+ break if occurrence.dtstart_property > local_time
53
+ end
54
+ end
55
+ end
56
+
41
57
  def last_before_local(local_time) #:nodoc:
42
- cand_occurrence = nil
43
- each do |occurrence|
44
- return cand_occurrence if occurrence.dtstart_property > local_time
45
- cand_occurrence = occurrence
58
+ if recurs?
59
+ fill_cache(local_time)
60
+ cand_occurrence = nil
61
+ occurrence_cache.each do |occurrence|
62
+ return cand_occurrence if occurrence.dtstart_property > local_time
63
+ cand_occurrence = occurrence
64
+ end
65
+ return cand_occurrence
66
+ else
67
+ return self
46
68
  end
47
- return cand_occurrence
69
+ end
70
+
71
+ def enumeration_instance
72
+ @enumeration_instance ||= super
48
73
  end
49
74
  end
50
75
  end
@@ -13,6 +13,7 @@ module RiCal
13
13
  # to see the methods for enumerating occurrences of recurring to-dos see the RiCal::OccurrenceEnumerator module
14
14
  class Todo < Component
15
15
  include Properties::Todo
16
+ include OccurrenceEnumerator
16
17
 
17
18
  def self.entity_name #:nodoc:
18
19
  "VTODO"
@@ -52,7 +52,8 @@ module RiCal
52
52
  # your_time = Time.parse("1/13/2009 1:13:03 P.M.") # => Tue Jan 13 13:13:03 -0500 2009
53
53
  # your_time.to_datetime # => Tue, 13 Jan 2009 13:13:03 -0500
54
54
  def to_datetime
55
- ::DateTime.civil(year, month, day, hour, min, sec, Rational(utc_offset, 86400))
55
+ # 86400 is the number of seconds in a day
56
+ ::DateTime.civil(year, month, day, hour, min, sec, RiCal.RationalOffset[utc_offset])
56
57
  end
57
58
  end
58
59
  end
@@ -15,7 +15,7 @@ module RiCal
15
15
  end
16
16
 
17
17
  def self.rational_utc_offset(local) #:nodoc:
18
- Rational(0, 24)
18
+ @offset = RiCal.RationalOffset[0]
19
19
  end
20
20
 
21
21
  # Return the time unchanged
@@ -5,7 +5,7 @@ module RiCal
5
5
  # OccurrenceEnumerator provides common methods for CalendarComponents that support recurrence
6
6
  # i.e. Event, Journal, Todo, and TimezonePeriod
7
7
  module OccurrenceEnumerator
8
-
8
+
9
9
  include Enumerable
10
10
 
11
11
  def default_duration # :nodoc:
@@ -20,17 +20,17 @@ module RiCal
20
20
  def self.next_occurrence
21
21
  nil
22
22
  end
23
-
23
+
24
24
  def self.bounded?
25
25
  true
26
26
  end
27
-
27
+
28
28
  def self.empty?
29
29
  true
30
30
  end
31
31
  end
32
-
33
- # OccurrenceMerger takes multiple recurrence rules and enumerates the combination in sequence.
32
+
33
+ # OccurrenceMerger takes multiple recurrence rules and enumerates the combination in sequence.
34
34
  class OccurrenceMerger # :nodoc:
35
35
  def self.for(component, rules)
36
36
  if rules.nil? || rules.empty?
@@ -41,20 +41,20 @@ module RiCal
41
41
  new(component, rules)
42
42
  end
43
43
  end
44
-
44
+
45
45
  attr_accessor :enumerators, :nexts
46
-
46
+
47
47
  def initialize(component, rules)
48
48
  self.enumerators = rules.map {|rrule| rrule.enumerator(component)}
49
49
  @bounded = enumerators.all? {|enumerator| enumerator.bounded?}
50
50
  @empty = enumerators.all? {|enumerator| enumerator.empty?}
51
51
  self.nexts = @enumerators.map {|enumerator| enumerator.next_occurrence}
52
52
  end
53
-
53
+
54
54
  def empty?
55
55
  @empty
56
56
  end
57
-
57
+
58
58
  # return the earliest of each of the enumerators next occurrences
59
59
  def next_occurrence
60
60
  result = nexts.compact.sort.first
@@ -63,25 +63,22 @@ module RiCal
63
63
  end
64
64
  result
65
65
  end
66
-
66
+
67
67
  def bounded?
68
68
  @bounded
69
69
  end
70
70
  end
71
-
71
+
72
72
  # EnumerationInstance holds the values needed during the enumeration of occurrences for a component.
73
73
  class EnumerationInstance # :nodoc:
74
74
  include Enumerable
75
-
76
- def initialize(component, options = {})
75
+
76
+ def initialize(component)
77
77
  @component = component
78
- @start = options[:starting]
79
- @cutoff = options[:before]
80
- @count = options[:count]
81
78
  @rrules = OccurrenceMerger.for(@component, [@component.rrule_property, @component.rdate_property].flatten.compact)
82
79
  @exrules = OccurrenceMerger.for(@component, [@component.exrule_property, @component.exdate_property].flatten.compact)
83
80
  end
84
-
81
+
85
82
  # return the next exclusion which starts at the same time or after the start time of the occurrence
86
83
  # return nil if this exhausts the exclusion rules
87
84
  def exclusion_for(occurrence)
@@ -96,48 +93,76 @@ module RiCal
96
93
  def exclusion_match?(occurrence, exclusion)
97
94
  exclusion && (occurrence.dtstart == exclusion.dtstart)
98
95
  end
99
-
96
+
100
97
  # Also exclude occurrences before the :starting date_time
101
- def exclude?(occurrence)
102
- exclusion_match?(occurrence, exclusion_for(occurrence)) ||
103
- (@start && occurrence.dtstart.to_datetime < @start)
98
+ def before_start?(occurrence)
99
+ (@start && occurrence.dtstart.to_datetime < @start)
100
+ end
101
+
102
+ def next_occurrence
103
+ @yielded ||= 0
104
+ @next_exclusion ||= @exrules.next_occurrence
105
+ occurrence = nil
106
+
107
+ until occurrence
108
+ if (occurrence = @rrules.next_occurrence)
109
+ if exclusion_match?(occurrence, exclusion_for(occurrence))
110
+ occurrence = nil # Look for the next one
111
+ end
112
+ else
113
+ break
114
+ end
115
+ end
116
+ occurrence
117
+ end
118
+
119
+ def options_stop(occurrence)
120
+ occurrence != :excluded &&
121
+ (@cutoff && occurrence.dtstart.to_datetime >= @cutoff) || (@count && @yielded >= @count)
104
122
  end
105
-
123
+
124
+
106
125
  # yield each occurrence to a block
107
- # some components may be open-ended, e.g. have no COUNT or DTEND
108
- def each
126
+ # some components may be open-ended, e.g. have no COUNT or DTEND
127
+ def each(options = nil)
128
+ process_options(options) if options
109
129
  if @rrules.empty?
110
130
  yield @component
111
131
  else
112
- occurrence = @rrules.next_occurrence
113
- yielded = 0
114
- @next_exclusion = @exrules.next_occurrence
132
+ occurrence = next_occurrence
115
133
  while (occurrence)
116
- if (@cutoff && occurrence.dtstart.to_datetime >= @cutoff) || (@count && yielded >= @count)
134
+ if options_stop(occurrence)
117
135
  occurrence = nil
118
136
  else
119
- unless exclude?(occurrence)
120
- yielded += 1
137
+ unless before_start?(occurrence)
138
+ @yielded += 1
121
139
  yield @component.recurrence(occurrence)
122
140
  end
123
- occurrence = @rrules.next_occurrence
141
+ occurrence = next_occurrence
124
142
  end
125
143
  end
126
144
  end
127
145
  end
128
-
146
+
129
147
  def bounded?
130
148
  @rrules.bounded? || @count || @cutoff
131
149
  end
132
-
133
- def to_a
150
+
151
+ def process_options(options)
152
+ @start = options[:starting]
153
+ @cutoff = options[:before]
154
+ @count = options[:count]
155
+ end
156
+
157
+ def to_a(options = {})
158
+ process_options(options)
134
159
  raise ArgumentError.new("This component is unbounded, cannot produce an array of occurrences!") unless bounded?
135
- super
160
+ super()
136
161
  end
137
-
162
+
138
163
  alias_method :entries, :to_a
139
164
  end
140
-
165
+
141
166
  # return an array of occurrences according to the options parameter. If a component is not bounded, and
142
167
  # the number of occurrences to be returned is not constrained by either the :before, or :count options
143
168
  # an ArgumentError will be raised.
@@ -148,22 +173,27 @@ module RiCal
148
173
  #
149
174
  # parameter options:
150
175
  # * :starting:: a Date, Time, or DateTime, no occurrences starting before this argument will be returned
151
- # * :before:: a Date, Time, or DateTime, no occurrences starting on or after this argument will be returned.
176
+ # * :before:: a Date, Time, or DateTime, no occurrences starting on or after this argument will be returned.
152
177
  # * :count:: an integer which limits the number of occurrences returned.
153
178
  def occurrences(options={})
154
- EnumerationInstance.new(self, options).to_a
179
+ enumeration_instance.to_a(options)
155
180
  end
156
-
181
+
182
+ # TODO: Thread safe?
183
+ def enumeration_instance #:nodoc:
184
+ EnumerationInstance.new(self)
185
+ end
186
+
157
187
  # execute the block for each occurrence
158
188
  def each(&block) # :yields: Component
159
- EnumerationInstance.new(self).each(&block)
189
+ enumeration_instance.each(&block)
160
190
  end
161
-
191
+
162
192
  # A predicate which determines whether the component has a bounded set of occurrences
163
193
  def bounded?
164
- EnumerationInstance.new(self).bounded?
194
+ enumeration_instance.bounded?
165
195
  end
166
-
196
+
167
197
  # Return a array whose first element is a UTC DateTime representing the start of the first
168
198
  # occurrence, and whose second element is a UTC DateTime representing the end of the last
169
199
  # occurrence.
@@ -182,7 +212,7 @@ module RiCal
182
212
  end
183
213
  [first.zulu_occurrence_range_start_time, last ? last.zulu_occurrence_range_finish_time : nil]
184
214
  end
185
-
215
+
186
216
  def set_occurrence_properties!(occurrence) # :nodoc:
187
217
  occurrence_end = occurrence.dtend
188
218
  occurrence_start = occurrence.dtstart
@@ -196,11 +226,16 @@ module RiCal
196
226
  end
197
227
  @dtstart_property = dtstart_property.for_occurrence(occurrence_start)
198
228
  @dtend_property = dtend_property.for_occurrence(occurrence_end) if @dtend_property
199
- self
229
+ self
200
230
  end
201
-
231
+
202
232
  def recurrence(occurrence) # :nodoc:
203
233
  result = self.dup.set_occurrence_properties!(occurrence)
204
234
  end
235
+
236
+ def recurs?
237
+ @rrule_property && @rrule_property.length > 0 || @rdate_property && @rdate_property.length > 0
238
+ end
239
+
205
240
  end
206
241
  end