Module: CATimeGrid

Defined in:
lib/carray/time.rb

Overview

Places a bucket grid on a storage resolution: resolves a timestep into whole storage ticks, resolves an origin into a tick count (or a month ordinal on the calendar path), and guards both against int64 overflow. Internal to the time surface: nothing here is part of the public API.

Constant Summary collapse

SU_LE_DAY =

Base units that are day-or-finer (a period head is representable exactly; the civil path targets these for a calendar bucket).

%i[D h m s ms us ns ps fs as].freeze
INT64_MIN =

Smallest value an int64 storage tick can hold.

-(2**63)
# Largest value an int64 storage tick can hold.
INT64_MAX =

Largest value an int64 storage tick can hold.

2**63 - 1

Class Method Summary collapse

Class Method Details

.calendar_bucket_head_ticks(k, st, storage_res, origin) ⇒ Object

Storage ticks of the bucket head at timestep k (int64 CArray) for a calendar bucket: ym0 + k*count months -> day 1 of that month -> ticks.



1960
1961
1962
1963
1964
1965
1966
1967
1968
# File 'lib/carray/time.rb', line 1960

def calendar_bucket_head_ticks(k, st, storage_res, origin)
  tpd   = ticks_per_day(storage_res)
  count = st.count * (st.base == :Y ? 12 : 1)
  ym0   = origin_month_ordinal(origin)
  ymk   = ym0 + k * count
  yy    = ymk / 12
  mm    = ymk - yy * 12 + 1
  CATimeCivil.days_from_civil(yy, mm, CArray.int64(*k.shape) { 1 }) * tpd
end

.check_calendar_origin(origin, storage_res) ⇒ Object

Raise unless origin can be the head of bucket 0 on a calendar grid: it has to be a month head, and a :Y tick starts in January. The day and time would otherwise be dropped silently -- the same loss a fixed unit already refuses (see resolve_origin_ticks).



2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
# File 'lib/carray/time.rb', line 2070

def check_calendar_origin(origin, storage_res)
  return if origin.nil?
  y, m = origin_year_month(origin)
  unless origin_on_month_head?(origin)
    raise ArgumentError,
          "origin #{origin.inspect} is not a month head: a calendar grid " \
          "is addressed by month, so its origin must be the 1st at 00:00"
  end
  if storage_res && storage_res.base == :Y && m != 1
    raise ArgumentError,
          "origin #{origin.inspect} is not on the #{storage_res} grid " \
          "(a #{storage_res} tick starts in January)"
  end
  [y, m]
end

.check_int64_range(v, what) ⇒ Object

Raise (rather than let an int64 CArray operand silently wrap) if a Ruby-domain quantity is out of int64 range. Returns the value on success so it composes inline.



1926
1927
1928
1929
1930
1931
1932
1933
1934
# File 'lib/carray/time.rb', line 1926

def check_int64_range(v, what)
  if v < INT64_MIN || v > INT64_MAX
    raise RangeError,
          "time step: #{what} = #{v} overflows int64 " \
          "(the storage unit is too fine for this step / origin / span; " \
          "use a coarser unit)"
  end
  v
end

.origin_month_ordinal(origin) ⇒ Object

Month ordinal (year*12 + month-1) of origin; day / time ignored. Default (nil) is the epoch month 1970-01.



1972
1973
1974
1975
1976
# File 'lib/carray/time.rb', line 1972

def origin_month_ordinal(origin)
  return 1970 * 12 if origin.nil?
  y, m = check_calendar_origin(origin, nil)
  y * 12 + (m - 1)
end

.origin_on_month_head?(origin) ⇒ Boolean

Whether origin sits exactly on the head of its month (the 1st at 00:00 UTC). A calendar grid is addressed by month ordinal, so its bucket heads are month heads and nothing else; an origin anywhere in between names a bucket that does not exist.

Returns:

  • (Boolean)


2056
2057
2058
2059
2060
2061
2062
2063
2064
# File 'lib/carray/time.rb', line 2056

def origin_on_month_head?(origin)
  # A calendar element is a month head by construction, and the shared
  # entry point says so by handing back that midnight -- no branch here
  # has to know it.
  secs = CATimeLiteral.epoch_seconds(origin)
  y, m = origin_year_month(origin)
  head = CATimeCivil.days_from_civil(CA_INT64([y]), CA_INT64([m]), CA_INT64([1]))[0]
  secs == head * 86400
end

.origin_seconds_exact(origin) ⇒ Object

Exact Rational seconds since the Unix epoch for a fixed-storage origin. Everything but a bare Integer goes through the shared entry point, so an origin reads the same here as it does for a start literal.



2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
# File 'lib/carray/time.rb', line 2041

def origin_seconds_exact(origin)
  case origin
  when Integer
    raise ArgumentError,
          "origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
          "Time / String / CATime scalar"
  else
    CATimeLiteral.epoch_seconds(origin)  # Time / String / DateTime / Element
  end
end

