Class: Horologium::Instant

Inherits:
Object
  • Object
show all
Includes:
PreciseValue
Defined in:
lib/horologium/instant.rb,
sig/horologium/instant.rbs

Overview

A single point on the timeline, independent of any scale. It is stored as a TAI Julian Date, in days, at a fixed precision.

An Instant is built from a Julian Date read in a scale, and can be read back in any scale the library knows: a scale is what turns a number into a point, and the point itself has none.

An Instant is frozen. Its precision is set when it is built, from the precision in effect unless you pass one. At :standard the Julian Date is a Numeric::TwoPartFloat, at :exact a Numeric::Exact.

You can add or subtract a Duration, and subtract another Instant to get the Duration between them. Adding two instants raises DimensionalError. Mixing a :standard and an :exact operand gives an :exact result.

Examples:

Shift an instant, then measure back to it

instant = Horologium::Instant.from_julian_date(2_460_000.5, scale: :tai)
later = instant + Horologium::Duration.seconds(3600)
(later - instant) == Horologium::Duration.seconds(3600)
# => true

Instance Attribute Summary

Attributes included from PreciseValue

#precision, #rational, #value

Class Method Summary collapse

Instance Method Summary collapse

Methods included from PreciseValue

#<=>, #eql?, #hash, #initialize

Class Method Details

.from_civil(civil, scale:, precision:) ⇒ Instant .from_civil(year, month, day, hour, minute, second, scale:, precision:) ⇒ Instant

Builds an instant from a calendar date and a time of day read in a scale. Nothing is lost: the date becomes a whole number of days and the time of day an exact fraction of one, so this is an exact way to build an instant where a Julian Date given as a single Float is not.

The second may carry a fraction under it. Give that fraction as a Rational to say it exactly; a Float second says only what a Float holds, which at this magnitude is far more than a clock reads.

A Representations::CivilTime may be passed on its own, which is what a reading taken with as(:civil) returns, so a civil time reads back into the instant it came from.

Examples:

A fractional second, said exactly

Horologium::Instant.from_civil(
  2025, 5, 1, 12, 0, Rational(1, 4), scale: :tt
)

A civil time, read back into the instant it came from

civil = instant.as(:civil, scale: :tt, as: :rational)
Horologium::Instant.from_civil(civil, scale: :tt) == instant # => true

Overloads:

  • .from_civil(civil, scale:, precision:) ⇒ Instant

    Parameters:

    Returns:

  • .from_civil(year, month, day, hour, minute, second, scale:, precision:) ⇒ Instant

    Parameters:

    • year (Integer)
    • month (Integer)
    • day (Integer)
    • hour (Integer)
    • minute (Integer)
    • second (Representations::Civil::second)
    • scale: (Symbol)
    • precision: (Symbol)

    Returns:

Parameters:

  • year (Integer, Horologium::Representations::CivilTime)

    the year, or a civil time holding every field

  • month (Integer, nil) (defaults to: nil)

    the month, from 1 to 12

  • day (Integer, nil) (defaults to: nil)

    the day of the month

  • hour (Integer) (defaults to: 0)

    the hour, from 0 to 23

  • minute (Integer) (defaults to: 0)

    the minute, from 0 to 59

  • second (Integer, Float, Rational) (defaults to: 0)

    the second, whole or with a fraction under it

  • scale (Symbol)

    the scale it is read in, such as :tt

  • precision (Symbol) (defaults to: Horologium.current_precision)

    :standard or :exact, taken from the precision in effect when omitted

Returns:

Raises:



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/horologium/instant.rb', line 148

def from_civil(
  year,
  month = nil,
  day = nil,
  hour = 0,
  minute = 0,
  second = 0,
  scale:,
  precision: Horologium.current_precision
)
  from_representation(
    Representations::Civil,
    civil_time(year, month, day, hour, minute, second),
    nil,
    scale,
    precision
  )
end

.from_iso8601(value, scale:, precision: Horologium.current_precision) ⇒ Horologium::Instant

Builds an instant from an ISO 8601 date and time read in a scale. The string is read in the strict subset Representations::Iso8601 parses: a calendar date, an optional time of day after a T down to a fraction of a second, and an optional Z or numeric offset. A date on its own is midnight in the scale.

The string names no scale of its own, so scale says which one it is read in. A numeric offset is subtracted as plain arithmetic, not a time zone: it consults no zone data, and Z is a zero offset.

Examples:

A numeric offset, subtracted to reach the scale

