time_calc 0.0.2 → 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.
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: