monotime 0.6.1 → 0.7.0

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: '09910c8dfa9bdc7777e55f6eb606b49f8c6ca23d563e09ae8e5109b116b7781f'
4
- data.tar.gz: 5db6e948d04836e8523cbeaf92e4f599c5ed9238055f5fb79d94dfcab1969a19
3
+ metadata.gz: c88bca715126cabf9bf5ea1af377e16d43da5ecc06ee45f98dad584360eaa15b
4
+ data.tar.gz: 8f741d6219cfe4b9050f892f2df905ed603f859e4a79f930a4eb954c213bf639
5
5
  SHA512:
6
- metadata.gz: '087abc138dd09dd52d5b3adb5bc6ad276e0ad5936e9aa381ce43dbf12a62dfc1f65449b0d36454a78527842d70ca3ab7752d94ffea7cecbf9f05b85917c11d95'
7
- data.tar.gz: e2b45aaef34322a3ef71fdf7317af1a363a5e7b1f3e86d6b8c65df8b444dcfa03d210a11ee7adb051cd22794776b02e984781fb69db5183fcd76659683dbee1b
6
+ metadata.gz: 5ed67631407691f935ffdd30727c4054b3fa7f5964a9cdcb0db0f94e1a2aecddb83eac4b1e4a8e032cd3c57145152e6649ddf14093643329ff23b5d43348000f
7
+ data.tar.gz: f38b77609652866fa4c1b7bfee164977a7f7895feeb0f76d8dd5e71a05da31b0cda24c680f5b2aef19c3f5c7787da237f580ee56ccb86e456dae576fc95d0e89
@@ -1,6 +1,17 @@
1
+ AllCops:
2
+ Exclude:
3
+ - "bin/*"
4
+ - "test/*"
5
+ - "*.gemspec"
6
+ - "Rakefile"
7
+ - "Gemfile"
8
+
1
9
  Metrics/LineLength:
2
10
  Max: 96
3
11
 
12
+ Metrics/ClassLength:
13
+ Max: 120
14
+
4
15
  Style/AsciiComments:
5
16
  Enabled: false
6
17
 
@@ -3,6 +3,8 @@ sudo: false
3
3
  language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
