tempr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []