# `Feel.Runtime.Temporal`
[🔗](https://github.com/FeeJai/BDM/blob/feel-v0.1.0/apps/feel/lib/feel/runtime/temporal.ex#L1)

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).

# `outcome`

```elixir
@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`.

# `add`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
