timerizer 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/timerizer.rb +360 -54
  2. metadata +1 -1
data/lib/timerizer.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  require 'date'
2
2
 
3
- # Represents a relative amount of time. For example, `5 days`, `4 years`, and `5 years, 4 hours, 3 minutes, 2 seconds` are all RelativeTimes.
3
+ # Represents a relative amount of time. For example, '`5 days`', '`4 years`', and '`5 years, 4 hours, 3 minutes, 2 seconds`' are all RelativeTimes.
4
4
  class RelativeTime
5
- # All potential units. Key is the unit name, and the value is its plural form.
6
5
  @@units = {
7
6
  :second => :seconds,
8
7
  :minute => :minutes,
@@ -16,7 +15,6 @@ class RelativeTime
16
15
  :millennium => :millennia
17
16
  }
18
17
 
19
- # Unit values in seconds. If a unit is not present in this hash, it is assumed to be in the {@@in_months} hash.
20
18
  @@in_seconds = {
21
19
  :second => 1,
22
20
  :minute => 60,
@@ -25,7 +23,6 @@ class RelativeTime
25
23
  :week => 604800
26
24
  }
27
25
 
28
- # Unit values in months. If a unit is not present in this hash, it is assumed to be in the {@@in_seconds} hash.
29
26
  @@in_months = {
30
27
  :month => 1,
31
28
  :year => 12,
@@ -40,6 +37,69 @@ class RelativeTime
40
37
  :year => 31556952
41
38
  }
42
39
 
40
+ # Default syntax formats that can be used with #to_s
41
+ # @see #to_s
42
+ @@syntaxes = {
43
+ :micro => {
44
+ :units => {
45
+ :seconds => 's',
46
+ :minutes => 'm',
47
+ :hours => 'h',
48
+ :days => 'd',
49
+ :weeks => 'w',
50
+ :months => 'mn',
51
+ :years => 'y',
52
+ },
53
+ :separator => '',
54
+ :delimiter => ' ',
55
+ :count => 1
56
+ },
57
+ :short => {
58
+ :units => {
59
+ :seconds => 'sec',
60
+ :minutes => 'min',
61
+ :hours => 'hr',
62
+ :days => 'd',
63
+ :weeks => 'wk',
64
+ :months => 'mn',
65
+ :years => 'yr',
66
+ :centuries => 'ct',
67
+ :millenia => 'ml'
68
+ },
69
+ :separator => '',
70
+ :delimiter => ' ',
71
+ :count => 2
72
+ },
73
+ :long => {
74
+ :units => {
75
+ :seconds => ['second', 'seconds'],
76
+ :minutes => ['minute', 'minutes'],
77
+ :hours => ['hour', 'hours'],
78
+ :days => ['day', 'days'],
79
+ :weeks => ['week', 'weeks'],
80
+ :months => ['month', 'months'],
81
+ :years => ['year', 'years'],
82
+ :centuries => ['century', 'centuries'],
83
+ :millennia => ['millenium', 'millenia'],
84
+ }
85
+ }
86
+ }
87
+
88
+ # All potential units. Key is the unit name, and the value is its plural form.
89
+ def self.units
90
+ @@units
91
+ end
92
+
93
+ # Unit values in seconds. If a unit is not present in this hash, it is assumed to be in the {@@in_months} hash.
94
+ def self.units_in_seconds
95
+ @@in_seconds
96
+ end
97
+
98
+ # Unit values in months. If a unit is not present in this hash, it is assumed to be in the {@@in_seconds} hash.
99
+ def self.units_in_months
100
+ @@in_months
101
+ end
102
+
43
103
  # Initialize a new instance of RelativeTime.
44
104
  # @overload new(hash)
45
105
  # @param [Hash] units The base units to initialize with
@@ -47,21 +107,20 @@ class RelativeTime
47
107
  # @option units [Fixnum] :months The number of months
48
108
  # @overload new(count, unit)
49
109
  # @param [Fixnum] count The number of units to initialize with
