monotime 0.5.0 → 0.7.1

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.
@@ -0,0 +1,179 @@
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
+ freeze
23
+ end
24
+
25
+ # An alias to +new+, and generally preferred over it.
26
+ #
27
+ # @return [Instant]
28
+ def self.now
29
+ new
30
+ end
31
+
32
+ # Return a +Duration+ between this +Instant+ and another.
33
+ #
34
+ # @param earlier [Instant]
35
+ # @return [Duration]
36
+ def duration_since(earlier)
37
+ raise TypeError, 'Not an Instant' unless earlier.is_a?(Instant)
38
+
39
+ earlier - self
40
+ end
41
+
42
+ # Return a +Duration+ since this +Instant+ and now.
43
+ #
44
+ # @return [Duration]
45
+ def elapsed
46
+ duration_since(self.class.now)
47
+ end
48
+
49
+ # Return whether this +Instant+ is in the past.
50
+ #
51
+ # @return [Boolean]
52
+ def in_past?
53
+ elapsed.positive?
54
+ end
55
+
56
+ alias past? in_past?
57
+
58
+ # Return whether this +Instant+ is in the future.
59
+ #
60
+ # @return [Boolean]
61
+ def in_future?
62
+ elapsed.negative?
63
+ end
64
+
65
+ alias future? in_future?
66
+
67
+ # Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+
68
+ # that's either positive if any time was slept, or negative if sleeping would
69
+ # require time travel.
70
+ #
71
+ # @example Sleeps for a second
72
+ # start = Instant.now
73
+ # sleep 0.5 # do stuff for half a second
74
+ # start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept)
75
+ # start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep)
76
+ #
77
+ # @example Also sleeps for a second.
78
+ # one_second_in_the_future = Instant.now + Duration.from_secs(1)
79
+ # one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept)
80
+ # one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep)
81
+ #
82
+ # @param duration [nil, Duration, #to_nanos]
83
+ # @return [Duration] the slept duration, if +#positive?+, else the overshot time
84
+ def sleep(duration = nil)
85
+ remaining = duration ? duration - elapsed : -elapsed
86
+
87
+ remaining.tap { |rem| rem.sleep if rem.positive? }
88
+ end
89
+
90
+ # Sleep for the given number of seconds past this +Instant+, if any.
91
+ #
92
+ # Equivalent to +#sleep(Duration.from_secs(secs))+
93
+ #
94
+ # @param secs [Numeric] number of seconds to sleep past this +Instant+
95
+ # @return [Duration] the slept duration, if +#positive?+, else the overshot time
96
+ # @see #sleep
97
+ def sleep_secs(secs)
98
+ sleep(Duration.from_secs(secs))
99
+ end
100
+
101
+ # Sleep for the given number of milliseconds past this +Instant+, if any.
102
+ #
103
+ # Equivalent to +#sleep(Duration.from_millis(millis))+
104
+ #
105
+ # @param millis [Numeric] number of milliseconds to sleep past this +Instant+
106
+ # @return [Duration] the slept duration, if +#positive?+, else the overshot time
107
+ # @see #sleep
108
+ def sleep_millis(millis)
109
+ sleep(Duration.from_millis(millis))
110
+ end
111
+
112
+ # Sugar for +#elapsed.to_s+.
113
+ #
114
+ # @see Duration#to_s
115
+ def to_s(*args)
116
+ elapsed.to_s(*args)
117
+ end
118
+
119
+ # Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning
120
+ # a new +Instant+.
121
+ #
122
+ # @example
123
+ # (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms"
124
+ #
125
+ # @param other [Duration, #to_nanos]
126
+ # @return [Instant]
127
+ def +(other)
128
+ return TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
129
+
130
+ Instant.new(@ns + other.to_nanos)
131
+ end
132
+
133
+ # Subtract another +Instant+ to generate a +Duration+ between the two,
134
+ # or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+
135
+ # offset by it.
136
+ #
137
+ # @example
138
+ # (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s"
139
+ # (Instant.now - Instant.now).to_s # => "-3.87μs"
140
+ #
141
+ # @param other [Instant, Duration, #to_nanos]
142
+ # @return [Duration, Instant]
143
+ def -(other)
144
+ if other.is_a?(Instant)
145
+ Duration.new(@ns - other.ns)
146
+ elsif other.respond_to?(:to_nanos)
147
+ Instant.new(@ns - other.to_nanos)
148
+ else
149
+ raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]'
150
+ end
151
+ end
152
+
153
+ # Determine if the given +Instant+ is before, equal to or after this one.
154
+ # +nil+ if not passed an +Instant+.
155
+ #
156
+ # @return [-1, 0, 1, nil]
157
+ def <=>(other)
158
+ @ns <=> other.ns if other.is_a?(Instant)
159
+ end
160
+
161
+ # Determine if +other+'s value equals that of this +Instant+.
162
+ # Use +eql?+ if type checks are desired for future compatibility.
163
+ #
164
+ # @return [Boolean]
165
+ # @see #eql?
166
+ def ==(other)
167
+ other.is_a?(Instant) && @ns == other.ns
168
+ end
169
+
170
+ alias eql? ==
171
+
172
+ # Generate a hash for this type and value.
173
+ #
174
+ # @return [Integer]
175
+ def hash
176
+ self.class.hash ^ @ns.hash
177
+ end
178
+ end
179
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Monotime
4
- VERSION = '0.5.0'
4
+ # Try to avoid blatting existing VERSION constants when we're included.
5
+ MONOTIME_VERSION = '0.7.1'
5
6
  end
