time_calc 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 277cc038ff0318d9b8aacb51c60ae7fda0730e24e913f89fd145dc4abb8420c4
4
- data.tar.gz: 233d29d03a7d4be6bf6656ca567b16066096af9c4a47f102dc9aafdd52691759
3
+ metadata.gz: 3222784ca4e6f520d4402ce84f5ea6f8a9becccb66e3640071d0a29b332297b7
4
+ data.tar.gz: 9746e03e5a3e560a5f63780e87ea75f87597faa44e594228f075ae5daca3a83b
5
5
  SHA512:
6
- metadata.gz: 1977f5b4e572a304ef4ccb440c1b7ecc16cfe922d7e34f1163b85507cc4143fcfb8607c1a5f8b66b677986b6926af97cbd548560f0c4c056aabcb6e9faa34de1
7
- data.tar.gz: 0f53ed256fa451adb9f30ccf256ac957c5370f0d0cbeba8d8dbe648066051ccbcea3ca45475a0a1234cd092014dd0059a14a21aa088f416b94c4efa341ad9295
6
+ metadata.gz: a6f3315fe5e4bc9334cbee7f089f50aebf7dfa854a6580f7cc53488afa0c8cb9ef0601f0d47c691b8427cbeea4dcc2842c97022666009e3b01b8e4056883b67c
7
+ data.tar.gz: d0ccd1540d9010f5c4cec6c3fb4708b75c3fc795cbe8913f7a7e43455caa6bf209f6d39ca828dd83bae8d73f4a7e8b7a0a7a432eaeaaec33442e6ef1afe5716d
data/Changelog.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # TimeCalc changelog
2
2
 
3
+ ## 0.0.3 / 2019-12-14
4
+
5
+ * Add `TimeCalc#iterate` to easily operate in "business date/time" contexts.
6
+
3
7
  ## 0.0.2 / 2019-07-08
4
8
 
5
9
  * Alias `TimeCalc[tm]` for those who disapporve on `TimeCalc.(tm)`;
6
- * More accurate zone infor preservation when time is in local timezone of current machine.
10
+ * More accurate zone info preservation when time is in local timezone of current machine.
7
11
 
8
12
  ## 0.0.1 / 2019-07-05
9
13
 
data/README.md CHANGED
@@ -67,6 +67,29 @@ TimeCalc.(t).+(3, :months) # jump over DST: we have +3 in summer and +2 in winte
67
67
  ```
68
68
  <small>(Random fun fact: it is Kyiv, not Kiev!)</small>
69
69
 
70
+ ### Math with skipping "non-business time"
71
+
72
+ [TimeCalc#iterate](https://www.rubydoc.info/gems/time_calc/TimeCalc#iterate-instance_method) allows to advance or decrease time values by skipping some of them (like weekends, holidays, and non-working hours):
73
+
74
+ ```ruby
75
+ # add 10 working days (weekends are not counted)
76
+ TimeCalc.(Time.parse('2019-07-03 23:28:54')).iterate(10, :days) { |t| (1..5).cover?(t.wday) }
77
+ # => 2019-07-17 23:28:54 +0300
78
+
79
+ # add 12 working hours
80
+ TimeCalc.(Time.parse('2019-07-03 13:28:54')).iterate(12, :hours) { |t| (9...18).cover?(t.hour) }
81
+ # => 2019-07-04 16:28:54 +0300
82
+
83
+ # negative spans are working, too:
84
+ TimeCalc.(Time.parse('2019-07-03 13:28:54')).iterate(-12, :hours) { |t| (9...18).cover?(t.hour) }
85
+ # => 2019-07-02 10:28:54 +0300
86
+
87
+ # zero span could be used to robustly enforce value into acceptable range
88
+ # (increasing forward till block is true):
89
+ TimeCalc.(Time.parse('2019-07-03 23:28:54')).iterate(0, :hours) { |t| (9...18).cover?(t.hour) }
90
+ # => 2019-07-04 09:28:54 +0300
91
+ ```
92
+
70
93
  ### Difference of two values
71
94
 
72
95
  ```ruby
