time_calc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: '079469c094dfa9462e10854c885d962aeb8f047c'
4
+ data.tar.gz: c7f5d1d7d6f665b490ec48bb2d85a4830a030761
5
+ SHA512:
6
+ metadata.gz: ba7158a430d4968675210fe5e981d203178d59aca4c2aadfd0de26fa856fff23740aa4b6280b9ce67d6e507f5c78ca8a1ee519cd0d74ffb5a6678fecd66209fd
7
+ data.tar.gz: c874789108c36d802ea023a7ea6dbb0956d4f835a05ca2318232f0cbddee5b3f942ded7f37d1373eec191ad7ab6b09aaca5df1a354db204e83b7a6a22d345bd7
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Victor 'Zverok' Shepelev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # TimeCalc -- next generation of Time arithmetic library
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/time_calc.svg)](http://badge.fury.io/rb/time_calc)
4
+ [![Build Status](https://travis-ci.org/zverok/time_calc.svg?branch=master)](https://travis-ci.org/zverok/time_calc)
5
+ [![Documentation](http://b.repl.ca/v1/yard-docs-blue.png)](http://rubydoc.info/gems/time_calc/frames)
6
+
7
+ **TimeCalc** tries to provide a way to do **simple time arithmetic** in a modern, readable, idiomatic, no-"magic" Ruby.
8
+
9
+ _**NB:** TimeCalc is a continuation of [TimeMath](https://github.com/zverok/time_math2) project. As I decided to change API significantly (completely, in fact) and drop a lot of "nice to have but nobody uses" features, it is a new project rather than "absolutely incompatible new version". See [API design](#api-design) section to understand how and why TimeCalc is different._
10
+
11
+ ## Features
12
+
13
+ * Small, clean, pure-Ruby, idiomatic, no monkey-patching, no dependencies (except `backports`);
14
+ * Arithmetic akin to what Ruby numbers provide: `+`/`-`, `floor`/`ceil`/`round`, enumerable sequences (`step`/`to`);
15
+ * Works with `Time`, `Date` and `DateTime` and allows to mix them freely (e.g. create sequences from `Date` to `Time`, calculate their diffs);
16
+ * Tries its best to preserve timezone/offset information:
17
+ * **on Ruby 2.6+**, for `Time` with real timezones, preserves them;
18
+ * on Ruby < 2.6, preserves at least `utc_offset` of `Time`;
19
+ * for `DateTime` preserves zone name.
20
+
21
+ ## Synopsis
22
+
23
+ ### Arithmetic with units
24
+
25
+ ```ruby
26
+ require 'time_calc'
27
+
28
+ TC = TimeCalc
29
+
30
+ t = Time.parse('2019-03-14 08:06:15')
31
+
32
+ TC.(t).+(3, :hours)
33
+ # => 2019-03-14 11:06:15 +0200
34
+ TC.(t).round(:week)
35
+ # => 2019-03-11 00:00:00 +0200
36
+
37
+ # TimeCalc.call(Time.now) shortcut:
38
+ TC.now.floor(:day)
39
+ # => beginning of the today
40
+ ```
41
+
42
+ Operations supported:
43
+
44
+ * `+`, `-`
45
+ * `ceil`, `round`, `floor`
46
+
47
+ Units supported:
48
+
49
+ * `:sec` (also `:second`, `:seconds`);
50
+ * `:min` (`:minute`, `:minutes`);
51
+ * `:hour`/`:hours`;
52
+ * `:day`/`:days`;
53
+ * `:week`/`:weeks`;
54
+ * `:month`/`:months`;
55
+ * `:year`/`:years`.
56
+
57
+ Timezone preservation on Ruby 2.6:
58
+
59
+ ```ruby
60
+ require 'tzinfo'
61
+ t = Time.new(2019, 9, 1, 14, 30, 12, TZInfo::Timezone.get('Europe/Kiev'))
62
+ # => 2019-09-01 14:30:12 +0300
63
+ # ^^^^^
64
+ TimeCalc.(t).+(3, :months) # jump over DST: we have +3 in summer and +2 in winter
65
+ # => 2019-12-01 14:30:12 +0200
66
+ # ^^^^^
67
+ ```
68
+ <small>(Random fun fact: it is Kyiv, not Kiev!)</small>
69
+
70
+ ### Difference of two values
71
+
72
+ ```ruby
73
+ diff = TC.(t) - Time.parse('2019-02-30 16:30')
74
+ # => #<TimeCalc::Diff(2019-03-14 08:06:15 +0200 − 2019-03-02 16:30:00 +0200)>
75
+ diff.days # or any other supported unit
76
+ # => 11
77
+ diff.factorize
78
+ # => {:year=>0, :month=>0, :week=>1, :day=>4, :hour=>15, :min=>36, :sec=>15}
79
+ ```
80
+
81
+ There are several options to [Diff#factorize](https://www.rubydoc.info/gems/time_calc/TimeCalc/Diff#factorize-instance_method) to obtain the most useful result.
82
+
83
+ ### Chains of operations
84
+
85
+ ```ruby
86
+ TC.wrap(t).+(1, :hour).round(:min).unwrap
87
+ # => 2019-03-14 09:06:00 +0200
88
+
89
+ # proc constructor synopsys:
90
+ times = ['2019-06-01 14:30', '2019-06-05 17:10', '2019-07-02 13:40'].map { |t| Time.parse(t) }
91
+ times.map(&TC.+(1, :hour).round(:min))
92
+ # => [2019-06-01 15:30:00 +0300, 2019-06-05 18:10:00 +0300, 2019-07-02 14:40:00 +0300]
93
+ ```
94
+
95
+ ### Enumerable time sequences
96
+
97
+ ```ruby
98
+ TC.(t).step(2, :weeks)
99
+ # => #<TimeCalc::Sequence (2019-03-14 08:06:15 +0200 - ...):step(2 weeks)>
100
+ TC.(t).step(2, :weeks).first(3)
101
+ # => [2019-03-14 08:06:15 +0200, 2019-03-28 08:06:15 +0200, 2019-04-11 09:06:15 +0300]
102
+ TC.(t).to(Time.parse('2019-04-30 16:30')).step(3, :weeks).to_a
103
+ # => [2019-03-14 08:06:15 +0200, 2019-04-04 09:06:15 +0300, 2019-04-25 09:06:15 +0300]
104
+ TC.(t).for(3, :months).step(4, :weeks).to_a
105
+ # => [2019-03-14 08:06:15 +0200, 2019-04-11 09:06:15 +0300, 2019-05-09 09:06:15 +0300, 2019-06-06 09:06:15 +0300]
106
+ ```
107
+
108
+ ## API design
109
+
110
+ The idea of this library (as well as the idea of the previous one) grew of the simple question "how do you say `<some time> + 1 hour` in good Ruby?" This question also leads (me) to notifying that other arithmetical operations (like rounding, or `<value> up to <value> with step <value>`) seem to be applicable to `Time` or `Date` values as well.
111
+
112
+ Prominent ActiveSupport's answer of extending simple numbers to respond to `1.year` never felt totally right to me. I am not completely against-any-monkey-patches kind of guy, it just doesn't sit right, to say "number has a method to produce duration". One of the attempts to find an alternative has led me to the creation of [time_math2](https://github.com/zverok/time_math2), which gained some (modest) popularity by presenting things this way: `TimeMath.year.advance(time, 1)`.
113
+
114
+ TBH, using the library myself only eventually, I have never been too happy with it: it never felt really natural, so I constantly forgot "what should I do to calculate '2 days ago'". This simplest use case (some time from now) in `TimeMath` looked too far from "how you pronounce it":
115
+
116
+ ```ruby
117
+ # Natural language: 2 days ago
118
+ # "Formalized": now - 2 days
119
+
120
+ # ActiveSupport:
121
+ Time.now + 2.days
122
+ # also there is 2.days.ago, but I am not a big fan of "1000 synonyms just for naturality"
123
+
124
+ # TimeMath:
125
+ TimMath.day.decrease(Time.now, 2) # Ughhh what? "Day decrease now 2"?
126
+ ```
127
+
128
+ The thought process that led to the new library is:
129
+
130
+ * `(2, days)` is just a _tuple_ of two unrelated data elements
131
+ * `days` is "internal name that makes sense inside the code", which we represent by `Symbol` in Ruby
132
+ * Math operators can be called just like regular methods: `.+(something)`, which may look unusual at first, but can be super-handy even with simple numbers, in method chaining -- I am grateful to my Verbit's colleague Roman Yarovoy to pointing at that fact (or rather its usefulness);
133
+ * To chain some calculations with Ruby core type without extending this type, we can just "wrap" it into a monad-like object, do the calculations, and unwrap at the end (TimeMath itself, and my Hash-processing gem [hm](https://github.com/zverok/hm) have used this approach).
134
+
135
+ So, here we go:
136
+ ```ruby
137
+ TimeCalc.(Time.now).-(2, :days)
138
+ # Small shortcut, as `Time.now` is the frequent start value for such calculations:
139
+ TimeCalc.now.-(2, :days)
140
+ ```
141
+
142
+ The rest of the design (see examples above) just followed naturally. There could be different opinions on the approach, but for myself the resulting API looks straightforward, hard to forget and very regular (in fact, all the hard time calculations, including support for different types, zones, DST and stuff, are done in two core methods, and the rest was easy to define in terms of those methods, which is a sign of consistency).
143
+
144
+ ¯\\\_(ツ)_/¯
145
+
146
+ ## Author & license
147
+
148
+ * [Victor Shepelev](https://zverok.github.io)
149
+ * [MIT](https://github.com/zverok/time_calc/blob/master/LICENSE.txt).
data/lib/time_calc.rb ADDED
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'time'
5
+
6
+ require_relative 'time_calc/units'
7
+ require_relative 'time_calc/types'
8
+ require_relative 'time_calc/dst'
9
+ require_relative 'time_calc/value'
10
+
11
+ # Module for time arithmetic.
12
+ #
13
+ # Examples of usage:
14
+ #
15
+ # ```ruby
16
+ # TimeCalc.(Time.now).+(1, :day)
17
+ # # => 2019-07-04 23:28:54 +0300
18
+ # TimeCalc.(Time.now).round(:hour)
19
+ # # => 2019-07-03 23:00:00 +0300
20
+ #
21
+ # # Operations with Time.now and Date.today also have their shortcuts:
22
+ # TimeCalc.now.-(3, :days)
23
+ # # => 2019-06-30 23:28:54 +0300
24
+ # TimeCalc.today.ceil(:month)
25
+ # # => #<Date: 2019-08-01 ((2458697j,0s,0n),+0s,2299161j)>
26
+ #
27
+ # # If you need to perform several operations TimeCalc.from wraps your value:
28
+ # TimeCalc.from(Time.parse('2019-06-14 13:40')).+(10, :days).floor(:week).unwrap
29
+ # # => 2019-06-24 00:00:00 +0300
30
+ #
31
+ # # TimeCalc#- also can be used to calculate difference between time values
32
+ # diff = TimeCalc.(Time.parse('2019-07-03 23:32')) - Time.parse('2019-06-14 13:40')
33
+ # # => #<TimeCalc::Diff(2019-07-03 23:32:00 +0300 − 2019-06-14 13:40:00 +0300)>
34
+ # diff.days # => 19
35
+ # diff.hours # => 465
36
+ # diff.factorize
37
+ # # => {:year=>0, :month=>0, :week=>2, :day=>5, :hour=>9, :min=>52, :sec=>0}
38
+ # diff.factorize(max: :day)
39
+ # # => {:day=>19, :hour=>9, :min=>52, :sec=>0}
40
+ #
41
+ # # Enumerable sequences of time values
42
+ # sequence = TimeCalc.(Time.parse('2019-06-14 13:40'))
43
+ # .to(Time.parse('2019-07-03 23:32'))
44
+ # .step(5, :hours)
45
+ # # => #<TimeCalc::Sequence (2019-06-14 13:40:00 +0300 - 2019-07-03 23:32:00 +0300):step(5 hours)>
46
+ # sequence.to_a
47
+ # # => [2019-06-14 13:40:00 +0300, 2019-06-14 18:40:00 +0300, 2019-06-14 23:40:00 +0300, ...
48
+ # sequence.first(2)
49
+ # # => [2019-06-14 13:40:00 +0300, 2019-06-14 18:40:00 +0300]
50
+ #
51
+ # # Construct operations to apply as a proc:
52
+ # times = ['2019-06-01 14:30', '2019-06-05 17:10', '2019-07-02 13:40'].map { |t| Time.parse(t) }
53
+ # # => [2019-06-01 14:30:00 +0300, 2019-06-05 17:10:00 +0300, 2019-07-02 13:40:00 +0300]
54
+ # times.map(&TimeCalc.+(1, :week).round(:day))
55
+ # # => [2019-06-09 00:00:00 +0300, 2019-06-13 00:00:00 +0300, 2019-07-10 00:00:00 +0300]
56
+ # ```
57
+ #
58
+ # See method docs below for details and supported arguments.
59
+ #
60
+ class TimeCalc
61
+ class << self
62
+ alias call new
63
+
64
+ # Shortcut for `TimeCalc.(Time.now)`
65
+ # @return [TimeCalc]
66
+ def now
67
+ new(Time.now)
68
+ end
69
+
70
+ # Shortcut for `TimeCalc.(Date.today)`
71
+ # @return [TimeCalc]
72
+ def today
73
+ new(Date.today)
74
+ end
75
+
76
+ # Returns {Value} wrapper, useful for performing several operations at once:
77
+ #
78
+ # ```ruby
79
+ # TimeCalc.from(Time.parse('2019-06-14 13:40')).+(10, :days).floor(:week).unwrap
80
+ # # => 2019-06-24 00:00:00 +0300
81
+ # ```
82
+ #
83
+ # @param date_or_time [Time, Date, DateTime]
84
+ # @return [Value]
85
+ def from(date_or_time)
86
+ Value.new(date_or_time)
87
+ end
88
+
89
+ # Shortcut for `TimeCalc.from(Time.now)`
90
+ # @return [Value]
91
+ def from_now
92
+ from(Time.now)
93
+ end
94
+
95
+ # Shortcut for `TimeCalc.from(Date.today)`
96
+ # @return [Value]
97
+ def from_today
98
+ from(Date.today)
99
+ end
100
+
101
+ alias wrap from
102
+ alias wrap_now from_now
103
+ alias wrap_today from_today
104
+ end
105
+
106
+ # @private
107
+ attr_reader :value
108
+
109
+ # Creates a "temporary" wrapper, which would be unwrapped after first operation:
110
+ #
111
+ # ```ruby
112
+ # TimeCalc.new(Time.now).round(:hour)
113
+ # # => 2019-07-03 23:00:00 +0300
114
+ # ```
115
+ #
116
+ # The constructor also aliased as `.call` which allows for nicer (for some eyes) code:
117
+ #
118
+ # ```ruby
119
+ # TimeCalc.(Time.now).round(:hour)
120
+ # # => 2019-07-03 23:00:00 +0300
121
+ # ```
122
+ #
123
+ # See {.from} if you need to perform several math operations on same value.
124
+ #
125
+ # @param date_or_time [Time, Date, DateTime]
126
+ def initialize(date_or_time)
127
+ @value = Value.new(date_or_time)
128
+ end
129
+
130
+ # @private
131
+ def inspect
132
+ '#<%s(%s)>' % [self.class, @value.unwrap]
133
+ end
134
+
135
+ # @return [true,false]
136
+ def ==(other)
137
+ other.is_a?(self.class) && other.value == value
138
+ end
139
+
140
+ # @!method merge(**attrs)
141
+ # Replaces specified components of date/time, preserves the rest.
142
+ #
143
+ # @example
144
+ # TimeCalc.(Date.parse('2018-06-01')).merge(year: 1983)
145
+ # # => #<Date: 1983-06-01>
146
+ #
147
+ # @param attrs [Hash<Symbol => Integer>]
148
+ # @return [Time, Date, DateTime] value of the same type that was initial wrapped value.
149
+
150
+ # @!method floor(unit)
151
+ # Floors (rounds down) date/time to nearest `unit`.
152
+ #
153
+ # @example
154
+ # TimeCalc.(Time.parse('2018-06-23 12:30')).floor(:month)
155
+ # # => 2018-06-01 00:00:00 +0300
156
+ #
157
+ # @param unit [Symbol]
158
+ # @return [Time, Date, DateTime] value of the same type that was initial wrapped value.
159
+
160
+ # @!method ceil(unit)
161
+ # Ceils (rounds up) date/time to nearest `unit`.
162
+ #
163
+ # @example
164
+ # TimeCalc.(Time.parse('2018-06-23 12:30')).ceil(:month)
165
+ # # => 2018-07-01 00:00:00 +0300
166
+ #
167
+ # @param unit [Symbol]
168
+ # @return [Time, Date, DateTime] value of the same type that was initial wrapped value.
169
+
170
+ # @!method round(unit)
171
+ # Rounds (up or down) date/time to nearest `unit`.
172
+ #
173
+ # @example
174
+ # TimeCalc.(Time.parse('2018-06-23 12:30')).round(:month)
175
+ # # => 2018-07-01 00:00:00 +0300
176
+ #
177
+ # @param unit [Symbol]
178
+ # @return [Time, Date, DateTime] value of the same type that was initial wrapped value.
179
+
180
+ # @!method +(span, unit)
181
+ # Add `<span units>` to wrapped value
182
+ # @example
183
+ # TimeCalc.(Time.parse('2019-07-03 23:28:54')).+(1, :day)
184
+ # # => 2019-07-04 23:28:54 +0300
185
+ # @param span [Integer]
186
+ # @param unit [Symbol]
187
+ # @return [Date, Time, DateTime] value of the same type that was initial wrapped value.
188
+
189
+ # @!method -(span_or_other, unit=nil)
190
+ # @overload -(span, unit)
191
+ # Subtracts `span units` from wrapped value.
192
+ # @param span [Integer]
193
+ # @param unit [Symbol]
194
+ # @return [Date, Time, DateTime] value of the same type that was initial wrapped value.
195
+ # @overload -(date_or_time)
196
+ # Produces {Diff}, allowing to calculate structured difference between two points in time.
197
+ # @example
198
+ # t1 = Time.parse('2019-06-01 14:50')
199
+ # t2 = Time.parse('2019-06-15 12:10')
200
+ # (TimeCalc.(t2) - t1).days
201
+ # # => 13
202
+ # @param date_or_time [Date, Time, DateTime]
203
+ # @return [Diff]
204
+ # @return [Time or Diff]
205
+
206
+ # @!method to(date_or_time)
207
+ # Produces {Sequence} from this value to `date_or_time`
208
+ #
209
+ # @param date_or_time [Date, Time, DateTime]
210
+ # @return [Sequence]
211
+
212
+ # @!method step(span, unit = nil)
213
+ # Produces endless {Sequence} from this value, with step specified.
214
+ #
215
+ # @overload step(unit)
216
+ # Shortcut for `step(1, unit)`
217
+ # @param unit [Symbol]
218
+ # @overload step(span, unit)
219
+ # @example
220
+ # TimeCalc.(Time.parse('2019-06-01 14:50')).step(1, :day).take(3)
221
+ # # => [2019-06-01 14:50:00 +0300, 2019-06-02 14:50:00 +0300, 2019-06-03 14:50:00 +0300]
222
+ # @param span [Integer]
223
+ # @param unit [Symbol]
224
+ # @return [Sequence]
225
+
226
+ # @!method for(span, unit)
227
+ # Produces {Sequence} from this value to `this + <span units>`
228
+ #
229
+ # @example
230
+ # TimeCalc.(Time.parse('2019-06-01 14:50')).for(2, :weeks).step(1, :day).count
231
+ # # => 15
232
+ # @param span [Integer]
233
+ # @param unit [Symbol]
234
+ # @return [Sequence]
235
+
236
+ # @private
237
+ MATH_OPERATIONS = %i[merge truncate floor ceil round + -].freeze
238
+ # @private
239
+ OPERATIONS = MATH_OPERATIONS.+(%i[to step for]).freeze
240
+
241
+ OPERATIONS.each do |name|
242
+ define_method(name) { |*args|
243
+ @value.public_send(name, *args).then { |res| res.is_a?(Value) ? res.unwrap : res }
244
+ }
245
+ end
246
+
247
+ class << self
248
+ MATH_OPERATIONS.each do |name|
249
+ define_method(name) { |*args| Op.new([[name, *args]]) }
250
+ end
251
+
252
+ # @!parse
253
+ # # Creates operation to perform {#+}`(span, unit)`
254
+ # # @return [Op]
255
+ # def TimeCalc.+(span, unit); end
256
+ # # Creates operation to perform {#-}`(span, unit)`
257
+ # # @return [Op]
258
+ # def TimeCalc.-(span, unit); end
259
+ # # Creates operation to perform {#floor}`(unit)`
260
+ # # @return [Op]
261
+ # def TimeCalc.floor(unit); end
262
+ # # Creates operation to perform {#ceil}`(unit)`
263
+ # # @return [Op]
264
+ # def TimeCalc.ceil(unit); end
265
+ # # Creates operation to perform {#round}`(unit)`
266
+ # # @return [Op]
267
+ # def TimeCalc.round(unit); end
268
+ end
269
+ end
270
+
271
+ require_relative 'time_calc/op'
272
+ require_relative 'time_calc/sequence'
273
+ require_relative 'time_calc/diff'