Feel.Runtime.Temporal (feel v0.1.0)

Copy Markdown View Source

Arithmetic and comparison of dates, times, dates and times, and durations (DMN 1.6, Tables 54, 55 and 58; decision D-54).

Feel.Runtime.Arithmetic and Feel.Runtime.Compare call this for the operations whose operands are a date, a time or a date and time. The functions take the values and return {:ok, value} or {:error, diagnostic}; a null operand never gets here.

Exact time

A time of day or a date and time is counted in ticks of 10^-34 of a second, as an integer, so that nothing is rounded on the way: a second is a decimal of FEEL with up to 34 digits, and a year can be nine digits long. A result that is a duration is rounded to the 34 digits of a number at the end (half to even, as a number is).

Three kinds of date and time, and of time

A time or a date and time has no zone (floating), a fixed offset from UTC, or the id of a zone of the IANA database (Feel.Value.Zone). A floating one is a local time at some place and has no instant; the other two do, and the offset of a zone id is the one it has on that date.

The operationBoth have a zoneNeither has oneOne has, one has not
= < <= > >=the instantsthe local fieldsthe local fields (the one without adopts the offset of the other, Table 55)
- of dates and times, or timesthe difference of instantsthe difference of local fieldsnull, with an error (Table 58)

Equality of times and dates and times that have an offset is by instant, not by what is written: 10:00:00+01:00 = 09:00:00Z. A date is UTC midnight where an operation converts it to a date and time, so date - date and time is null if the date and time is floating (notes 02, A-18).

Arithmetic

OperationResult
date and time ± years and months durationthe months are added to the year and month, and the day, the time of day and the zone are kept; a day the month does not have is the last day of it
date ± years and months durationthe same, on a date
date and time ± days and time durationthe seconds are added as elapsed time, and the zone is kept
date ± days and time durationthe date is UTC midnight, the seconds are added, and the date of that is the result: the days are the floor of the seconds over 86,400
time ± days and time durationthe seconds are added to the time of day, round the clock (modulo 24 hours), and the zone is kept
date and time - date and time, date - date and time, date and time - date, date - date, time - timea days and time duration of the seconds between them
a duration + a date, a time or a date and timethe same as the other way round

A result with a year outside -999,999,999 to 999,999,999 is null, with a :date_out_of_range error (notes 05, E29).

A date and time in a zone id that is added seconds is moved as elapsed time: the local time is made an instant (see Feel.Value.Zone for the gap and the overlap), the seconds are added, and the instant is made a local time again, so a day across a change to daylight saving time can be 23 or 25 hours of the clock. Months and a time of day are not elapsed time: the local fields are changed and the zone id is kept.

Times with a zone id

A time of day has no date, so a zone id does not give it an offset (notes 02, A-16). Between two times, the offset of a zone id is the standard offset of the zone (Feel.Value.Zone.standard_offset/1), so that two times in one zone compare as their local fields do, and 10:00:00@Etc/UTC is 10:00:00Z. A time with an offset is compared with the offset taken off, without wrapping at the edge of the day: 00:30:00+01:00 is the time -1,800 seconds, which is before 00:00:00Z (notes 02, A-15).

Summary

Types

What an operation gives: a value, or the diagnostic of the error that makes it null.

Functions

Adds two values, one of which is a date, a time or a date and time and the other a duration.

Compares two times, or two dates and times, and says whether the first is before, equal to or after the second: {:ok, :lt | :eq | :gt}. It is {:error, diagnostic} for a zone that is not in the time zone database.

The whole seconds that compare/3 looks at in a time or a date and time: {:floating, local} for one with no time offset or zone, and {:zoned, instant} for one that has either. Two floating values are equal as compare/3 says exactly when their local is the same, and two zoned ones when their instant is (a time of day less the offset for a time, the seconds since the start of year 0 for a date and time). A floating value and a zoned one are compared by their local fields, which this does not give, and equality between them is not transitive (see the table above): 10:00:00+01:00 is equal to 10:00:00 and that to 10:00:00Z, and the first is not equal to the third. It is {:error, diagnostic} for a zone that is not in the time zone database.

Subtracts the second value from the first: a duration from a date, a time or a date and time, or a date, a time or a date and time from another of the same kind (a date and a date and time mix).

The value property of a date, a time or a date and time (Table 67, D-68): the number that the specification's value function gives, in seconds.

Types

outcome()

@type outcome() :: {:ok, Feel.Value.t()} | {:error, Feel.Diagnostic.t()}

What an operation gives: a value, or the diagnostic of the error that makes it null.

Functions

add(date_time, date_time)

@spec add(Feel.Value.t(), Feel.Value.t()) :: outcome()

Adds two values, one of which is a date, a time or a date and time and the other a duration.

iex> {:ok, date} = Feel.Runtime.Temporal.add(~D[2021-01-31], Feel.Value.YearsAndMonthsDuration.new(1))
iex> date
~D[2021-02-28]
iex> {:ok, date} = Feel.Runtime.Temporal.add(~D[2021-01-01], Feel.Value.DaysAndTimeDuration.new(36 * 3600))
iex> date
~D[2021-01-02]

compare(atom, left, right)