Horologium::Instant.from_iso8601(
  "2025-05-01T13:00:00+01:00",
  scale: :tt
)

Parameters:

  • value (String)

    the date and time, in extended ISO 8601

  • scale (Symbol)

    the scale it is read in, such as :tt

  • precision (Symbol) (defaults to: Horologium.current_precision)

    :standard or :exact, taken from the precision in effect when omitted

  • scale: (Symbol)
  • precision: (Symbol) (defaults to: Horologium.current_precision)

Returns:

Raises:



320
321
322
323
324
325
326
327
328
# File 'lib/horologium/instant.rb', line 320

def from_iso8601(value, scale:, precision: Horologium.current_precision)
  from_representation(
    Representations::Iso8601,
    value,
    nil,
    scale,
    precision
  )
end

.from_julian_date(value, low = nil, scale:, precision: Horologium.current_precision) ⇒ Horologium::Instant

Builds an instant from a Julian Date read in a scale. The Julian Date is read back in TAI, the scale an instant is stored in, so a point given in one scale can be read in another.

The lossless shapes come first: a String and a Rational say the Julian Date exactly, and a high and a low Float say it to about twice what one Float holds. A single Float is the lossy one, worth about 40 microseconds at a modern date, and the loss is already in the literal by the time the library sees it. See Representations::JulianDate.parse.

Examples:

The same instant, given in TT and read back in TAI

instant = Horologium::Instant.from_julian_date(
  "2443144.5003725",
  scale: :tt,
  precision: :exact
)
instant.as(:julian_date, scale: :tai) # => 2443144.5

A Julian Date given as a high and a low part

Horologium::Instant.from_julian_date(
  2_456_463.0,
  0.052272,
  scale: :tt
)

Parameters:

  • value (String, Rational, Integer, Float)

    the Julian Date, in days, or its high part when a low part follows

  • low (Float, Integer, nil) (defaults to: nil)

    the low part of the Julian Date, in days

  • scale (Symbol)

    the scale the Julian Date is read in, such as :tt

  • precision (Symbol) (defaults to: Horologium.current_precision)

    :standard or :exact, taken from the precision in effect when omitted

  • scale: (Symbol)
  • precision: (Symbol) (defaults to: Horologium.current_precision)

Returns:

Raises:

  • (UnknownScaleError)

    when no scale is registered under that name

  • (ParseError)

    when a String does not spell a Julian Date

  • (ArgumentError)

    when the Julian Date is none of the shapes above

  • (UnknownPrecisionError)

    when the precision is not recognised



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/horologium/instant.rb', line 65

def from_julian_date(
  value,
  low = nil,
  scale:,
  precision: Horologium.current_precision
)
  from_representation(
    Representations::JulianDate,
    value,
    low,
    scale,
    precision
  )
end

.from_modified_julian_date(value, low = nil, scale:, precision: Horologium.current_precision) ⇒ Horologium::Instant

Builds an instant from a Modified Julian Date read in a scale. It is the Julian Date counted from a later origin, and it is given in the same shapes as from_julian_date.

Parameters:

  • value (String, Rational, Integer, Float)

    the Modified Julian Date, in days, or its high part when a low part follows

  • low (Float, Integer, nil) (defaults to: nil)

    the low part, in days

  • scale (Symbol)

    the scale it is read in, such as :tt

  • precision (Symbol) (defaults to: Horologium.current_precision)

    :standard or :exact, taken from the precision in effect when omitted

  • scale: (Symbol)
  • precision: (Symbol) (defaults to: Horologium.current_precision)

Returns:

Raises:



96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/horologium/instant.rb', line 96

def from_modified_julian_date(
  value,
  low = nil,
  scale:,
  precision: Horologium.current_precision
)
  from_representation(
    Representations::ModifiedJulianDate,
    value,
    low,
    scale,
    precision
  )
end

.from_tai(civil, precision:) ⇒ Instant .from_tai(year, month, day, hour, minute, second, precision:) ⇒ Instant

Builds an instant from a TAI calendar date and time. It is from_civil read in TAI, the continuous scale the library stores instants in.

The fields are from_civil's, and a Representations::CivilTime may be passed on its own.

Overloads:

  • .from_tai(civil, precision:) ⇒ Instant

    Parameters:

    Returns:

  • .from_tai(year, month, day, hour, minute, second, precision:) ⇒ Instant

    Parameters:

    • year (Integer)
    • month (Integer)
    • day (Integer)
    • hour (Integer)
    • minute (Integer)
    • second (Representations::Civil::second)
    • precision: (Symbol)

    Returns:

