time_math2 0.0.3

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.
@@ -0,0 +1,264 @@
1
+ module TimeMath
2
+ module Units
3
+ # It is a main class representing most of TimeMath functionality.
4
+ # It (or rather its descendants) represents "unit of time" and
5
+ # connected calculations logic. Typical usage:
6
+ #
7
+ # ```ruby
8
+ # TimeMath.day.advance(tm, 5) # advances tm by 5 days
9
+ # ```
10
+ #
11
+ class Base
12
+ # Creates unit of time. Typically you don't need it, as it is
13
+ # easier to do `TimeMath.day` or `TimeMath[:day]` to obtain it.
14
+ #
15
+ # @param name [Symbol] one of {TimeMath.units}.
16
+ def initialize(name)
17
+ @name = name
18
+ end
19
+
20
+ attr_reader :name
21
+
22
+ # Rounds `tm` down to nearest unit (this means, `TimeMath.day.floor(tm)`
23
+ # will return beginning of `tm`-s day, and so on).
24
+ #
25
+ # @param tm [Time,DateTime] time value to floor.
26
+ # @return [Time,DateTime] floored time value; class and timezone info
27
+ # of origin would be preserved.
28
+ def floor(tm)
29
+ components = [tm.year,
30
+ tm.month,
31
+ tm.day,
32
+ tm.hour,
33
+ tm.min,
34
+ tm.sec].first(index + 1)
35
+
36
+ new_from_components(tm, *components)
37
+ end
38
+
39
+ # Rounds `tm` up to nearest unit (this means, `TimeMath.day.ceil(tm)`
40
+ # will return beginning of day next after `tm`, and so on).
41
+ #
42
+ # @param tm [Time,DateTime] time value to ceil.
43
+ # @return [Time,DateTime] ceiled time value; class and timezone info
44
+ # of origin would be preserved.
45
+ def ceil(tm)
46
+ f = floor(tm)
47
+
48
+ f == tm ? f : advance(f)
49
+ end
50
+
51
+ # Rounds `tm` up or down to nearest unit (this means, `TimeMath.day.round(tm)`
52
+ # will return beginning of `tm` day if `tm` is before noon, and
53
+ # day next after `tm` if it is after, and so on).
54
+ #
55
+ # @param tm [Time,DateTime] time value to round.
56
+ # @return [Time,DateTime] rounded time value; class and timezone info
57
+ # of origin would be preserved.
58
+ def round(tm)
59
+ f, c = floor(tm), ceil(tm)
60
+
61
+ (tm - f).abs < (tm - c).abs ? f : c
62
+ end
63
+
64
+ # Like {#floor}, but always return value lower than `tm` (e.g. if
65
+ # `tm` is exactly midnight, then `TimeMath.day.prev(tm)` will return
66
+ # _previous midnight_).
67
+ #
68
+ # @param tm [Time,DateTime] time value to calculate prev on.
69
+ # @return [Time,DateTime] prev time value; class and timezone info
70
+ # of origin would be preserved.
71
+ def prev(tm)
72
+ f = floor(tm)
73
+ f == tm ? decrease(f) : f
74
+ end
75
+
76
+ # Like {#ceil}, but always return value greater than `tm` (e.g. if
77
+ # `tm` is exactly midnight, then `TimeMath.day.next(tm)` will return
78
+ # _next midnight_).
79
+ #
80
+ # @param tm [Time,DateTime] time value to calculate next on.
81
+ # @return [Time,DateTime] next time value; class and timezone info
82
+ # of origin would be preserved.
83
+ def next(tm)
84
+ c = ceil(tm)
85
+ c == tm ? advance(c) : c
86
+ end
87
+
88
+ # Checks if `tm` is exactly rounded to unit.
89
+ #
90
+ # @param tm [Time,DateTime] time value to check.
91
+ # @return [Boolean] whether `tm` is exactly round to unit.
92
+ def round?(tm)
93
+ floor(tm) == tm
94
+ end
95
+
96
+ # Advances `tm` by given amount of unit.
97
+ #
98
+ # @param tm [Time,DateTime] time value to advance;
99
+ # @param amount [Integer] how many units forward to go.
100
+ #
101
+ # @return [Time,DateTime] advanced time value; class and timezone info
102
+ # of origin would be preserved.
103
+ def advance(tm, amount = 1)
104
+ return decrease(tm, -amount) if amount < 0
105
+ _advance(tm, amount)
106
+ end
107
+
108
+ # Decreases `tm` by given amount of unit.
109
+ #
110
+ # @param tm [Time,DateTime] time value to decrease;
111
+ # @param amount [Integer] how many units forward to go.
112
+ #
113
+ # @return [Time,DateTime] decrease time value; class and timezone info
114
+ # of origin would be preserved.
115
+ def decrease(tm, amount = 1)
116
+ return advance(tm, -amount) if amount < 0
117
+ _decrease(tm, amount)
118
+ end
119
+
120
+ # Creates range from `tm` to `tm` increased by amount of units.
121
+ #
122
+ # ```ruby
123
+ # tm = Time.parse('2016-05-28 16:30')
124
+ # TimeMath.day.range(tm, 5)
125
+ # # => 2016-05-28 16:30:00 +0300...2016-06-02 16:30:00 +0300
126
+ # ```
127
+ #
128
+ # @param tm [Time,DateTime] time value to create range from;
129
+ # @param amount [Integer] how many units should be between range
130
+ # start and end.
131
+ #
132
+ # @return [Range]
133
+ def range(tm, amount = 1)
134
+ (tm...advance(tm, amount))
135
+ end
136
+
137
+ # Creates range from `tm` decreased by amount of units to `tm`.
138
+ #
139
+ # ```ruby
140
+ # tm = Time.parse('2016-05-28 16:30')
141
+ # TimeMath.day.range_back(tm, 5)
142
+ # # => 2016-05-23 16:30:00 +0300...2016-05-28 16:30:00 +0300
143
+ # ```
144
+ #
145
+ # @param tm [Time,DateTime] time value to create range from;
146
+ # @param amount [Integer] how many units should be between range
147
+ # start and end.
148
+ #
149
+ # @return [Range]
150
+ def range_back(tm, amount = 1)
151
+ (decrease(tm, amount)...tm)
152
+ end
153
+
154
+ # Measures distance between `from` and `to` in units of this class.
155
+ #
156
+ # @param from [Time,DateTime] start of period;
157
+ # @param to [Time,DateTime] end of period.
158
+ #
159
+ # @return [Integer] how many full units are inside the period.
160
+ # :nocov:
161
+ def measure(from, to) # rubocop:disable Lint/UnusedMethodArgument
162
+ raise NotImplementedError,
163
+ '#measure should be implemented in subclasses'
164
+ end
165
+ # :nocov:
166
+
167
+ # Like {#measure} but also returns "remainder": the time where
168
+ # it would be **exactly** returned amount of units between `from`
169
+ # and `to`:
170
+ #
171
+ # ```ruby
172
+ # TimeMath.day.measure(Time.parse('2016-05-01 16:20'), Time.parse('2016-05-28 15:00'))
173
+ # # => 26
174
+ # TimeMath.day.measure_rem(Time.parse('2016-05-01 16:20'), Time.parse('2016-05-28 15:00'))
175
+ # # => [26, 2016-05-27 16:20:00 +0300]
176
+ # ```
177
+ #
178
+ # @param from [Time,DateTime] start of period;
179
+ # @param to [Time,DateTime] end of period.
180
+ #
181
+ # @return [Array<Integer, Time or DateTime>] how many full units
182
+ # are inside the period; exact value of `from` + full units.
183
+ def measure_rem(from, to)
184
+ m = measure(from, to)
185
+ [m, advance(from, m)]
186
+ end
187
+
188
+ # Creates {Span} instance representing amount of units.
189
+ #
190
+ # Use it like this:
191
+ #
192
+ # ```ruby
193
+ # span = TimeMath.day.span(5) # => #<TimeMath::Span(day): +5>
194
+ # # now you can save this variable or path it to the methods...
195
+ # # and then:
196
+ # span.before(Time.parse('2016-05-01')) # => 2016-04-26 00:00:00 +0300
197
+ # span.after(Time.parse('2016-05-01')) # => 2016-05-06 00:00:00 +0300
198
+ # ```
199
+ #
200
+ # @param amount [Integer]
201
+ # @return [Span]
202
+ def span(amount = 1)
203
+ TimeMath::Span.new(name, amount)
204
+ end
205
+
206
+ # Creates {Sequence} instance for producing all time units between
207
+ # from and too. See {Sequence} class documentation for available
208
+ # options and functionality.
209
+ #
210
+ # @param from [Time,DateTime] start of sequence;
211
+ # @param to [Time,DateTime] upper limit of sequence;
212
+ # @param options [Hash]
213
+ # @option options [Boolean] :expand round sequence ends on creation
214
+ # (from is floored and to is ceiled);
215
+ # @option options [Boolean] :floor sequence will be rounding'ing all
216
+ # the intermediate values.
217
+ #
218
+ # @return [Sequence]
219
+ def sequence(from, to, options = {})
220
+ TimeMath::Sequence.new(name, from, to, options)
221
+ end
222
+
223
+ def inspect
224
+ "#<#{self.class}>"
225
+ end
226
+
227
+ protected
228
+
229
+ # all except :week
230
+ NATURAL_UNITS = [:year, :month, :day, :hour, :min, :sec].freeze
231
+ EMPTY_VALUES = [nil, 1, 1, 0, 0, 0].freeze
232
+
233
+ def index
234
+ NATURAL_UNITS.index(name) or
235
+ raise NotImplementedError, "Can not be used for #{name}"
236
+ end
237
+
238
+ def generate(tm, replacements = {})
239
+ hash_to_tm(tm, tm_to_hash(tm).merge(replacements))
240
+ end
241
+
242
+ def tm_to_hash(tm)
243
+ Hash[*NATURAL_UNITS.flat_map { |s| [s, tm.send(s)] }]
244
+ end
245
+
246
+ def hash_to_tm(origin, hash)
247
+ components = NATURAL_UNITS.map { |s| hash[s] || 0 }
248
+ new_from_components(origin, *components)
249
+ end
250
+
251
+ def new_from_components(origin, *components)
252
+ components = EMPTY_VALUES.zip(components).map { |d, c| c || d }
253
+ case origin
254
+ when Time
255
+ Time.mktime(*components.reverse, nil, nil, nil, origin.zone)
256
+ when DateTime
257
+ DateTime.new(*components, origin.zone)
258
+ end
259
+ end
260
+
261
+ include TimeMath # now we can use something like #day inside other units
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,34 @@
1
+ module TimeMath
2
+ module Units
3
+ # @private
4
+ class Day < Simple
5
+ def initialize
6
+ super(:day)
7
+ end
8
+
9
+ protected
10
+
11
+ def _advance(tm, steps)
12
+ fix_dst(super(tm, steps), tm)
13
+ end
14
+
15
+ def _decrease(tm, steps)
16
+ fix_dst(super(tm, steps), tm)
17
+ end
18
+
19
+ # :nocov: - somehow Travis env thinks other things about DST
20
+ def fix_dst(res, src)
21
+ return res unless res.is_a?(Time)
22
+
23
+ if res.dst? && !src.dst?
24
+ hour.decrease(res)
25
+ elsif !res.dst? && src.dst?
26
+ hour.advance(res)
27
+ else
28
+ res
29
+ end
30
+ end
31
+ # :nocov:
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+ module TimeMath
3
+ module Units
4
+ # @private
5
+ class Month < Base
6
+ def initialize
7
+ super(:month)
8
+ end
9
+
10
+ def measure(from, to)
11
+ ydiff = to.year - from.year
12
+ mdiff = to.month - from.month
13
+
14
+ to.day >= from.day ? (ydiff * 12 + mdiff) : (ydiff * 12 + mdiff - 1)
15
+ end
16
+
17
+ protected
18
+
19
+ def succ(tm)
20
+ return generate(tm, year: tm.year + 1, month: 1) if tm.month == 12
21
+
22
+ t = generate(tm, month: tm.month + 1)
23
+ fix_month(t, t.month + 1)
24
+ end
25
+
26
+ def prev(tm)
27
+ return generate(tm, year: tm.year - 1, month: 12) if tm.month == 1
28
+
29
+ t = generate(tm, month: tm.month - 1)
30
+ fix_month(t, t.month - 1)
31
+ end
32
+
33
+ def _advance(tm, steps)
34
+ steps.times.inject(tm) { |t| succ(t) }
35
+ end
36
+
37
+ def _decrease(tm, steps)
38
+ steps.times.inject(tm) { |t| prev(t) }
39
+ end
40
+
41
+ # fix for too far advance/insufficient decrease:
42
+ # Time.new(2013,2,31) #=> 2013-03-02 00:00:00 +0200
43
+ def fix_month(t, expected)
44
+ t.month == expected ? day.decrease(t, t.day) : t
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,58 @@
1
+ module TimeMath
2
+ module Units
3
+ # @private
4
+ class Simple < Base
5
+ def to_seconds(sz = 1)
6
+ sz * MULTIPLIERS[index..-1].inject(:*)
7
+ end
8
+
9
+ def measure(from, to)
10
+ ((to.to_time - from.to_time) / to_seconds).to_i
11
+ end
12
+
13
+ protected
14
+
15
+ def _advance(tm, steps)
16
+ _shift(tm, to_seconds(steps))
17
+ end
18
+
19
+ def _decrease(tm, steps)
20
+ _shift(tm, -to_seconds(steps))
21
+ end
22
+
23
+ def _shift(tm, seconds)
24
+ case tm
25
+ when Time
26
+ tm + seconds
27
+ when DateTime
28
+ tm + Rational(seconds, 86_400)
29
+ else
30
+ raise ArgumentError, "Expected Time or DateTime, got #{tm.class}"
31
+ end
32
+ end
33
+
34
+ MULTIPLIERS = [12, 30, 24, 60, 60, 1].freeze
35
+ end
36
+
37
+ # @private
38
+ class Sec < Simple
39
+ def initialize
40
+ super(:sec)
41
+ end
42
+ end
43
+
44
+ # @private
45
+ class Min < Simple
46
+ def initialize
47
+ super(:min)
48
+ end
49
+ end
50
+
51
+ # @private
52
+ class Hour < Simple
53
+ def initialize
54
+ super(:hour)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,30 @@
1
+ module TimeMath
2
+ module Units
3
+ # @private
4
+ class Week < Simple
5
+ def initialize
6
+ super(:week)
7
+ end
8
+
9
+ def floor(tm)
10
+ f = day.floor(tm)
11
+ extra_days = tm.wday == 0 ? 6 : tm.wday - 1
12
+ day.decrease(f, extra_days)
13
+ end
14
+
15
+ def to_seconds(sz = 1)
16
+ day.to_seconds(sz * 7)
17
+ end
18
+
19
+ protected
20
+
21
+ def _advance(tm, steps)
22
+ day.advance(tm, steps * 7)
23
+ end
24
+
25
+ def _decrease(tm, steps)
26
+ day.decrease(tm, steps * 7)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module TimeMath
2
+ module Units
3
+ # @private
4
+ class Year < Base
5
+ def initialize
6
+ super(:year)
7
+ end
8
+
9
+ def measure(from, to)
10
+ if generate(from, year: to.year) < to
11
+ to.year - from.year
12
+ else
13
+ to.year - from.year - 1
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ def _advance(tm, steps)
20
+ generate(tm, year: tm.year + steps)
21
+ end
22
+
23
+ def _decrease(tm, steps)
24
+ generate(tm, year: tm.year - steps)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'units/base'
2
+ require_relative 'units/simple'
3
+ require_relative 'units/day'
4
+ require_relative 'units/week'
5
+ require_relative 'units/month'
6
+ require_relative 'units/year'
7
+
8
+ module TimeMath
9
+ # See {Units::Base} for detailed description of all units functionality.
10
+ module Units
11
+ # @private
12
+ UNITS = {
13
+ sec: Units::Sec.new, min: Units::Min.new, hour: Units::Hour.new,
14
+ day: Units::Day.new, week: Units::Week.new, month: Units::Month.new,
15
+ year: Units::Year.new
16
+ }.freeze
17
+
18
+ # @private
19
+ def self.names
20
+ UNITS.keys
21
+ end
22
+
23
+ # @private
24
+ def self.get(name)
25
+ UNITS[name] or
26
+ raise ArgumentError, "Unsupported unit: #{name}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+ module TimeMath
2
+ # @private
3
+ VERSION = '0.0.3'.freeze
4
+ end
data/lib/time_math.rb ADDED
@@ -0,0 +1,99 @@
1
+ require 'time'
2
+
3
+ require_relative './time_math/units'
4
+ require_relative './time_math/sequence'
5
+ require_relative './time_math/measure'
6
+ require_relative './time_math/span'
7
+
8
+ # TimeMath is a small library for easy time units arithmetics (like "floor
9
+ # the timestamp to the nearest hour", "advance the time value by 3 days"
10
+ # and so on).
11
+ #
12
+ # It has clean and easy-to-remember API, just like this:
13
+ #
14
+ # ```ruby
15
+ # TimeMath.day.floor(Time.now)
16
+ # # or
17
+ # TimeMath[:day].floor(Time.now)
18
+ # ```
19
+ #
20
+ # `TimeMath[unit]` and `TimeMath.<unit>` give you an instance of
21
+ # time unit, which incapsulates most of the functionality. Refer to
22
+ # {Units::Base} to see what you can get of it.
23
+ #
24
+ module TimeMath
25
+ # rubocop:disable Style/ModuleFunction
26
+ extend self
27
+ # rubocop:enable Style/ModuleFunction
28
+
29
+ # List all unit names known.
30
+ #
31
+ # @return [Array<Symbol>]
32
+ def units
33
+ Units.names
34
+ end
35
+
36
+ # Main method to do something with TimeMath. Returns an object
37
+ # representing some time measurement unit. See {Units::Base} documentation
38
+ # to know what you can do with it.
39
+ #
40
+ # @return [Units::Base]
41
+ def [](unit)
42
+ Units.get(unit)
43
+ end
44
+
45
+ # @!method sec
46
+ # Shortcut to get second unit.
47
+ # @return [Units::Base]
48
+ #
49
+ # @!method min
50
+ # Shortcut to get minute unit.
51
+ # @return [Units::Base]
52
+ #
53
+ # @!method hour
54
+ # Shortcut to get hour unit.
55
+ # @return [Units::Base]
56
+ #
57
+ # @!method day
58
+ # Shortcut to get day unit.
59
+ # @return [Units::Base]
60
+ #
61
+ # @!method week
62
+ # Shortcut to get week unit.
63
+ # @return [Units::Base]
64
+ #
65
+ # @!method month
66
+ # Shortcut to get month unit.
67
+ # @return [Units::Base]
68
+ #
69
+ # @!method year
70
+ # Shortcut to get year unit.
71
+ # @return [Units::Base]
72
+ #
73
+ Units.names.each do |unit|
74
+ define_method(unit) { Units.get(unit) }
75
+ end
76
+
77
+ # Measures distance between two time values in all units at once.
78
+ #
79
+ # Just like this:
80
+ #
81
+ # ```ruby
82
+ # birthday = Time.parse('1983-02-14 13:30')
83
+ #
84
+ # TimeMath.measure(birthday, Time.now)
85
+ # # => {:years=>33, :months=>3, :weeks=>2, :days=>0, :hours=>1, :minutes=>25, :seconds=>52}
86
+ # ```
87
+ #
88
+ # @param from [Time,DateTime]
89
+ # @param to [Time,DateTime]
90
+ # @param options [Hash] options
91
+ # @option options [Boolean] :weeks pass `false` to exclude weeks from calculation;
92
+ # @option options [Symbol] :upto pass max unit to use (e.g. if you'll
93
+ # pass `:day`, period would be measured in days, hours, minutes and seconds).
94
+ #
95
+ # @return [Hash]
96
+ def measure(from, to, options = {})
97
+ Measure.measure(from, to, options)
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+ require './lib/time_math/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'time_math2'
5
+ s.version = TimeMath::VERSION
6
+ s.authors = ['Victor Shepelev']
7
+ s.email = 'zverok.offline@gmail.com'
8
+ s.homepage = 'https://github.com/zverok/time_math2'
9
+
10
+ s.summary = 'Easy time math'
11
+ s.description = <<-EOF
12
+ TimeMath is small, no-dependencies library attemting to make work with
13
+ time units easier. It provides you with simple, easy remembered API, without
14
+ any monkey patching of core Ruby classes, so it can be used alongside
15
+ Rails or without it, for any purpose.
16
+ EOF
17
+ s.licenses = ['MIT']
18
+
19
+ s.files = `git ls-files`.split($RS).reject do |file|
20
+ file =~ /^(?:
21
+ spec\/.*
22
+ |Gemfile
23
+ |Rakefile
24
+ |\.rspec
25
+ |\.gitignore
26
+ |\.rubocop.yml
27
+ |\.travis.yml
28
+ )$/x
29
+ end
30
+ s.require_paths = ["lib"]
31
+
32
+ s.add_development_dependency 'rubocop', '>= 0.30'
33
+ s.add_development_dependency 'rspec', '>= 3'
34
+ s.add_development_dependency 'rspec-its', '~> 1'
35
+ s.add_development_dependency 'simplecov', '~> 0.9'
36
+ s.add_development_dependency 'rake'
37
+ s.add_development_dependency 'rubygems-tasks'
38
+ s.add_development_dependency 'yard'
39
+ end