Class: Horologium::Scales::UTC

Inherits:
Base
  • Object
show all
Defined in:
lib/horologium/scales/utc.rb,
sig/horologium/scales/utc.rbs

Overview

Coordinated Universal Time, the civil scale of clocks and calendars. It keeps close to the Earth's rotation by holding a leap second now and then, so a UTC day is usually 86,400 SI seconds but 86,401 on a day that gains one. TAI runs without them, so the gap between the two, TAI - UTC, steps up by a second at each.

A UTC Julian Date is not uniform time: a day gaining a leap second still spans one unit of it, so its fraction is stretched over 86,401 seconds. This is the convention ERFA uses, and it is why Representations::Civil asks the scale how long the day is. The conversion to and from TAI reads the leap seconds from Data::LeapSeconds, and does the arithmetic at the instant's precision, so a UTC to TAI round trip is exact at :exact and within a nanosecond at :standard.

UTC runs from 1961-01-01, the start of the published TAI - UTC series. From 1972 it steps by whole leap seconds. From 1961 to 1972 it was steered by rate adjustments instead: TAI - UTC drifts by a fraction of a second a day, so a UTC second there is fractionally longer than an SI second and the civil clock still counts 86,400 of them a day, with no second 60. Data::LeapSeconds reads both regimes. A reading before 1961 raises OutOfRangeError and names the continuous scales, which reach any date.

Examples:

A leap second is read and written as second 60

instant = Horologium::Instant.from_utc(2016, 12, 31, 23, 59, 60)
instant.as(:iso8601, scale: :utc)
# => "2016-12-31T23:59:60.000000000Z"

Constant Summary collapse

FIRST_DAY =

The Julian Day Number of 1961-01-01, the first day the published TAI - UTC series covers. A reading on an earlier day is refused.

Returns:

  • (Integer)
2_437_301
HALF =

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 a day starts at.

Returns:

  • (Rational)
Rational(1, 2)
MAX_STEPS =

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 refinement takes at most one step, since a guess from the TAI date is at most a day out. Two more are room to spare before a moment is taken as out of range.

Returns:

  • (Integer)
3

Class Method Summary collapse

Class Method Details

.from_reference(value, precision) ⇒ Horologium::Numeric::TwoPartFloat, Horologium::Numeric::Exact

A TAI Julian Date, read in UTC. It finds the UTC day the instant falls in, then takes the fraction through that day back off the leap second spread and the drift, so it reads as a plain time of day. This inverts to_reference in closed form, exactly at :exact.

Parameters:

Returns:

Raises:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/horologium/scales/utc.rb', line 66

def from_reference(value, precision)
  day = (value.to_r + HALF).floor

  MAX_STEPS.times do
    break if day < FIRST_DAY

    if before_midnight?(value, day, precision)
      day -= 1
    elsif !before_midnight?(value, day + 1, precision)
      day += 1
    else
      enforce_horizon(day)
      return Numeric::Precision.add(
        Numeric::Precision.build(day - HALF, precision),
        unstretch(value, day, precision)
      )
    end
  end

  refuse
end

.provenance(value) ⇒ Symbol

How well founded a UTC reading is. :measured up to the date the leap second data vouches for, :extrapolated past it, where the offset is the last known one and a new leap second could overturn it. A source that states no expiry is taken as :measured throughout, since there is no horizon to be past.

Parameters:

Returns:

  • (Symbol)

    :measured or :extrapolated



168
169
170
# File 'lib/horologium/scales/utc.rb', line 168

def provenance(value)
  past_horizon?((value.to_r + HALF).floor) ? :extrapolated : :measured
end

.seconds_in_day(day_number) ⇒ Integer

The seconds the civil clock counts in a UTC day: 86,400, and 86,401 on a day that holds a whole leap second, where the last minute reaches second 60. Before 1972 the day is always 86,400 civil seconds; the drift in TAI - UTC there is spread across the day as slightly longer seconds, not shown as an extra one. The conversion to and from TAI uses the SI length instead, which the drift stretches.

Parameters:

  • day_number (Integer)

    the Julian Day Number of the day

Returns:

  • (Integer)

    the civil seconds in that day

Raises:



129
130
131
132
133
# File 'lib/horologium/scales/utc.rb', line 129

def seconds_in_day(day_number)
  refuse unless day_number >= FIRST_DAY

  Duration::SECONDS_PER_DAY + whole_leap_seconds(day_number)
end

.si_seconds_in_day(day_number) ⇒ Integer, Rational

The SI seconds a UTC day spans: 86,400, one more on a day that holds a leap second, and a fraction more through the pre-1972 drift. It is the length the conversion to TAI stretches the day over, where seconds_in_day is the whole count the civil clock shows. A numeric ISO 8601 offset counts against this, so it shifts by SI seconds even on a drift day.

Parameters:

  • day_number (Integer)

    the Julian Day Number of the day

Returns:

  • (Integer, Rational)

    the SI seconds in that day

Raises:



145
146
147
148
149
150
# File 'lib/horologium/scales/utc.rb', line 145

def si_seconds_in_day(day_number)
  refuse unless day_number >= FIRST_DAY

  day_scale(day_number, tai_utc_at(day_number)) *
    Duration::SECONDS_PER_DAY
end

.to_reference(value, precision) ⇒ Horologium::Numeric::TwoPartFloat, Horologium::Numeric::Exact

A UTC Julian Date, read back in TAI. It takes the fraction through the UTC day, spreads it over the day's real length with the leap second and the drift included, and adds it to the TAI of that day's 0h. This is the conversion ERFA performs in eraUtctai.

Parameters:

Returns:

Raises:



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/horologium/scales/utc.rb', line 101

def to_reference(value, precision)
  day = (value.to_r + HALF).floor
  refuse unless day >= FIRST_DAY
  enforce_horizon(day)

  dat0 = tai_utc_at(day)
  scaled = day_fraction(value, day, precision) * day_scale(day, dat0)

  Numeric::Precision.add(
    Numeric::Precision.build(day - HALF, precision),
    Numeric::Precision.add(
      scaled,
      Numeric::Precision.build(dat0, precision) /
        Duration::SECONDS_PER_DAY
    )
  )
end

.zone_designatorString

UTC writes Z, where a zero offset is a real thing.

Returns:

  • (String)


155
156
157
# File 'lib/horologium/scales/utc.rb', line 155

def zone_designator
  "Z"
end