- - 2.5.1
7
- - jruby
8
- before_install: gem install bundler -v 1.16.3
6
+ - 2.4.6
7
+ - 2.5.5
8
+ - 2.6.3
9
+ - jruby-9.2.7.0
10
+ before_install: gem install bundler -v "~> 2"
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## [0.6.1] - 2018-10-27
3
+ ## [0.7.0] - 2019-04-24
4
+ ### Added
5
+ - `Duration.with_measure`, which yields and returns an array containing its
6
+ evaluated return value and its `Duration`.
7
+
8
+ ### Changed
9
+ - Break `Duration` and `Instant` into their own files.
10
+ - Rename `Monotime::VERSION` to `Monotime::MONOTIME_VERSION` to reduce
11
+ potential for collision if the module is included.
12
+ - Update to bundler 2.0.
13
+ - Rework README.md. Includes fix for [issue #1] (added a "See Also" section).
14
+
15
+ ## [0.6.1] - 2018-10-26
4
16
  ### Fixed
5
17
  - Build gem from a clean git checkout, not my local development directory.
6
18
  No functional changes.
@@ -81,4 +93,6 @@
81
93
  [0.5.0]: https://github.com/Freaky/monotime/commits/v0.5.0
82
94
  [0.6.0]: https://github.com/Freaky/monotime/commits/v0.6.0
83
95
  [0.6.1]: https://github.com/Freaky/monotime/commits/v0.6.1
96
+ [0.7.0]: https://github.com/Freaky/monotime/commits/v0.7.0
97
+ [issue #1]: https://github.com/Freaky/monotime/issues/1
84
98
  [@celsworth]: https://github.com/celsworth
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/monotime.svg)](https://badge.fury.io/rb/monotime)
2
2
  [![Build Status](https://travis-ci.org/Freaky/monotime.svg?branch=master)](https://travis-ci.org/Freaky/monotime)
3
+ [![Inline docs](http://inch-ci.org/github/Freaky/monotime.svg?branch=master)](http://inch-ci.org/github/Freaky/monotime)
4
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/gems/monotime)
3
5
 
4
6
  # Monotime
5
7
 
@@ -21,21 +23,17 @@ Or install it yourself as:
21
23
 
22
24
  $ gem install monotime
23
25
 
24
- ## Usage
25
-
26
- The typical way everyone does "correct" elapsed-time measurements in Ruby is
27
- this pile of nonsense:
26
+ `Monotime` is tested on Ruby 2.4—2.6 and recent JRuby 9.x releases.
28
27
 
29
- ```ruby
30
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
31
- do_something
32
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
33
- ```
28
+ ## Usage
34
29
 
35
- Not only is it long-winded, it's imprecise, converting to floating point instead
36
- of working off precise timestamps.
30
+ `Monotime` offers a `Duration` type for describing spans of time, and an
31
+ `Instant` type for describing points in time. Both operate at nanosecond
32
+ resolution to the limits of whatever your Ruby implementation supports.
37
33
 
38
- `Monotime` offers this alternative:
34
+ For example, to measure an elapsed time, either create an `Instant` to mark the
35
+ start point, perform the action and then ask for the `Duration` that has elapsed
36
+ since:
39
37
 
40
38
  ```ruby
41
39
  include Monotime
@@ -43,44 +41,54 @@ include Monotime
43
41
  start = Instant.now
44
42
  do_something
45
43
  elapsed = start.elapsed
44
+ ```
46
45
 
47
- # or
46
+ Or use a convenience method:
47
+
48
+ ```ruby
48
49
  elapsed = Duration.measure { do_something }
50
+
51
+ # or
52
+
53
+ return_value, elapsed = Duration.with_measure { compute_something }
49
54
  ```
50
55
 
51
- `elapsed` is not a dimensionless `Float`, but a `Duration` type, and internally
52
- both `Instant` and `Duration` operate in *nanoseconds* to most closely match
53
- the native timekeeping types used by most operating systems.
56
+ `Duration` offers formatting:
57
+
58
+ ```ruby
59
+ Duration.millis(42).to_s # => "42ms"
60
+ Duration.nanos(12345).to_s # => "12.345μs"
61
+ Duration.secs(1.12345).to_s(2) # => "1.12s"
62
+ ```
54
63
 
55
- `Duration` knows how to format itself:
64
+ Conversions:
56
65
 
57
66
  ```ruby
58
- Duration.from_millis(42).to_s # => "42ms"
59
- Duration.from_nanos(12345).to_s # => "12.345μs"
60
- Duration.from_secs(1.12345).to_s(2) # => "1.12s"
67
+ Duration.secs(10).millis # => 10000.0
68
+ Duration.micros(12345).secs # => 0.012345
61
69
  ```
62
70
 
63
- And how to do basic maths on itself:
71
+ And basic mathematical operations:
64
72
 
65
73
  ```ruby
66
- (Duration.from_millis(42) + Duration.from_secs(1)).to_s # => "1.042s"
67
- (Duration.from_millis(42) - Duration.from_secs(1)).to_s # => "-958ms"
68
- (Duration.from_secs(42) * 2).to_s # => "84s"
69
- (Duration.from_secs(42) / 2).to_s # => "21s"
74
+ (Duration.millis(42) + Duration.secs(1)).to_s # => "1.042s"
75
+ (Duration.millis(42) - Duration.secs(1)).to_s # => "-958ms"
76
+ (Duration.secs(42) * 2).to_s # => "84s"
77
+ (Duration.secs(42) / 2).to_s # => "21s"
70
78
  ```
71
79
 
72
80
  `Instant` does some simple maths too:
73
81
 
74
82
  ```ruby
75
83
  # Instant - Duration => Instant
76
- (Instant.now - Duration.from_secs(1)).elapsed.to_s # => "1.000014627s"
84
+ (Instant.now - Duration.secs(1)).elapsed.to_s # => "1.000014627s"
77
85
 
78
86
  # Instant - Instant => Duration
79
87
  (Instant.now - Instant.now).to_s # => "-5.585μs"
80
88
  ```
81
89
 
82
90
  `Duration` and `Instant` are also `Comparable` with other instances of their
83
- type, and support `#hash` for use in, er, hashes.
91
+ type, and can be used in hashes, sets, and similar structures.
84
92
 
85
93
  ## Sleeping
86
94
 
@@ -89,9 +97,9 @@ is not yet implemented):
89
97
 
90
98
  ```ruby
91
99
  # Equivalent
92
- sleep(Duration.from_secs(1).to_secs) # => 1
100
+ sleep(Duration.secs(1).secs) # => 1
93
101
 
94
- Duration.from_secs(1).sleep # => 1
102
+ Duration.secs(1).sleep # => 1
95
103
  ```
96
104
 
97
105
  So can `Instant`, taking a `Duration` and sleeping until the given `Duration`
@@ -99,7 +107,7 @@ past the time the `Instant` was created, if any. This may be useful if you wish
99
107
  to maintain an approximate interval while performing work in between:
100
108
 
101
109
  ```ruby
102
- poke_duration = Duration.from_secs(60)
110
+ poke_duration = Duration.secs(60)
103
111
  loop do
104
112
  start = Instant.now
105
113
  poke_my_api(api_to_poke, what_to_poke_it_with)
@@ -111,13 +119,13 @@ end
111
119
  Or you can declare a future `Instant` and ask to sleep until it passes:
112
120
 
113
121
  ```ruby
114
- next_minute = Instant.now + Duration.from_secs(60)
122
+ next_minute = Instant.now + Duration.secs(60)
115
123
  do_stuff
116
124
  next_minute.sleep # => sleeps any remaining seconds
117
125
  ```
118
126
 
119
- `Instant#sleep` returns a `Duration` which was slept, or a negative `Duration` if
120
- the desired sleep period has passed.
127
+ `Instant#sleep` returns a `Duration` which was slept, or a negative `Duration`
128
+ if the desired sleep period has passed.
121
129
 
122
130
  ## Duration duck typing
123
131
 
@@ -133,8 +141,8 @@ class Numeric
133
141
  end
134
142
  end
135
143
 
136
- (Duration.from_secs(1) + 41).to_s # => "42s"
137
- (Instant.now - 42).to_s # => "42.000010545s"
144
+ (Duration.secs(1) + 41).to_s # => "42s"
145
+ (Instant.now - 42).to_s # => "42.000010545s"
138
146
  ```
139
147
 
140
148
  ## Development
@@ -150,3 +158,21 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Freaky
150
158
  ## License
151
159
 
152
160
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
161
+
162
+ ## See Also
163
+
164
+ ### Core Ruby
165
+
166
+ For a zero-dependency alternative, see
167
+ [`Process.clock_gettime`](https://ruby-doc.org/core-2.6.3/Process.html#method-c-clock_gettime).
168
+ `monotime` currently only uses `Process::CLOCK_MONOTONIC`, but others may offer higher precision
169
+ depending on platform.
170
+
171
+ ### Other Gems
172
+
173
+ [hitimes](https://rubygems.org/gems/hitimes) is a popular and mature alternative
174
+ which also includes a variety of features for gathering statistics about
175
+ measurements, and may offer higher precision on some platforms.
176
+
177
+ Note until [#73](https://github.com/copiousfreetime/hitimes/pull/73) is closed it
178
+ depends on compiled C/Java extensions.
@@ -1,469 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'monotime/version'
4
-
5
- module Monotime
6
- # A measurement from the operating system's monotonic clock, with up to
7
- # nanosecond precision.
8
- class Instant
9
- # A measurement, in nanoseconds. Should be considered opaque and
10
- # non-portable outside the process that created it.
11
- attr_reader :ns
12
- protected :ns
13
-
14
- include Comparable
15
-
16
- # Create a new +Instant+ from an optional nanosecond measurement.
17
- #
18
- # Users should generally *not* pass anything to this function.
19
- #
20
- # @param nanos [Integer]
21
- # @see #now
22
- def initialize(nanos = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond))
23
- @ns = Integer(nanos)
24
- end
25
-
26
- # An alias to +new+, and generally preferred over it.
27
- #
28
- # @return [Instant]
29
- def self.now
30
- new
31
- end
32
-
33
- # Return a +Duration+ between this +Instant+ and another.
34
- #
35
- # @param earlier [Instant]
36
- # @return [Duration]
37
- def duration_since(earlier)
38
- raise TypeError, 'Not an Instant' unless earlier.is_a?(Instant)
39
-
40
- earlier - self
41
- end
42
-
43
- # Return a +Duration+ since this +Instant+ and now.
44
- #
45
- # @return [Duration]
46
- def elapsed
47
- duration_since(self.class.now)
48
- end
49
-
50
- # Return whether this +Instant+ is in the past.
51
- #
52
- # @return [Boolean]
53
- def in_past?
54
- elapsed.positive?
55
- end
56
-
57
- alias past? in_past?
58
-
59
- # Return whether this +Instant+ is in the future.
60
- #
61
- # @return [Boolean]
62
- def in_future?
63
- elapsed.negative?
64
- end
65
-
66
- alias future? in_future?
67
-
68
- # Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+
69
- # that's either positive if any time was slept, or negative if sleeping would
70
- # require time travel.
71
- #
72
- # @example Sleeps for a second
73
- # start = Instant.now
74
- # sleep 0.5 # do stuff for half a second
75
- # start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept)
76
- # start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep)
77
- #
78
- # @example Also sleeps for a second.
79
- # one_second_in_the_future = Instant.now + Duration.from_secs(1)
80
- # one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept)
81
- # one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep)
82
- #
83
- # @param duration [nil, Duration, #to_nanos]
84
- # @return [Duration] the slept duration, if +#positive?+, else the overshot time
85
- def sleep(duration = nil)
86
- remaining = duration ? duration - elapsed : -elapsed
87
-
88
- remaining.tap { |rem| rem.sleep if rem.positive? }
89
- end
90
-
91
- # Sleep for the given number of seconds past this +Instant+, if any.
92
- #
93
- # Equivalent to +#sleep(Duration.from_secs(secs))+
94
- #
95
- # @param secs [Numeric] number of seconds to sleep past this +Instant+
96
- # @return [Duration] the slept duration, if +#positive?+, else the overshot time
97
- # @see #sleep
98
- def sleep_secs(secs)
99
- sleep(Duration.from_secs(secs))
100
- end
101
-
102
- # Sleep for the given number of milliseconds past this +Instant+, if any.
103
- #
104
- # Equivalent to +#sleep(Duration.from_millis(millis))+
105
- #
106
- # @param millis [Numeric] number of milliseconds to sleep past this +Instant+
107
- # @return [Duration] the slept duration, if +#positive?+, else the overshot time
108
- # @see #sleep
109
- def sleep_millis(millis)
110
- sleep(Duration.from_millis(millis))
111
- end
112
-
113
- # Sugar for +#elapsed.to_s+.
114
- #
115
- # @see Duration#to_s
116
- def to_s(*args)
117
- elapsed.to_s(*args)
118
- end
119
-
120
- # Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning
121
- # a new +Instant+.
122
- #
123
- # @example
124
- # (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms"
125
- #
126
- # @param other [Duration, #to_nanos]
127
- # @return [Instant]
128
- def +(other)
129
- return TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
130
-
131
- Instant.new(@ns + other.to_nanos)
132
- end
133
-
134
- # Subtract another +Instant+ to generate a +Duration+ between the two,
135
- # or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+
136
- # offset by it.
137
- #
138
- # @example
139
- # (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s"
140
- # (Instant.now - Instant.now).to_s # => "-3.87μs"
141
- #
142
- # @param other [Instant, Duration, #to_nanos]
143
- # @return [Duration, Instant]
144
- def -(other)
145
- if other.is_a?(Instant)
146
- Duration.new(@ns - other.ns)
147
- elsif other.respond_to?(:to_nanos)
148
- Instant.new(@ns - other.to_nanos)
149
- else
150
- raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]'
151
- end
152
- end
153
-
154
- # Determine if the given +Instant+ is before, equal to or after this one.
155
- # +nil+ if not passed an +Instant+.
156
- #
157
- # @return [-1, 0, 1, nil]
158
- def <=>(other)
159
- @ns <=> other.ns if other.is_a?(Instant)
160
- end
161
-
162
- # Determine if +other+'s value equals that of this +Instant+.
163
- # Use +eql?+ if type checks are desired for future compatibility.
164
- #
165
- # @return [Boolean]
166
- # @see #eql?
167
- def ==(other)
168
- other.is_a?(Instant) && @ns == other.ns
169
- end
170
-
171
- alias eql? ==
172
-
173
- # Generate a hash for this type and value.
174
- #
175
- # @return [Integer]
176
- def hash
177
- self.class.hash ^ @ns.hash
178
- end
179
- end
180
-
181
- # A type representing a span of time in nanoseconds.
182
- class Duration
183
- include Comparable
184
-
185
- # Create a new +Duration+ of a specified number of nanoseconds, zero by
186
- # default.
187
- #
188
- # Users are strongly advised to use +#from_nanos+ instead.
189
- #
190
- # @param nanos [Integer]
191
- # @see #from_nanos
192
- def initialize(nanos = 0)
193
- @ns = Integer(nanos)
194
- end
195
-
196
- class << self
197
- # Generate a new +Duration+ measuring the given number of seconds.
198
- #
199
- # @param secs [Numeric]
200
- # @return [Duration]
201
- def from_secs(secs)
202
- new(Integer(secs * 1_000_000_000))
203
- end
204
-
205
- alias secs from_secs
206
-
207
- # Generate a new +Duration+ measuring the given number of milliseconds.
208
- #
209
- # @param millis [Numeric]
210
- # @return [Duration]
211
- def from_millis(millis)
212
- new(Integer(millis * 1_000_000))
213
- end
214
-
215
- alias millis from_millis
216
-
217
- # Generate a new +Duration+ measuring the given number of microseconds.
218
- #
219
- # @param micros [Numeric]
220
- # @return [Duration]
221
- def from_micros(micros)
222
- new(Integer(micros * 1_000))
223
- end
224
-
225
- alias micros from_micros
226
-
227
- # Generate a new +Duration+ measuring the given number of nanoseconds.
228
- #
229
- # @param nanos [Numeric]
230
- # @return [Duration]
231
- def from_nanos(nanos)
232
- new(Integer(nanos))
233
- end
234
-
235
- alias nanos from_nanos
236
-
237
- # Return a +Duration+ measuring the elapsed time of the yielded block.
238
- #
239
- # @example
240
- # Duration.measure { sleep(0.5) }.to_s # => "512.226109ms"
241
- #
242
- # @return [Duration]
243
- def measure
244
- Instant.now.tap { yield }.elapsed
245
- end
246
- end
247
-
248
- # Add another +Duration+ or +#to_nanos+-coercible object to this one,
249
- # returning a new +Duration+.
250
- #
251
- # @example
252
- # (Duration.from_secs(10) + Duration.from_secs(5)).to_s # => "15s"
253
- #
254
- # @param [Duration, #to_nanos]
255
- # @return [Duration]
256
- def +(other)
257
- raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
258
-
259
- Duration.new(to_nanos + other.to_nanos)
260
- end
261
-
262
- # Subtract another +Duration+ or +#to_nanos+-coercible object from this one,
263
- # returning a new +Duration+.
264
- #
265
- # @example
266
- # (Duration.from_secs(10) - Duration.from_secs(5)).to_s # => "5s"
267
- #
268
- # @param [Duration, #to_nanos]
269
- # @return [Duration]
270
- def -(other)
271
- raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
272
-
273
- Duration.new(to_nanos - other.to_nanos)
274
- end
275
-
276
- # Divide this duration by a +Numeric+.
277
- #
278
- # @example
279
- # (Duration.from_secs(10) / 2).to_s # => "5s"
280
- #
281
- # @param [Numeric]
282
- # @return [Duration]
283
- def /(other)
284
- Duration.new(to_nanos / other)
285
- end
286
-
287
- # Multiply this duration by a +Numeric+.
288
- #
289
- # @example
290
- # (Duration.from_secs(10) * 2).to_s # => "20s"
291
- #
292
- # @param [Numeric]
293
- # @return [Duration]
294
- def *(other)
295
- Duration.new(to_nanos * other)
296
- end
297
-
298
- # Unary minus: make a positive +Duration+ negative, and vice versa.
299
- #
300
- # @example
301
- # -Duration.from_secs(-1).to_s # => "1s"
302
- # -Duration.from_secs(1).to_s # => "-1s"
303
- #
304
- # @return [Duration]
305
- def -@
306
- Duration.new(-to_nanos)
307
- end
308
-
309
- # Return a +Duration+ that's absolute (positive).
310
- #
311
- # @example
312
- # Duration.from_secs(-1).abs.to_s # => "1s"
313
- # Duration.from_secs(1).abs.to_s # => "1s"
314
- #
315
- # @return [Duration]
316
- def abs
317
- return self if positive? || zero?
318
- Duration.new(to_nanos.abs)
319
- end
320
-
321
- # Compare the *value* of this +Duration+ with another, or any +#to_nanos+-coercible
322
- # object, or nil if not comparable.
323
- #
324
- # @param [Duration, #to_nanos, Object]
325
- # @return [-1, 0, 1, nil]
326
- def <=>(other)
327
- to_nanos <=> other.to_nanos if other.respond_to?(:to_nanos)
328
- end
329
-
330
- # Compare the equality of the *value* of this +Duration+ with another, or
331
- # any +#to_nanos+-coercible object, or nil if not comparable.
332
- #
333
- # @param [Duration, #to_nanos, Object]
334
- # @return [Boolean]
335
- def ==(other)
336
- other.respond_to?(:to_nanos) && to_nanos == other.to_nanos
337
- end
338
-
339
- # Check equality of the value and type of this +Duration+ with another.
340
- #
341
- # @param [Duration, Object]
342
- # @return [Boolean]
343
- def eql?(other)
344
- other.is_a?(Duration) && to_nanos == other.to_nanos
345
- end
346
-
347
- # Generate a hash for this type and value.
348
- #
349
- # @return [Integer]
350
- def hash
351
- self.class.hash ^ to_nanos.hash
352
- end
353
-
354
- # Return this +Duration+ in seconds.
355
- #
356
- # @return [Float]
357
- def to_secs
358
- to_nanos / 1_000_000_000.0
359
- end
360
-
361
- alias secs to_secs
362
-
363
- # Return this +Duration+ in milliseconds.
364
- #
365
- # @return [Float]
366
- def to_millis
367
- to_nanos / 1_000_000.0
368
- end
369
-
370
- alias millis to_millis
371
-
372
- # Return this +Duration+ in microseconds.
373
- #
374
- # @return [Float]
375
- def to_micros
376
- to_nanos / 1_000.0
377
- end
378
-
379
- alias micros to_micros
380
-
381
- # Return this +Duration+ in nanoseconds.
382
- #
383
- # @return [Integer]
384
- def to_nanos
385
- @ns
386
- end
387
-
388
- alias nanos to_nanos
389
-
390
- # Return true if this +Duration+ is positive.
391
- #
392
- # @return [Boolean]
393
- def positive?
394
- to_nanos.positive?
395
- end
396
-
397
- # Return true if this +Duration+ is negative.
398
- #
399
- # @return [Boolean]
400
- def negative?
401
- to_nanos.negative?
402
- end
403
-
404
- # Return true if this +Duration+ is zero.
405
- #
406
- # @return [Boolean]
407
- def zero?
408
- to_nanos.zero?
409
- end
410
-
411
- # Return true if this +Duration+ is non-zero.
412
- #
413
- # @return [Boolean]
414
- def nonzero?
415
- to_nanos.nonzero?
416
- end
417
-
418
- # Sleep for the duration of this +Duration+. Equivalent to
419
- # +Kernel.sleep(duration.to_secs)+.
420
- #
421
- # @example
422
- # Duration.from_secs(1).sleep # => 1
423
- # Duration.from_secs(-1).sleep # => raises NotImplementedError
424
- #
425
- # @raise [NotImplementedError] negative +Duration+ sleeps are not yet supported.
426
- # @return [Integer]
427
- # @see Instant#sleep
428
- def sleep
429
- raise NotImplementedError, 'time travel module missing' if negative?
430
- Kernel.sleep(to_secs)
431
- end
432
-
433
- DIVISORS = [
434
- [1_000_000_000.0, 's'],
435
- [1_000_000.0, 'ms'],
436
- [1_000.0, 'μs'],
437
- [0, 'ns']
438
- ].map(&:freeze).freeze
439
-
440
- private_constant :DIVISORS
441
-
442
- # Format this +Duration+ into a human-readable string, with a given number
443
- # of decimal places.
444
- #
445
- # The exact format is subject to change, users with specific requirements
446
- # are encouraged to use their own formatting methods.
447
- #
448
- # @example
449
- # Duration.from_nanos(100).to_s # => "100ns"
450
- # Duration.from_micros(100).to_s # => "100μs"
451
- # Duration.from_millis(100).to_s # => "100ms"
452
- # Duration.from_secs(100).to_s # => "100s"
453
- # Duration.from_nanos(1234567).to_s # => "1.234567ms"
454
- # Duration.from_nanos(1234567).to_s(2) # => "1.23ms"
455
- #
456
- # @param precision [Integer] the maximum number of decimal places
457
- # @return [String]
458
- def to_s(precision = 9)
459
- precision = Integer(precision).abs
460
- div, unit = DIVISORS.find { |d, _| to_nanos.abs >= d }
461
-
462
- if div.zero?
463
- format('%d%s', to_nanos, unit)
464
- else
465
- format("%#.#{precision}f", to_nanos / div).sub(/\.?0*\z/, '') << unit
466
- end
467
- end
468
- end
469
- end
3
+ require_relative 'monotime/version'
4
+ require_relative 'monotime/duration'
5
+ require_relative 'monotime/instant'
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monotime
4
+ # A type representing a span of time in nanoseconds.
5
+ class Duration
6
+ include Comparable
7
+
8
+ # Create a new +Duration+ of a specified number of nanoseconds, zero by
9
+ # default.
10
+ #
11
+ # Users are strongly advised to use +#from_nanos+ instead.
12
+ #
13
+ # @param nanos [Integer]
14
+ # @see #from_nanos
15
+ def initialize(nanos = 0)
16
+ @ns = Integer(nanos)
17
+ end
18
+
19
+ class << self
20
+ # Generate a new +Duration+ measuring the given number of seconds.
21
+ #
22
+ # @param secs [Numeric]
23
+ # @return [Duration]
24
+ def from_secs(secs)
25
+ new(Integer(secs * 1_000_000_000))
26
+ end
27
+
28
+ alias secs from_secs
29
+
30
+ # Generate a new +Duration+ measuring the given number of milliseconds.
31
+ #
32
+ # @param millis [Numeric]
33
+ # @return [Duration]
34
+ def from_millis(millis)
35
+ new(Integer(millis * 1_000_000))
36
+ end
37
+
38
+ alias millis from_millis
39
+
40
+ # Generate a new +Duration+ measuring the given number of microseconds.
41
+ #
42
+ # @param micros [Numeric]
43
+ # @return [Duration]
44
+ def from_micros(micros)
45
+ new(Integer(micros * 1_000))
46
+ end
47
+
48
+ alias micros from_micros
49
+
50
+ # Generate a new +Duration+ measuring the given number of nanoseconds.
51
+ #
52
+ # @param nanos [Numeric]
53
+ # @return [Duration]
54
+ def from_nanos(nanos)
55
+ new(Integer(nanos))
56
+ end
57
+
58
+ alias nanos from_nanos
59
+
60
+ # Return a +Duration+ measuring the elapsed time of the yielded block.
61
+ #
62
+ # @example
63
+ # Duration.measure { sleep(0.5) }.to_s # => "512.226109ms"
64
+ #
65
+ # @return [Duration]
66
+ def measure
67
+ Instant.now.tap { yield }.elapsed
68
+ end
69
+
70
+ # Return the result of the yielded block alongside a +Duration+.
71
+ #
72
+ # @example
73
+ # Duration.with_measure { "bloop" } # => ["bloop", #<Monotime::Duration: ...>]
74
+ #
75
+ # @return [Object, Duration]
76
+ def with_measure
77
+ start = Instant.now
78
+ ret = yield
79
+ [ret, start.elapsed]
80
+ end
81
+ end
82
+
83
+ # Add another +Duration+ or +#to_nanos+-coercible object to this one,
84
+ # returning a new +Duration+.
85
+ #
86
+ # @example
87
+ # (Duration.from_secs(10) + Duration.from_secs(5)).to_s # => "15s"
88
+ #
89
+ # @param [Duration, #to_nanos]
90
+ # @return [Duration]
91
+ def +(other)
92
+ raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
93
+
94
+ Duration.new(to_nanos + other.to_nanos)
95
+ end
96
+
97
+ # Subtract another +Duration+ or +#to_nanos+-coercible object from this one,
98
+ # returning a new +Duration+.
99
+ #
100
+ # @example
101
+ # (Duration.from_secs(10) - Duration.from_secs(5)).to_s # => "5s"
102
+ #
103
+ # @param [Duration, #to_nanos]
104
+ # @return [Duration]
105
+ def -(other)
106
+ raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
107
+
108
+ Duration.new(to_nanos - other.to_nanos)
109
+ end
110
+
111
+ # Divide this duration by a +Numeric+.
112
+ #
113
+ # @example
114
+ # (Duration.from_secs(10) / 2).to_s # => "5s"
115
+ #
116
+ # @param [Numeric]
117
+ # @return [Duration]
118
+ def /(other)
119
+ Duration.new(to_nanos / other)
120
+ end
121
+
122
+ # Multiply this duration by a +Numeric+.
123
+ #
124
+ # @example
125
+ # (Duration.from_secs(10) * 2).to_s # => "20s"
126
+ #
127
+ # @param [Numeric]
128
+ # @return [Duration]
129
+ def *(other)
130
+ Duration.new(to_nanos * other)
131
+ end
132
+
133
+ # Unary minus: make a positive +Duration+ negative, and vice versa.
134
+ #
135
+ # @example
136
+ # -Duration.from_secs(-1).to_s # => "1s"
137
+ # -Duration.from_secs(1).to_s # => "-1s"
138
+ #
139
+ # @return [Duration]
140
+ def -@
141
+ Duration.new(-to_nanos)
142
+ end
143
+
144
+ # Return a +Duration+ that's absolute (positive).
145
+ #
146
+ # @example
147
+ # Duration.from_secs(-1).abs.to_s # => "1s"
148
+ # Duration.from_secs(1).abs.to_s # => "1s"
149
+ #
150
+ # @return [Duration]
151
+ def abs
152
+ return self if positive? || zero?
153
+
154
+ Duration.new(to_nanos.abs)
155
+ end
156
+
157
+ # Compare the *value* of this +Duration+ with another, or any +#to_nanos+-coercible
158
+ # object, or nil if not comparable.
159
+ #
160
+ # @param [Duration, #to_nanos, Object]
161
+ # @return [-1, 0, 1, nil]
162
+ def <=>(other)
163
+ to_nanos <=> other.to_nanos if other.respond_to?(:to_nanos)
164
+ end
165
+
166
+ # Compare the equality of the *value* of this +Duration+ with another, or
167
+ # any +#to_nanos+-coercible object, or nil if not comparable.
168
+ #
169
+ # @param [Duration, #to_nanos, Object]
170
+ # @return [Boolean]
171
+ def ==(other)
172
+ other.respond_to?(:to_nanos) && to_nanos == other.to_nanos
173
+ end
174
+
175
+ # Check equality of the value and type of this +Duration+ with another.
176
+ #
177
+ # @param [Duration, Object]
178
+ # @return [Boolean]
179
+ def eql?(other)
180
+ other.is_a?(Duration) && to_nanos == other.to_nanos
181
+ end
182
+
183
+ # Generate a hash for this type and value.
184
+ #
185
+ # @return [Integer]
186
+ def hash
187
+ self.class.hash ^ to_nanos.hash
188
+ end
189
+
190
+ # Return this +Duration+ in seconds.
191
+ #
192
+ # @return [Float]
193
+ def to_secs
194
+ to_nanos / 1_000_000_000.0
195
+ end
196
+
197
+ alias secs to_secs
198
+
199
+ # Return this +Duration+ in milliseconds.
200
+ #
201
+ # @return [Float]
202
+ def to_millis
203
+ to_nanos / 1_000_000.0
204
+ end
205
+
206
+ alias millis to_millis
207
+
208
+ # Return this +Duration+ in microseconds.
209
+ #
210
+ # @return [Float]
211
+ def to_micros
212
+ to_nanos / 1_000.0
213
+ end
214
+
215
+ alias micros to_micros
216
+
217
+ # Return this +Duration+ in nanoseconds.
218
+ #
219
+ # @return [Integer]
220
+ def to_nanos
221
+ @ns
222
+ end
223
+
224
+ alias nanos to_nanos
225
+
226
+ # Return true if this +Duration+ is positive.
227
+ #
228
+ # @return [Boolean]
229
+ def positive?
230
+ to_nanos.positive?
231
+ end
232
+
233
+ # Return true if this +Duration+ is negative.
234
+ #
235
+ # @return [Boolean]
236
+ def negative?
237
+ to_nanos.negative?
238
+ end
239
+
240
+ # Return true if this +Duration+ is zero.
241
+ #
242
+ # @return [Boolean]
243
+ def zero?
244
+ to_nanos.zero?
245
+ end
246
+
247
+ # Return true if this +Duration+ is non-zero.
248
+ #
249
+ # @return [Boolean]
250
+ def nonzero?
251
+ to_nanos.nonzero?
252
+ end
253
+
254
+ # Sleep for the duration of this +Duration+. Equivalent to
255
+ # +Kernel.sleep(duration.to_secs)+.
256
+ #
257
+ # @example
258
+ # Duration.from_secs(1).sleep # => 1
259
+ # Duration.from_secs(-1).sleep # => raises NotImplementedError
260
+ #
261
+ # @raise [NotImplementedError] negative +Duration+ sleeps are not yet supported.
262
+ # @return [Integer]
263
+ # @see Instant#sleep
264
+ def sleep
265
+ raise NotImplementedError, 'time travel module missing' if negative?
266
+
267
+ Kernel.sleep(to_secs)
268
+ end
269
+
270
+ DIVISORS = [
271
+ [1_000_000_000.0, 's'],
272
+ [1_000_000.0, 'ms'],
273
+ [1_000.0, 'μs'],
274
+ [0, 'ns']
275
+ ].map(&:freeze).freeze
276
+
277
+ private_constant :DIVISORS
278
+
279
+ # Format this +Duration+ into a human-readable string, with a given number
280
+ # of decimal places.
281
+ #
282
+ # The exact format is subject to change, users with specific requirements
283
+ # are encouraged to use their own formatting methods.
284
+ #
285
+ # @example
286
+ # Duration.from_nanos(100).to_s # => "100ns"
287
+ # Duration.from_micros(100).to_s # => "100μs"
288
+ # Duration.from_millis(100).to_s # => "100ms"
289
+ # Duration.from_secs(100).to_s # => "100s"
290
+ # Duration.from_nanos(1234567).to_s # => "1.234567ms"
291
+ # Duration.from_nanos(1234567).to_s(2) # => "1.23ms"
292
+ #
293
+ # @param precision [Integer] the maximum number of decimal places
294
+ # @return [String]
295
+ def to_s(precision = 9)
296
+ precision = Integer(precision).abs
297
+ div, unit = DIVISORS.find { |d, _| to_nanos.abs >= d }
298
+
299
+ if div.zero?
300
+ format('%d%s', to_nanos, unit)
301
+ else
302
+ format("%#.#{precision}f", to_nanos / div).sub(/\.?0*\z/, '') << unit
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monotime
4
+ # A measurement from the operating system's monotonic clock, with up to
5
+ # nanosecond precision.
6
+ class Instant
7
+ # A measurement, in nanoseconds. Should be considered opaque and
8
+ # non-portable outside the process that created it.
9
+ attr_reader :ns
10
+ protected :ns
11
+
12
+ include Comparable
13
+
14
+ # Create a new +Instant+ from an optional nanosecond measurement.
15
+ #
16
+ # Users should generally *not* pass anything to this function.
17
+ #
18
+ # @param nanos [Integer]
19
+ # @see #now
20
+ def initialize(nanos = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond))
21
+ @ns = Integer(nanos)
22
+ end
23
+
24
+ # An alias to +new+, and generally preferred over it.
25
+ #
26
+ # @return [Instant]
27
+ def self.now
28
+ new
29
+ end
30
+
31
+ # Return a +Duration+ between this +Instant+ and another.
32
+ #
33
+ # @param earlier [Instant]
34
+ # @return [Duration]
35
+ def duration_since(earlier)
36
+ raise TypeError, 'Not an Instant' unless earlier.is_a?(Instant)
37
+
38
+ earlier - self
39
+ end
40
+
41
+ # Return a +Duration+ since this +Instant+ and now.
42
+ #
43
+ # @return [Duration]
44
+ def elapsed
45
+ duration_since(self.class.now)
46
+ end
47
+
48
+ # Return whether this +Instant+ is in the past.
49
+ #
50
+ # @return [Boolean]
51
+ def in_past?
52
+ elapsed.positive?
53
+ end
54
+
55
+ alias past? in_past?
56
+
57
+ # Return whether this +Instant+ is in the future.
58
+ #
59
+ # @return [Boolean]
60
+ def in_future?
61
+ elapsed.negative?
62
+ end
63
+
64
+ alias future? in_future?
65
+
66
+ # Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+
67
+ # that's either positive if any time was slept, or negative if sleeping would
68
+ # require time travel.
69
+ #
70
+ # @example Sleeps for a second
71
+ # start = Instant.now
72
+ # sleep 0.5 # do stuff for half a second
73
+ # start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept)
74
+ # start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep)
75
+ #
76
+ # @example Also sleeps for a second.
77
+ # one_second_in_the_future = Instant.now + Duration.from_secs(1)
78
+ # one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept)
79
+ # one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep)
80
+ #
81
+ # @param duration [nil, Duration, #to_nanos]
82
+ # @return [Duration] the slept duration, if +#positive?+, else the overshot time
83
+ def sleep(duration = nil)
84
+ remaining = duration ? duration - elapsed : -elapsed
85
+
86
+ remaining.tap { |rem| rem.sleep if rem.positive? }
87
+ end
88
+
89
+ # Sleep for the given number of seconds past this +Instant+, if any.
90
+ #
91
+ # Equivalent to +#sleep(Duration.from_secs(secs))+
92
+ #
93
+ # @param secs [Numeric] number of seconds to sleep past this +Instant+
94
+ # @return [Duration] the slept duration, if +#positive?+, else the overshot time
95
+ # @see #sleep
96
+ def sleep_secs(secs)
97
+ sleep(Duration.from_secs(secs))
98
+ end
99
+
100
+ # Sleep for the given number of milliseconds past this +Instant+, if any.
101
+ #
102
+ # Equivalent to +#sleep(Duration.from_millis(millis))+
103
+ #
104
+ # @param millis [Numeric] number of milliseconds to sleep past this +Instant+
105
+ # @return [Duration] the slept duration, if +#positive?+, else the overshot time
106
+ # @see #sleep
107
+ def sleep_millis(millis)
108
+ sleep(Duration.from_millis(millis))
109
+ end
110
+
111
+ # Sugar for +#elapsed.to_s+.
112
+ #
113
+ # @see Duration#to_s
114
+ def to_s(*args)
115
+ elapsed.to_s(*args)
116
+ end
117
+
118
+ # Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning
119
+ # a new +Instant+.
120
+ #
121
+ # @example
122
+ # (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms"
123
+ #
124
+ # @param other [Duration, #to_nanos]
125
+ # @return [Instant]
126
+ def +(other)
127
+ return TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
128
+
129
+ Instant.new(@ns + other.to_nanos)
130
+ end
131
+
132
+ # Subtract another +Instant+ to generate a +Duration+ between the two,
133
+ # or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+
134
+ # offset by it.
135
+ #
136
+ # @example
137
+ # (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s"
138
+ # (Instant.now - Instant.now).to_s # => "-3.87μs"
139
+ #
140
+ # @param other [Instant, Duration, #to_nanos]
141
+ # @return [Duration, Instant]
142
+ def -(other)
143
+ if other.is_a?(Instant)
144
+ Duration.new(@ns - other.ns)
145
+ elsif other.respond_to?(:to_nanos)
146
+ Instant.new(@ns - other.to_nanos)
147
+ else
148
+ raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]'
149
+ end
150
+ end
151
+
152
+ # Determine if the given +Instant+ is before, equal to or after this one.
153
+ # +nil+ if not passed an +Instant+.
154
+ #
155
+ # @return [-1, 0, 1, nil]
156
+ def <=>(other)
157
+ @ns <=> other.ns if other.is_a?(Instant)
158
+ end
159
+
160
+ # Determine if +other+'s value equals that of this +Instant+.
161
+ # Use +eql?+ if type checks are desired for future compatibility.
162
+ #
163
+ # @return [Boolean]
164
+ # @see #eql?
165
+ def ==(other)
166
+ other.is_a?(Instant) && @ns == other.ns
167
+ end
168
+
169
+ alias eql? ==
170
+
171
+ # Generate a hash for this type and value.
172
+ #
173
+ # @return [Integer]
174
+ def hash
175
+ self.class.hash ^ @ns.hash
176
+ end
177
+ end
178
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Monotime
4
- VERSION = '0.6.1'
4
+ # Try to avoid blatting existing VERSION constants when we're included.
5
+ MONOTIME_VERSION = '0.7.0'
5
6
  end
@@ -5,7 +5,7 @@ require "monotime/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "monotime"
8
- spec.version = Monotime::VERSION
8
+ spec.version = Monotime::MONOTIME_VERSION
9
9
  spec.authors = ["Thomas Hurst"]
10
10
  spec.email = ["tom@hur.st"]
11
11
 
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- spec.add_development_dependency "bundler", "~> 1.16"
34
+ spec.add_development_dependency "bundler", "~> 2"
35
35
  spec.add_development_dependency "rake", "~> 10.0"
36
36
  spec.add_development_dependency "minitest", "~> 5.0"
37
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monotime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Hurst
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-26 00:00:00.000000000 Z
11
+ date: 2019-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.16'
19
+ version: '2'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.16'
26
+ version: '2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -70,6 +70,8 @@ files:
70
70
  - bin/console
71
71
  - bin/setup
72
72
  - lib/monotime.rb
73
+ - lib/monotime/duration.rb
74
+ - lib/monotime/instant.rb
73
75
  - lib/monotime/version.rb
74
76
  - monotime.gemspec
75
77
  homepage: https://github.com/Freaky/monotime