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 operation | Both have a zone | Neither has one | One has, one has not |
|---|---|---|---|
= < <= > >= | the instants | the local fields | the local fields (the one without adopts the offset of the other, Table 55) |
- of dates and times, or times | the difference of instants | the difference of local fields | null, 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
| Operation | Result |
|---|---|
date and time ± years and months duration | the 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 duration | the same, on a date |
date and time ± days and time duration | the seconds are added as elapsed time, and the zone is kept |
date ± days and time duration | the 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 duration | the 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 - time | a days and time duration of the seconds between them |
a duration + a date, a time or a date and time | the 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
@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
@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]
@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}
@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}
@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]
@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 value | Its value |
|---|---|
| a date | the 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 id | the 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 zone | no value: {:error, diagnostic}, an :invalid_property, as the specification defines the value of a date and time with a UTC form only |
| a time | the 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)