@spec compare(
  :time | :date_and_time,
  Feel.Value.Time.t() | Feel.Value.DateTime.t(),
  Feel.Value.Time.t() | Feel.Value.DateTime.t()
) :: {:ok, :lt | :eq | :gt} | {:error, Feel.Diagnostic.t()}

Compares two times, or two dates and times, and says whether the first is before, equal to or after the second: {:ok, :lt | :eq | :gt}. It is {:error, diagnostic} for a zone that is not in the time zone database.

The instants are compared when both have a zone, and the local fields when one has none (see the table above). The resolution is the second: 10:30:00.0001 and 10:30:00.0002 are equal, as the TCK has it (0068#time_005), and so for the other four operators, so that they stay consistent with =.

iex> {:ok, a} = Feel.Value.DateTime.parse("2002-04-02T12:00:00-01:00")
iex> {:ok, b} = Feel.Value.DateTime.parse("2002-04-02T17:00:00+04:00")
iex> Feel.Runtime.Temporal.compare(:date_and_time, a, b)
{:ok, :eq}
iex> {:ok, a} = Feel.Value.DateTime.parse("2018-12-08T00:00:00@Europe/Paris")
iex> {:ok, b} = Feel.Value.DateTime.parse("2018-12-08T00:00:00@Asia/Dhaka")
iex> Feel.Runtime.Temporal.compare(:date_and_time, a, b)
{:ok, :gt}

seconds(time)

@spec seconds(Feel.Value.Time.t() | Feel.Value.DateTime.t()) ::
  {:floating, integer()} | {:zoned, integer()} | {:error, Feel.Diagnostic.t()}

The whole seconds that compare/3 looks at in a time or a date and time: {:floating, local} for one with no time offset or zone, and {:zoned, instant} for one that has either. Two floating values are equal as compare/3 says exactly when their local is the same, and two zoned ones when their instant is (a time of day less the offset for a time, the seconds since the start of year 0 for a date and time). A floating value and a zoned one are compared by their local fields, which this does not give, and equality between them is not transitive (see the table above): 10:00:00+01:00 is equal to 10:00:00 and that to 10:00:00Z, and the first is not equal to the third. It is {:error, diagnostic} for a zone that is not in the time zone database.

iex> {:ok, a} = Feel.Value.Time.parse("10:00:00+01:00")
iex> {:ok, b} = Feel.Value.Time.parse("09:00:00Z")
iex> Feel.Runtime.Temporal.seconds(a) == Feel.Runtime.Temporal.seconds(b)
true
iex> {:ok, floating} = Feel.Value.Time.parse("10:00:00.5")
iex> Feel.Runtime.Temporal.seconds(floating)
{:floating, 36_000}

subtract(date_time, right)

@spec subtract(Feel.Value.t(), Feel.Value.t()) :: outcome()

Subtracts the second value from the first: a duration from a date, a time or a date and time, or a date, a time or a date and time from another of the same kind (a date and a date and time mix).

iex> {:ok, difference} = Feel.Runtime.Temporal.subtract(~D[2021-01-02], ~D[2021-01-01])
iex> Decimal.to_string(difference.seconds)
"86400"
iex> {:ok, date} = Feel.Runtime.Temporal.subtract(~D[2021-01-02], Feel.Value.DaysAndTimeDuration.new(3600))
iex> date
~D[2021-01-01]

value(date)

@spec value(Date.t() | Feel.Value.Time.t() | Feel.Value.DateTime.t()) ::
  {:ok, Decimal.t()} | {:error, Feel.Diagnostic.t()}

The value property of a date, a time or a date and time (Table 67, D-68): the number that the specification's value function gives, in seconds.

The valueIts value
a datethe seconds from 1970-01-01T00:00:00Z to midnight UTC of the date, as the date is where an operation needs a date and time (2018-12-10 is 1,544,400,000)
a date and time with an offset or a zone idthe seconds from the epoch 1970-01-01T00:00:00Z to the instant, with the fraction (1970-01-01T01:00:00+01:00 is 0)
a date and time with no zoneno value: {:error, diagnostic}, an :invalid_property, as the specification defines the value of a date and time with a UTC form only
a timethe seconds of the day less the offset, not wrapped, the number that orders times (10:00:00+01:00 is 32,400, and so is 09:00:00Z; 00:30:00+01:00 is -1,800); a time with no zone is its seconds of the day, and one with a zone id has the offset of the zone in standard time, as in a comparison

The number is exact up to 34 digits, as every number is.

iex> Feel.Runtime.Temporal.value(~D[1970-01-02])
{:ok, Decimal.new(86400)}
iex> {:ok, date_time} = Feel.Value.DateTime.parse("1970-01-01T01:00:00.5+01:00")
iex> Feel.Runtime.Temporal.value(date_time)
{:ok, Decimal.new("0.5")}
iex> {:ok, time} = Feel.Value.Time.parse("00:30:00+01:00")
iex> Feel.Runtime.Temporal.value(time)
{:ok, Decimal.new(-1800)}
iex> {:ok, floating} = Feel.Value.DateTime.parse("1970-01-01T00:00:00")
iex> {:error, %Feel.Diagnostic{code: :invalid_property}} = Feel.Runtime.Temporal.value(floating)