monotime 0.6.1 → 0.7.0

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: '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