horologium 0.0.3 → 0.0.4
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 +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +81 -2
- data/lib/horologium/duration.rb +158 -4
- data/lib/horologium/epochs.rb +49 -0
- data/lib/horologium/instant.rb +1 -6
- data/lib/horologium/numeric/exact.rb +15 -0
- data/lib/horologium/numeric/precision.rb +51 -4
- data/lib/horologium/numeric/two_part_float.rb +146 -59
- data/lib/horologium/precise_value.rb +9 -6
- data/lib/horologium/representations/iso8601.rb +2 -2
- data/lib/horologium/representations/julian_date.rb +26 -1
- data/lib/horologium/scale_reading.rb +24 -9
- data/lib/horologium/scales/tt.rb +6 -1
- data/lib/horologium/scales/utc.rb +82 -48
- data/lib/horologium/version.rb +1 -1
- data/lib/horologium.rb +1 -0
- data/sig/horologium/duration.rbs +32 -0
- data/sig/horologium/epochs.rbs +13 -0
- data/sig/horologium/numeric/exact.rbs +6 -0
- data/sig/horologium/numeric/precision.rbs +4 -0
- data/sig/horologium/numeric/two_part_float.rbs +16 -7
- data/sig/horologium/precise_value.rbs +1 -3
- data/sig/horologium/representations/julian_date.rbs +2 -0
- data/sig/horologium/scale_reading.rbs +5 -3
- data/sig/horologium/scales/utc.rbs +9 -5
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65e0d322bb82639c79c818b3c8473f507ecfcb25215c522f20d41c5ef7f5be25
|
|
4
|
+
data.tar.gz: 1d2d822d56768a6d9383cc29a700208e03ef3e6f639460d1a99b4eb18e01e1de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4c87cfb9449bb41e362d2596c03ff44e2907ab593c332eb37155fb833b7d8a366911eece54a0759fff1e72410599c244c82d6757d49df66edf904783115f811
|
|
7
|
+
data.tar.gz: 3fda2d236fb3c9afaa33e921a0a3e3738518df46cefc9e10146acf590f144cdd67f4c162448fef7c2a002ba62cd74efbd45f500b069c91e8a8020341ec082f22
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.4 - 2026-09-05
|
|
4
|
+
|
|
5
|
+
The epochs astronomy counts from arrive as instants, a duration reads back in
|
|
6
|
+
the unit you want it in, and the conversions cost a good deal less than they
|
|
7
|
+
did.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add `Epochs`, with `J2000`, `J1900`, `GPS_ZERO`, `UNIX` and
|
|
12
|
+
`TT_TCG_TCB_ORIGIN`. An epoch is an ordinary `Instant`, so the time elapsed
|
|
13
|
+
since one is a subtraction and no Julian Date is involved
|
|
14
|
+
- Add the `Duration` constructors `minutes`, `hours`, `julian_years`,
|
|
15
|
+
`julian_centuries` and `zero`, where a Julian year is exactly 365.25 days and
|
|
16
|
+
a Julian century 36,525
|
|
17
|
+
- Add `Duration#in_seconds`, `#in_minutes`, `#in_hours`, `#in_days`,
|
|
18
|
+
`#in_julian_years` and `#in_julian_centuries`, which come out as a Float at
|
|
19
|
+
`:standard` and a Rational at `:exact`
|
|
20
|
+
- Add `zero?`, `negative?` and `positive?` to `Numeric::TwoPartFloat` and
|
|
21
|
+
`Numeric::Exact`, and `Numeric::Precision.compare`, which orders two values
|
|
22
|
+
by the number they denote whatever precision each is held in
|
|
23
|
+
|
|
24
|
+
### Improvements
|
|
25
|
+
|
|
26
|
+
- Build an instant in about half the time at `:standard` and a third less at
|
|
27
|
+
`:exact`, allocating 3 objects where it allocated 30. An instant no longer
|
|
28
|
+
works out its exact Rational value when it is built, the two-part arithmetic
|
|
29
|
+
keeps its intermediate parts in Floats, and a Julian Date given as a single
|
|
30
|
+
Float skips a step it does not need
|
|
31
|
+
- Read UTC twice as fast at `:standard` and a third faster at `:exact`. The
|
|
32
|
+
conversion reads each leap second offset once, and settles the day without
|
|
33
|
+
spelling the value out as a Rational
|
|
34
|
+
- Read `ScaleReading#provenance` from the scale that took the reading, when it
|
|
35
|
+
is asked rather than on every reading
|
|
36
|
+
|
|
37
|
+
**Full Changelog**: https://github.com/rhannequin/horologium/compare/v0.0.3...v0.0.4
|
|
38
|
+
|
|
3
39
|
## 0.0.3 - 2026-08-22
|
|
4
40
|
|
|
5
41
|
The scale conversions arrive. An instant is now a point with no scale of its
|
data/README.md
CHANGED
|
@@ -94,6 +94,10 @@ Horologium::Instant.from_tai(2025, 5, 1, 12, 0, 0)
|
|
|
94
94
|
Horologium::Instant.from_tdb(2025, 5, 1, 12, 0, 0)
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
TDB is the one scale that rests on a model rather than a definition. Horologium
|
|
98
|
+
runs the full 787-term Fairhead and Bretagnon series, the one [ERFA] evaluates
|
|
99
|
+
in `dtdb`, rather than a truncation of it.
|
|
100
|
+
|
|
97
101
|
A date that does not exist is refused rather than rolled over, and the message
|
|
98
102
|
says which field is wrong.
|
|
99
103
|
|
|
@@ -218,6 +222,20 @@ so a duration and a calendar day are different things.
|
|
|
218
222
|
Horologium::Duration.days(1) == Horologium::Duration.seconds(86_400) # => true
|
|
219
223
|
Horologium::Duration.nanoseconds(1_000_000_000) ==
|
|
220
224
|
Horologium::Duration.seconds(1) # => true
|
|
225
|
+
|
|
226
|
+
Horologium::Duration.minutes(90)
|
|
227
|
+
Horologium::Duration.hours(6)
|
|
228
|
+
Horologium::Duration.zero
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A Julian year is exactly 365.25 days and a Julian century is 36,525 days. They
|
|
232
|
+
are astronomical constants. A calendar year holds 365 or 366 days, so a Julian
|
|
233
|
+
year lands a few hours away from the same date next year.
|
|
234
|
+
|
|
235
|
+
```rb
|
|
236
|
+
Horologium::Duration.julian_years(1) ==
|
|
237
|
+
Horologium::Duration.days(365.25) # => true
|
|
238
|
+
Horologium::Duration.julian_centuries(0.25)
|
|
221
239
|
```
|
|
222
240
|
|
|
223
241
|
Durations add, subtract, and negate among themselves, and read back out in SI
|
|
@@ -232,6 +250,19 @@ Horologium::Duration.days(1).to_r # => (86400/1), the whole value
|
|
|
232
250
|
Horologium::Duration.days(1).to_f # => 86400.0
|
|
233
251
|
```
|
|
234
252
|
|
|
253
|
+
A duration also reads in a unit. The division happens inside the precision the
|
|
254
|
+
duration is held in, so a `:standard` duration keeps the digits a collapsed
|
|
255
|
+
`Float` would lose, and an `:exact` one reads as a `Rational`.
|
|
256
|
+
|
|
257
|
+
```rb
|
|
258
|
+
Horologium::Duration.days(1).in_hours # => 24.0
|
|
259
|
+
Horologium::Duration.hours(12).in_days # => 0.5
|
|
260
|
+
Horologium::Duration.days(36_525).in_julian_centuries # => 1.0
|
|
261
|
+
|
|
262
|
+
exact = Horologium::Duration.julian_years(1, precision: :exact)
|
|
263
|
+
exact.in_days # => (1461/4)
|
|
264
|
+
```
|
|
265
|
+
|
|
235
266
|
Adding a duration to an instant makes sense, but adding two instants together
|
|
236
267
|
does not, so it raises an error.
|
|
237
268
|
|
|
@@ -239,6 +270,30 @@ does not, so it raises an error.
|
|
|
239
270
|
instant + instant # => raises Horologium::DimensionalError
|
|
240
271
|
```
|
|
241
272
|
|
|
273
|
+
The astronomical epochs are built as instants, so the elapsed time since one is
|
|
274
|
+
a subtraction and no Julian Date is involved.
|
|
275
|
+
|
|
276
|
+
```rb
|
|
277
|
+
instant = Horologium::Instant.from_tt(2026, 1, 1)
|
|
278
|
+
|
|
279
|
+
(instant - Horologium::Epochs::J2000).to_f # => 820497600.0 SI seconds
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The scale is part of an epoch's definition. `J2000` is noon TT, which is 64.184
|
|
283
|
+
seconds away from noon UTC on the same day.
|
|
284
|
+
|
|
285
|
+
```rb
|
|
286
|
+
Horologium::Epochs::J2000 # 2000-01-01 12:00:00 TT
|
|
287
|
+
Horologium::Epochs::J1900 # 1899-12-31 12:00:00 TT
|
|
288
|
+
Horologium::Epochs::GPS_ZERO # 1980-01-06 00:00:00 UTC
|
|
289
|
+
Horologium::Epochs::UNIX # 1970-01-01 00:00:00 UTC
|
|
290
|
+
Horologium::Epochs::TT_TCG_TCB_ORIGIN # 1977-01-01 00:00:00 TAI
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
An epoch is fixed at `:exact`, since it is a definition. Subtracting one from a
|
|
294
|
+
`:standard` instant gives an `:exact` duration, the way any other mix of the two
|
|
295
|
+
does.
|
|
296
|
+
|
|
242
297
|
Exact equality is rarely what scientific code wants, so you can compare within a
|
|
243
298
|
tolerance:
|
|
244
299
|
|
|
@@ -261,8 +316,8 @@ with ordinary floating-point arithmetic.
|
|
|
261
316
|
|
|
262
317
|
Every value carries one of two precisions, fixed when it is built:
|
|
263
318
|
|
|
264
|
-
- `:standard`, the default, keeps the value as a two-part float. It
|
|
265
|
-
|
|
319
|
+
- `:standard`, the default, keeps the value as a two-part float. It stays
|
|
320
|
+
within a few nanoseconds of the true value.
|
|
266
321
|
- `:exact` keeps the value as a `Rational`, with no rounding. The test suite
|
|
267
322
|
uses it to check that `:standard` stays within its stated precision.
|
|
268
323
|
|
|
@@ -291,12 +346,30 @@ Horologium.with_precision(:exact) do
|
|
|
291
346
|
end
|
|
292
347
|
```
|
|
293
348
|
|
|
349
|
+
A library can depend on Horologium without taking the configuration away from
|
|
350
|
+
the application that depends on it. Reading the configuration does not freeze
|
|
351
|
+
it, so a gem that converts an instant while it loads still leaves
|
|
352
|
+
`Horologium.configure` open to the host. And a value built with an explicit
|
|
353
|
+
`precision:` ignores the default, so a gem that pins its own precision computes
|
|
354
|
+
the same thing whatever the host sets.
|
|
355
|
+
|
|
294
356
|
Exactness is contagious. An operation between two `:standard` values stays
|
|
295
357
|
`:standard`. Mixing a `:standard` and an `:exact` value gives an `:exact`
|
|
296
358
|
result, so precision is not quietly lost. `:exact` guarantees the arithmetic
|
|
297
359
|
Horologium performs. It cannot bring back precision that an input already lost
|
|
298
360
|
when it was built.
|
|
299
361
|
|
|
362
|
+
A `:standard` value turns a `Rational` into a pair of `Float`s on the way in and
|
|
363
|
+
back again on the way out, and that conversion costs more than the arithmetic it
|
|
364
|
+
feeds. So `:exact` is the faster one whenever a `Rational` goes in or comes out,
|
|
365
|
+
by up to about twice on a build and a read. With a `Float` in and a `Float` out
|
|
366
|
+
the two are close enough that the machine decides. `:standard` is the cheaper
|
|
367
|
+
one for a long run of arithmetic between values already held that way. Mixing
|
|
368
|
+
the two is the expensive case, because promoting a `:standard` value to a
|
|
369
|
+
`Rational` costs more than the operation it is promoted for, so inside a loop it
|
|
370
|
+
is worth keeping to one precision. `bin/benchmark` measures all of this on your
|
|
371
|
+
own machine.
|
|
372
|
+
|
|
300
373
|
## Status
|
|
301
374
|
|
|
302
375
|
This library is in early development, before its first public release. The
|
|
@@ -315,6 +388,12 @@ which is enforced at 100% of lines and branches in CI. You can also run
|
|
|
315
388
|
holds stubs for gems that ship none of their own, and stays out of the gem so
|
|
316
389
|
it cannot clash with a downstream RBS collection.
|
|
317
390
|
|
|
391
|
+
Run `bin/benchmark` to time the paths a consumer runs in a loop and count the
|
|
392
|
+
objects each one allocates. Timings drift by a few percent between runs, so it
|
|
393
|
+
rotates the order of the cases and reports the fastest round for each. The
|
|
394
|
+
allocation counts do not drift, so where a timing and a count disagree, trust
|
|
395
|
+
the count. Run it on a branch and on `main` to compare.
|
|
396
|
+
|
|
318
397
|
Run `bin/ci` to run every check that GitHub Actions runs (RuboCop, Steep, YARD
|
|
319
398
|
documentation coverage, and the tests with coverage) in a single pass. It runs
|
|
320
399
|
each check even when an earlier one fails, so you see everything that needs
|
data/lib/horologium/duration.rb
CHANGED
|
@@ -15,15 +15,31 @@ module Horologium
|
|
|
15
15
|
# Adding an Instant to a Duration raises {DimensionalError}; it is
|
|
16
16
|
# {Instant#+} that shifts a point by a span.
|
|
17
17
|
#
|
|
18
|
+
# A duration reads back in a unit with {#in_seconds}, {#in_minutes},
|
|
19
|
+
# {#in_hours}, {#in_days}, {#in_julian_years} and {#in_julian_centuries}.
|
|
20
|
+
# They come out as a Float at +:standard+ and a Rational at +:exact+.
|
|
21
|
+
#
|
|
18
22
|
# @example A day is a fixed number of SI seconds
|
|
19
23
|
# Horologium::Duration.days(1) == Horologium::Duration.seconds(86_400)
|
|
20
24
|
# # => true
|
|
21
25
|
class Duration
|
|
22
26
|
include PreciseValue
|
|
23
27
|
|
|
28
|
+
# The number of SI seconds in a minute.
|
|
29
|
+
SECONDS_PER_MINUTE = 60
|
|
30
|
+
|
|
31
|
+
# The number of SI seconds in an hour.
|
|
32
|
+
SECONDS_PER_HOUR = 3_600
|
|
33
|
+
|
|
24
34
|
# The number of SI seconds in a day.
|
|
25
35
|
SECONDS_PER_DAY = 86_400
|
|
26
36
|
|
|
37
|
+
# The number of SI seconds in a Julian year of 365.25 days.
|
|
38
|
+
SECONDS_PER_JULIAN_YEAR = 31_557_600
|
|
39
|
+
|
|
40
|
+
# The number of SI seconds in a Julian century of 36,525 days.
|
|
41
|
+
SECONDS_PER_JULIAN_CENTURY = 3_155_760_000
|
|
42
|
+
|
|
27
43
|
# The number of nanoseconds in a second.
|
|
28
44
|
NANOSECONDS_PER_SECOND = 1_000_000_000
|
|
29
45
|
|
|
@@ -40,6 +56,30 @@ module Horologium
|
|
|
40
56
|
from_seconds(count, precision)
|
|
41
57
|
end
|
|
42
58
|
|
|
59
|
+
# A duration of +count+ minutes.
|
|
60
|
+
#
|
|
61
|
+
# @param count [Numeric] the number of minutes
|
|
62
|
+
# @param precision [Symbol] +:standard+ or +:exact+, taken from the
|
|
63
|
+
# precision in effect when omitted
|
|
64
|
+
# @return [Horologium::Duration]
|
|
65
|
+
# @example
|
|
66
|
+
# Horologium::Duration.minutes(90)
|
|
67
|
+
def minutes(count, precision: Horologium.current_precision)
|
|
68
|
+
from_seconds(count * SECONDS_PER_MINUTE, precision)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A duration of +count+ hours.
|
|
72
|
+
#
|
|
73
|
+
# @param count [Numeric] the number of hours
|
|
74
|
+
# @param precision [Symbol] +:standard+ or +:exact+, taken from the
|
|
75
|
+
# precision in effect when omitted
|
|
76
|
+
# @return [Horologium::Duration]
|
|
77
|
+
# @example
|
|
78
|
+
# Horologium::Duration.hours(6)
|
|
79
|
+
def hours(count, precision: Horologium.current_precision)
|
|
80
|
+
from_seconds(count * SECONDS_PER_HOUR, precision)
|
|
81
|
+
end
|
|
82
|
+
|
|
43
83
|
# A duration of +count+ days, each of {SECONDS_PER_DAY} SI seconds. This
|
|
44
84
|
# counts time and is not tied to the calendar.
|
|
45
85
|
#
|
|
@@ -54,6 +94,36 @@ module Horologium
|
|
|
54
94
|
from_seconds(count * SECONDS_PER_DAY, precision)
|
|
55
95
|
end
|
|
56
96
|
|
|
97
|
+
# A duration of +count+ Julian years, each of exactly 365.25 days. It is
|
|
98
|
+
# the astronomical constant, and a calendar year holds 365 or 366 days,
|
|
99
|
+
# so shifting an instant by a Julian year lands a few hours away from
|
|
100
|
+
# the same date next year.
|
|
101
|
+
#
|
|
102
|
+
# @param count [Numeric] the number of Julian years
|
|
103
|
+
# @param precision [Symbol] +:standard+ or +:exact+, taken from the
|
|
104
|
+
# precision in effect when omitted
|
|
105
|
+
# @return [Horologium::Duration]
|
|
106
|
+
# @example
|
|
107
|
+
# Horologium::Duration.julian_years(1) ==
|
|
108
|
+
# Horologium::Duration.days(365.25) # => true
|
|
109
|
+
def julian_years(count, precision: Horologium.current_precision)
|
|
110
|
+
from_seconds(count * SECONDS_PER_JULIAN_YEAR, precision)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A duration of +count+ Julian centuries, each of a hundred Julian
|
|
114
|
+
# years, or 36,525 days. It is the unit the astronomical series count
|
|
115
|
+
# their time in.
|
|
116
|
+
#
|
|
117
|
+
# @param count [Numeric] the number of Julian centuries
|
|
118
|
+
# @param precision [Symbol] +:standard+ or +:exact+, taken from the
|
|
119
|
+
# precision in effect when omitted
|
|
120
|
+
# @return [Horologium::Duration]
|
|
121
|
+
# @example
|
|
122
|
+
# Horologium::Duration.julian_centuries(0.25)
|
|
123
|
+
def julian_centuries(count, precision: Horologium.current_precision)
|
|
124
|
+
from_seconds(count * SECONDS_PER_JULIAN_CENTURY, precision)
|
|
125
|
+
end
|
|
126
|
+
|
|
57
127
|
# A duration of +count+ nanoseconds.
|
|
58
128
|
#
|
|
59
129
|
# @param count [Numeric] the number of nanoseconds
|
|
@@ -66,6 +136,17 @@ module Horologium
|
|
|
66
136
|
from_seconds(Rational(count) / NANOSECONDS_PER_SECOND, precision)
|
|
67
137
|
end
|
|
68
138
|
|
|
139
|
+
# A duration of no time at all.
|
|
140
|
+
#
|
|
141
|
+
# @param precision [Symbol] +:standard+ or +:exact+, taken from the
|
|
142
|
+
# precision in effect when omitted
|
|
143
|
+
# @return [Horologium::Duration]
|
|
144
|
+
# @example
|
|
145
|
+
# Horologium::Duration.zero.zero? # => true
|
|
146
|
+
def zero(precision: Horologium.current_precision)
|
|
147
|
+
from_seconds(0, precision)
|
|
148
|
+
end
|
|
149
|
+
|
|
69
150
|
private
|
|
70
151
|
|
|
71
152
|
# Builds a duration of +seconds+ SI seconds at the given precision. At
|
|
@@ -134,24 +215,83 @@ module Horologium
|
|
|
134
215
|
|
|
135
216
|
# @return [Boolean]
|
|
136
217
|
def zero?
|
|
137
|
-
|
|
218
|
+
value.zero?
|
|
138
219
|
end
|
|
139
220
|
|
|
140
221
|
# @return [Boolean]
|
|
141
222
|
def negative?
|
|
142
|
-
|
|
223
|
+
value.negative?
|
|
143
224
|
end
|
|
144
225
|
|
|
145
226
|
# @return [Boolean]
|
|
146
227
|
def positive?
|
|
147
|
-
|
|
228
|
+
value.positive?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The duration in SI seconds.
|
|
232
|
+
#
|
|
233
|
+
# @return [Float, Rational] a Float at +:standard+, a Rational at
|
|
234
|
+
# +:exact+
|
|
235
|
+
# @example
|
|
236
|
+
# Horologium::Duration.days(1).in_seconds # => 86400.0
|
|
237
|
+
def in_seconds
|
|
238
|
+
in_unit(1)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# The duration in minutes of {SECONDS_PER_MINUTE} SI seconds each.
|
|
242
|
+
#
|
|
243
|
+
# @return [Float, Rational] a Float at +:standard+, a Rational at
|
|
244
|
+
# +:exact+
|
|
245
|
+
# @example
|
|
246
|
+
# Horologium::Duration.hours(1).in_minutes # => 60.0
|
|
247
|
+
def in_minutes
|
|
248
|
+
in_unit(SECONDS_PER_MINUTE)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# The duration in hours of {SECONDS_PER_HOUR} SI seconds each.
|
|
252
|
+
#
|
|
253
|
+
# @return [Float, Rational] a Float at +:standard+, a Rational at
|
|
254
|
+
# +:exact+
|
|
255
|
+
# @example
|
|
256
|
+
# Horologium::Duration.days(1).in_hours # => 24.0
|
|
257
|
+
def in_hours
|
|
258
|
+
in_unit(SECONDS_PER_HOUR)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# The duration in days of {SECONDS_PER_DAY} SI seconds each.
|
|
262
|
+
#
|
|
263
|
+
# @return [Float, Rational] a Float at +:standard+, a Rational at
|
|
264
|
+
# +:exact+
|
|
265
|
+
# @example
|
|
266
|
+
# Horologium::Duration.hours(12).in_days # => 0.5
|
|
267
|
+
def in_days
|
|
268
|
+
in_unit(SECONDS_PER_DAY)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# The duration in Julian years of 365.25 days each.
|
|
272
|
+
#
|
|
273
|
+
# @return [Float, Rational] a Float at +:standard+, a Rational at
|
|
274
|
+
# +:exact+
|
|
275
|
+
def in_julian_years
|
|
276
|
+
in_unit(SECONDS_PER_JULIAN_YEAR)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# The duration in Julian centuries of 36,525 days each. It is the time
|
|
280
|
+
# argument the astronomical series are written for.
|
|
281
|
+
#
|
|
282
|
+
# @return [Float, Rational] a Float at +:standard+, a Rational at
|
|
283
|
+
# +:exact+
|
|
284
|
+
# @example
|
|
285
|
+
# Horologium::Duration.days(36_525).in_julian_centuries # => 1.0
|
|
286
|
+
def in_julian_centuries
|
|
287
|
+
in_unit(SECONDS_PER_JULIAN_CENTURY)
|
|
148
288
|
end
|
|
149
289
|
|
|
150
290
|
# The duration in SI seconds, exactly.
|
|
151
291
|
#
|
|
152
292
|
# @return [Rational]
|
|
153
293
|
def to_r
|
|
154
|
-
|
|
294
|
+
value.to_r
|
|
155
295
|
end
|
|
156
296
|
|
|
157
297
|
# The duration in SI seconds. A Float has about 15 digits, so a long
|
|
@@ -166,5 +306,19 @@ module Horologium
|
|
|
166
306
|
def inspect
|
|
167
307
|
format("#<%s %s s (%s)>", self.class, to_f, precision)
|
|
168
308
|
end
|
|
309
|
+
|
|
310
|
+
private
|
|
311
|
+
|
|
312
|
+
# The duration counted in a unit. The division happens in the precision
|
|
313
|
+
# the duration is held in, so the digits survive it.
|
|
314
|
+
#
|
|
315
|
+
# @param seconds_per_unit [Integer] the SI seconds one unit holds
|
|
316
|
+
# @return [Float, Rational] a Float at +:standard+, a Rational at
|
|
317
|
+
# +:exact+
|
|
318
|
+
def in_unit(seconds_per_unit)
|
|
319
|
+
return value.to_r / seconds_per_unit if precision == :exact
|
|
320
|
+
|
|
321
|
+
(value / seconds_per_unit).to_f
|
|
322
|
+
end
|
|
169
323
|
end
|
|
170
324
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Horologium
|
|
4
|
+
# The astronomical epochs, built as instants. An epoch is an ordinary
|
|
5
|
+
# {Instant}, so the elapsed time since one is a subtraction and no Julian
|
|
6
|
+
# Date is involved.
|
|
7
|
+
#
|
|
8
|
+
# The scale is part of an epoch's definition. {J2000} is noon TT, which is
|
|
9
|
+
# 64.184 seconds away from noon UTC on the same day. The two epochs defined
|
|
10
|
+
# in UTC are given in TAI, with the TAI - UTC offset of their day written
|
|
11
|
+
# into the constant, so requiring the library reads no leap second data.
|
|
12
|
+
#
|
|
13
|
+
# An epoch is fixed at +:exact+, since it is a definition. Subtracting one
|
|
14
|
+
# from a +:standard+ instant gives an +:exact+ {Duration}, the way any other
|
|
15
|
+
# mix of the two does.
|
|
16
|
+
#
|
|
17
|
+
# @example The time elapsed since J2000
|
|
18
|
+
# instant = Horologium::Instant.from_tt(2026, 1, 1)
|
|
19
|
+
# (instant - Horologium::Epochs::J2000).to_f # => 820497600.0
|
|
20
|
+
module Epochs
|
|
21
|
+
# 2000-01-01 12:00:00 TT, Julian Date 2451545.0 in TT. The epoch the
|
|
22
|
+
# current theories of the solar system motion are written for.
|
|
23
|
+
J2000 = Instant.from_tt(2000, 1, 1, 12, precision: :exact)
|
|
24
|
+
|
|
25
|
+
# 1899-12-31 12:00:00 TT, Julian Date 2415020.0 in TT. It is one Julian
|
|
26
|
+
# century of 36,525 days before {J2000}, and it is what the older formulae
|
|
27
|
+
# count from. The date is a day earlier than the name suggests.
|
|
28
|
+
J1900 = Instant.from_tt(1899, 12, 31, 12, precision: :exact)
|
|
29
|
+
|
|
30
|
+
# 1980-01-06 00:00:00 UTC, where GPS time starts. TAI - UTC was 19 seconds
|
|
31
|
+
# on that day. GPS time counts SI seconds from here, so it stays a fixed
|
|
32
|
+
# offset from TAI.
|
|
33
|
+
GPS_ZERO = Instant.from_tai(1980, 1, 6, 0, 0, 19, precision: :exact)
|
|
34
|
+
|
|
35
|
+
# 1970-01-01 00:00:00 UTC, where Unix time starts. UTC was still steered
|
|
36
|
+
# by rate adjustments in 1970, so TAI - UTC was 8.000082 seconds on that
|
|
37
|
+
# day.
|
|
38
|
+
UNIX = Instant.from_tai(
|
|
39
|
+
1970, 1, 1, 0, 0,
|
|
40
|
+
Rational(8_000_082, 1_000_000),
|
|
41
|
+
precision: :exact
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 1977-01-01 00:00:00 TAI, Julian Date 2443144.5003725 in TT. TT, TCG and
|
|
45
|
+
# TCB were set to read the same here, and the rates that separate them are
|
|
46
|
+
# counted from this point.
|
|
47
|
+
TT_TCG_TCB_ORIGIN = Instant.from_tai(1977, 1, 1, precision: :exact)
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/horologium/instant.rb
CHANGED
|
@@ -440,12 +440,7 @@ module Horologium
|
|
|
440
440
|
time_scale = Horologium.configuration.scale(scale)
|
|
441
441
|
reading = time_scale.from_reference(value, precision)
|
|
442
442
|
|
|
443
|
-
ScaleReading.new(
|
|
444
|
-
scale,
|
|
445
|
-
reading,
|
|
446
|
-
precision,
|
|
447
|
-
time_scale.provenance(reading)
|
|
448
|
-
)
|
|
443
|
+
ScaleReading.new(scale, reading, precision, time_scale)
|
|
449
444
|
end
|
|
450
445
|
|
|
451
446
|
# The instant in a representation, read in a scale. This is the shorthand
|
|
@@ -54,6 +54,21 @@ module Horologium
|
|
|
54
54
|
self.class.new(value / scalar_rational(scalar))
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def zero?
|
|
59
|
+
value.zero?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def negative?
|
|
64
|
+
value.negative?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def positive?
|
|
69
|
+
value.positive?
|
|
70
|
+
end
|
|
71
|
+
|
|
57
72
|
# @param other [Object]
|
|
58
73
|
# @return [Boolean]
|
|
59
74
|
def ==(other)
|
|
@@ -25,9 +25,12 @@ module Horologium
|
|
|
25
25
|
# @return [Symbol] the same precision
|
|
26
26
|
# @raise [UnknownPrecisionError] when it is not one of {NAMES}
|
|
27
27
|
def validate!(precision)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
case precision
|
|
29
|
+
when :standard, :exact
|
|
30
|
+
precision
|
|
31
|
+
else
|
|
32
|
+
raise UnknownPrecisionError.new(precision, NAMES)
|
|
33
|
+
end
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
# The precision a result takes from its two operands. Same precision
|
|
@@ -102,6 +105,8 @@ module Horologium
|
|
|
102
105
|
def add(left, right)
|
|
103
106
|
if left.is_a?(TwoPartFloat) && right.is_a?(TwoPartFloat)
|
|
104
107
|
left + right
|
|
108
|
+
elsif left.is_a?(Exact) && right.is_a?(Exact)
|
|
109
|
+
left + right
|
|
105
110
|
else
|
|
106
111
|
promote(left) + promote(right)
|
|
107
112
|
end
|
|
@@ -116,11 +121,36 @@ module Horologium
|
|
|
116
121
|
def subtract(left, right)
|
|
117
122
|
if left.is_a?(TwoPartFloat) && right.is_a?(TwoPartFloat)
|
|
118
123
|
left - right
|
|
124
|
+
elsif left.is_a?(Exact) && right.is_a?(Exact)
|
|
125
|
+
left - right
|
|
119
126
|
else
|
|
120
127
|
promote(left) - promote(right)
|
|
121
128
|
end
|
|
122
129
|
end
|
|
123
130
|
|
|
131
|
+
# Orders two values by the number they denote, whatever precision each
|
|
132
|
+
# is held in. Two two-part floats are compared through the difference
|
|
133
|
+
# of their parts, which is where the answer already is.
|
|
134
|
+
#
|
|
135
|
+
# That difference answers only when it is finite and away from zero.
|
|
136
|
+
# Reaching zero says the values are close rather than equal, and parts
|
|
137
|
+
# large enough to overflow can cancel into a NaN, so both cases are
|
|
138
|
+
# settled exactly instead.
|
|
139
|
+
#
|
|
140
|
+
# @param left [TwoPartFloat, Exact] one value
|
|
141
|
+
# @param right [TwoPartFloat, Exact] the other value
|
|
142
|
+
# @return [Integer] -1, 0, or 1
|
|
143
|
+
def compare(left, right)
|
|
144
|
+
if left.is_a?(TwoPartFloat) && right.is_a?(TwoPartFloat)
|
|
145
|
+
difference = (left.high - right.high) + (left.low - right.low)
|
|
146
|
+
|
|
147
|
+
return sign_of(difference) if difference.finite? &&
|
|
148
|
+
!difference.zero?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
sign_of(left.to_r - right.to_r)
|
|
152
|
+
end
|
|
153
|
+
|
|
124
154
|
# Checks that a value matches a precision: a {TwoPartFloat} for
|
|
125
155
|
# +:standard+, an {Exact} for +:exact+.
|
|
126
156
|
#
|
|
@@ -147,11 +177,28 @@ module Horologium
|
|
|
147
177
|
# @return [Class] the type its values take
|
|
148
178
|
# @raise [UnknownPrecisionError] when the precision is not recognised
|
|
149
179
|
def value_type(precision)
|
|
150
|
-
|
|
180
|
+
case precision
|
|
181
|
+
when :standard
|
|
182
|
+
TwoPartFloat
|
|
183
|
+
when :exact
|
|
184
|
+
Exact
|
|
185
|
+
else
|
|
186
|
+
raise UnknownPrecisionError.new(precision, NAMES)
|
|
187
|
+
end
|
|
151
188
|
end
|
|
152
189
|
|
|
153
190
|
private
|
|
154
191
|
|
|
192
|
+
# Where a number sits against zero.
|
|
193
|
+
#
|
|
194
|
+
# @param number [Float, Rational] the number to read
|
|
195
|
+
# @return [Integer] -1 below zero, 0 at it, 1 above it
|
|
196
|
+
def sign_of(number)
|
|
197
|
+
return 0 if number.zero?
|
|
198
|
+
|
|
199
|
+
number.negative? ? -1 : 1
|
|
200
|
+
end
|
|
201
|
+
|
|
155
202
|
# One side of an operation, as an exact value.
|
|
156
203
|
#
|
|
157
204
|
# @param value [TwoPartFloat, Exact] the value to promote
|