Returns:

Raises:

See Also:



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/horologium/instant.rb', line 179

def from_tai(
  year,
  month = nil,
  day = nil,
  hour = 0,
  minute = 0,
  second = 0,
  precision: Horologium.current_precision
)
  from_representation(
    Representations::Civil,
    civil_time(year, month, day, hour, minute, second),
    nil,
    :tai,
    precision
  )
end

.from_tdb(civil, precision:) ⇒ Instant .from_tdb(year, month, day, hour, minute, second, precision:) ⇒ Instant

Builds an instant from a TDB calendar date and time. It is from_civil read in TDB, the scale the planetary ephemerides are written in.

The fields are from_civil's, and a Representations::CivilTime may be passed on its own.

Overloads:

  • .from_tdb(civil, precision:) ⇒ Instant

    Parameters:

    Returns:

  • .from_tdb(year, month, day, hour, minute, second, precision:) ⇒ Instant

    Parameters:

    • year (Integer)
    • month (Integer)
    • day (Integer)
    • hour (Integer)
    • minute (Integer)
    • second (Representations::Civil::second)
    • precision: (Symbol)

    Returns:

Returns:

Raises:

See Also:



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/horologium/instant.rb', line 239

def from_tdb(
  year,
  month = nil,
  day = nil,
  hour = 0,
  minute = 0,
  second = 0,
  precision: Horologium.current_precision
)
  from_representation(
    Representations::Civil,
    civil_time(year, month, day, hour, minute, second),
    nil,
    :tdb,
    precision
  )
end

.from_tt(civil, precision:) ⇒ Instant .from_tt(year, month, day, hour, minute, second, precision:) ⇒ Instant

Builds an instant from a TT calendar date and time. It is from_civil read in TT, the scale the theories of the solar system motion are written in.

The fields are from_civil's, and a Representations::CivilTime may be passed on its own.

Overloads:

  • .from_tt(civil, precision:) ⇒ Instant

    Parameters:

    Returns:

  • .from_tt(year, month, day, hour, minute, second, precision:) ⇒ Instant

    Parameters:

    • year (Integer)
    • month (Integer)
    • day (Integer)
    • hour (Integer)
    • minute (Integer)
    • second (Representations::Civil::second)
    • precision: (Symbol)

    Returns:

Returns:

Raises:

See Also:



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/horologium/instant.rb', line 209

def from_tt(
  year,
  month = nil,
  day = nil,
  hour = 0,
  minute = 0,
  second = 0,
  precision: Horologium.current_precision
)
  from_representation(
    Representations::Civil,
    civil_time(year, month, day, hour, minute, second),
    nil,
    :tt,
    precision
  )
end

.from_utc(civil, precision:) ⇒ Instant .from_utc(year, month, day, hour, minute, second, precision:) ⇒ Instant

Builds an instant from a UTC calendar date and time. It is from_civil read in UTC, the scale of civil clocks, so a leap second is a legal reading: the second may be 60 on a day that holds one.

UTC runs from 1961-01-01, whole leap seconds from 1972 and the earlier rate-adjustment drift before that. An earlier date raises OutOfRangeError and names the continuous constructors, which reach any date.

The fields are from_civil's.

Examples:

The 2016 leap second, a moment that existed

Horologium::Instant.from_utc(2016, 12, 31, 23, 59, 60)

Overloads:

  • .from_utc(civil, precision:) ⇒ Instant

    Parameters:

    Returns:

  • .from_utc(year, month, day, hour, minute, second, precision:) ⇒ Instant

    Parameters:

    • year (Integer)
    • month (Integer)
    • day (Integer)
    • hour (Integer)
    • minute (Integer)
    • second (Representations::Civil::second)
    • precision: (Symbol)

    Returns:

Returns:

Raises:

See Also:



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/horologium/instant.rb', line 276

def from_utc(
  year,
  month = nil,
  day = nil,
  hour = 0,
  minute = 0,
  second = 0,
  precision: Horologium.current_precision
)
  from_representation(
    Representations::Civil,
    civil_time(year, month, day, hour, minute, second),
    nil,
    :utc,
    precision
  )
end

Instance Method Details

#+(duration) ⇒ Horologium::Instant

Adds a duration and returns a later instant.

Parameters:

Returns:

Raises:



386
387
388
389
390
391
392
393
394
395
396
# File 'lib/horologium/instant.rb', line 386

def +(duration) # rubocop:disable Naming/BinaryOperatorParameterName
  unless duration.is_a?(Duration)
    raise DimensionalError,
      "cannot add a #{duration.class} to an Instant; " \
      "only a Duration shifts an Instant"
  end

  precision = Numeric::Precision.resolve(self.precision, duration.precision)
  days = seconds_to_days(duration, precision)
  self.class.new(Numeric::Precision.add(value, days), precision)
end

#-(arg0) ⇒ Instant #-(arg0) ⇒ Duration

Subtracts a duration to get an earlier instant, or another instant to get the Duration between them.

Examples:

The Duration between two instants

a = Horologium::Instant.from_julian_date(2_460_000.5, scale: :tai)
b = Horologium::Instant.from_julian_date(2_460_001.5, scale: :tai)
b - a == Horologium::Duration.days(1) # => true

Overloads:

Parameters:

Returns:

Raises:



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/horologium/instant.rb', line 408

def -(other)
  case other
  when Duration
    precision = Numeric::Precision.resolve(self.precision, other.precision)
    days = seconds_to_days(other, precision)
    self.class.new(
      Numeric::Precision.subtract(value, days),
      precision
    )
  when Instant
    precision = Numeric::Precision.resolve(self.precision, other.precision)
    gap = Numeric::Precision.subtract(value, other.value)
    Duration.new(gap * Duration::SECONDS_PER_DAY, precision)
  else
    raise DimensionalError,
      "cannot subtract a #{other.class} from an Instant; " \
      "subtract a Duration or another Instant"
  end
end

#as(representation, scale:, as: :float) ⇒ Object

The instant in a representation, read in a scale. This is the shorthand for to(scale).as(representation).

Parameters:

  • representation (Symbol)

    the representation, such as :julian_date

  • scale (Symbol)

    the name of a registered scale, such as :tt

  • as (Symbol) (defaults to: :float)

    the type to come out as

  • scale: (Symbol)
  • as: (Symbol) (defaults to: :float)

Returns:

  • (Object)

    the instant, in that representation

Raises:



461
462
463
# File 'lib/horologium/instant.rb', line 461

def as(representation, scale:, as: :float)
  to(scale).as(representation, as: as)
end

#equal_within?(other, tolerance) ⇒ Boolean

Whether two instants fall within a tolerance of each other. Use this rather than == in scientific code.

Parameters:

Returns:

  • (Boolean)


471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/horologium/instant.rb', line 471

def equal_within?(other, tolerance)
  unless other.is_a?(Instant)
    raise DimensionalError,
      "cannot compare an Instant with a #{other.class}"
  end
  unless tolerance.is_a?(Duration)
    raise DimensionalError,
      "a tolerance must be a Duration, got a #{tolerance.class}"
  end

  (self - other).abs <= tolerance
end

#inspectString

The stored TAI Julian Date, so inspecting an instant needs no scale and no date the calendar conversion has to reach.

Returns:

  • (String)


488
489
490
# File 'lib/horologium/instant.rb', line 488

def inspect
  format("#<%s %s TAI JD (%s)>", self.class, value.to_f, precision)
end

#seconds_to_days(duration, precision) ⇒ Horologium::Numeric::TwoPartFloat, Horologium::Numeric::Exact

The duration's seconds counted in days, at the given precision. A Julian Date counts days, so a duration is scaled before it is added.

Parameters:

Returns:



500
501
502
503
# File 'lib/horologium/instant.rb', line 500

def seconds_to_days(duration, precision)
  Numeric::Precision.coerce(duration.value, to: precision) /
    Duration::SECONDS_PER_DAY
end

#to(scale) ⇒ Horologium::ScaleReading

The instant read in a time scale. An instant has no scale of its own, so the scale is chosen here. Take the representation from the reading it returns.

Parameters:

  • scale (Symbol)

    the name of a registered scale, such as :tt

Returns:

Raises:

  • (UnknownScaleError)

    when no scale is registered under that name

  • (OutOfRangeError)

    when the scale does not reach the instant, such as UTC before 1972

  • (OutOfDataRangeError)

    when UTC is past the leap second data horizon and leap_second_horizon is :raise



439
440
441
442
443
444
445
446
447
448
449
# File 'lib/horologium/instant.rb', line 439

def to(scale)
  time_scale = Horologium.configuration.scale(scale)
  reading = time_scale.from_reference(value, precision)

  ScaleReading.new(
    scale,
    reading,
    precision,
    time_scale.provenance(reading)
  )
end