@@ -118,7 +141,7 @@ TBH, using the library myself only eventually, I have never been too happy with
118
141
  # "Formalized": now - 2 days
119
142
 
120
143
  # ActiveSupport:
121
- Time.now + 2.days
144
+ Time.now - 2.days
122
145
  # also there is 2.days.ago, but I am not a big fan of "1000 synonyms just for naturality"
123
146
 
124
147
  # TimeMath:
@@ -146,4 +169,4 @@ The rest of the design (see examples above) just followed naturally. There could
146
169
  ## Author & license
147
170
 
148
171
  * [Victor Shepelev](https://zverok.github.io)
149
- * [MIT](https://github.com/zverok/time_calc/blob/master/LICENSE.txt).
172
+ * [MIT](https://github.com/zverok/time_calc/blob/master/LICENSE.txt).
@@ -285,7 +285,7 @@ class TimeCalc
285
285
  # Will coerce Date to Time or DateTime, with the _zone of the latter_
286
286
  def coerce_date(date, other)
287
287
  TimeCalc.(other)
288
- .merge(Units::DEFAULTS.merge(year: date.year, month: date.month, day: date.day))
288
+ .merge(**Units::DEFAULTS.merge(year: date.year, month: date.month, day: date.day))
289
289
  end
290
290
  end
291
291
  end
data/lib/time_calc/op.rb CHANGED
@@ -22,17 +22,21 @@ class TimeCalc
22
22
 
23
23
  # @private
24
24
  def inspect
25
- '<%s %s>' % [self.class, @chain.map { |name, *args| "#{name}(#{args.join(' ')})" }.join('.')]
25
+ '<%s %s>' % [self.class, @chain.map { |name, args, _| "#{name}(#{args.join(' ')})" }.join('.')]
26
26
  end
27
27
 
28
28
  TimeCalc::MATH_OPERATIONS.each do |name|
29
- define_method(name) { |*args| Op.new([*@chain, [name, *args]]) }
29
+ define_method(name) { |*args, &block| Op.new([*@chain, [name, args, block].compact]) }
30
30
  end
31
31
 
32
32
  # @!method +(span, unit)
33
33
  # Adds `+(span, unit)` to method chain
34
34
  # @see TimeCalc#+
35
35
  # @return [Op]
36
+ # @!method iterate(span, unit, &block)
37
+ # Adds `iterate(span, unit, &block)` to method chain
38
+ # @see TimeCalc#iterate
39
+ # @return [Op]
36
40
  # @!method -(span, unit)
37
41
  # Adds `-(span, unit)` to method chain
38
42
  # @see TimeCalc#-
@@ -55,8 +59,8 @@ class TimeCalc
55
59
  # @param date_or_time [Date, Time, DateTime]
56
60
  # @return [Date, Time, DateTime] Type of the result is always the same as type of the parameter
57
61
  def call(date_or_time)
58
- @chain.reduce(Value.new(date_or_time)) { |val, (name, *args)|
59
- val.public_send(name, *args)
62
+ @chain.reduce(Value.new(date_or_time)) { |val, (name, args, block)|
63
+ val.public_send(name, *args, &block)
60
64
  }.unwrap
61
65
  end
62
66
 
@@ -7,6 +7,27 @@ require 'backports/2.6.0/kernel/then'
7
7
  require 'backports/2.5.0/hash/slice'
8
8
  require 'backports/2.5.0/enumerable/all'
9
9
 
10
+ if RUBY_VERSION < '2.7'
11
+ # @private
12
+ # TODO: Replace with backports after 2.7 release
13
+ class Enumerator
14
+ NOVALUE = Object.new.freeze
15
+
16
+ def self.produce(initial = NOVALUE)
17
+ fail ArgumentError, 'No block given' unless block_given?
18
+
19
+ Enumerator.new do |y|
20
+ val = initial == NOVALUE ? yield() : initial
21
+
22
+ loop do
23
+ y << val
24
+ val = yield(val)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
10
31
  class TimeCalc
11
32
  # Wrapper (one can say "monad") around date/time value, allowing to perform several TimeCalc
12
33
  # operations in a chain.
@@ -99,7 +120,7 @@ class TimeCalc
99
120
  .drop_while { |u| u != unit }
100
121
  .drop(1)
101
122
  .then { |keys| Units::DEFAULTS.slice(*keys) }
102
- .then(&method(:merge))
123
+ .then { |attrs| merge(**attrs) } # can't simplify to &method(:merge) due to 2.7 keyword param problem
103
124
  end
104
125
 
105
126
  alias floor truncate
@@ -163,6 +184,28 @@ class TimeCalc
163
184
  unit.nil? ? Diff.new(self, span_or_other) : self.+(-span_or_other, unit)
164
185
  end
165
186
 
187
+ # Like {#+}, but allows conditional skipping of some periods. Increases value by `unit`
188
+ # at least `span` times, on each iteration checking with block provided if this point
189
+ # matches desired period; if it is not, it is skipped without increasing iterations
190
+ # counter. Useful for "business date/time" algorithms.
191
+ #
192
+ # See {TimeCalc#iterate} for examples.
193
+ #
194
+ # @param span [Integer]
195
+ # @param unit [Symbol]
196
+ # @return [Value]
197
+ # @yield [Time/Date/DateTime] Object of wrapped class
198
+ # @yieldreturn [true, false] If this point in time is "suitable". If the falsey value is returned,
199
+ # iteration is skipped without increasing the counter.
200
+ def iterate(span, unit)
201
+ block_given? or fail ArgumentError, 'No block given'
202
+ Integer === span or fail ArgumentError, 'Only integer spans are supported' # rubocop:disable Style/CaseEquality
203
+
204
+ Enumerator.produce(self) { |v| v.+((span <=> 0).nonzero? || 1, unit) }
205
+ .lazy.select { |v| yield(v.internal) }
206
+ .drop(span.abs).first
207
+ end
208
+
166
209
  # Produces {Sequence} from this value to `date_or_time`
167
210
  #
168
211
  # @param date_or_time [Date, Time, DateTime]
@@ -2,5 +2,5 @@
2
2
 
3
3
  class TimeCalc
4
4
  # @private
5
- VERSION = '0.0.2'
5
+ VERSION = '0.0.3'
6
6
  end
data/lib/time_calc.rb CHANGED
@@ -190,6 +190,37 @@ class TimeCalc
190
190
  # @param unit [Symbol]
191
191
  # @return [Date, Time, DateTime] value of the same type that was initial wrapped value.
192
192
 
193
+ # @!method iterate(span, unit)
194
+ # Like {#+}, but allows conditional skipping of some periods. Increases value by `unit`
195
+ # at least `span` times, on each iteration checking with block provided if this point
196
+ # matches desired period; if it is not, it is skipped without increasing iterations
197
+ # counter. Useful for "business date/time" algorithms.
198
+ #
199
+ # @example
200
+ # # add 10 working days.
201
+ # TimeCalc.(Time.parse('2019-07-03 23:28:54')).iterate(10, :days) { |t| (1..5).cover?(t.wday) }
202
+ # # => 2019-07-17 23:28:54 +0300
203
+ #
204
+ # # add 12 working hours
205
+ # TimeCalc.(Time.parse('2019-07-03 13:28:54')).iterate(12, :hours) { |t| (9...18).cover?(t.hour) }
206
+ # # => 2019-07-04 16:28:54 +0300
207
+ #
208
+ # # negative spans are working, too:
209
+ # TimeCalc.(Time.parse('2019-07-03 13:28:54')).iterate(-12, :hours) { |t| (9...18).cover?(t.hour) }
210
+ # # => 2019-07-02 10:28:54 +0300
211
+ #
212
+ # # zero span could be used to robustly enforce value into acceptable range
213
+ # # (increasing forward till block is true):
214
+ # TimeCalc.(Time.parse('2019-07-03 23:28:54')).iterate(0, :hours) { |t| (9...18).cover?(t.hour) }
215
+ # # => 2019-07-04 09:28:54 +0300
216
+ #
217
+ # @param span [Integer] Could be positive or negative
218
+ # @param unit [Symbol]
219
+ # @return [Date, Time, DateTime] value of the same type that was initial wrapped value.
220
+ # @yield [Time/Date/DateTime] Object of wrapped class
221
+ # @yieldreturn [true, false] If this point in time is "suitable". If the falsey value is returned,
222
+ # iteration is skipped without increasing the counter.
223
+
193
224
  # @!method -(span_or_other, unit=nil)
194
225
  # @overload -(span, unit)
195
226
  # Subtracts `span units` from wrapped value.
@@ -238,25 +269,39 @@ class TimeCalc
238
269
  # @return [Sequence]
239
270
 
240
271
  # @private
241
- MATH_OPERATIONS = %i[merge truncate floor ceil round + -].freeze
272
+ MATH_OPERATIONS = %i[merge truncate floor ceil round + - iterate].freeze
242
273
  # @private
243
274
  OPERATIONS = MATH_OPERATIONS.+(%i[to step for]).freeze
244
275
 
245
276
  OPERATIONS.each do |name|
246
- define_method(name) { |*args|
247
- @value.public_send(name, *args).then { |res| res.is_a?(Value) ? res.unwrap : res }
248
- }
277
+ # https://bugs.ruby-lang.org/issues/16421 :shrug:
278
+ # FIXME: In fact, the only kwargs op seem to be merge(...). If the problem is unsolvable,
279
+ # it is easier to define it separately.
280
+ if RUBY_VERSION < '2.7'
281
+ define_method(name) { |*args, &block|
282
+ @value.public_send(name, *args, &block)
283
+ .then { |res| res.is_a?(Value) ? res.unwrap : res }
284
+ }
285
+ else
286
+ define_method(name) { |*args, **kwargs, &block|
287
+ @value.public_send(name, *args, **kwargs, &block)
288
+ .then { |res| res.is_a?(Value) ? res.unwrap : res }
289
+ }
290
+ end
249
291
  end
250
292
 
251
293
  class << self
252
294
  MATH_OPERATIONS.each do |name|
253
- define_method(name) { |*args| Op.new([[name, *args]]) }
295
+ define_method(name) { |*args, &block| Op.new([[name, args, block].compact]) }
254
296
  end
255
297
 
256
298
  # @!parse
257
299
  # # Creates operation to perform {#+}`(span, unit)`
258
300
  # # @return [Op]
259
301
  # def TimeCalc.+(span, unit); end
302
+ # # Creates operation to perform {#iterate}`(span, unit, &block)`
303
+ # # @return [Op]
304
+ # def TimeCalc.iterate(span, unit, &block); end
260
305
  # # Creates operation to perform {#-}`(span, unit)`
261
306
  # # @return [Op]
262
307
  # def TimeCalc.-(span, unit); end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: time_calc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Shepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-08 00:00:00.000000000 Z
11
+ date: 2019-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backports
@@ -30,28 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.72.0
33
+ version: 0.77.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.72.0
40
+ version: 0.77.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rubocop-rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.17.1
47
+ version: 1.37.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.17.1
54
+ version: 1.37.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: 0.0.6
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '0'
96
+ version: 0.0.6
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: simplecov
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -188,7 +188,12 @@ files:
188
188
  homepage: https://github.com/zverok/time_calc
189
189
  licenses:
190
190
  - MIT
191
- metadata: {}
191
+ metadata:
192
+ bug_tracker_uri: https://github.com/zverok/time_calc/issues
193
+ changelog_uri: https://github.com/zverok/time_calc/blob/master/Changelog.md
194
+ documentation_uri: https://www.rubydoc.info/gems/time_calc/
195
+ homepage_uri: https://github.com/zverok/time_calc
196
+ source_code_uri: https://github.com/zverok/time_calc
192
197
  post_install_message:
193
198
  rdoc_options: []
194
199
  require_paths: