horologium 0.0.2 → 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 +79 -0
- data/README.md +271 -14
- data/lib/horologium/configuration.rb +199 -8
- data/lib/horologium/data/barycentric_model.rb +901 -0
- data/lib/horologium/data/leap_seconds.rb +55 -0
- data/lib/horologium/duration.rb +241 -13
- data/lib/horologium/epochs.rb +49 -0
- data/lib/horologium/error.rb +86 -3
- data/lib/horologium/instant.rb +405 -62
- data/lib/horologium/numeric/exact.rb +61 -60
- data/lib/horologium/numeric/precision.rb +138 -8
- data/lib/horologium/numeric/two_part_float.rb +195 -124
- data/lib/horologium/precise_value.rb +13 -16
- data/lib/horologium/representations/civil.rb +518 -0
- data/lib/horologium/representations/civil_time.rb +162 -0
- data/lib/horologium/representations/iso8601.rb +286 -0
- data/lib/horologium/representations/julian_date.rb +252 -0
- data/lib/horologium/representations/modified_julian_date.rb +106 -0
- data/lib/horologium/scale_reading.rb +141 -0
- data/lib/horologium/scales/base.rb +118 -0
- data/lib/horologium/scales/tai.rb +41 -0
- data/lib/horologium/scales/tdb.rb +75 -0
- data/lib/horologium/scales/tt.rb +76 -0
- data/lib/horologium/scales/utc.rb +408 -0
- data/lib/horologium/version.rb +1 -1
- data/lib/horologium.rb +15 -1
- data/sig/horologium/configuration.rbs +32 -0
- data/sig/horologium/data/barycentric_model.rbs +19 -0
- data/sig/horologium/data/leap_seconds.rbs +11 -0
- data/sig/horologium/duration.rbs +50 -0
- data/sig/horologium/epochs.rbs +13 -0
- data/sig/horologium/instant.rbs +34 -5
- data/sig/horologium/numeric/exact.rbs +12 -0
- data/sig/horologium/numeric/precision.rbs +14 -0
- data/sig/horologium/numeric/two_part_float.rbs +19 -6
- data/sig/horologium/precise_value.rbs +1 -3
- data/sig/horologium/representations/civil.rbs +63 -0
- data/sig/horologium/representations/civil_time.rbs +49 -0
- data/sig/horologium/representations/iso8601.rbs +27 -0
- data/sig/horologium/representations/julian_date.rbs +27 -0
- data/sig/horologium/representations/modified_julian_date.rbs +15 -0
- data/sig/horologium/scale_reading.rbs +35 -0
- data/sig/horologium/scales/base.rbs +17 -0
- data/sig/horologium/scales/tai.rbs +9 -0
- data/sig/horologium/scales/tdb.rbs +11 -0
- data/sig/horologium/scales/tt.rbs +17 -0
- data/sig/horologium/scales/utc.rbs +53 -0
- data/sig/horologium.rbs +32 -0
- metadata +51 -9
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,84 @@
|
|
|
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
|
+
|
|
39
|
+
## 0.0.3 - 2026-08-22
|
|
40
|
+
|
|
41
|
+
The scale conversions arrive. An instant is now a point with no scale of its
|
|
42
|
+
own, given in one scale and read back in another, and it comes out as a Julian
|
|
43
|
+
Date, a calendar date, or an ISO 8601 string.
|
|
44
|
+
|
|
45
|
+
### Features
|
|
46
|
+
|
|
47
|
+
- Add time scales, each converting to and from TAI, the scale an instant is
|
|
48
|
+
stored in: `TAI`, `TT` at its fixed 32.184 s, `TDB` over the full 787-term
|
|
49
|
+
Fairhead and Bretagnon model, and `UTC` with its leap seconds
|
|
50
|
+
- Add `Instant#to`, which reads an instant in a scale, and `ScaleReading`,
|
|
51
|
+
which is that reading, with `#as` for the shape it comes out in
|
|
52
|
+
- Add the representations a reading comes out as: `julian_date`,
|
|
53
|
+
`modified_julian_date`, `civil`, and `iso8601`, each as a Float, a Rational,
|
|
54
|
+
or a two-part Float where that makes sense
|
|
55
|
+
- Add `Instant.from_julian_date`, `from_modified_julian_date`, `from_civil`,
|
|
56
|
+
and `from_iso8601`, each reading its value in a named scale
|
|
57
|
+
- Add `Instant.from_tai`, `from_tt`, `from_tdb`, and `from_utc`, the same
|
|
58
|
+
calendar constructor with the scale in the name
|
|
59
|
+
- Add `CivilTime`, the calendar fields a clock and a calendar show, in the
|
|
60
|
+
proleptic Gregorian calendar with astronomical year numbering
|
|
61
|
+
- Read and write a leap second as second 60, on the days that hold one, and
|
|
62
|
+
refuse it on the days that do not
|
|
63
|
+
- Add `ScaleReading#provenance`, `:measured` up to the date the leap second
|
|
64
|
+
data vouches for and `:extrapolated` after it, with
|
|
65
|
+
`Configuration#leap_second_horizon` to refuse an extrapolation instead
|
|
66
|
+
- Add `Configuration#register_scale`, so a caller can add a scale of its own,
|
|
67
|
+
and `Configuration#leap_second_source`, so it can supply its own leap seconds
|
|
68
|
+
- Add duration arithmetic: `+`, `-`, unary `-`, `zero?`, `negative?`,
|
|
69
|
+
`positive?`, and `to_r` and `to_f` in SI seconds
|
|
70
|
+
- Add value equality to `ScaleReading`, and `inspect` to `Instant`, `Duration`,
|
|
71
|
+
and `ScaleReading`
|
|
72
|
+
- Refuse a calendar reading before -4799, where the conversion stops, so a
|
|
73
|
+
reading out always reads back in
|
|
74
|
+
|
|
75
|
+
### Fixes
|
|
76
|
+
|
|
77
|
+
- Build the configuration under a lock, so two threads reaching it at once
|
|
78
|
+
cannot each build one and lose the other's scales
|
|
79
|
+
|
|
80
|
+
**Full Changelog**: https://github.com/rhannequin/horologium/compare/v0.0.2...v0.0.3
|
|
81
|
+
|
|
3
82
|
## 0.0.2 - 2026-07-14
|
|
4
83
|
|
|
5
84
|
The first functional release. It ships the numeric core and the two value
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/rhannequin/horologium/actions?query=workflow%3ACI)
|
|
4
4
|
|
|
5
5
|
Horologium is a Ruby library dedicated to **scientific time**: the time scales
|
|
6
|
-
(
|
|
6
|
+
(TAI, TT, TDB, and UTC so far), high-precision instants, Julian Dates,
|
|
7
7
|
intervals, and rigorous conversions between scales that astronomy and physics
|
|
8
8
|
require.
|
|
9
9
|
|
|
@@ -45,14 +45,173 @@ get the duration between them.
|
|
|
45
45
|
```rb
|
|
46
46
|
require "horologium"
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
a = Horologium::Instant.from_julian_date(2_460_000.5, scale: :tai)
|
|
49
|
+
b = Horologium::Instant.from_julian_date(2_460_001.5, scale: :tai)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
a + Horologium::Duration.days(1) == b # => true
|
|
52
|
+
a < b # => true
|
|
53
|
+
b - a == Horologium::Duration.days(1) # => true
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
An instant has no scale of its own. You give it a Julian Date read in a scale,
|
|
57
|
+
and you read it back in any scale the library knows: `to` chooses the scale, and
|
|
58
|
+
`as` chooses the shape it comes out in.
|
|
59
|
+
|
|
60
|
+
```rb
|
|
61
|
+
instant = Horologium::Instant.from_julian_date(2_443_144.5, scale: :tai)
|
|
62
|
+
|
|
63
|
+
instant.to(:tt).as(:julian_date) # => 2443144.5003725
|
|
64
|
+
instant.as(:modified_julian_date, scale: :tt) # => 43144.0003725
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A calendar date is a shape too, and the one a person reads. It comes out as a
|
|
68
|
+
`CivilTime`, whose fields are the ones a clock and a calendar show, in the
|
|
69
|
+
proleptic Gregorian calendar.
|
|
70
|
+
|
|
71
|
+
```rb
|
|
72
|
+
civil = instant.as(:civil, scale: :tt)
|
|
73
|
+
|
|
74
|
+
civil.year # => 1977
|
|
75
|
+
civil.month # => 1
|
|
76
|
+
civil.second # => 32
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can build an instant from those fields as well, and nothing is lost on the
|
|
80
|
+
way in: the date becomes a whole number of days and the time of day an exact
|
|
81
|
+
fraction of one. Give a fractional second as a `Rational` to say it exactly.
|
|
82
|
+
|
|
83
|
+
```rb
|
|
84
|
+
Horologium::Instant.from_civil(2025, 5, 1, 12, 0, 0, scale: :tt)
|
|
85
|
+
Horologium::Instant.from_civil(2025, 5, 1, 12, 0, Rational(1, 4), scale: :tt)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Each scale has its own shortcut, so the scale is in the name instead of a
|
|
89
|
+
keyword.
|
|
90
|
+
|
|
91
|
+
```rb
|
|
92
|
+
Horologium::Instant.from_tt(2025, 5, 1, 12, 0, 0)
|
|
93
|
+
Horologium::Instant.from_tai(2025, 5, 1, 12, 0, 0)
|
|
94
|
+
Horologium::Instant.from_tdb(2025, 5, 1, 12, 0, 0)
|
|
95
|
+
```
|
|
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
|
+
|
|
101
|
+
A date that does not exist is refused rather than rolled over, and the message
|
|
102
|
+
says which field is wrong.
|
|
103
|
+
|
|
104
|
+
```rb
|
|
105
|
+
Horologium::Instant.from_civil(1900, 2, 29, scale: :tt)
|
|
106
|
+
# => raises Horologium::InvalidCivilTimeError
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The same date and time write out as an extended ISO 8601 string, and read back
|
|
110
|
+
from one. The scale is not written into the string: there is no ISO 8601
|
|
111
|
+
designator for TAI or TT, and `Z` means UTC, so a bare time is a coordinate in
|
|
112
|
+
the scale you asked for.
|
|
113
|
+
|
|
114
|
+
```rb
|
|
115
|
+
instant.as(:iso8601, scale: :tt) # => "1977-01-01T00:00:32.184000000"
|
|
116
|
+
|
|
117
|
+
Horologium::Instant.from_iso8601("2025-05-01T12:00:00", scale: :tt)
|
|
118
|
+
Horologium::Instant.from_iso8601("2025-05-01", scale: :tt) # midnight
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The parser reads a strict subset: a calendar date, an optional time of day
|
|
122
|
+
after a `T`, a fraction of a second kept to every digit, and an optional `Z` or
|
|
123
|
+
numeric offset applied as plain arithmetic, not a time zone. A week date, an
|
|
124
|
+
ordinal date, or anything outside the subset is refused with a `ParseError`.
|
|
125
|
+
|
|
126
|
+
UTC is the scale of civil clocks, the one that holds a leap second now and
|
|
127
|
+
then to keep step with the Earth's rotation. `from_utc` reads a UTC date, and
|
|
128
|
+
a leap second is a legal reading: the second is 60 on a day that holds one, and
|
|
129
|
+
that moment really existed.
|
|
130
|
+
|
|
131
|
+
```rb
|
|
132
|
+
Horologium::Instant.from_utc(2025, 5, 1, 12, 0, 0)
|
|
133
|
+
|
|
134
|
+
leap = Horologium::Instant.from_utc(2016, 12, 31, 23, 59, 60)
|
|
135
|
+
leap.as(:iso8601, scale: :utc) # => "2016-12-31T23:59:60.000000000Z"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A leap second is a real second on the timeline, so the arithmetic is right
|
|
139
|
+
across it: the second before 23:59:60, the leap second, and the next midnight
|
|
140
|
+
are one SI second apart each. Second 60 on a day with no leap second is
|
|
141
|
+
refused.
|
|
142
|
+
|
|
143
|
+
```rb
|
|
144
|
+
before = Horologium::Instant.from_utc(
|
|
145
|
+
2016, 12, 31, 23, 59, 59,
|
|
146
|
+
precision: :exact
|
|
147
|
+
)
|
|
148
|
+
leap = Horologium::Instant.from_utc(
|
|
149
|
+
2016, 12, 31, 23, 59, 60,
|
|
150
|
+
precision: :exact
|
|
151
|
+
)
|
|
152
|
+
after = Horologium::Instant.from_utc(
|
|
153
|
+
2017, 1, 1, 0, 0, 0,
|
|
154
|
+
precision: :exact
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
leap - before == Horologium::Duration.seconds(1) # => true
|
|
158
|
+
after - leap == Horologium::Duration.seconds(1) # => true
|
|
159
|
+
|
|
160
|
+
Horologium::Instant.from_utc(2020, 6, 15, 23, 59, 60)
|
|
161
|
+
# => raises Horologium::InvalidCivilTimeError
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The `:exact` above is what makes `==` the right question to ask. At the default
|
|
165
|
+
`:standard` precision the same three instants land a rounding step apart, well
|
|
166
|
+
under a nanosecond but not zero, so compare those with `equal_within?`.
|
|
167
|
+
|
|
168
|
+
UTC runs from 1961, whole leap seconds from 1972 and the earlier
|
|
169
|
+
rate-adjustment drift before that, where a UTC second was fractionally longer
|
|
170
|
+
than an SI one. An earlier UTC date raises `Horologium::OutOfRangeError`. The
|
|
171
|
+
instant is still reachable, only its UTC label is not, so the error names the
|
|
172
|
+
continuous scales, which have no lower bound. The leap seconds and the drift
|
|
173
|
+
come from the [iers] gem, with no network access: the data ships with the gem.
|
|
53
174
|
|
|
54
|
-
|
|
55
|
-
|
|
175
|
+
```rb
|
|
176
|
+
Horologium::Instant.from_utc(1960, 12, 31) # => OutOfRangeError
|
|
177
|
+
Horologium::Instant.from_civil(1960, 12, 31, scale: :tt) # reaches any date
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Leap seconds are announced about six months ahead, so past the date its data
|
|
181
|
+
vouches for, the last known offset is the best there is. A UTC reading says
|
|
182
|
+
which it rests on: `:measured` up to that date, `:extrapolated` after, where a
|
|
183
|
+
leap second announced since would not be known.
|
|
184
|
+
|
|
185
|
+
```rb
|
|
186
|
+
instant.to(:utc).provenance # => :measured, or :extrapolated past the horizon
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A pipeline that must not rest on an offset a leap second could overturn sets a
|
|
190
|
+
strict horizon, and a reading past it raises instead.
|
|
191
|
+
|
|
192
|
+
```rb
|
|
193
|
+
Horologium.configure { |c| c.leap_second_horizon = :raise }
|
|
194
|
+
# => reading a date past the data horizon raises OutOfDataRangeError
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The configuration is set once, in a single `Horologium.configure` block, and
|
|
198
|
+
frozen when the block returns. See [Precision](#precision).
|
|
199
|
+
|
|
200
|
+
A Julian Date is around 2.46 million, which leaves a single `Float` about 40
|
|
201
|
+
microseconds for the fraction of a day, and the loss is already in the literal
|
|
202
|
+
before Horologium sees it. So the lossless shapes come first: a `String` and a
|
|
203
|
+
`Rational` say the Julian Date exactly, and a high and a low part say it to
|
|
204
|
+
about twice what one `Float` holds.
|
|
205
|
+
|
|
206
|
+
```rb
|
|
207
|
+
Horologium::Instant.from_julian_date("2456463.052272", scale: :tt)
|
|
208
|
+
Horologium::Instant.from_julian_date(
|
|
209
|
+
Rational(2_456_463_052_272, 1_000_000),
|
|
210
|
+
scale: :tt
|
|
211
|
+
)
|
|
212
|
+
Horologium::Instant.from_julian_date(2_456_463.0, 0.052272, scale: :tt)
|
|
213
|
+
|
|
214
|
+
Horologium::Instant.from_modified_julian_date(60_796.0, scale: :tai)
|
|
56
215
|
```
|
|
57
216
|
|
|
58
217
|
A `Duration` counts SI seconds, so `Duration.days(1)` is always 86,400 SI
|
|
@@ -63,6 +222,45 @@ so a duration and a calendar day are different things.
|
|
|
63
222
|
Horologium::Duration.days(1) == Horologium::Duration.seconds(86_400) # => true
|
|
64
223
|
Horologium::Duration.nanoseconds(1_000_000_000) ==
|
|
65
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)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Durations add, subtract, and negate among themselves, and read back out in SI
|
|
242
|
+
seconds.
|
|
243
|
+
|
|
244
|
+
```rb
|
|
245
|
+
Horologium::Duration.seconds(30) + Horologium::Duration.seconds(12)
|
|
246
|
+
Horologium::Duration.seconds(30) - Horologium::Duration.seconds(42) # negative
|
|
247
|
+
-Horologium::Duration.seconds(3)
|
|
248
|
+
|
|
249
|
+
Horologium::Duration.days(1).to_r # => (86400/1), the whole value
|
|
250
|
+
Horologium::Duration.days(1).to_f # => 86400.0
|
|
251
|
+
```
|
|
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)
|
|
66
264
|
```
|
|
67
265
|
|
|
68
266
|
Adding a duration to an instant makes sense, but adding two instants together
|
|
@@ -72,11 +270,35 @@ does not, so it raises an error.
|
|
|
72
270
|
instant + instant # => raises Horologium::DimensionalError
|
|
73
271
|
```
|
|
74
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
|
+
|
|
75
297
|
Exact equality is rarely what scientific code wants, so you can compare within a
|
|
76
298
|
tolerance:
|
|
77
299
|
|
|
78
300
|
```rb
|
|
79
|
-
a = Horologium::Instant.
|
|
301
|
+
a = Horologium::Instant.from_julian_date(2_460_000.5, scale: :tai)
|
|
80
302
|
near = a + Horologium::Duration.nanoseconds(1)
|
|
81
303
|
|
|
82
304
|
a.equal_within?(near, Horologium::Duration.nanoseconds(2)) # => true
|
|
@@ -94,35 +316,60 @@ with ordinary floating-point arithmetic.
|
|
|
94
316
|
|
|
95
317
|
Every value carries one of two precisions, fixed when it is built:
|
|
96
318
|
|
|
97
|
-
- `:standard`, the default, keeps the value as a two-part float. It
|
|
98
|
-
|
|
319
|
+
- `:standard`, the default, keeps the value as a two-part float. It stays
|
|
320
|
+
within a few nanoseconds of the true value.
|
|
99
321
|
- `:exact` keeps the value as a `Rational`, with no rounding. The test suite
|
|
100
322
|
uses it to check that `:standard` stays within its stated precision.
|
|
101
323
|
|
|
102
|
-
Set the default once at boot
|
|
324
|
+
Set the default once at boot. `Horologium.configure` freezes the configuration
|
|
325
|
+
when its block returns, so it is called once and everything is set in the one
|
|
326
|
+
block. A second call raises `Horologium::ConfigurationError`.
|
|
103
327
|
|
|
104
328
|
```rb
|
|
105
329
|
Horologium.configure do |c|
|
|
106
330
|
c.default_precision = :exact
|
|
331
|
+
c.leap_second_horizon = :raise
|
|
107
332
|
end
|
|
108
333
|
```
|
|
109
334
|
|
|
110
335
|
Choose it for a single value, or for a scoped block:
|
|
111
336
|
|
|
112
337
|
```rb
|
|
113
|
-
Horologium::Instant.
|
|
338
|
+
Horologium::Instant.from_julian_date(
|
|
339
|
+
2_460_000.5,
|
|
340
|
+
scale: :tai,
|
|
341
|
+
precision: :exact
|
|
342
|
+
)
|
|
114
343
|
|
|
115
344
|
Horologium.with_precision(:exact) do
|
|
116
345
|
# instants and durations built here default to :exact
|
|
117
346
|
end
|
|
118
347
|
```
|
|
119
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
|
+
|
|
120
356
|
Exactness is contagious. An operation between two `:standard` values stays
|
|
121
357
|
`:standard`. Mixing a `:standard` and an `:exact` value gives an `:exact`
|
|
122
358
|
result, so precision is not quietly lost. `:exact` guarantees the arithmetic
|
|
123
359
|
Horologium performs. It cannot bring back precision that an input already lost
|
|
124
360
|
when it was built.
|
|
125
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
|
+
|
|
126
373
|
## Status
|
|
127
374
|
|
|
128
375
|
This library is in early development, before its first public release. The
|
|
@@ -134,8 +381,18 @@ changes until a 1.0 release. Changes are documented in the [CHANGELOG].
|
|
|
134
381
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
135
382
|
`rake` to run the tests and RuboCop, or `rake steep` to type-check the
|
|
136
383
|
signatures in `sig/`. Run `COVERAGE=true rake test` to measure test coverage,
|
|
137
|
-
which is enforced at
|
|
138
|
-
for an interactive prompt that will allow you to experiment.
|
|
384
|
+
which is enforced at 100% of lines and branches in CI. You can also run
|
|
385
|
+
`bin/console` for an interactive prompt that will allow you to experiment.
|
|
386
|
+
|
|
387
|
+
`sig/` holds Horologium's own signatures and ships with the gem. `sig-vendor/`
|
|
388
|
+
holds stubs for gems that ship none of their own, and stays out of the gem so
|
|
389
|
+
it cannot clash with a downstream RBS collection.
|
|
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.
|
|
139
396
|
|
|
140
397
|
Run `bin/ci` to run every check that GitHub Actions runs (RuboCop, Steep, YARD
|
|
141
398
|
documentation coverage, and the tests with coverage) in a single pass. It runs
|