lib/azan/fajr_time.ex

defmodule Azan.FajrTime do
  @moduledoc """
  Documentation for `FajrTime`.
  """

  use TypedStruct

  alias Azan.{
    CalculationParameter,
    DateUtils,
    SafeFajr,
    SolarTime,
    TimeComponent
  }

  typedstruct do
    field :calculation_parameter, CalculationParameter.t()
    field :sunrise_time, DateTime.t()
    field :night, number()
    field :latitude, number()
    field :date, Date.t()
    field :solar_time, SolarTime.t()
  end

  def find(
        solar_time,
        sunrise_time,
        night,
        latitude,
        date,
        %CalculationParameter{} = calculation_parameter
      ) do
    case %__MODULE__{
           calculation_parameter: calculation_parameter,
           sunrise_time: sunrise_time,
           solar_time: solar_time,
           night: night,
           latitude: latitude,
           date: date
         }
         |> __MODULE__.find!() do
      %DateTime{} = fajr_time ->
        {:ok, fajr_time}

      {:error, error} ->
        {:error, error}
    end
  end

  def find!(%__MODULE__{
        calculation_parameter:
          %CalculationParameter{method: method, fajr_angle: fajr_angle} = calculation_parameter,
        solar_time: solar_time,
        sunrise_time: sunrise_time,
        night: night,
        latitude: latitude,
        date: date
      }) do
    naive_fajr_time =
      solar_time
      |> naive_fajr_time(fajr_angle, date)
      |> consider_latitude_and_method(night, method, latitude)

    safe_fajr_time =
      %SafeFajr{
        calculation_parameter: calculation_parameter,
        sunrise_time: sunrise_time,
        night: night,
        latitude: latitude,
        date: date
      }
      |> SafeFajr.find_time()

    naive_fajr_time |> naive_or_safe_fajr(safe_fajr_time)
  end

  def naive_fajr_time(%SolarTime{} = solar_time, fajr_angle, date) do
    case solar_time |> SolarTime.hour_angle(-1 * fajr_angle, false) do
      {:error, _} ->
        {:error, :invalid_datetime}

      corrected_hour_angle ->
        corrected_hour_angle
        |> TimeComponent.new()
        |> TimeComponent.create_utc_datetime(date)
    end
  end

  @spec consider_latitude_and_method(DateTime.t(), number, atom(), number()) :: DateTime.t()
  def consider_latitude_and_method(fajr_time, night, :moonsighting_committee, latitude)
      when latitude >= 55 do
    night_fraction = night / 7
    fajr_time |> DateUtils.shift_by_seconds(-1 * night_fraction)
  end

  def consider_latitude_and_method(fajr_time, _night, _method, _latitude), do: fajr_time

  def naive_or_safe_fajr({:error, :invalid_datetime}, safe_fajr_time), do: safe_fajr_time

  def naive_or_safe_fajr(naive_fajr_time, safe_fajr_time) do
    case DateTime.compare(safe_fajr_time, naive_fajr_time) do
      :gt -> safe_fajr_time
      _ -> naive_fajr_time
    end
  end

  def adjust(datetime, %CalculationParameter{
        adjustments: %{fajr: adjustment},
        method_adjustments: %{fajr: method_adjustment},
        rounding: rounding
      }) do
    adjustment = adjustment |> DateUtils.sum_adjustment(method_adjustment)
    datetime |> DateUtils.rounded_time(adjustment, rounding)
  end
end