50
- # @param [Symbol] unit The unit to initialize. See {@@units}
110
+ # @param [Symbol] unit The unit to initialize. See {RelativeTime#units}
51
111
  def initialize(count = 0, unit = :second)
52
- if(count.is_a? Hash)
53
- @seconds = count[:seconds] || 0
54
- @months = count[:months] || 0
55
- return
56
- end
57
-
58
- @seconds = 0
59
- @months = 0
112
+ if count.is_a? Hash
113
+ units = count
114
+ units.default = 0
115
+ @seconds, @months = units.values_at(:seconds, :months)
116
+ else
117
+ @seconds = @months = 0
60
118
 
61
- if(@@in_seconds.has_key?(unit))
62
- @seconds = count * @@in_seconds.fetch(unit)
63
- elsif(@@in_months.has_key?(unit))
64
- @months = count * @@in_months.fetch(unit)
119
+ if @@in_seconds.has_key?(unit)
120
+ @seconds = count * @@in_seconds.fetch(unit)
121
+ elsif @@in_months.has_key?(unit)
122
+ @months = count * @@in_months.fetch(unit)
123
+ end
65
124
  end
66
125
  end
67
126
 
@@ -70,8 +129,11 @@ class RelativeTime
70
129
  # @return [Boolean] True if both RelativeTimes are equal
71
130
  # @note Be weary of rounding; this method compares both RelativeTimes' base units
72
131
  def ==(time)
73
- raise ArgumentError unless time.is_a?(RelativeTime)
74
- return @seconds == time.get(:seconds) && @months == time.get(:months)
132
+ if time.is_a?(RelativeTime)
133
+ @seconds == time.get(:seconds) && @months == time.get(:months)
134
+ else
135
+ false
136
+ end
75
137
  end
76
138
 
77
139
  # Return the number of base units in a RelativeTime.
@@ -79,16 +141,20 @@ class RelativeTime
79
141
  # @return [Fixnum] The requested unit count
80
142
  # @raise [ArgumentError] Unit requested was not :seconds or :months
81
143
  def get(unit)
82
- return @seconds if unit == :seconds
83
- return @months if unit == :months
84
- raise ArgumentError
144
+ if unit == :seconds
145
+ @seconds
146
+ elsif unit == :months
147
+ @months
148
+ else
149
+ raise ArgumentError
150
+ end
85
151
  end
86
152
 
87
153
  # Determines the time between RelativeTime and the given time.
88
154
  # @param [Time] time The initial time.
89
155
  # @return [Time] The difference between the current RelativeTime and the given time
90
156
  # @example 5 hours before January 1st, 2000 at noon
91
- # 5.minutes.before(Time.now(2000, 1, 1, 12, 00, 00))
157
+ # 5.minutes.before(Time.new(2000, 1, 1, 12, 00, 00))
92
158
  # => 2000-01-01 11:55:00 -0800
93
159
  # @see #ago
94
160
  # @see #after
@@ -102,7 +168,7 @@ class RelativeTime
102
168
  new_month += 12
103
169
  new_year -= 1
104
170
  end
105
- if(Date.valid_date?(new_year, new_month, time.day))
171
+ if Date.valid_date?(new_year, new_month, time.day)
106
172
  new_day = time.day
107
173
  else
108
174
  new_day = Date.new(new_year, new_month).days_in_month
@@ -135,7 +201,7 @@ class RelativeTime
135
201
  new_year += 1
136
202
  new_month -= 12
137
203
  end
138
- if(Date.valid_date?(new_year, new_month, time.day))
204
+ if Date.valid_date?(new_year, new_month, time.day)
139
205
  new_day = time.day
140
206
  else
141
207
  new_day = Date.new(new_year, new_month).days_in_month
@@ -161,9 +227,9 @@ class RelativeTime
161
227
  superior_unit = @@units.keys.index(unit) + 1
162
228
 
163
229
  define_method(in_method) do
164
- if(@@in_seconds.has_key?(unit))
230
+ if @@in_seconds.has_key?(unit)
165
231
  @seconds / @@in_seconds[unit]
