rubyredrick-ri_cal 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt 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]
@@ -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
@@ -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
@@ -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,
@@ -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
@@ -19,13 +19,13 @@ module RiCal
19
19
  end
20
20
 
21
21
  def compute_advance(d, options) # :nodoc:
22
- d = d >> options[:years] * 12 if options[:years]
23
- d = d >> options[:months] if options[:months]
24
- d = d + options[:weeks] * 7 if options[:weeks]
25
- d = d + options[:days] if options[:days]
22
+ months_advance = (options[:years] || 0) * 12 + (options[:months] || 0)
23
+ d = d >> months_advance unless months_advance == 0
24
+ days_advance = (options[:weeks] || 0) * 7 + (options[:days] || 0)
25
+ d = d + days_advance unless days_advance == 0
26
26
  datetime_advanced_by_date = compute_change(@date_time_value, :year => d.year, :month => d.month, :day => d.day)
27
27
  seconds_to_advance = (options[:seconds] || 0) + (options[:minutes] || 0) * 60 + (options[:hours] || 0) * 3600
28
- seconds_to_advance == 0 ? datetime_advanced_by_date : datetime_advanced_by_date + Rational(seconds_to_advance.round, 86400)
28
+ seconds_to_advance == 0 ? datetime_advanced_by_date : datetime_advanced_by_date + RiCal.RationalOffset[seconds_to_advance.round]
29
29
  end
30
30
 
31
31
  def advance(options) # :nodoc:
@@ -13,7 +13,11 @@ module RiCal
13
13
  def tzid=(timezone_id) #:nodoc:
14
14
  timezone_id = default_tzid if timezone_id == :default
15
15
  @tzid = timezone_id
16
- @timezone = nil
16
+ reset_cached_values
17
+ end
18
+
19
+ def reset_cached_values #:nodoc:
20
+ @timezone = @utc = @rational_tz_offset = nil
17
21
  end
18
22
 
19
23
  def find_timezone #:nodoc:
@@ -46,7 +50,7 @@ module RiCal
46
50
  # Returns a instance that represents the time in UTC.
47
51
  def utc
48
52
  if has_local_timezone?
49
- timezone.local_to_utc(self)
53
+ @utc ||= timezone.local_to_utc(self)
50
54
  else # Already local or a floating time
51
55
  self
52
56
  end
@@ -54,9 +58,9 @@ module RiCal
54
58
 
55
59
  def rational_tz_offset #:nodoc:
56
60
  if has_local_timezone?
57
- timezone.rational_utc_offset(@date_time_value)
61
+ @rational_tz_offset ||= timezone.rational_utc_offset(@date_time_value)
58
62
  else
59
- Rational(0,24)
63
+ @rational_tz_offset ||= RiCal.RationalOffset[0]
60
64
  end
61
65
  end
62
66
 
@@ -69,7 +73,7 @@ module RiCal
69
73
  def floating?
70
74
  tzid.nil?
71
75
  end
72
-
76
+
73
77
  def has_valid_tzinfo_tzid? #:nodoc:
74
78
  if tzid && tzid != :floating
75
79
  TZInfo::Timezone.get(tzid) rescue false
@@ -96,6 +96,7 @@ module RiCal
96
96
  when ::Date, ::Time
97
97
  @date_time_value = ::DateTime.parse(val.to_s)
98
98
  end
99
+ reset_cached_values
99
100
  end
100
101
 
101
102
  # Extract the time and timezone identifier from an object used to set the value of a DATETIME property.
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