Feel.Value.Time (feel v0.1.0)

Copy Markdown View Source

The FEEL type time (DMN 1.6, section 10.3.2.3.4): a time of day.

Neither Elixir's Time nor Erlang's calendar types can represent a FEEL time: seconds need to be a decimal, and the zone has to say whether the time is floating (no zone), has a fixed offset from UTC, or names an IANA time zone. A floating time is not interchangeable with a zoned one, so the distinction is part of the value.

Fields

Summary

Types

t()

The time zone of a time.

Functions

Builds a time, checking the ranges of its parts.

Parses a time string, as time("...") and @"..." read it: hh:mm:ss, with an optional fraction of a second and an optional zone (DMN 1.6, section 10.3.4.1; the forms are those of notes 06, AE-1).

Parses a time string as parse/1 does, and says whether it was the end of the day: the carry is 1 for 24:00:00, which is 00:00:00 of the next day, and 0 for any other time.

The zone that a text names: Z (or z) for UTC, an offset such as +01:00 or -05:30, or the id of a zone of the database. :error for anything else, and for no zone: :floating is not something a text can name.

Returns the type descriptor of time.

Returns true for a t/0 whose parts are all in range.

Types

t()

@type t() :: %Feel.Value.Time{
  hour: 0..23,
  minute: 0..59,
  second: Feel.Value.Number.t(),
  zone: zone()
}

zone()

@type zone() :: :floating | {:offset, -50400..50400} | {:iana, String.t()}

The time zone of a time.

  • :floating - no zone: a local time of day at some unspecified place.
  • {:offset, seconds} - a fixed offset from UTC, between -14 and +14 hours. An offset of 0 is a UTC form.
  • {:iana, id} - an IANA time zone, such as "Europe/Paris".

Functions

new(hour, minute, second, zone \\ :floating)

@spec new(integer(), integer(), Feel.Value.Number.t() | integer(), zone()) ::
  {:ok, t()} | :error

Builds a time, checking the ranges of its parts.

Returns :error when a part is out of range or the zone is not valid. An IANA zone has to be one of the time zone database (Feel.Value.Zone, D-54).

iex> {:ok, time} = Feel.Value.Time.new(12, 30, 15, {:offset, 0})
iex> {time.hour, time.minute, Decimal.to_string(time.second), time.zone}
{12, 30, "15", {:offset, 0}}
iex> Feel.Value.Time.new(24, 0, 0)
:error
iex> Feel.Value.Time.new(12, 0, 0, {:iana, "Europe/Atlantis"})
:error

parse(text)

@spec parse(String.t()) :: {:ok, t()} | :error

Parses a time string, as time("...") and @"..." read it: hh:mm:ss, with an optional fraction of a second and an optional zone (DMN 1.6, section 10.3.4.1; the forms are those of notes 06, AE-1).

The zone is Z (or z) for UTC, an offset such as +01:00 or -05:30, or an IANA zone id after an @, as in 10:30:00@Europe/Paris. Without a zone the time is floating. White space around the string is ignored. Returns :error for anything else, including an offset beyond fourteen hours, a zone id that the database does not have, and a time without seconds.

24:00:00 is the end of the day, which XML Schema reads as midnight, and so does the TCK: it is 00:00:00, with the zone it has. Nothing else of hour 24 is a time. parse_with_carry/1 says that the day has ended, which a date and time needs.

iex> {:ok, time} = Feel.Value.Time.parse("23:59:00")
iex> {time.hour, time.minute, Decimal.to_string(time.second), time.zone}
{23, 59, "0", :floating}
iex> {:ok, time} = Feel.Value.Time.parse("11:00:30.5Z")
iex> {Decimal.to_string(time.second), time.zone}
{"30.5", {:offset, 0}}
iex> {:ok, time} = Feel.Value.Time.parse("10:00:00-05:30")
iex> time.zone
{:offset, -19_800}
iex> {:ok, time} = Feel.Value.Time.parse("10:30:00@Europe/Paris")
iex> time.zone
{:iana, "Europe/Paris"}
iex> {:ok, time} = Feel.Value.Time.parse("24:00:00Z")
iex> {time.hour, time.minute, time.zone}
{0, 0, {:offset, 0}}
iex> Feel.Value.Time.parse("24:00:01")
:error
iex> Feel.Value.Time.parse("10:00:00@Europe/Atlantis")
:error
iex> Feel.Value.Time.parse("10:00")
:error

parse_with_carry(text)

@spec parse_with_carry(String.t()) :: {:ok, t(), 0 | 1} | :error

Parses a time string as parse/1 does, and says whether it was the end of the day: the carry is 1 for 24:00:00, which is 00:00:00 of the next day, and 0 for any other time.

iex> {:ok, time, carry} = Feel.Value.Time.parse_with_carry("24:00:00")
iex> {time.hour, carry}
{0, 1}
iex> {:ok, _time, carry} = Feel.Value.Time.parse_with_carry("23:59:59")
iex> carry
0

parse_zone(text)

@spec parse_zone(String.t()) :: {:ok, zone()} | :error

The zone that a text names: Z (or z) for UTC, an offset such as +01:00 or -05:30, or the id of a zone of the database. :error for anything else, and for no zone: :floating is not something a text can name.

iex> Feel.Value.Time.parse_zone("Z")
{:ok, {:offset, 0}}
iex> Feel.Value.Time.parse_zone("-05:30")
{:ok, {:offset, -19_800}}
iex> Feel.Value.Time.parse_zone("Europe/Paris")
{:ok, {:iana, "Europe/Paris"}}
iex> Feel.Value.Time.parse_zone("Europe/Atlantis")
:error

type()

@spec type() :: Feel.Type.t()

Returns the type descriptor of time.

value?(arg1)

@spec value?(term()) :: boolean()

Returns true for a t/0 whose parts are all in range.

iex> {:ok, time} = Feel.Value.Time.new(1, 2, 3)
iex> Feel.Value.Time.value?(time)
true
iex> Feel.Value.Time.value?(%Feel.Value.Time{hour: 25, minute: 0, second: Decimal.new(0)})
false