timetastic 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/timetastic.rb +133 -0
  2. data/spec/timetastic_spec.rb +320 -0
  3. metadata +64 -0
@@ -0,0 +1,133 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ module Timetastic
5
+ Domains = [ :hours, :weeks, :days, :months, :years ]
6
+
7
+ class << self
8
+ attr_accessor :fixed_time # see Timetastic#fixate() below
9
+
10
+ def last(delta = 1, relative_to = nil)
11
+ Traveller.new(-1, delta, relative_to)
12
+ end
13
+
14
+ alias_method :past, :last
15
+
16
+ def next(delta = 1, relative_to = nil)
17
+ Traveller.new(1, delta, relative_to)
18
+ end
19
+
20
+ alias_method :coming, :next
21
+
22
+ # Fixes the relative time by which all of Timetastic operations
23
+ # are carried out for the duration of the given block.
24
+ #
25
+ # Mainly used for tests and gem development.
26
+ #
27
+ # An alternative way to 'fix' the time is to directly use the exposed
28
+ # attribute Timetastic#fixed_time, ie:
29
+ #
30
+ # => Timetastic.fixed_time = Time.new(Time.now.year, 6, 1)
31
+ #
32
+ # You can simply set the attribute to nil to reset the time back to Time.now
33
+ def fixate(y, m = 1, d = 1, h = 0, mi = 0, s = 0, &block)
34
+ if y.is_a?(Time)
35
+ @fixed_time = y
36
+ else
37
+ @fixed_time = Time.new(y,m,d,h,mi,s)
38
+ end
39
+ block.call(@fixed_time)
40
+ @fixed_time = nil
41
+ end
42
+
43
+ # Snippet that calculates the number of days in any given month
44
+ # taking into account leap years.
45
+ #
46
+ # Credit goes to this SO thread: bit.ly/4GjMor
47
+ def days_in_month(year, month)
48
+ (Date.new(year, 12, 31) << (12-month)).day
49
+ end
50
+
51
+ # Returns the relative time by which operations are carried out
52
+ def now()
53
+ @fixed_time || Time.now
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ class Traveller
60
+ attr_reader :direction, :distance
61
+
62
+ def initialize(direction, distance, relative_to = nil)
63
+ @distance = distance.to_i
64
+ @relative_to = relative_to
65
+ @direction = case direction
66
+ when -1; :ago
67
+ when 1; :hence
68
+ end
69
+ end
70
+
71
+ Domains.each { |domain|
72
+ define_method(domain) { relative_anchored_time(domain) }
73
+ alias_method :"#{domain[0..-2]}", domain
74
+ }
75
+
76
+ protected
77
+
78
+ def relative_anchored_time(domain)
79
+ t = @distance.send(domain).send(@direction, @relative_to)
80
+
81
+ case domain
82
+ when :days, :weeks;
83
+ Time.new(t.year, t.month, t.day)
84
+ when :months; Time.new(t.year, t.month, 1)
85
+ when :years; Time.new(t.year, 1, 1)
86
+ end
87
+ end
88
+ end # Timetastic::Traveller
89
+
90
+ end # end of Module#Timetastic
91
+
92
+ class Fixnum
93
+ Timetastic::Domains.each do |domain|
94
+ define_method(domain) { @time_offset_domain = domain; self }
95
+
96
+ # an alias for the singular version of the domain
97
+ alias_method :"#{domain[0..-2]}", domain
98
+ end
99
+
100
+ def ago(relative_to = nil)
101
+ relative_time(-1, relative_to)
102
+ end
103
+
104
+ def hence(relative_to = nil)
105
+ relative_time(1, relative_to)
106
+ end
107
+
108
+ alias_method :ahead, :hence
109
+ alias_method :from_now, :hence
110
+
111
+ protected
112
+
113
+ attr_reader :time_offset_domain
114
+
115
+ def relative_time(coef, relative_to = nil)
116
+ d = (coef.to_i) / (coef.to_i.abs) * self # delta
117
+ n = relative_to || Timetastic.now
118
+
119
+ case @time_offset_domain
120
+ when :hours
121
+ Time.at(n.to_i + d * 3600)
122
+ when :days
123
+ Time.at(n.to_i + d * 86400)
124
+ when :weeks
125
+ Time.at(n.to_i + d * 604800)
126
+ when :months
127
+ Time.at(n.to_i + d * 2592000)
128
+ when :years
129
+ Time.new(n.year + d, n.month, n.day, n.hour, n.min, n.sec)
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,320 @@
1
+ describe Timetastic do
2
+ tt = Timetastic # alias it to reduce clutter
3
+
4
+ before do
5
+ end
6
+
7
+ after do
8
+ end
9
+
10
+ describe "Past features" do
11
+
12
+ it "should locate the last year from today" do
13
+ tt.fixate(2012, 8, 1) { |t|
14
+ 1.year.ago.should == Time.new(2011, 8, 1)
15
+ }
16
+ end
17
+
18
+ it "should locate the last month from today" do
19
+ tt.fixate(2012, 12, 26) { |t|
20
+ 1.month.ago.should == Time.new(2012, 11, 26)
21
+ }
22
+ end
23
+
24
+ it "should locate the last week from today" do
25
+ tt.fixate(2012, 12, 26) {
26
+ 1.week.ago.should == Time.new(2012, 12, 19)
27
+ }
28
+ end
29
+
30
+ it "should locate the last day from today" do
31
+ tt.fixate(2012, 12, 26) {
32
+ 1.day.ago.should == Time.new(2012, 12, 25)
33
+ }
34
+ end
35
+
36
+ it "should locate the last hour from now" do
37
+ tt.fixate(2012, 12, 26, 14) {
38
+ 1.hour.ago.should == Time.new(2012, 12, 26, 13)
39
+ 12.hours.ago.should == Time.new(2012, 12, 26, 2)
40
+ }
41
+ end
42
+
43
+ end # Past locators
44
+
45
+ describe "Future locators" do
46
+ it "should locate the coming year from today" do
47
+ tt.fixate(2012, 12, 26) {
48
+ 1.year.hence.should == Time.new(2013, 12, 26)
49
+ }
50
+ end
51
+
52
+ it "should locate the coming month from today" do
53
+ tt.fixate(2012, 10, 26) {
54
+ 1.month.hence.should == Time.new(2012, 11, 25)
55
+ }
56
+ end
57
+
58
+ it "should locate the coming week from today" do
59
+ tt.fixate(2012, 12, 20) {
60
+ 1.week.ahead.should == Time.new(2012, 12, 27)
61
+ }
62
+ end
63
+
64
+ it "should locate the coming day from today" do
65
+ tt.fixate(2012, 12, 26) {
66
+ 1.day.hence.should == Time.new(2012, 12, 27)
67
+ }
68
+ end
69
+
70
+ end # Future locators
71
+
72
+ describe "Anchored past locators" do
73
+ it "should point to the start of last year" do
74
+ tt.fixate(2012, 3, 3, 5, 32, 16) {
75
+ tt.last.year.should == Time.new(2011, 1, 1)
76
+ }
77
+ end
78
+
79
+ it "should point to the start of last month" do
80
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
81
+ tt.last.month.should == Time.new(2012, 3, 1)
82
+ }
83
+ end
84
+
85
+ it "should point to the start of last day" do
86
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
87
+ tt.last.day.should == Time.new(2012, 4, 15)
88
+ }
89
+ end
90
+
91
+ it "should point to the start of the year before last year" do
92
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
93
+ tt.last(2).years.should == Time.new(2010, 1, 1)
94
+ }
95
+ end
96
+
97
+ it "should point to the start of the month before last month" do
98
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
99
+ tt.last(2).months.should == Time.new(2012, 2, 1)
100
+ }
101
+ end
102
+
103
+ it "should point to the start of the day before last day" do
104
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
105
+ tt.last(2).days.should == Time.new(2012, 4, 14)
106
+ }
107
+ end
108
+ end # Anchored past locators
109
+
110
+ describe "Anchored future locators" do
111
+ it "should point to the start of next year" do
112
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
113
+ tt.next.year.should == Time.new(2013, 1, 1)
114
+ }
115
+ end
116
+
117
+ it "should point to the start of next month" do
118
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
119
+ tt.next.month.should == Time.new(2012, 5, 1)
120
+ }
121
+ end
122
+
123
+ it "should point to the start of next day" do
124
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
125
+ tt.next.day.should == Time.new(2012, 4, 17)
126
+ }
127
+ end
128
+
129
+ it "should point to the start of the year after the coming year" do
130
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
131
+ tt.next(2).years.should == Time.new(2014, 1, 1)
132
+ }
133
+ end
134
+
135
+ it "should point to the start of the month after the coming month" do
136
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
137
+ tt.next(2).months.should == Time.new(2012, 6, 1)
138
+ }
139
+ end
140
+
141
+ it "should point to the start of the day after tomorrow" do
142
+ tt.fixate(2012, 4, 16, 5, 32, 16) {
143
+ tt.next(2).days.should == Time.new(2012, 4, 18)
144
+ }
145
+ end
146
+ end # Anchored future locators
147
+
148
+ describe "Forward wrapping" do
149
+
150
+ it "should wrap forward a day to locate the hour" do
151
+ tt.fixate(2012, 6, 26, 21) {
152
+ # this shouldn't wrap
153
+ 1.hour.ahead.should == Time.new(2012, 6, 26, 22)
154
+ # but this should
155
+ 4.hour.ahead.should == Time.new(2012, 6, 27, 1)
156
+ }
157
+ end
158
+
159
+ it "should wrap forward a month to locate the day" do
160
+ tt.fixate(2012, 6, 29) {
161
+ # this should wrap
162
+ 4.days.ahead.should == Time.new(2012, 7, 3)
163
+ # and this shouldn't
164
+ 1.days.ahead.should == Time.new(2012, 6, 30)
165
+ }
166
+ end
167
+
168
+ # we know that 2011 is a leap (gregorian) year, and 2012 isn't
169
+ # nr of days in february in a leap year is 28, otherwise it's 29
170
+ it "should wrap forward a month to locate the day in a leap year" do
171
+ tt.fixate(2011, 2, 28) {
172
+ 2.days.ahead.should == Time.new(2011, 3, 2)
173
+ 1.days.ahead.should == Time.new(2011, 3, 1)
174
+ 0.days.ahead.should == Time.new(2011, 2, 28)
175
+ -1.days.ahead.should == Time.new(2011, 2, 27)
176
+ }
177
+ end
178
+
179
+ it "should wrap forward a month to locate the week" do
180
+ tt.fixate(2012, 6, 20) {
181
+ # shouldn't wrap
182
+ 1.week.ahead.should == Time.new(2012, 6, 27)
183
+ # this should wrap
184
+ 2.weeks.ahead.should == Time.new(2012, 7, 4)
185
+ }
186
+
187
+
188
+ # february has 29 on non-leap
189
+ tt.fixate(2012, 2, 22) {
190
+ 1.week.ahead.should == Time.new(2012, 2, 29)
191
+ }
192
+
193
+ # 28 on leap year
194
+ tt.fixate(2011, 2, 22) {
195
+ 1.week.ahead.should == Time.new(2011, 3, 1)
196
+ }
197
+ end
198
+
199
+ it "should wrap forward a year to locate a month" do
200
+ tt.fixate(2011, 12, 5) {
201
+ 1.month.ahead.should == Time.new(2012, 1, 4)
202
+ }
203
+ end
204
+ end # Forward wrapping
205
+
206
+ describe "Backwards wrapping" do
207
+ it "should wrap backwards a day to locate the hour" do
208
+ tt.fixate(2012, 6, 26, 1) {
209
+ # this shouldn't wrap
210
+ 1.hour.ago.should == Time.new(2012, 6, 26, 0)
211
+ # but these should
212
+ 2.hours.ago.should == Time.new(2012, 6, 25, 23)
213
+ 4.hour.ago.should == Time.new(2012, 6, 25, 21)
214
+ }
215
+ end
216
+
217
+ it "should wrap backwards a month to locate the day" do
218
+ tt.fixate(2012, 3, 2) {
219
+ 4.days.ago.should == Time.new(2012, 2, 27)
220
+ 2.days.ago.should == Time.new(2012, 2, 29)
221
+ tt.last(2).days.should == Time.new(2012, 2, 29)
222
+ tt.last(4).days.should == Time.new(2012, 2, 27)
223
+ }
224
+ end
225
+
226
+ it "should wrap backwards a month to locate the day in a leap year" do
227
+ tt.fixate(2011, 3, 2) {
228
+ 2.days.ago.should == Time.new(2011, 2, 28)
229
+ 4.days.ago.should == Time.new(2011, 2, 26)
230
+ tt.last(2).days.should == Time.new(2011, 2, 28)
231
+ tt.last(4).days.should == Time.new(2011, 2, 26)
232
+ }
233
+ end
234
+
235
+ it "should wrap backwards more than a month to locate a day" do
236
+ Timetastic.fixate(2012, 6, 15) {
237
+ 2.days.ago.should == Time.new(2012, 6, 13)
238
+ 15.days.ago.should == Time.new(2012, 5, 31)
239
+ 17.days.ago.should == Time.new(2012, 5, 29)
240
+ }
241
+ Timetastic.fixate(2012, 6, 10) {
242
+ 29.days.ago.should == Time.new(2012, 5, 12)
243
+ 30.days.ago.should == Time.new(2012, 5, 11)
244
+ 32.days.ago.should == Time.new(2012, 5, 9)
245
+ 64.days.ago.should == Time.new(2012, 4, 7)
246
+ }
247
+ end
248
+
249
+ it "should wrap backwards a month to locate the week" do
250
+ tt.fixate(2012, 6, 8) {
251
+ 1.week.ago.should == Time.new(2012, 6, 1)
252
+ # this should wrap
253
+ 2.weeks.ago.should == Time.new(2012, 5, 25)
254
+ tt.last(2).weeks.should == Time.new(2012, 5, 25)
255
+ }
256
+ end
257
+
258
+ it "should wrap backwards a year to locate a month" do
259
+ tt.fixate(2011, 3, 5) {
260
+ # this should wrap
261
+ 6.month.ago.should == Time.new(2010, 9, 6, 1)
262
+ 3.month.ago.should == Time.new(2010, 12, 5)
263
+ # these shouldn't
264
+ 1.month.ago.should == Time.new(2011, 2, 3)
265
+ 2.month.ago.should == Time.new(2011, 1, 4)
266
+ }
267
+ end
268
+
269
+ end # Backwards wrapping
270
+
271
+ describe "Far-off, OOR wrapping" do
272
+ it "should wrap backwards more than a year to locate a month" do
273
+ Timetastic.fixate(2012, 6, 1) {
274
+ 10.months.ago.should == Time.new(2011, 8, 6)
275
+ 12.months.ago.should == Time.new(2011, 6, 7)
276
+ 13.months.ago.should == Time.new(2011, 5, 8)
277
+ 18.months.ago.should == Time.new(2010, 12, 8, 23)
278
+ 20.months.ago.should == Time.new(2010, 10, 10)
279
+ 36.months.ago.should == Time.new(2009, 6, 17)
280
+ 48.months.ago.should == Time.new(2008, 6, 22)
281
+ 60.months.ago.should == Time.new(2007, 6, 28)
282
+ 66.months.ago.should == Time.new(2006, 12, 29, 23)
283
+ }
284
+
285
+ Timetastic.fixate(2007, 8, 15) { |t|
286
+ 8.months.ago.should == Time.new(2006,12,17, 23)
287
+ 19.months.ago.should == Time.new(2006,1,21, 23)
288
+ 20.months.ago.should == Time.new(2005,12,22, 23)
289
+ }
290
+ end
291
+
292
+ it "should wrap forward more than a month to locate a day" do
293
+ Timetastic.fixate(2012, 6, 15) {
294
+ 32.days.ahead.should == Time.new(2012, 7, 17)
295
+ 64.days.ahead.should == Time.new(2012, 8, 18)
296
+ 65.days.ahead.should == Time.new(2012, 8, 19)
297
+ 15.days.ahead.should == Time.new(2012, 6, 30)
298
+ 16.days.ahead.should == Time.new(2012, 7, 1)
299
+ 90.days.ahead.should == Time.new(2012, 9, 13)
300
+ 365.days.ahead.should == Time.new(2013, 6, 15)
301
+ 2700.days.ahead.should == Time.new(2019, 11, 5, 23)
302
+ }
303
+ end
304
+
305
+ it "should wrap forwards more than a year to locate a month" do
306
+ Timetastic.fixate(2012, 6, 1) {
307
+ 6.months.ahead.should == Time.new(2012, 11, 28)
308
+ 18.months.ahead.should == Time.new(2013, 11, 22, 23)
309
+ 23.months.ahead.should == Time.new(2014, 4, 22)
310
+ }
311
+
312
+ Timetastic.fixate(2020, 7, 30) {
313
+ 1.months.ahead.should == Time.new(2020, 8, 29)
314
+ 14.months.ahead.should == Time.new(2021, 9, 23)
315
+ 25.months.ahead.should == Time.new(2022, 8, 19)
316
+ }
317
+ end
318
+ end
319
+
320
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timetastic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ahmad Amireh
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !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: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Pure Ruby date selection using an easy and readable interface. Calculation
31
+ of dates accounts for wrapping across days, months, and years.
32
+ email: ahmad@algollabs.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/timetastic.rb
38
+ - spec/timetastic_spec.rb
39
+ homepage: https://github.com/amireh/timetastic
40
+ licenses: []
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 1.8.23
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: Utility collection of relative date selectors similar to ActiveRecord's.
63
+ test_files:
64
+ - spec/timetastic_spec.rb