timetastic 0.1.1

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.
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