Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/bacnet_time.ex
defmodule BACnet.Protocol.BACnetTime do
@moduledoc """
A BACnet Time is used to represent timepoints of the day, but also can represent
unspecific timepoints, such as a single component being unspecified
(i.e. can match anything in that component).
This module provides some helpers to convert `Time` into a `BACnetTime` and back.
"""
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
@typedoc """
Represents a BACnet Time, which can have unspecified values (= any).
One hundredth corresponds to 0.01 of a second.
"""
@type t :: %__MODULE__{
hour: 0..23 | :unspecified,
minute: 0..59 | :unspecified,
second: 0..59 | :unspecified,
hundredth: 0..99 | :unspecified
}
@fields [
:hour,
:minute,
:second,
:hundredth
]
@enforce_keys @fields
defstruct @fields
@doc """
Compares two BACnet Time.
Returns `:gt` if first time is later than the second,
and `:lt` for vice versa.
If the two times are equal, `:eq` is returned.
Note that this is achieved by converting to `Time` and then
comparing them.
"""
@spec compare(t(), t()) :: :gt | :eq | :lt
def compare(%__MODULE__{} = time1, %__MODULE__{} = time2) do
Time.compare(to_time!(time1), to_time!(time2))
end
@doc """
Encodes the given BACnet Time into an application tag.
"""
@spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(%__MODULE__{} = time, _opts \\ []) do
{:ok, [{:time, time}]}
end
@doc """
Converts a `Time` into a BACnet Time.
"""
@spec from_time(Time.t()) :: t()
def from_time(%Time{} = time) do
%__MODULE__{
hour: time.hour,
minute: time.minute,
second: time.second,
hundredth: patch_microsecond(time.microsecond)
}
end
@doc """
Parses a BACnet Time from BACnet application tags encoding.
"""
@spec parse(ApplicationTags.encoding_list()) ::
{:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term}
def parse(tags) when is_list(tags) do
with [{:time, %__MODULE__{} = time} | rest] <- tags do
{:ok, {time, rest}}
else
_else -> {:error, :invalid_tags}
end
end
@doc """
Checks whether the given BACnet Time is a specific time value
(every component is a numeric value).
"""
@spec specific?(t()) :: boolean()
def specific?(%__MODULE__{} = time) do
case time do
%{hour: :unspecified} -> false
%{minute: :unspecified} -> false
%{second: :unspecified} -> false
%{hundredth: :unspecified} -> false
_else -> true
end
end
@doc """
Converts a BACnet Time into a `Time`.
If any of the fields are unspecified, the reference time (current UTC value) is used.
"""
@spec to_time(t(), Time.t()) :: {:ok, Time.t()} | {:error, term()}
def to_time(%__MODULE__{} = time, ref_time \\ Time.utc_now()) do
hundredth = get_component(:hundredth, time, ref_time)
Time.new(
get_component(:hour, time, ref_time),
get_component(:minute, time, ref_time),
get_component(:second, time, ref_time),
{hundredth, if(hundredth > 0, do: 6, else: 0)}
)
end
@doc """
Bang-version of `to_time/1`.
"""
@spec to_time!(t(), Time.t()) :: Time.t() | no_return()
def to_time!(%__MODULE__{} = time, ref_time \\ Time.utc_now()) do
hundredth = get_component(:hundredth, time, ref_time)
Time.new!(
get_component(:hour, time, ref_time),
get_component(:minute, time, ref_time),
get_component(:second, time, ref_time),
{hundredth, if(hundredth > 0, do: 6, else: 0)}
)
end
@doc """
Creates a new BACnet Time with the current UTC time.
"""
@spec utc_now() :: t()
def utc_now() do
from_time(Time.utc_now())
end
@doc """
Validates whether the given BACnet time is in form valid.
It only validates the struct is valid as per type specification.
"""
@spec valid?(t()) :: boolean()
def valid?(
%__MODULE__{
hour: hour,
minute: minute,
second: second,
hundredth: hundredth
} = _t
)
when (hour in 0..23 or hour == :unspecified) and (minute in 0..59 or minute == :unspecified) and
(second in 0..59 or second == :unspecified) and
(hundredth in 0..99 or hundredth == :unspecified),
do: true
def valid?(%__MODULE__{} = _t), do: false
defp get_component(:hundredth, %__MODULE__{hundredth: :unspecified} = _time, %Time{} = utc_now) do
patch_microsecond(utc_now.microsecond)
end
defp get_component(:hundredth, %__MODULE__{hundredth: hundredth} = _time, _utc_now),
do: hundredth * 10_000
defp get_component(component, %__MODULE__{} = time, utc_now)
when :erlang.map_get(component, time) == :unspecified,
do: Map.fetch!(utc_now, component)
defp get_component(component, %__MODULE__{} = time, _utc_now), do: Map.fetch!(time, component)
@spec patch_microsecond(Calendar.microsecond()) :: non_neg_integer()
defp patch_microsecond(microsecond_prec) do
case microsecond_prec do
{_value, 0} -> 0
{value, 1} -> value * 10
{value, 2} -> value
{value, 3} -> Integer.floor_div(value, 10)
{value, 4} -> Integer.floor_div(value, 100)
{value, 5} -> Integer.floor_div(value, 1000)
{value, 6} -> Integer.floor_div(value, 10_000)
end
end
end