166
- elsif(@@in_months.has_key?(unit))
232
+ elsif @@in_months.has_key?(unit)
167
233
  @months / @@in_months[unit]
168
234
  end
169
235
  end
@@ -173,7 +239,7 @@ class RelativeTime
173
239
  count_superior = @@units.keys[superior_unit]
174
240
 
175
241
  time = self.send(in_method)
176
- if(@@units.length > superior_unit)
242
+ if @@units.length > superior_unit
177
243
  time -= self.send(in_superior).send(count_superior).send(in_method)
178
244
  end
179
245
  time
@@ -188,14 +254,16 @@ class RelativeTime
188
254
  # @see #average!
189
255
  # @see #unaverage
190
256
  def average
191
- return self unless @seconds > 0
192
-
193
- months = (@seconds / @@average_seconds[:month])
194
- seconds = @seconds - months.months.unaverage.get(:seconds)
195
- RelativeTime.new({
196
- :seconds => seconds,
197
- :months => months + @months
198
- })
257
+ if @seconds > 0
258
+ months = (@seconds / @@average_seconds[:month])
259
+ seconds = @seconds - months.months.unaverage.get(:seconds)
260
+ RelativeTime.new(
261
+ :seconds => seconds,
262
+ :months => months + @months
263
+ )
264
+ else
265
+ self
266
+ end
199
267
  end
200
268
 
201
269
  # Destructively average second-based units to month-based units.
@@ -217,7 +285,7 @@ class RelativeTime
217
285
  def unaverage
218
286
  seconds = @@average_seconds[:month] * @months
219
287
  seconds += @seconds
220
- RelativeTime.new({:seconds => seconds})
288
+ RelativeTime.new(:seconds => seconds)
221
289
  end
222
290
 
223
291
  # Destructively average month-based units to second-based units.
@@ -251,32 +319,252 @@ class RelativeTime
251
319
  })
252
320
  end
253
321
 
322
+ # Converts {RelativeTime} to {WallClock}
323
+ # @return [WallClock] {RelativeTime} as {WallClock}
324
+ # @example
325
+ # (17.hours 30.minutes).to_wall
326
+ # # => 5:30:00 PM
327
+ def to_wall
328
+ raise WallClock::TimeOutOfBoundsError if @months > 0
329
+ WallClock.new(:second => @seconds)
330
+ end
331
+
254
332
  # Convert {RelativeTime} to a human-readable format.
333
+ # @overload to_s(syntax)
334
+ # @param [Symbol] syntax The syntax from @@syntaxes to use
335
+ # @overload to_s(hash)
336
+ # @param [Hash] hash The custom hash to use
337
+ # @option hash [Hash] :units The unit names to use. See @@syntaxes for examples
338
+ # @option hash [Fixnum] :count The maximum number of units to output. `1` would output only the unit of greatest example (such as the hour value in `1.hour 3.minutes 2.seconds`).
339
+ # @option hash [String] :separator The separator to use in between a unit and its value
340
+ # @option hash [String] :delimiter The delimiter to use in between different unit-value pairs
255
341
  # @example
256
342
  # (14.months 49.hours).to_s
257
343
  # => 2 years, 2 months, 3 days, 1 hour
258
- def to_s
259
- times = []
344
+ # (1.day 3.hours 4.minutes).to_s(:short)
345
+ # => 1d 3hr
346
+ # @raise KeyError Symbol argument isn't in @@syntaxes
347
+ # @raise ArgumentError Argument isn't a hash (if not a symbol)
348
+ # @see @@syntaxes
349
+ def to_s(syntax = :long)
350
+ if syntax.is_a? Symbol
351
+ syntax = @@syntaxes.fetch(syntax)
352
+ end
260
353
 