data/lib/monotime.rb CHANGED
@@ -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
- # Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+
51
- # that's either positive if any time was slept, or negative if sleeping would
52
- # require time travel.
53
- #
54
- # @example Sleeps for a second
55
- # start = Instant.now
56
- # sleep 0.5 # do stuff for half a second
57
- # start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept)
58
- # start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep)
59
- #
60
- # @example Also sleeps for a second.
61
- # one_second_in_the_future = Instant.now + Duration.from_secs(1)
62
- # one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept)
63
- # one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep)
64
- #
65
- # @param duration [nil, Duration, #to_nanos]
66
- # @return [Duration] the slept duration, if +#positive?+, else the overshot time
67
- def sleep(duration = nil)
68
- remaining = duration ? duration - elapsed : -elapsed
69
-
70
- remaining.tap { |rem| rem.sleep if rem.positive? }
71
- end
72
-
73
- # Sleep for the given number of seconds past this +Instant+, if any.
74
- #
75
- # Equivalent to +#sleep(Duration.from_secs(secs))+
76
- #
77
- # @param secs [Numeric] number of seconds to sleep past this +Instant+
78
- # @return [Duration] the slept duration, if +#positive?+, else the overshot time
79
- # @see #sleep
80
- def sleep_secs(secs)
81
- sleep(Duration.from_secs(secs))
82
- end
83
-
84
- # Sleep for the given number of milliseconds past this +Instant+, if any.
85
- #
86
- # Equivalent to +#sleep(Duration.from_millis(millis))+
87
- #
88
- # @param millis [Numeric] number of milliseconds to sleep past this +Instant+
89
- # @return [Duration] the slept duration, if +#positive?+, else the overshot time
90
- # @see #sleep
91
- def sleep_millis(millis)
92
- sleep(Duration.from_millis(millis))
93
- end
94
-
95
- # Sugar for +#elapsed.to_s+.
96
- #
97
- # @see Duration#to_s
98
- def to_s(*args)
99
- elapsed.to_s(*args)
100
- end
101
-
102
- # Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning
103
- # a new +Instant+.
104
- #
105
- # @example
106
- # (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms"
107
- #
108
- # @param other [Duration, #to_nanos]
109
- # @return [Instant]
110
- def +(other)
111
- return TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
112
-
113
- Instant.new(@ns + other.to_nanos)
114
- end
115
-
116
- # Subtract another +Instant+ to generate a +Duration+ between the two,
117
- # or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+
118
- # offset by it.
119
- #
120
- # @example
121
- # (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s"
122
- # (Instant.now - Instant.now).to_s # => "-3.87μs"
123
- #
124
- # @param other [Instant, Duration, #to_nanos]
125
- # @return [Duration, Instant]
126
- def -(other)
127
- if other.is_a?(Instant)
128
- Duration.new(@ns - other.ns)
129
- elsif other.respond_to?(:to_nanos)
130
- Instant.new(@ns - other.to_nanos)
131
- else
132
- raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]'
133
- end
134
- end
135
-
136
- # Determine if the given +Instant+ is before, equal to or after this one.
137
- # +nil+ if not passed an +Instant+.
138
- #
139
- # @return [-1, 0, 1, nil]
140
- def <=>(other)
141
- @ns <=> other.ns if other.is_a?(Instant)
142
- end
143
-
144
- # Determine if +other+'s value equals that of this +Instant+.
145
- # Use +eql?+ if type checks are desired for future compatibility.
146
- #
147
- # @return [Boolean]
148
- # @see #eql?
149
- def ==(other)
150
- other.is_a?(Instant) && @ns == other.ns
151
- end
152
-
153
- alias eql? ==
154
-
155
- # Generate a hash for this type and value.
156
- #
157
- # @return [Fixnum]
158
- def hash
159
- self.class.hash ^ @ns.hash
160
- end
161
- end
162
-
163
- # A type representing a span of time in nanoseconds.
164
- class Duration
165
- include Comparable
166
-
167
- # Create a new +Duration+ of a specified number of nanoseconds, zero by
168
- # default.
169
- #
170
- # Users are strongly advised to use +#from_nanos+ instead.
171
- #
172
- # @param nanos [Integer]
173
- # @see #from_nanos
174
- def initialize(nanos = 0)
175
- @ns = Integer(nanos)
176
- end
177
-
178
- class << self
179
- # Generate a new +Duration+ measuring the given number of seconds.
180
- #
181
- # @param secs [Numeric]
182
- # @return [Duration]
183
- def from_secs(secs)
184
- new(Integer(secs * 1_000_000_000))
185
- end
186
-
187
- # Generate a new +Duration+ measuring the given number of milliseconds.
188
- #
189
- # @param millis [Numeric]
190
- # @return [Duration]
191
- def from_millis(millis)
192
- new(Integer(millis * 1_000_000))
193
- end
194
-
195
- # Generate a new +Duration+ measuring the given number of microseconds.
196
- #
197
- # @param micros [Numeric]
198
- # @return [Duration]
199
- def from_micros(micros)
200
- new(Integer(micros * 1_000))
201
- end
202
-
203
- # Generate a new +Duration+ measuring the given number of nanoseconds.
204
- #
205
- # @param nanos [Numeric]
206
- # @return [Duration]
207
- def from_nanos(nanos)
208
- new(Integer(nanos))
209
- end
210
-
211
- # Return a +Duration+ measuring the elapsed time of the yielded block.
212
- #
213
- # @example
214
- # Duration.measure { sleep(0.5) }.to_s # => "512.226109ms"
215
- #
216
- # @return [Duration]
217
- def measure
218
- Instant.now.tap { yield }.elapsed
219
- end
220
- end
221
-
222
- # Add another +Duration+ or +#to_nanos+-coercible object to this one,
223
- # returning a new +Duration+.
224
- #
225
- # @example
226
- # (Duration.from_secs(10) + Duration.from_secs(5)).to_s # => "15s"
227
- #
228
- # @param [Duration, #to_nanos]
229
- # @return [Duration]
230
- def +(other)
231
- raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
232
-
233
- Duration.new(to_nanos + other.to_nanos)
234
- end
235
-
236
- # Subtract another +Duration+ or +#to_nanos+-coercible object from this one,
237
- # returning a new +Duration+.
238
- #
239
- # @example
240
- # (Duration.from_secs(10) - Duration.from_secs(5)).to_s # => "5s"
241
- #
242
- # @param [Duration, #to_nanos]
243
- # @return [Duration]
244
- def -(other)
245
- raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
246
-
247
- Duration.new(to_nanos - other.to_nanos)
248
- end
249
-
250
- # Divide this duration by a +Numeric+.
251
- #
252
- # @example
253
- # (Duration.from_secs(10) / 2).to_s # => "5s"
254
- #
255
- # @param [Numeric]
256
- # @return [Duration]
257
- def /(other)
258
- Duration.new(to_nanos / other)
259
- end
260
-
261
- # Multiply this duration by a +Numeric+.
262
- #
263
- # @example
264
- # (Duration.from_secs(10) * 2).to_s # => "20s"
265
- #
266
- # @param [Numeric]
267
- # @return [Duration]
268
- def *(other)
269
- Duration.new(to_nanos * other)
270
- end
271
-
272
- # Unary minus: make a positive +Duration+ negative, and vice versa.
273
- #
274
- # @example
275
- # -Duration.from_secs(-1).to_s # => "1s"
276
- # -Duration.from_secs(1).to_s # => "-1s"
277
- #
278
- # @return [Duration]
279
- def -@
280
- Duration.new(-to_nanos)
281
- end
282
-
283
- # Return a +Duration+ that's absolute (positive).
284
- #
285
- # @example
286
- # Duration.from_secs(-1).abs.to_s # => "1s"
287
- # Duration.from_secs(1).abs.to_s # => "1s"
288
- #
289
- # @return [Duration]
290
- def abs
291
- return self if positive? || zero?
292
- Duration.new(to_nanos.abs)
293
- end
294
-
295
- # Compare the *value* of this +Duration+ with another, or any +#to_nanos+-coercible
296
- # object, or nil if not comparable.
297
- #
298
- # @param [Duration, #to_nanos, Object]
299
- # @return [-1, 0, 1, nil]
300
- def <=>(other)
301
- to_nanos <=> other.to_nanos if other.respond_to?(:to_nanos)
302
- end
303
-
304
- # Compare the equality of the *value* of this +Duration+ with another, or
305
- # any +#to_nanos+-coercible object, or nil if not comparable.
306
- #
307
- # @param [Duration, #to_nanos, Object]
308
- # @return [Boolean]
309
- def ==(other)
310
- other.respond_to?(:to_nanos) && to_nanos == other.to_nanos
311
- end
312
-
313
- # Check equality of the value and type of this +Duration+ with another.
314
- #
315
- # @param [Duration, Object]
316
- # @return [Boolean]
317
- def eql?(other)
318
- other.is_a?(Duration) && to_nanos == other.to_nanos
319
- end
320
-
321
- # Generate a hash for this type and value.
322
- #
323
- # @return [Fixnum]
324
- def hash
325
- self.class.hash ^ to_nanos.hash
326
- end
327
-
328
- # Return this +Duration+ in seconds.
329
- #
330
- # @return [Float]
331
- def to_secs
332
- to_nanos / 1_000_000_000.0
333
- end
334
-
335
- # Return this +Duration+ in milliseconds.
336
- #
337
- # @return [Float]
338
- def to_millis
339
- to_nanos / 1_000_000.0
340
- end
341
-
342
- # Return this +Duration+ in microseconds.
343
- #
344
- # @return [Float]
345
- def to_micros
346
- to_nanos / 1_000.0
347
- end
348
-
349
- # Return this +Duration+ in nanoseconds.
350
- #
351
- # @return [Integer]
352
- def to_nanos
353
- @ns
354
- end
355
-
356
- # Return true if this +Duration+ is positive.
357
- #
358
- # @return [Boolean]
359
- def positive?
360
- to_nanos.positive?
361
- end
362
-
363
- # Return true if this +Duration+ is negative.
364
- #
365
- # @return [Boolean]
366
- def negative?
367
- to_nanos.negative?
368
- end
369
-
370
- # Return true if this +Duration+ is zero.
371
- #
372
- # @return [Boolean]
373
- def zero?
374
- to_nanos.zero?
375
- end
376
-
377
- # Sleep for the duration of this +Duration+. Equivalent to
378
- # +Kernel.sleep(duration.to_secs)+.
379
- #
380
- # @example
381
- # Duration.from_secs(1).sleep # => 1
382
- # Duration.from_secs(-1).sleep # => raises NotImplementedError
383
- #
384
- # @raise [NotImplementedError] negative +Duration+ sleeps are not yet supported.
385
- # @return [Integer]
386
- # @see Instant#sleep
387
- def sleep
388
- raise NotImplementedError, 'time travel module missing' if negative?
389
- Kernel.sleep(to_secs)
390
- end
391
-
392
- DIVISORS = [
393
- [1_000_000_000.0, 's'],
394
- [1_000_000.0, 'ms'],
395
- [1_000.0, 'μs'],
396
- [0, 'ns']
397
- ].map(&:freeze).freeze
398
-
399
- private_constant :DIVISORS
400
-
401
- # Format this +Duration+ into a human-readable string, with a given number
402
- # of decimal places.
403
- #
404
- # The exact format is subject to change, users with specific requirements
405
- # are encouraged to use their own formatting methods.
406
- #
407
- # @example
408
- # Duration.from_nanos(100).to_s # => "100ns"
409
- # Duration.from_micros(100).to_s # => "100μs"
410
- # Duration.from_millis(100).to_s # => "100ms"
411
- # Duration.from_secs(100).to_s # => "100s"
412
- # Duration.from_nanos(1234567).to_s # => "1.234567ms"
413
- # Duration.from_nanos(1234567).to_s(2) # => "1.23ms"
414
- #
415
- # @param precision [Integer] the maximum number of decimal places
416
- # @return [String]
417
- def to_s(precision = 9)
418
- precision = Integer(precision).abs
419
- div, unit = DIVISORS.find { |d, _| to_nanos.abs >= d }
420
-
421
- if div.zero?
422
- format('%d%s', to_nanos, unit)
423
- else
424
- format("%#.#{precision}f", to_nanos / div).sub(/\.?0*\z/, '') << unit
425
- end
426
- end
427
-
428
- # def to_s_bigdecimal(precision = 9)
429
- # require 'bigdecimal'
430
- # precision = Integer(precision).abs
431
- # div, unit = DIVISORS.find { |d, _| to_nanos.abs >= d }
432
-
433
- # if div.zero?
434
- # format('%d%s', to_nanos, unit)
435
- # else
436
- # num = (BigDecimal(to_nanos) / div.to_i)
437
- # .round(precision, :banker).to_s('F').sub(/\.?0*$/, '')
438
- # format('%s%s', num, unit)
439
- # end
440
- # end
441
-
442
- # def to_s_divmod(precision = 9)
443
- # precision = Integer(precision).abs
444
-
445
- # ns = to_nanos.abs
446
- # div, unit = DIVISORS.find { |d, _| ns >= d }
447
-
448
- # return format('%dns', to_nanos) if div.zero?
449
-
450
- # whole, frac = to_nanos.divmod(div.to_i)
451
-
452
- # if precision.zero? || frac.zero?
453
- # whole += 1 if frac > div / 2
454
- # return format('%d%s', whole, unit)
455
- # end
456
-
457
- # # XXX: still need to round: Duration.from_nanos(99999999999).to_s_divmod 7 # => 99.1s
458
- # p frac
459
- # frac = ((frac / div.to_f) * (10 ** precision)).round
460
- # if frac.to_s =~ /\A10*\z/ # erm...
461
- # whole += 1
462
- # frac = 0
463
- # end
464
- # num = format('%d.%d', whole, frac)
465
- # num.sub!(/\.?0*\z/, '') if precision.nonzero?
466
- # num << unit
467
- # end
468
- end
469
- end
3
+ require_relative 'monotime/version'
4
+ require_relative 'monotime/duration'
5
+ require_relative 'monotime/instant'
data/monotime.gemspec CHANGED
@@ -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,8 @@ 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"
35
- spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "bundler", "~> 2"
35
+ spec.add_development_dependency "rake", "~> 13.0"
36
36
  spec.add_development_dependency "minitest", "~> 5.0"
37
+ spec.add_development_dependency "simplecov", "~> 0.21"
37
38
  end