tempr 0.1.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.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/README.markdown +5 -0
- data/lib/tempr.rb +2 -0
- data/lib/tempr/date_time_range.rb +600 -0
- data/lib/tempr/version.rb +9 -0
- data/test/at_time.rb +36 -0
- data/test/suite.rb +3 -0
- data/test/test_helper.rb +6 -0
- data/test/time_subrange.rb +257 -0
- metadata +66 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
data/lib/tempr.rb
ADDED
@@ -0,0 +1,600 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Tempr
|
5
|
+
|
6
|
+
# Extensions for date or time ranges (or range-like objects)
|
7
|
+
# To generate subranges from chainable rules
|
8
|
+
#
|
9
|
+
# For example,
|
10
|
+
#
|
11
|
+
# To generate a recurring hourly appointment at 2pm on the third Thursdays of each month in 2012:
|
12
|
+
#
|
13
|
+
# range = (Date.civil(2012,1,1)...Date.civil(2013,1,1)).extend(Tempr::DateTimeRange)
|
14
|
+
# subrange = range.each_month.thursday(2).at_time('2:00pm',60*60)
|
15
|
+
#
|
16
|
+
# This gives you an enumerable you can iterate over:
|
17
|
+
#
|
18
|
+
# pp subrange.to_a
|
19
|
+
# #=> [ 2012-01-19 14:00:00 -0500...2012-01-19 15:00:00 -0500,
|
20
|
+
# 2012-02-16 14:00:00 -0500...2012-02-16 15:00:00 -0500,
|
21
|
+
# 2012-03-15 14:00:00 -0400...2012-03-15 15:00:00 -0400,
|
22
|
+
# 2012-04-19 14:00:00 -0400...2012-04-19 15:00:00 -0400,
|
23
|
+
# 2012-05-17 14:00:00 -0400...2012-05-17 15:00:00 -0400,
|
24
|
+
# 2012-06-21 14:00:00 -0400...2012-06-21 15:00:00 -0400,
|
25
|
+
# 2012-07-19 14:00:00 -0400...2012-07-19 15:00:00 -0400,
|
26
|
+
# 2012-08-16 14:00:00 -0400...2012-08-16 15:00:00 -0400,
|
27
|
+
# 2012-09-20 14:00:00 -0400...2012-09-20 15:00:00 -0400,
|
28
|
+
# 2012-10-18 14:00:00 -0400...2012-10-18 15:00:00 -0400,
|
29
|
+
# 2012-11-15 14:00:00 -0500...2012-11-15 15:00:00 -0500,
|
30
|
+
# 2012-12-20 14:00:00 -0500...2012-12-20 15:00:00 -0500 ]
|
31
|
+
#
|
32
|
+
# Or check for inclusion of a date/time:
|
33
|
+
#
|
34
|
+
# subrange.any? {|r| r.cover?(Time.parse("2012-05-17 2:30pm")) }
|
35
|
+
#
|
36
|
+
# Note that the order of the chained rules is important, they must be defined
|
37
|
+
# from the widest to the narrowest date/time range.
|
38
|
+
#
|
39
|
+
# During iteration, each rule is applied on the array of ranges defined by the
|
40
|
+
# previous rule.
|
41
|
+
#
|
42
|
+
# The methods are roughly divided into methods for generating recurring subranges (e.g., "each_month"),
|
43
|
+
# and methods for finding a single subrange, by offset (e.g., "wednesday(1)" for the second wednesday)
|
44
|
+
#
|
45
|
+
# In both cases, an enumerable is returned so that you can continue to chain rules together.
|
46
|
+
#
|
47
|
+
module DateTimeRange
|
48
|
+
|
49
|
+
# day of week shortcuts - as methods so accessible to mixin target classes
|
50
|
+
# Note probably should change this so it copies constants over in extend_object or something
|
51
|
+
def Sunday ; Date::DAYNAMES.index("Sunday"); end
|
52
|
+
def Monday ; Date::DAYNAMES.index("Monday"); end
|
53
|
+
def Tuesday ; Date::DAYNAMES.index("Tuesday"); end
|
54
|
+
def Wednesday ; Date::DAYNAMES.index("Wednesday"); end
|
55
|
+
def Thursday ; Date::DAYNAMES.index("Thursday"); end
|
56
|
+
def Friday ; Date::DAYNAMES.index("Friday"); end
|
57
|
+
def Saturday ; Date::DAYNAMES.index("Saturday"); end
|
58
|
+
def Sun ; self.Sunday; end
|
59
|
+
def Mon ; self.Monday; end
|
60
|
+
def Tue ; self.Tuesday; end
|
61
|
+
def Wed ; self.Wednesday; end
|
62
|
+
def Thu ; self.Thursday; end
|
63
|
+
def Fri ; self.Friday; end
|
64
|
+
def Sat ; self.Saturday; end
|
65
|
+
|
66
|
+
def WEEKDAYS; [self.Mon, self.Tue, self.Wed, self.Thu, self.Fri]; end
|
67
|
+
def WEEKENDS; [self.Sat, self.Sun]; end
|
68
|
+
|
69
|
+
# month shortcuts - as methods so accessible to mixin target classes
|
70
|
+
|
71
|
+
def January ; Date::MONTHNAMES.index("January"); end
|
72
|
+
def February ; Date::MONTHNAMES.index("February"); end
|
73
|
+
def March ; Date::MONTHNAMES.index("March"); end
|
74
|
+
def April ; Date::MONTHNAMES.index("April"); end
|
75
|
+
def May ; Date::MONTHNAMES.index("May"); end
|
76
|
+
def June ; Date::MONTHNAMES.index("June"); end
|
77
|
+
def July ; Date::MONTHNAMES.index("July"); end
|
78
|
+
def August ; Date::MONTHNAMES.index("August"); end
|
79
|
+
def September ; Date::MONTHNAMES.index("September"); end
|
80
|
+
def October ; Date::MONTHNAMES.index("October"); end
|
81
|
+
def November ; Date::MONTHNAMES.index("November"); end
|
82
|
+
def December ; Date::MONTHNAMES.index("December"); end
|
83
|
+
def Jan ; self.January; end
|
84
|
+
def Feb ; self.February; end
|
85
|
+
def Mar ; self.March; end
|
86
|
+
def Apr ; self.April; end
|
87
|
+
def Jun ; self.June; end
|
88
|
+
def Jul ; self.July; end
|
89
|
+
def Aug ; self.August; end
|
90
|
+
def Sep ; self.September; end
|
91
|
+
def Oct ; self.October; end
|
92
|
+
def Nov ; self.November; end
|
93
|
+
def Dec ; self.December; end
|
94
|
+
|
95
|
+
# ---
|
96
|
+
|
97
|
+
# seconds iterator:
|
98
|
+
# "every +n+ seconds, starting at +offset+, grouped into +dur+ second intervals"
|
99
|
+
#
|
100
|
+
# if no parameters passed,
|
101
|
+
# "every second of range grouped into one-second intervals"
|
102
|
+
def each_seconds(n=1, offset=0, dur=1)
|
103
|
+
build_subrange do |s|
|
104
|
+
s.step = n
|
105
|
+
s.adjust_range { |r| time_range(r) }
|
106
|
+
s.offset { |tm| tm.to_time + offset }
|
107
|
+
s.increment { |tm,i| tm.to_time + i }
|
108
|
+
s.span { |tm| tm.to_time + dur }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
alias each_second each_seconds
|
112
|
+
|
113
|
+
def second(offset=0); each_seconds(1,offset).limit_to(1); end
|
114
|
+
|
115
|
+
# minutes iterator:
|
116
|
+
# "every +n+ minutes, starting at +offset+ minutes, +dur+ minute intervals"
|
117
|
+
#
|
118
|
+
# if no parameters passed,
|
119
|
+
# "every minute of range grouped into one-minute intervals"
|
120
|
+
def each_minutes(n=1, offset=0, dur=1)
|
121
|
+
each_seconds(n*60, offset*60, dur*60)
|
122
|
+
end
|
123
|
+
alias each_minute each_minutes
|
124
|
+
|
125
|
+
def minute(offset=0); each_minutes(1,offset).limit_to(1); end
|
126
|
+
|
127
|
+
# hours iterator:
|
128
|
+
# "every +n+ hours, starting at +offset+ hours, +dur+ hour intervals"
|
129
|
+
#
|
130
|
+
# if no parameters passed,
|
131
|
+
# "every hour of range grouped into one-hour intervals"
|
132
|
+
def each_hours(n=1, offset=0, dur=1)
|
133
|
+
each_seconds(n*60*60, offset*60*60, dur*60*60)
|
134
|
+
end
|
135
|
+
alias each_hour each_hours
|
136
|
+
|
137
|
+
def hour(offset=0); each_hours(1,offset).limit_to(1); end
|
138
|
+
|
139
|
+
# days iterator:
|
140
|
+
# "every +n+ days, starting at +offset+ days, +dur+ day intervals"
|
141
|
+
#
|
142
|
+
# if no parameters passed,
|
143
|
+
# "every day of range grouped into one-day intervals"
|
144
|
+
def each_days(n=1,offset=0,dur=1)
|
145
|
+
build_subrange do |s|
|
146
|
+
s.step = n
|
147
|
+
s.adjust_range { |r| day_range(r) }
|
148
|
+
s.offset { |dt| dt.to_date + offset }
|
149
|
+
s.increment { |dt,i| dt.to_date + i }
|
150
|
+
s.span { |dt| dt.to_date + dur }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
alias each_day each_days
|
154
|
+
|
155
|
+
def day(offset=0); each_day(1,offset).limit_to(1); end
|
156
|
+
|
157
|
+
# weeks iterator:
|
158
|
+
# "every +n+ weeks, starting at +offset+ weeks, +dur+ week intervals"
|
159
|
+
#
|
160
|
+
# if no parameters passed,
|
161
|
+
# "every week of range grouped into one-week intervals"
|
162
|
+
def each_weeks(n=1, offset=0, dur=1)
|
163
|
+
each_days(n*7, offset*7, dur*7)
|
164
|
+
end
|
165
|
+
alias each_week each_weeks
|
166
|
+
|
167
|
+
def week(offset=0); each_weeks(1,offset).limit_to(1); end
|
168
|
+
|
169
|
+
# single day-of-week iterator:
|
170
|
+
# "every +n+th weekday +wd+, starting at +offset+ weeks, +dur+ day intervals"
|
171
|
+
#
|
172
|
+
# +wd+ is required. Typically, `each_sunday`, `each_monday` called instead.
|
173
|
+
#
|
174
|
+
# if no other parameters passed,
|
175
|
+
# "every weekday +wd+ of range grouped into one-day intervals"
|
176
|
+
def each_wdays(wd,n=1,offset=0,dur=1)
|
177
|
+
build_subrange do |s|
|
178
|
+
s.step = n
|
179
|
+
s.adjust_range { |r| day_range(r) }
|
180
|
+
s.offset { |dt| dt.to_date + (wd - dt.to_date.wday)%7 + offset*7 }
|
181
|
+
s.increment { |dt,i| dt.to_date + i*7 }
|
182
|
+
s.span { |dt| dt.to_date + dur }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
alias each_wday each_wdays
|
186
|
+
|
187
|
+
# "every +n+th Sunday, starting at +offset+ weeks, +dur+ day intervals"
|
188
|
+
def each_sunday( n=1, offset=0, dur=1); each_wdays(self.Sun,n,offset,dur); end
|
189
|
+
|
190
|
+
# "every +n+th Monday, starting at +offset+ weeks, +dur+ day intervals"
|
191
|
+
def each_monday( n=1, offset=0, dur=1); each_wdays(self.Mon,n,offset,dur); end
|
192
|
+
|
193
|
+
# "every +n+th Tuesday, starting at +offset+ weeks, +dur+ day intervals"
|
194
|
+
def each_tuesday( n=1, offset=0, dur=1); each_wdays(self.Tue,n,offset,dur); end
|
195
|
+
|
196
|
+
# "every +n+th Wednesday, starting at +offset+ weeks, +dur+ day intervals"
|
197
|
+
def each_wednesday(n=1, offset=0, dur=1); each_wdays(self.Wed,n,offset,dur); end
|
198
|
+
|
199
|
+
# "every +n+th Thursday, starting at +offset+ weeks, +dur+ day intervals"
|
200
|
+
def each_thursday( n=1, offset=0, dur=1); each_wdays(self.Thu,n,offset,dur); end
|
201
|
+
|
202
|
+
# "every +n+th Friday, starting at +offset+ weeks, +dur+ day intervals"
|
203
|
+
def each_friday( n=1, offset=0, dur=1); each_wdays(self.Fri,n,offset,dur); end
|
204
|
+
|
205
|
+
# "every +n+th Saturday, starting at +offset+ weeks, +dur+ day intervals"
|
206
|
+
def each_saturday( n=1, offset=0, dur=1); each_wdays(self.Sat,n,offset,dur); end
|
207
|
+
|
208
|
+
|
209
|
+
def wday(wd,offset=0)
|
210
|
+
each_wdays(wd,1,offset).limit_to(1)
|
211
|
+
end
|
212
|
+
|
213
|
+
def sunday(offset=0); wday(self.Sun,offset); end
|
214
|
+
def monday(offset=0); wday(self.Mon,offset); end
|
215
|
+
def tuesday(offset=0); wday(self.Tue,offset); end
|
216
|
+
def wednesday(offset=0); wday(self.Wed,offset); end
|
217
|
+
def thursday(offset=0); wday(self.Thu,offset); end
|
218
|
+
def friday(offset=0); wday(self.Fri,offset); end
|
219
|
+
def saturday(offset=0); wday(self.Sat,offset); end
|
220
|
+
|
221
|
+
# multiple day-of-week iterator:
|
222
|
+
# "every days of the week +wdays+"
|
223
|
+
#
|
224
|
+
# For example,
|
225
|
+
# `range.each_days_of_week(range.Tue, range.Thu)`
|
226
|
+
# # "every Tuesday and Thursday in range"
|
227
|
+
#
|
228
|
+
# if no parameter passed, identical to each_days
|
229
|
+
def each_days_of_week(*wdays)
|
230
|
+
if wdays.empty?
|
231
|
+
each_days
|
232
|
+
else
|
233
|
+
each_days.except {|dt| !wdays.include?(dt.wday) }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
alias each_day_of_week each_days_of_week
|
237
|
+
|
238
|
+
# every weekday
|
239
|
+
def each_weekdays
|
240
|
+
each_days_of_week(*self.WEEKDAYS)
|
241
|
+
end
|
242
|
+
alias each_weekday each_weekdays
|
243
|
+
|
244
|
+
# every weekend (Saturday and Sunday)
|
245
|
+
def each_weekends
|
246
|
+
each_days_of_week(*self.WEEKENDS)
|
247
|
+
end
|
248
|
+
alias each_weekend each_weekends
|
249
|
+
|
250
|
+
# every Friday, Saturday, and Sunday
|
251
|
+
def each_weekends_including_friday
|
252
|
+
each_days_of_week(*([self.Fri] + self.WEEKENDS))
|
253
|
+
end
|
254
|
+
alias each_weekend_including_friday each_weekends_including_friday
|
255
|
+
|
256
|
+
|
257
|
+
# month iterator:
|
258
|
+
# "every +n+ months, starting at +offset+ months, +dur+ month intervals"
|
259
|
+
#
|
260
|
+
# if no parameters passed,
|
261
|
+
# "every month of range grouped into one-month intervals"
|
262
|
+
def each_months(n=1,offset=0,dur=1)
|
263
|
+
build_subrange do |s|
|
264
|
+
s.step = n
|
265
|
+
s.adjust_range { |r| day_range(r) }
|
266
|
+
s.offset { |dt| dt.to_date >> offset }
|
267
|
+
s.increment { |dt,i| dt.to_date >> i }
|
268
|
+
s.span { |dt| dt.to_date >> dur }
|
269
|
+
end
|
270
|
+
end
|
271
|
+
alias each_month each_months
|
272
|
+
|
273
|
+
def month(offset=0); each_months(1,offset).limit_to(1); end
|
274
|
+
|
275
|
+
# month-of-year iterator:
|
276
|
+
# "every +n+th month +nmonth+ grouped into one-month intervals"
|
277
|
+
#
|
278
|
+
# +nmonth+ is required. Typically, `each_january`, etc. called instead.
|
279
|
+
#
|
280
|
+
# if +n+ parameter not passed,
|
281
|
+
# "every month number +nmonth+ of range grouped into one-month intervals"
|
282
|
+
def each_monthnum(nmonth,n=1)
|
283
|
+
build_subrange do |s|
|
284
|
+
s.step = n
|
285
|
+
s.adjust_range { |r| day_range(r) }
|
286
|
+
s.offset { |dt| dt >> (nmonth - dt.month)%12 }
|
287
|
+
s.increment { |dt,i| dt.to_date >> i*12 }
|
288
|
+
s.span { |dt| dt.to_date >> 1 }
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# "every +n+th January, grouped into one-month intervals"
|
293
|
+
def each_january( n=1); each_monthnum(self.Jan,n); end
|
294
|
+
|
295
|
+
# "every +n+th February, grouped into one-month intervals"
|
296
|
+
def each_february( n=1); each_monthnum(self.Feb,n); end
|
297
|
+
|
298
|
+
# "every +n+th Mary, grouped into one-month intervals"
|
299
|
+
def each_march( n=1); each_monthnum(self.Mar,n); end
|
300
|
+
|
301
|
+
# "every +n+th April, grouped into one-month intervals"
|
302
|
+
def each_april( n=1); each_monthnum(self.Apr,n); end
|
303
|
+
|
304
|
+
# "every +n+th May, grouped into one-month intervals"
|
305
|
+
def each_may( n=1); each_monthnum(self.May,n); end
|
306
|
+
|
307
|
+
# "every +n+th June, grouped into one-month intervals"
|
308
|
+
def each_june( n=1); each_monthnum(self.Jun,n); end
|
309
|
+
|
310
|
+
# "every +n+th July, grouped into one-month intervals"
|
311
|
+
def each_july( n=1); each_monthnum(self.Jul,n); end
|
312
|
+
|
313
|
+
# "every +n+th August, grouped into one-month intervals"
|
314
|
+
def each_august( n=1); each_monthnum(self.Aug,n); end
|
315
|
+
|
316
|
+
# "every +n+th September, grouped into one-month intervals"
|
317
|
+
def each_september(n=1); each_monthnum(self.Sep,n); end
|
318
|
+
|
319
|
+
# "every +n+th October, grouped into one-month intervals"
|
320
|
+
def each_october( n=1); each_monthnum(self.Oct,n); end
|
321
|
+
|
322
|
+
# "every +n+th November, grouped into one-month intervals"
|
323
|
+
def each_november( n=1); each_monthnum(self.Nov,n); end
|
324
|
+
|
325
|
+
# "every +n+th December, grouped into one-month intervals"
|
326
|
+
def each_december( n=1); each_monthnum(self.Dec,n); end
|
327
|
+
|
328
|
+
|
329
|
+
# year iterator:
|
330
|
+
# "every +n+ years, starting at +offset+ years, +dur+ year intervals"
|
331
|
+
#
|
332
|
+
# if no parameters passed,
|
333
|
+
# "every year of range grouped into one-year intervals"
|
334
|
+
def each_years(n=1,offset=0,dur=1)
|
335
|
+
build_subrange do |s|
|
336
|
+
s.step = n
|
337
|
+
s.adjust_range { |r| day_range(r) }
|
338
|
+
s.offset { |dt| Date.civil(dt.year + offset, dt.month, dt.day) }
|
339
|
+
s.increment { |dt,i| Date.civil(dt.year + i, dt.month, dt.day) }
|
340
|
+
s.span { |dt| Date.civil(dt.year + dur, dt.month, dt.day) }
|
341
|
+
end
|
342
|
+
end
|
343
|
+
alias each_year each_years
|
344
|
+
|
345
|
+
def year(offset=0); each_year(1,offset).limit_to(1); end
|
346
|
+
|
347
|
+
# ---
|
348
|
+
|
349
|
+
# day-of-month iterator:
|
350
|
+
# "every +nday+th day of the month, grouped into +dur+ day intervals"
|
351
|
+
#
|
352
|
+
# +nday+ is required.
|
353
|
+
#
|
354
|
+
# if no +dur+ parameter passed,
|
355
|
+
# "every +nday+th day of the month, grouped into one-day intervals"
|
356
|
+
def on_day(nday,dur=1)
|
357
|
+
build_subrange do |s|
|
358
|
+
s.step = 1
|
359
|
+
s.adjust_range { |r| day_range(r) }
|
360
|
+
s.offset do |dt|
|
361
|
+
totdays = ((Date.civil(dt.year,dt.month,1) >> 1)-1).day
|
362
|
+
dt.to_date + (nday - dt.day)%totdays
|
363
|
+
end
|
364
|
+
s.increment { |dt,i| dt.to_date >> i }
|
365
|
+
s.span { |dt| dt.to_date + dur }
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# time-of-day iterator:
|
370
|
+
# "every day at +tm+, grouped into +dur+ second intervals"
|
371
|
+
#
|
372
|
+
# +tm+ is any string that can be Time.parse'd. Note the date portion is ignored, if given.
|
373
|
+
#
|
374
|
+
# if no +dur+ parameter passed, intervals are 'instantaneous' time ranges
|
375
|
+
def at_time(tm,dur=0)
|
376
|
+
tm_p = Time.parse(tm)
|
377
|
+
build_subrange do |s|
|
378
|
+
s.step = 60*60*24
|
379
|
+
s.adjust_range { |r| time_range(r) }
|
380
|
+
s.offset do |tm|
|
381
|
+
Time.new(
|
382
|
+
tm.year, tm.month, tm.day,
|
383
|
+
tm_p.hour, tm_p.min, tm_p.sec, (tm + s.step).utc_offset
|
384
|
+
)
|
385
|
+
end
|
386
|
+
s.increment { |tm,i| tm.to_time + i }
|
387
|
+
s.span { |tm| tm.to_time + dur }
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# ---
|
392
|
+
|
393
|
+
# Helper methods - these are a bit hacky and possibly buggy.
|
394
|
+
# Used in iterator `adjust_range` procs to ensure the right data type
|
395
|
+
# before iterating
|
396
|
+
|
397
|
+
# convert to date range
|
398
|
+
# and make exclusive if not already
|
399
|
+
# For example,
|
400
|
+
#
|
401
|
+
# `2012-02-01..2012-02-29` becomes
|
402
|
+
# `2012-02-01...2012-03-01`
|
403
|
+
#
|
404
|
+
# while
|
405
|
+
#
|
406
|
+
# `2012-02-01...2012-02-29` is unmodified.
|
407
|
+
#
|
408
|
+
def day_range(rng=self)
|
409
|
+
if rng.respond_to?(:exclude_end?) && rng.exclude_end?
|
410
|
+
Range.new(rng.begin.to_date, rng.end.to_date, true)
|
411
|
+
else
|
412
|
+
Range.new(rng.begin.to_date, rng.end.to_date + 1, true)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# unless already a time range,
|
417
|
+
# convert to exclusive date range, and then to time range
|
418
|
+
# For example,
|
419
|
+
#
|
420
|
+
# `2012-02-01..2012-02-29` becomes
|
421
|
+
# `2012-02-01 00:00:00 UTC...2012-03-01 00:00:00 UTC`
|
422
|
+
#
|
423
|
+
def time_range(rng=self)
|
424
|
+
if rng.begin.respond_to?(:sec) && rng.end.respond_to?(:sec)
|
425
|
+
rng.dup
|
426
|
+
else
|
427
|
+
adj_rng = day_range(rng)
|
428
|
+
Range.new(adj_rng.begin.to_time, adj_rng.end.to_time, true)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# convenience wrapper for SubRangeIterator.new(self) { ... }
|
433
|
+
def build_subrange(&builder)
|
434
|
+
SubRangeIterator.new(self, &builder)
|
435
|
+
end
|
436
|
+
|
437
|
+
# ---
|
438
|
+
|
439
|
+
# Iterators are defined by
|
440
|
+
#
|
441
|
+
# - `range`: base range (required)
|
442
|
+
# - `step`: repetition length (default = 1)
|
443
|
+
# - `adjust_range`: proc that adjusts base range before iteration (optional)
|
444
|
+
# - `offset`: proc that adjusts start of adjusted range prior to iteration (optional)
|
445
|
+
# - `increment`: proc that defines scale of each step (required)
|
446
|
+
# - `span`: proc that defines duration of each returned subrange (required)
|
447
|
+
# - `except`: proc(s) that don't yield subrange if true of current step date (but don't stop iteration)
|
448
|
+
# - `limit`: stop iteration after self.limit steps (yields)
|
449
|
+
#
|
450
|
+
# TODO:
|
451
|
+
# - `until`: proc that stops iteration if true of current step date
|
452
|
+
#
|
453
|
+
# Note that SubRangeIterator is coupled to DateTimeRange since it itself includes DateTimeRange (for chaining);
|
454
|
+
# However, otherwise it could be used just as well on other (e.g. numeric) ranges
|
455
|
+
#
|
456
|
+
class SubRangeIterator
|
457
|
+
include Enumerable
|
458
|
+
include DateTimeRange
|
459
|
+
|
460
|
+
attr_accessor :range, :step, :limit
|
461
|
+
def step; @step ||= 1; end
|
462
|
+
|
463
|
+
# a bit hacky - used to extend concrete subranges
|
464
|
+
# with the same extensions as the range
|
465
|
+
def range_extensions
|
466
|
+
@range_extensions ||=
|
467
|
+
class << self.range
|
468
|
+
self.included_modules - [Kernel]
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def initialize(range)
|
473
|
+
self.range = range
|
474
|
+
yield self if block_given?
|
475
|
+
end
|
476
|
+
|
477
|
+
# note: useful for chaining instead of step=
|
478
|
+
def step_by(n)
|
479
|
+
self.step = n
|
480
|
+
self
|
481
|
+
end
|
482
|
+
|
483
|
+
# note: useful for chaining instead of limit=
|
484
|
+
def limit_to(n)
|
485
|
+
self.limit = n
|
486
|
+
self
|
487
|
+
end
|
488
|
+
|
489
|
+
def adjust_range(&p)
|
490
|
+
self.range_proc = p
|
491
|
+
self
|
492
|
+
end
|
493
|
+
|
494
|
+
def offset(&p)
|
495
|
+
self.offset_proc = p
|
496
|
+
self
|
497
|
+
end
|
498
|
+
|
499
|
+
def increment(&p)
|
500
|
+
self.step_proc = p
|
501
|
+
self
|
502
|
+
end
|
503
|
+
|
504
|
+
def span(&p)
|
505
|
+
self.span_proc = p
|
506
|
+
self
|
507
|
+
end
|
508
|
+
|
509
|
+
def except(&p)
|
510
|
+
exception_procs << p
|
511
|
+
self
|
512
|
+
end
|
513
|
+
|
514
|
+
# Recursive madness...
|
515
|
+
# note this could possibly use cached results stored by #all method,
|
516
|
+
# similar to Sequel
|
517
|
+
def each(&b)
|
518
|
+
if self.range.respond_to?(:each_by_step)
|
519
|
+
self.range.each do |sub|
|
520
|
+
each_by_step(sub, &b)
|
521
|
+
end
|
522
|
+
else
|
523
|
+
each_by_step do |sub|
|
524
|
+
# puts "self.range = #{self.range} yielded: #{sub}"
|
525
|
+
yield sub
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# Iteration
|
531
|
+
# 1. adjust base range
|
532
|
+
# 2. get offset
|
533
|
+
# 3. for each step,
|
534
|
+
# 3.1. if limit reached, break
|
535
|
+
# 3.2. find begin of next subrange (step_proc)
|
536
|
+
# 3.3. find end of next subrange (span_proc)
|
537
|
+
# 3.4. check if begin in adjusted base range, stop iteration if not
|
538
|
+
# 3.5. check if begin matches any exceptions (exception_procs)
|
539
|
+
# 3.5.1 if not, increment the yield count (i)
|
540
|
+
# 3.5.2 and yield the subrange, extended with same modules as base range
|
541
|
+
def each_by_step(rng=self.range)
|
542
|
+
rng = range_proc.call(rng)
|
543
|
+
# puts "each_by_step range: #{rng}"
|
544
|
+
initial = offset_proc.call(rng.begin)
|
545
|
+
i=0
|
546
|
+
by_step(self.step).each do |n|
|
547
|
+
break if self.limit && self.limit <= i
|
548
|
+
next_begin = step_proc.call(initial,n)
|
549
|
+
next_end = span_proc.call(next_begin)
|
550
|
+
if rng.respond_to?(:cover?) && !rng.cover?(next_begin)
|
551
|
+
raise StopIteration
|
552
|
+
end
|
553
|
+
unless exception_procs.any? {|except| except.call(next_begin)}
|
554
|
+
i+=1
|
555
|
+
yield((next_begin...next_end).extend(*range_extensions))
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
# 'stateless' step enumerator
|
561
|
+
# simply generates infinite integer sequence
|
562
|
+
# if ruby already has such a facility built-in, let me know
|
563
|
+
def by_step(n)
|
564
|
+
@step_enumerator ||= Enumerator.new do |y|
|
565
|
+
i=0
|
566
|
+
loop do
|
567
|
+
y << i; i+=n
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
private
|
573
|
+
|
574
|
+
attr_accessor :offset_proc, :step_proc, :span_proc, :range_proc
|
575
|
+
def range_proc
|
576
|
+
@range_proc ||= lambda {|r| r}
|
577
|
+
end
|
578
|
+
def offset_proc
|
579
|
+
@offset_proc ||= lambda {|dt| dt}
|
580
|
+
end
|
581
|
+
|
582
|
+
def exception_procs; @exception_procs ||= []; end
|
583
|
+
|
584
|
+
end
|
585
|
+
|
586
|
+
end
|
587
|
+
|
588
|
+
end
|
589
|
+
|
590
|
+
|
591
|
+
if $0 == __FILE__
|
592
|
+
|
593
|
+
require 'pp'
|
594
|
+
|
595
|
+
range = (Date.civil(2012,1,1)...Date.civil(2013,1,1)).extend(Tempr::DateTimeRange)
|
596
|
+
subrange = range.each_month.thursday(2).at_time('2:00pm',60*60)
|
597
|
+
|
598
|
+
pp subrange.to_a
|
599
|
+
|
600
|
+
end
|
data/test/at_time.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.expand_path('test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
module AtTimeTests
|
4
|
+
module Fixtures
|
5
|
+
|
6
|
+
BaseRanges = {:local => Time.parse('2012-01-01 00:00:00 -0500')...
|
7
|
+
Time.parse('2013-01-01 00:00:00 -0500'),
|
8
|
+
:utc => Time.parse('2012-01-01 00:00:00 UTC')...
|
9
|
+
Time.parse('2013-01-01 00:00:00 UTC')
|
10
|
+
}
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'DateTimeRange#at_time' do
|
15
|
+
|
16
|
+
[:local, :utc].each do |time_type|
|
17
|
+
|
18
|
+
describe "across daylight savings time boundaries for #{time_type} times" do
|
19
|
+
|
20
|
+
let(:subject) { Fixtures::BaseRanges[time_type].extend(Tempr::DateTimeRange) }
|
21
|
+
|
22
|
+
it 'should be at the same time of day regardless of time zone' do
|
23
|
+
subject.each_day.at_time("2:00pm",60*60).each do |range|
|
24
|
+
offset = range.begin.utc_offset
|
25
|
+
actual = range.begin.getlocal(offset).hour
|
26
|
+
#puts "#{range}"
|
27
|
+
assert_equal 14, actual, "for range: #{range}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end # namespace
|
data/test/suite.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
require File.expand_path('test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
module TimeSubRangeTests
|
4
|
+
module Fixtures
|
5
|
+
|
6
|
+
# 20 seconds / minutes / hours
|
7
|
+
BaseRanges = { :each_seconds => Time.parse('2012-02-13 15:32:41')...
|
8
|
+
Time.parse('2012-02-13 15:33:01'),
|
9
|
+
:each_minutes => Time.parse('2012-02-13 13:46:25')...
|
10
|
+
Time.parse('2012-02-13 14:06:25'),
|
11
|
+
:each_hours => Time.parse('2012-02-13 12:52:22')...
|
12
|
+
Time.parse('2012-02-14 08:52:22')
|
13
|
+
}
|
14
|
+
|
15
|
+
ExclusiveDateRange = Date.parse('2012-02-13')...Date.parse('2012-02-15')
|
16
|
+
NonExclusiveDateRange = Date.parse('2012-02-13')..Date.parse('2012-02-14')
|
17
|
+
|
18
|
+
# note: I don't particularly like generating fixtures like this,
|
19
|
+
# but it's probably just as suceptible to error as hand-typing here
|
20
|
+
module Expected
|
21
|
+
|
22
|
+
Default = {
|
23
|
+
:each_seconds =>
|
24
|
+
(0..19).inject([]) do |memo, i|
|
25
|
+
s0 = BaseRanges[:each_seconds].begin
|
26
|
+
memo << (s0+i...s0+i+1)
|
27
|
+
end,
|
28
|
+
:each_minutes =>
|
29
|
+
(0..19).inject([]) do |memo, i|
|
30
|
+
s0 = BaseRanges[:each_minutes].begin
|
31
|
+
memo << (s0+i*60...s0+(i+1)*60)
|
32
|
+
end,
|
33
|
+
:each_hours =>
|
34
|
+
(0..19).inject([]) do |memo, i|
|
35
|
+
s0 = BaseRanges[:each_hours].begin
|
36
|
+
memo << (s0+i*60*60...s0+(i+1)*60*60)
|
37
|
+
end
|
38
|
+
}
|
39
|
+
|
40
|
+
Params3_0_1 = {
|
41
|
+
:each_seconds =>
|
42
|
+
(0..6).inject([]) do |memo, i|
|
43
|
+
s0 = BaseRanges[:each_seconds].begin
|
44
|
+
memo << (s0+(i*3)...s0+(i*3)+1)
|
45
|
+
end,
|
46
|
+
:each_minutes =>
|
47
|
+
(0..6).inject([]) do |memo, i|
|
48
|
+
s0 = BaseRanges[:each_minutes].begin
|
49
|
+
memo << (s0+(i*3)*60...s0+(i*3+1)*60)
|
50
|
+
end,
|
51
|
+
:each_hours =>
|
52
|
+
(0..6).inject([]) do |memo, i|
|
53
|
+
s0 = BaseRanges[:each_hours].begin
|
54
|
+
memo << (s0+(i*3)*60*60...s0+(i*3+1)*60*60)
|
55
|
+
end
|
56
|
+
}
|
57
|
+
|
58
|
+
Params1_5_1 = {
|
59
|
+
:each_seconds =>
|
60
|
+
(0..14).inject([]) do |memo, i|
|
61
|
+
s0 = BaseRanges[:each_seconds].begin+5
|
62
|
+
memo << (s0+i...s0+i+1)
|
63
|
+
end,
|
64
|
+
:each_minutes =>
|
65
|
+
(0..14).inject([]) do |memo, i|
|
66
|
+
s0 = BaseRanges[:each_minutes].begin+5*60
|
67
|
+
memo << (s0+i*60...s0+(i+1)*60)
|
68
|
+
end,
|
69
|
+
:each_hours =>
|
70
|
+
(0..14).inject([]) do |memo, i|
|
71
|
+
s0 = BaseRanges[:each_hours].begin+5*60*60
|
72
|
+
memo << (s0+i*60*60...s0+(i+1)*60*60)
|
73
|
+
end
|
74
|
+
}
|
75
|
+
|
76
|
+
Params3_0_3 = {
|
77
|
+
:each_seconds =>
|
78
|
+
(0..6).inject([]) do |memo, i|
|
79
|
+
s0 = BaseRanges[:each_seconds].begin
|
80
|
+
memo << (s0+(i*3)...s0+(i*3)+3)
|
81
|
+
end,
|
82
|
+
:each_minutes =>
|
83
|
+
(0..6).inject([]) do |memo, i|
|
84
|
+
s0 = BaseRanges[:each_minutes].begin
|
85
|
+
memo << (s0+(i*3)*60...s0+(i*3+3)*60)
|
86
|
+
end,
|
87
|
+
:each_hours =>
|
88
|
+
(0..6).inject([]) do |memo, i|
|
89
|
+
s0 = BaseRanges[:each_hours].begin
|
90
|
+
memo << (s0+(i*3)*60*60...s0+(i*3+3)*60*60)
|
91
|
+
end
|
92
|
+
}
|
93
|
+
|
94
|
+
Params5_0_2 = {
|
95
|
+
:each_seconds =>
|
96
|
+
(0..3).inject([]) do |memo, i|
|
97
|
+
s0 = BaseRanges[:each_seconds].begin
|
98
|
+
memo << (s0+(i*5)...s0+(i*5)+2)
|
99
|
+
end,
|
100
|
+
:each_minutes =>
|
101
|
+
(0..3).inject([]) do |memo, i|
|
102
|
+
s0 = BaseRanges[:each_minutes].begin
|
103
|
+
memo << (s0+(i*5)*60...s0+(i*5+2)*60)
|
104
|
+
end,
|
105
|
+
:each_hours =>
|
106
|
+
(0..3).inject([]) do |memo, i|
|
107
|
+
s0 = BaseRanges[:each_hours].begin
|
108
|
+
memo << (s0+(i*5)*60*60...s0+(i*5+2)*60*60)
|
109
|
+
end
|
110
|
+
}
|
111
|
+
|
112
|
+
Params1_0_3 = {
|
113
|
+
:each_seconds =>
|
114
|
+
(0..19).inject([]) do |memo, i|
|
115
|
+
s0 = BaseRanges[:each_seconds].begin
|
116
|
+
memo << (s0+i...s0+i+3)
|
117
|
+
end,
|
118
|
+
:each_minutes =>
|
119
|
+
(0..19).inject([]) do |memo, i|
|
120
|
+
s0 = BaseRanges[:each_minutes].begin
|
121
|
+
memo << (s0+i*60...s0+(i+3)*60)
|
122
|
+
end,
|
123
|
+
:each_hours =>
|
124
|
+
(0..19).inject([]) do |memo, i|
|
125
|
+
s0 = BaseRanges[:each_hours].begin
|
126
|
+
memo << (s0+i*60*60...s0+(i+3)*60*60)
|
127
|
+
end
|
128
|
+
}
|
129
|
+
|
130
|
+
Params7_1_3 = {
|
131
|
+
:each_seconds =>
|
132
|
+
(0..2).inject([]) do |memo, i|
|
133
|
+
s0 = BaseRanges[:each_seconds].begin+1
|
134
|
+
memo << (s0+(i*7)...s0+(i*7)+3)
|
135
|
+
end,
|
136
|
+
:each_minutes =>
|
137
|
+
(0..2).inject([]) do |memo, i|
|
138
|
+
s0 = BaseRanges[:each_minutes].begin+1*60
|
139
|
+
memo << (s0+(i*7)*60...s0+(i*7+3)*60)
|
140
|
+
end,
|
141
|
+
:each_hours =>
|
142
|
+
(0..2).inject([]) do |memo, i|
|
143
|
+
s0 = BaseRanges[:each_hours].begin+1*60*60
|
144
|
+
memo << (s0+(i*7)*60*60...s0+(i*7+3)*60*60)
|
145
|
+
end
|
146
|
+
}
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
# ---
|
153
|
+
|
154
|
+
describe 'DateTimeRange, single time iterators' do
|
155
|
+
|
156
|
+
[:each_seconds, :each_minutes, :each_hours].each do |meth|
|
157
|
+
describe meth do
|
158
|
+
|
159
|
+
let(:subject) { Fixtures::BaseRanges[meth].extend(Tempr::DateTimeRange) }
|
160
|
+
|
161
|
+
it 'must exhibit default behavior if no parameters passed' do
|
162
|
+
results = subject.send(meth).to_a
|
163
|
+
assert_equal Fixtures::Expected::Default[meth], results
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'must iterate using passed step length' do
|
167
|
+
results = subject.send(meth,3).to_a
|
168
|
+
assert_equal Fixtures::Expected::Params3_0_1[meth], results
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'must iterate starting at passed offset' do
|
172
|
+
results = subject.send(meth,1,5).to_a
|
173
|
+
assert_equal Fixtures::Expected::Params1_5_1[meth], results
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'must return ranges of passed duration, duration == step' do
|
177
|
+
results = subject.send(meth,3,0,3).to_a
|
178
|
+
assert_equal Fixtures::Expected::Params3_0_3[meth], results
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'must return ranges of passed duration, duration < step' do
|
182
|
+
results = subject.send(meth,5,0,2).to_a
|
183
|
+
assert_equal Fixtures::Expected::Params5_0_2[meth], results
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'must return ranges of passed duration, duration > step' do
|
187
|
+
results = subject.send(meth,1,0,3).to_a
|
188
|
+
assert_equal Fixtures::Expected::Params1_0_3[meth], results
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'must return ranges of passed step, offset, and duration' do
|
192
|
+
results = subject.send(meth,7,1,3).to_a
|
193
|
+
assert_equal Fixtures::Expected::Params7_1_3[meth], results
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "with exclusive date ranges" do
|
201
|
+
|
202
|
+
let(:subject) { Fixtures::ExclusiveDateRange.extend(Tempr::DateTimeRange) }
|
203
|
+
|
204
|
+
it 'each_seconds must return ranges starting up to 23:59:59 of the end date' do
|
205
|
+
last_result = subject.each_seconds.to_a.last
|
206
|
+
assert_equal ( (Fixtures::ExclusiveDateRange.end.to_time - 1)...
|
207
|
+
Fixtures::ExclusiveDateRange.end.to_time ),
|
208
|
+
last_result
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'each_minutes must return ranges starting up to 23:59:00 of the end date' do
|
212
|
+
last_result = subject.each_minutes.to_a.last
|
213
|
+
assert_equal ( (Fixtures::ExclusiveDateRange.end.to_time - 60)...
|
214
|
+
Fixtures::ExclusiveDateRange.end.to_time ),
|
215
|
+
last_result
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'each_minutes must return ranges starting up to 23:00:00 of the end date' do
|
219
|
+
last_result = subject.each_hours.to_a.last
|
220
|
+
assert_equal ( (Fixtures::ExclusiveDateRange.end.to_time - 60*60)...
|
221
|
+
Fixtures::ExclusiveDateRange.end.to_time ),
|
222
|
+
last_result
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
describe "with non-exclusive date ranges" do
|
228
|
+
|
229
|
+
let(:subject) { Fixtures::NonExclusiveDateRange.extend(Tempr::DateTimeRange) }
|
230
|
+
|
231
|
+
it 'each_seconds must return ranges starting up to 23:59:59 of the end date + 1' do
|
232
|
+
last_result = subject.each_seconds.to_a.last
|
233
|
+
assert_equal ( ((Fixtures::NonExclusiveDateRange.end+1).to_time - 1)...
|
234
|
+
(Fixtures::NonExclusiveDateRange.end+1).to_time ),
|
235
|
+
last_result
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'each_minutes must return ranges starting up to 23:59:00 of the end date + 1' do
|
239
|
+
last_result = subject.each_minutes.to_a.last
|
240
|
+
assert_equal ( ((Fixtures::NonExclusiveDateRange.end+1).to_time - 60)...
|
241
|
+
(Fixtures::NonExclusiveDateRange.end+1).to_time ),
|
242
|
+
last_result
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'each_minutes must return ranges starting up to 23:00:00 of the end date + 1' do
|
246
|
+
last_result = subject.each_hours.to_a.last
|
247
|
+
assert_equal ( ((Fixtures::NonExclusiveDateRange.end+1).to_time - 60*60)...
|
248
|
+
(Fixtures::NonExclusiveDateRange.end+1).to_time ),
|
249
|
+
last_result
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
end # namespace
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tempr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Gjertsen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-16 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minitest
|
16
|
+
requirement: &16129140 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *16129140
|
25
|
+
description: ''
|
26
|
+
email:
|
27
|
+
- ericgj72@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- Gemfile
|
34
|
+
- README.markdown
|
35
|
+
- lib/tempr.rb
|
36
|
+
- lib/tempr/date_time_range.rb
|
37
|
+
- lib/tempr/version.rb
|
38
|
+
- test/at_time.rb
|
39
|
+
- test/suite.rb
|
40
|
+
- test/test_helper.rb
|
41
|
+
- test/time_subrange.rb
|
42
|
+
homepage: http://github.com/ericgj/tempr
|
43
|
+
licenses: []
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.9.2
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 1.3.6
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project: tempr
|
62
|
+
rubygems_version: 1.8.10
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: No-fussin' temporal expressions library
|
66
|
+
test_files: []
|