261
- @@units.each do |unit, plural|
262
- time = self.respond_to?(plural) ? self.send(plural) : 0
263
- times << [time, (time != 1) ? plural : unit] if time > 0
354
+ raise ArgumentError unless syntax.is_a? Hash
355
+ times = []
356
+
357
+ if syntax[:count].nil? || syntax[:count] == :all
358
+ syntax[:count] = @@units.count
359
+ end
360
+ units = syntax.fetch(:units)
361
+
362
+ count = 0
363
+ units = Hash[units.to_a.reverse]
364
+ units.each do |unit, (singular, plural)|
365
+ if count < syntax.fetch(:count)
366
+ time = self.respond_to?(unit) ? self.send(unit) : 0
367
+
368
+ if time > 1 && !plural.nil?
369
+ times << [time, plural]
370
+ count += 1
371
+ elsif time > 0
372
+ times << [time, singular]
373
+ count += 1
374
+ end
375
+ end
264
376
  end
265
377
 
266
378
  times.map do |time|
267
- time.join(' ')
268
- end.reverse.join(', ')
379
+ time.join(syntax[:separator] || ' ')
380
+ end.join(syntax[:delimiter] || ', ')
381
+ end
382
+ end
383
+
384
+ # Represents a time, but not a date. '`7:00 PM`' would be an example of a WallClock object
385
+ class WallClock
386
+ # Represents an error where an invalid meridiem was passed to WallClock.
387
+ # @see #new
388
+ class InvalidMeridiemError < ArgumentError; end
389
+ # Represents an error where a time beyond 24 hours was passed to WallClock.
390
+ # @see #new
391
+ class TimeOutOfBoundsError < ArgumentError; end
392
+
393
+ # Initialize a new instance of WallClock
394
+ # @overload new(hash)
395
+ # @param [Hash] units The units to initialize with
396
+ # @option units [Fixnum] :hour The hour to initialize with
397
+ # @option units [Fixnum] :minute The minute to initialize with
398
+ # @option units [Fixnum] :second The second to initialize with
399
+ # @overload new(hour, minute, meridiem)
400
+ # @param [Fixnum] hour The hour to initialize with
401
+ # @param [Fixnum] minute The minute to initialize with
402
+ # @param [Symbol] meridiem The meridiem to initialize with (`:am` or `:pm`)
403
+ # @overload new(hour, minute, second, meridiem)
404
+ # @param [Fixnum] hour The hour to initialize with
405
+ # @param [Fixnum] minute The minute to initialize with
406
+ # @param [Fixnum] second The second to initialize with
407
+ # @param [Symbol] meridiem The meridiem to initialize with (`:am` or `:pm`)
408
+ # @raise InvalidMeridiemError Meridiem is not `:am` or `:pm`
409
+ def initialize(hour = 0, minute = 0, second = 0, meridiem = :am)
410
+ if hour.is_a?(Hash)
411
+ units = hour
412
+
413
+ second = units[:second] || 0
414
+ minute = units[:minute] || 0
415
+ hour = units[:hour] || 0
416
+ else
417
+ if second.is_a?(String) || second.is_a?(Symbol)
418
+ meridiem = second
419
+ second = 0
420
+ end
421
+
422
+ meridiem = meridiem.downcase.to_sym
423
+ if !(meridiem == :am || meridiem == :pm)
424
+ raise InvalidMeridiemError
425
+ elsif meridiem == :pm && hour > 12
426
+ raise TimeOutOfBoundsError, "hour must be <= 12 for PM"
427
+ elsif hour >= 24 || minute >= 60 || second >= 60
428
+ raise TimeOutOfBoundsError
429
+ end
430
+
431
+ hour += 12 if meridiem == :pm
432
+ end
433
+
434
+ @seconds =
435
+ RelativeTime.units_in_seconds.fetch(:hour) * hour +
436
+ RelativeTime.units_in_seconds.fetch(:minute) * minute +
437
+ second
438
+
439
+ if @seconds >= RelativeTime.units_in_seconds.fetch(:day)
440
+ raise TimeOutOfBoundsError
441
+ end
442
+ end
443
+
444
+ # Returns the time of the WallClock on a date
445
+ # @param [Date] date The date to apply the time on
446
+ # @return [Time] The time after the given date
447
+ # @example yesterday at 5:00
448
+ # time = WallClock.new(5, 00, :pm)
449
+ # time.on(Date.yesterday)
450
+ # => 2000-1-1 17:00:00 -0800
451
+ def on(date)
452
+ date.to_date.to_time + @seconds
453
+ end
454
+
455
+ # Comparse two {WallClock}s.
456
+ # @return [Boolean] True if the WallClocks are identical
457
+ def ==(time)
458
+ if time.is_a? WallClock
459
+ self.in_seconds == time.in_seconds
460
+ else
461
+ false
462
+ end
463
+ end
464
+
465
+ # Get the time of the WallClock, in seconds
466
+ # @return [Fixnum] The total time of the WallClock, in seconds
467
+ def in_seconds
468
+ @seconds
469
+ end
470
+
471
+ # Get the time of the WallClock, in minutes
472
+ # @return [Fixnum] The total time of the WallClock, in minutes
473
+ def in_minutes
474
+ @seconds / RelativeTime.units_in_seconds[:minute]
475
+ end
476
+
477
+ # Get the time of the WallClock, in hours
478
+ # @return [Fixnum] The total time of the WallClock, in hours
479
+ def in_hours
480
+ @seconds / RelativeTime.units_in_seconds[:hour]
481
+ end
482
+
483
+ # Get the second of the WallClock.
484
+ # @return [Fixnum] The second component of the WallClock
485
+ def second
486
+ self.to_relative.seconds
487
+ end
488
+
489
+ # Get the minute of the WallClock.
490
+ # @return [Fixnum] The minute component of the WallClock
491
+ def minute
492
+ self.to_relative.minutes
493
+ end
494
+
495
+ # Get the hour of the WallClock.
496
+ # @param [Symbol] system The houring system to use (either `:twelve_hour` or `:twenty_four_hour`; default `:twenty_four_hour`)
497
+ # @return [Fixnum] The hour component of the WallClock
498
+ def hour(system = :twenty_four_hour)
499
+ hour = self.to_relative.hours
500
+ if (system == :twelve_hour) && hour > 12
501
+ hour - 12
502
+ elsif (system == :twenty_four_hour)
503
+ hour
504
+ else
505
+ raise ArgumentError, "system should be :twelve_hour or :twenty_four_hour"
506
+ end
507
+ end
508
+
509
+ # Get the meridiem of the WallClock.
510
+ # @return [Symbol] The meridiem (either `:am` or `:pm`)
511
+ def meridiem
512
+ if self.hour > 12
513
+ :pm
514
+ else
515
+ :am
516
+ end
517
+ end
518
+
519
+ # Converts self to {WallClock}
520
+ # @see Time#to_wall
521
+ def to_wall
522
+ self
523
+ end
524
+
525
+ # Converts {WallClock} to {RelativeTime}
526
+ # @return [RelativeTime] {WallClock} as {RelativeTime}
527
+ # @example
528
+ # time = WallClock.new(5, 30, :pm)
529
+ # time.to_relative
530
+ # => 5 hours, 30 minutes
531
+ def to_relative
532
+ @seconds.seconds
533
+ end
534
+
535
+ # Convert {WallClock} to a human-readable format.
536
+ # @param [Symbol] system The hour system to use (`:twelve_hour` or `:twenty_four_hour`; default `:twelve_hour`)
537
+ # @example
538
+ # time = WallClock.new(5, 37, :pm)
539
+ # time.to_s
540
+ # => "5:37:00 PM"
541
+ # time.to_s(:twenty_four_hour)
542
+ # => "17:37:00"
543
+ # @raise ArgumentError Argument isn't a proper system
544
+ def to_s(system = :twelve_hour)
545
+ if(system == :twelve_hour)
546
+ meridiem = self.meridiem.to_s.upcase
547
+ "#{self.hour(system)}:#{self.minute}:#{self.second} #{meridiem}"
548
+ elsif(system == :twenty_four_hour)
549
+ "#{self.hour(system)}:#{self.minute}:#{self.second}"
550
+ else
551
+ raise ArgumentError, "system should be :twelve_hour or :twenty_four_hour"
552
+ end
269
553
  end