.origin_year_month(origin) ⇒ Object

[year, month] of a calendar-storage origin (finer fields ignored).



2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
# File 'lib/carray/time.rb', line 2087

def origin_year_month(origin)
  case origin
  when Integer
    raise ArgumentError,
          "origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
          "Time / String / CATime scalar"
  else
    CATimeLiteral.year_month(origin)  # Time / String / DateTime / Element
  end
end

.resolve_integer_grid(step_res, storage_res, origin) ⇒ Object

Return [mul, step_ticks, origin_ticks] (all Integer) for the integer path -- step_ticks and origin_ticks in numerator ticks, the finer of the storage and step grids (see #resolve_timestep_grid). The calendar path is handled by the caller.



1940
1941
1942
1943
1944
# File 'lib/carray/time.rb', line 1940

def resolve_integer_grid(step_res, storage_res, origin)
  mul, step_ticks = resolve_step_scale(step_res, storage_res)
  [mul, step_ticks, resolve_origin_ticks(origin, step_res,
                                          mul == 1 ? storage_res : step_res)]
end

.resolve_origin_ticks(origin, step_res, storage_res) ⇒ Object

origin -> integer tick count in storage ticks. Default (nil) is the epoch, except a week bucket defaults to ISO Monday (1970-01-05). A lossy conversion (origin not landing exactly on the storage grid) raises; a bare Integer is rejected (epoch-dependent, ambiguous).



2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
# File 'lib/carray/time.rb', line 2007

def resolve_origin_ticks(origin, step_res, storage_res)
  if origin.nil?
    if step_res.base == :W && SU_LE_DAY.include?(storage_res.base)
      return check_int64_range(ticks_per_day(storage_res) * 4,
                    "ISO-Monday week origin in ticks of #{storage_res}")
    end
    return 0
  end
  if CATimeUnitAlgebra::FIXED.key?(storage_res.base)
    secs = origin_seconds_exact(origin)
    tick = secs / storage_res.tick_ratio               # Rational
    unless tick.denominator == 1
      raise ArgumentError,
            "origin #{origin.inspect} is not representable losslessly " \
            "in storage resolution #{storage_res} (would truncate the grid phase)"
    end
    check_int64_range(tick.numerator, "origin #{origin.inspect} in ticks of #{storage_res}")
  else                                                 # :Y / :M storage
    y, m = check_calendar_origin(origin, storage_res)
    ord  = storage_res.base == :Y ? (y - 1970) : (y * 12 + (m - 1) - 1970 * 12)
    if storage_res.count > 1
      unless (ord % storage_res.count).zero?
        raise ArgumentError,
              "origin #{origin.inspect} is not on the #{storage_res} grid"
      end
      ord /= storage_res.count
    end
    ord
  end
end

.resolve_step_scale(step_res, storage_res) ⇒ Object

[mul, step_ticks] for the integer path, or :civil (calendar bucket on day-or-finer storage), or raise. Bucket arithmetic runs in the finer of the two grids, so exactly one of the pair is > 1:

- bucket at or coarser than the storage tick (a :h bucket on :s
storage) -> [1, N]: N storage ticks per bucket.
- bucket finer than the storage tick (a :h bucket on :D storage) ->
[N, 1]: one storage tick spans N buckets exactly, so a timestep is a
plain widening.  This needs the storage tick to be a *whole* multiple
of the bucket tick; a partial multiple ("90 minutes" storage against
an :h bucket) has no integer timestep and raises.

Raises:

  • (ArgumentError)


1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
# File 'lib/carray/time.rb', line 1988

def resolve_step_scale(step_res, storage_res)
  r = CATimeUnitAlgebra.ratio(step_res, storage_res)  # storage ticks / step tick
  if r
    what = "bucket #{step_res} in ticks of #{storage_res}"
    return [1, check_int64_range(r.numerator, what)] if r.denominator == 1
    return [check_int64_range(r.denominator, what), 1] if r.numerator == 1
  end
  if CATimeUnitAlgebra::CALENDAR.key?(step_res.base) && SU_LE_DAY.include?(storage_res.base)
    return :civil
  end
  raise ArgumentError,
        "cannot express bucket #{step_res} on storage resolution #{storage_res} " \
        "(neither is a whole multiple of the other)"
end

.ticks_per_day(storage_res) ⇒ Object

Storage ticks per day of storage_res (must be a whole number, else the storage grid does not tile a day -- a calendar bucket is unrepresentable).



1948
1949
1950
1951
1952
1953
1954
1955
1956
# File 'lib/carray/time.rb', line 1948

def ticks_per_day(storage_res)
  r = CATimeUnitAlgebra.ratio(CATime::Resolution.new(1, :D), storage_res)
  unless r.denominator == 1
    raise ArgumentError,
          "cannot place a calendar bucket on storage resolution " \
          "#{storage_res} (its tick does not tile a day)"
  end
  check_int64_range(r.numerator, "ticks per day for #{storage_res}")
end