monotime 0.5.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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