270
554
  end
271
555
 
272
556
  # {Time} class monkeywrenched with {RelativeTime} support.
273
557
  class Time
274
- class TimeIsInThePastException < Exception; end
275
- class TimeIsInTheFutureException < Exception; end
558
+ # Represents an error where two times were expected to be in the future, but were in the past.
559
+ # @see #until
560
+ class TimeIsInThePastError < ArgumentError; end
561
+ # Represents an error where two times were expected to be in the past, but were in the future.
562
+ # @see #since
563
+ class TimeIsInTheFutureError < ArgumentError; end
276
564
 
277
565
  add = instance_method(:+)
278
566
  define_method(:+) do |time|
279
- if(time.class == RelativeTime)
567
+ if time.is_a? RelativeTime
280
568
  time.after(self)
281
569
  else
282
570
  add.bind(self).(time)
@@ -285,7 +573,7 @@ class Time
285
573
 
286
574
  subtract = instance_method(:-)
287
575
  define_method(:-) do |time|
288
- if(time.class == RelativeTime)
576
+ if time.is_a? RelativeTime
289
577
  time.before(self)
290
578
  else
291
579
  subtract.bind(self).(time)
@@ -302,13 +590,13 @@ class Time
302
590
  # @see Time#since
303
591
  # @see Time#between
