lib/angle/radian.ex

defmodule Angle.Radian do
  import Angle.Utils, only: [string_to_number: 1]

  @moduledoc """
  Functions relating to dealing with angles in Radians.
  """

  @two_pi 2 * :math.pi()

  @doc """
  Initialize an Angle from a number `n` of radians

  ## Examples

      iex> init(13)
      #Angle<13㎭>

      iex> init(13.2)
      #Angle<13.2㎭>

      iex> init(0)
      #Angle<0>

      iex> init(0.0)
      #Angle<0>
  """
  @spec init(number) :: Angle.t()
  def init(n) when n == 0, do: Angle.zero()
  def init(n) when is_number(n), do: %Angle{r: n}

  @doc """
  Attempt to parse decimal radians.

  ## Examples

      iex> "13" |> parse() |> inspect()
      "{:ok, #Angle<13㎭>}"

      iex> "13.2" |> parse() |> inspect()
      "{:ok, #Angle<13.2㎭>}"

      iex> "13㎭" |> parse() |> inspect()
      "{:ok, #Angle<13㎭>}"

      iex> "13.2㎭" |> parse() |> inspect()
      "{:ok, #Angle<13.2㎭>}"

      iex> "-13.2㎭" |> parse() |> inspect()
      "{:ok, #Angle<-13.2㎭>}"
  """
  @spec parse(String.t()) :: {:ok, Angle.t()} | {:error, term}
  def parse(value) do
    case Regex.run(~r/^-?[0-9]+(?:\.[0-9]+)?/, value) do
      [value] ->
        case string_to_number(value) do
          {:ok, n} -> {:ok, init(n)}
          {:error, _} -> {:error, "Unable to parse value as radians"}
        end

      _ ->
        {:error, "Unable to parse value as radians"}
    end
  end

  @doc """
  Ensure that a radian representation is present for this angle, otherwise
  calculate one.

  ## Examples

      iex> ~a(90)d
      ...> |> ensure()
      ...> |> Map.get(:r)
      1.5707963267948966

      iex> ~a(76.3944)g
      ...> |> ensure()
      ...> |> Map.get(:r)
      1.2000004290770006

      iex> ~a(90,0,0)dms
      ...> |> ensure()
      ...> |> Map.get(:r)
      1.5707963267948966
  """
  @spec ensure(Angle.t()) :: Angle.t()
  def ensure(%Angle{r: radians} = angle) when is_number(radians), do: angle

  def ensure(%Angle{d: degrees} = angle) when is_number(degrees) do
    radians = degrees / 180.0 * :math.pi()
    %{angle | r: radians}
  end

  def ensure(%Angle{g: gradians} = angle) when is_number(gradians) do
    radians = gradians * :math.pi() / 200
    %{angle | r: radians}
  end

  def ensure(%Angle{dms: {d, m, s}} = angle)
      when is_integer(d) and is_integer(m) and is_number(s) do
    degrees = d + m / 60.0 + s / 3600.0
    radians = degrees / 180.0 * :math.pi()
    %{angle | r: radians}
  end

  @doc """
  Return the radians representation of the Angle.

  ## Examples

      iex> ~a(90)d
      ...> |> to_radians()
      ...> |> inspect()
      "{#Angle<90°>, 1.5707963267948966}"
  """
  @spec to_radians(Angle.t()) :: {Angle.t(), number}
  def to_radians(%Angle{r: number} = angle) when is_number(number), do: {angle, number}

  def to_radians(angle) do
    angle
    |> ensure()
    |> to_radians()
  end

  @doc """
  Convert the angle to it's absolute value by discarding complete revolutions
  and converting negatives.

  ## Examples

      iex> ~a(-4.71238898038469)r
      ...> |> Angle.Radian.abs()
      #Angle<1.5707963267948966㎭>

      iex> ~a(20.420352248333657)r
      ...> |> Angle.Radian.abs()
      #Angle<1.5707963267948983㎭>
  """
  @spec abs(Angle.t()) :: Angle.t()
  def abs(%Angle{r: r}), do: init(calculate_abs(r))

  defp calculate_abs(r) when r >= 0 and r <= @two_pi, do: r
  defp calculate_abs(r) when r > @two_pi, do: calculate_abs(r - @two_pi)
  defp calculate_abs(r) when r < 0, do: calculate_abs(r + @two_pi)
end