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 ADDED
@@ -0,0 +1,2 @@
1
+ scratch
2
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,5 @@
1
+ # Tempr
2
+ ## a Ruby temporal expressions library
3
+
4
+
5
+ This is a work in progress. See date_time_range.rb for usage examples.
data/lib/tempr.rb ADDED
@@ -0,0 +1,2 @@
1
+ require File.expand_path('tempr/version', File.dirname(__FILE__))
2
+ require File.expand_path('tempr/date_time_range', File.dirname(__FILE__))
@@ -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
@@ -0,0 +1,9 @@
1
+
2
+ module Tempr
3
+ module Version
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ TINY = 0
7
+ STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
8
+ end
9
+ 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
@@ -0,0 +1,3 @@
1
+ %w[ time_subrange at_time ].each do |test|
2
+ require File.expand_path(test,File.dirname(__FILE__))
3
+ end
@@ -0,0 +1,6 @@
1
+ require File.expand_path('../lib/tempr', File.dirname(__FILE__))
2
+
3
+ gem "minitest"
4
+
5
+ require 'minitest/spec'
6
+ MiniTest::Unit.autorun
@@ -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: []