304
592
  def self.until(time)
305
- raise TimeIsInThePastException if Time.now > time.to_time
593
+ raise TimeIsInThePastError if Time.now > time.to_time
306
594
 
307
595
  Time.between(Time.now, time)
308
596
  end
309
597
 
310
598
  # Calculates the time since a given time
311
- # @param [Time] since The time to calculate since now
599
+ # @param [Time] time The time to calculate since now
312
600
  # @return [RelativeTime] The time since the provided time
313
601
  # @raise[TimeIsInTheFutureException] The provided time is in the future
314
602
  # @example
@@ -317,7 +605,7 @@ class Time
317
605
  # @see Time#since
318
606
  # @see Time#between
319
607
  def self.since(time)
320
- raise TimeIsInTheFutureException if time.to_time > Time.now
608
+ raise TimeIsInTheFutureError if time.to_time > Time.now
321
609
 
322
610
  Time.between(Time.now, time)
323
611
  end
@@ -352,6 +640,17 @@ class Time
352
640
  def to_time
353
641
  self
354
642
  end
643
+
644
+ # Converts {Time} to {WallClock}
645
+ # @return [WallClock] {Time} as {WallClock}
646
+ # @example
647
+ # time = Time.now.to_wall
648
+ # Date.tomorrow.at(time)
649
+ # => 2000-1-2 13:13:27 -0800
650
+ # # "Same time tomorrow?"
651
+ def to_wall
652
+ WallClock.new(self.hour, self.min, self.sec)
653
+ end
355
654
  end
356
655
 
357
656
  # {Date} class monkeywrenched with {RelativeTime} helpers.
@@ -377,6 +676,14 @@ class Date
377
676
  self
378
677
  end
379
678
 
679
+ # Apply a time to a date
680
+ # @example yesterday at 5:00
681
+ # Date.yesterday.at(WallClock.new(5, 00, :pm))
682
+ # => 2000-1-1 17:00:00 -0800
683
+ def at(time)
684
+ time.to_wall.on(self)
685
+ end
686
+
380
687
  # Return tomorrow as {Date}.
381
688
  # @see Date#yesterday
382
689
  def self.tomorrow
@@ -394,10 +701,9 @@ end
394
701
  # @example
395
702
  # 5.minutes
396
703
  # => 5 minutes
397
- # @see RelativeTime @@units
704
+ # @see {RelativeTime#units}
398
705
  class Fixnum
399
- units = RelativeTime.class_variable_get(:@@units)
400
- units.each do |unit, plural|
706
+ RelativeTime.units.each do |unit, plural|
401
707
  define_method(unit) do |added_time = RelativeTime.new|
402
708
  time = RelativeTime.new(self, unit)
403
709
  time + added_time
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timerizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: