Class: Horologium::Representations::Iso8601

Inherits:
Object
  • Object
show all
Defined in:
lib/horologium/representations/iso8601.rb,
sig/horologium/representations/iso8601.rbs

Overview

An instant written as an extended ISO 8601 date and time, in the scale it is read in: 2025-05-01T12:00:00.000000000. It is a formatting of the calendar date Civil reads, so the two agree on every field, and the string is the shape a log, a fixture, or another tool reads.

The scale is not written into the string. There is no ISO 8601 designator for TAI or TT, and Z means UTC, so a bare time here is a coordinate in the scale you asked for, not a claim about which scale that is. UTC writes the Z that belongs to it, where a leap second and a zero offset are real; the continuous scales carry neither.

The fraction of a second is written to nanosecond resolution, nine digits, the resolution the example in the design carries. That is display resolution, not the whole of what an instant holds: for the exact value, read it as a JulianDate or a Civil with as: :rational. On the way in, the parser keeps every digit it is given, unbounded at :exact, so a string says as much as it likes and nothing is dropped before the library.

Examples:

instant = Horologium::Instant.from_julian_date(2_443_144.5, scale: :tai)
instant.as(:iso8601, scale: :tai) # => "1977-01-01T00:00:00.000000000"
instant.as(:iso8601, scale: :tt)  # => "1977-01-01T00:00:32.184000000"

Constant Summary collapse

PATTERN =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

The strict subset of ISO 8601 the parser reads: a full calendar date, and an optional time of day after a T, down to an optional fraction of a second and an optional Z or numeric offset. The year is four digits or more, with a minus sign for a year before 1. A numeric offset runs from -23:59 to +23:59. Anything outside that shape is refused rather than read part way: a week date, an ordinal date, a bare hour, a comma for the decimal point, a space for the T, an offset out of range.

Returns:

  • (Regexp)
/
  \A
  (?<year>-?\d{4,})-(?<month>\d{2})-(?<day>\d{2})
  (?:
    T
    (?<hour>\d{2}):(?<minute>\d{2})
    (?::(?<second>\d{2})(?:\.(?<fraction>\d+))?)?
    (?<zone>Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)?
  )?
  \z
/x
HALF_DAY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Half a day, the gap between a Julian Date, which starts at noon, and the midnight the calendar counts a day from.

Returns:

  • (Rational)
Rational(1, 2)

Class Method Summary collapse

Class Method Details

.parse(value, _low, scale, precision) ⇒ Horologium::Numeric::TwoPartFloat, Horologium::Numeric::Exact

An ISO 8601 string, read as a Julian Date in days, at the precision asked for. This is the way in, where render is the way out. The Julian Date is in the scale the string is read in; it is Instant.from_iso8601 that reads it back in TAI.

Nothing is dropped: the date and time become an exact fraction of a day, and every digit of the fraction of a second is kept, unbounded at :exact. A numeric offset is subtracted here, in the scale, as plain arithmetic on the fields; it is not a time zone and consults no zone data. It counts against the day's SI length, so it shifts by SI seconds on a leap second day and through the pre-1972 drift alike, not by a stretched fraction of the day.

Examples:

Horologium::Representations::Iso8601.parse(
  "2016-12-31T23:59:59.5Z",
  nil,
  Horologium::Scales::TAI,
  :exact
)

Parameters:

  • value (String)

    the date and time, in extended ISO 8601

  • _low (nil)

    unused; an ISO 8601 string has no low part

  • scale (Class)

    the scale the string is read in, passed on to Civil.parse to place a leap second

  • precision (Symbol)

    :standard or :exact

Returns:

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/horologium/representations/iso8601.rb', line 137

def parse(value, _low, scale, precision)
  fields = fields(value)
  in_scale = Civil.parse(fields.fetch(:civil), nil, scale, precision)
  offset_seconds = fields.fetch(:offset_seconds)
  return in_scale if offset_seconds.zero?

  day = (in_scale.to_r + HALF_DAY).floor
  Numeric::Precision.subtract(
    in_scale,
    Numeric::Precision.build(
      Rational(offset_seconds, scale.si_seconds_in_day(day)),
      precision
    )
  )
end

.render(reading, _output = :string) ⇒ String

The reading, written as an ISO 8601 string. The as type a Julian Date chooses does not apply here: an ISO 8601 reading is always a String, so the type is ignored.

The instant is rounded to the nearest nanosecond first, so a fraction that would round up to a whole second carries into the clock before the fields are read, and the fields never show a time that does not exist.

Examples:

instant = Horologium::Instant.from_julian_date(
  2_443_144.5,
  scale: :tai
)
instant.to(:tai).as(:iso8601) # => "1977-01-01T00:00:00.000000000"

Parameters:

  • reading (Horologium::ScaleReading)

    the instant, read in a scale

  • _output (Symbol) (defaults to: :string)

    ignored; an ISO 8601 reading is a String

Returns:

  • (String)

    the date and time, in extended ISO 8601

Raises:



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/horologium/representations/iso8601.rb', line 81

def render(reading, _output = :string)
  civil = Civil.render(nanosecond_reading(reading), :rational)
  nanoseconds =
    (civil.second_fraction * Duration::NANOSECONDS_PER_SECOND).round

  # A continuous scale writes no designator, so a bare time is a
  # coordinate in the scale it was read in; UTC writes "Z".
  designator = Horologium
    .configuration
    .scale(reading.scale)
    .zone_designator

  format(
    "%<date>sT%<hour>02d:%<minute>02d:%<second>02d.%<nanoseconds>09d" \
    "%<designator>s",
    date: date(civil),
    hour: civil.hour,
    minute: civil.minute,
    second: civil.second,
    nanoseconds: nanoseconds,
    designator: designator
  )
end