lib/azan/prayer_time.ex

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

  alias Azan.{
    AsrTime,
    CalculationParameter,
    Coordinate,
    DateUtils,
    DhuhrTime,
    FajrTime,
    IshaTime,
    MaghribTime,
    PrayerTime,
    SolarTime,
    SunriseTime,
    SunsetTime
  }

  use TypedStruct

  require Logger

  typedstruct do
    field :fajr, DateTime.t()
    field :sunrise, DateTime.t()
    field :dhuhr, DateTime.t()
    field :asr, DateTime.t()
    field :sunset, DateTime.t()
    field :maghrib, DateTime.t()
    field :isha, DateTime.t()
  end

  @doc """
  Returns a `PrayerTime` struct for the given `date`, `coordinate` and `calculation_parameter`.

      ## Examples

      iex> date = ~D[2022-10-01]
      ~D[2022-10-01]
      iex> coordinate = %Azan.Coordinate{latitude: 35.671494, longitude: 139.90181}
      %Azan.Coordinate{latitude: 35.671494, longitude: 139.90181}
      iex> params = CalculationMethod.moonsighting_committee()
      %Azan.CalculationParameter{
        adjustments: %{asr: 0, dhuhr: 0, fajr: 0, isha: 0, maghrib: 0, sunrise: 0},
        fajr_angle: 18.0,
        high_latitude_rule: :middle_of_the_night,
        isha_angle: 18.0,
        isha_interval: 0,
        madhab: :shafi,
        maghrib_angle: 0.0,
        method: :moonsighting_committee,
        method_adjustments: %{
          asr: 0,
          dhuhr: 5,
          fajr: 0,
          isha: 0,
          maghrib: 3,
          sunrise: 0
        },
        polar_circle_resolution: :unresolved,
        rounding: :nearest,
        shafaq: :general
      }
      iex> coordinate |> PrayerTime.find(date, params)
      %Azan.PrayerTime{
        asr: ~U[2022-10-01 05:51:00Z],
        dhuhr: ~U[2022-10-01 02:35:00Z],
        fajr: ~U[2022-09-30 19:10:00Z],
        isha: ~U[2022-10-01 09:43:00Z],
        maghrib: ~U[2022-10-01 08:28:00Z],
        sunrise: ~U[2022-09-30 20:35:00Z],
        sunset: ~U[2022-10-01 08:25:00Z]
      }

  """
  def find(
        %Coordinate{latitude: latitude} = coordinate,
        date,
        %CalculationParameter{} = calculation_parameter
      ) do
    tomorrow = date |> Timex.shift(days: 1)

    with {:ok,
          {%SolarTime{transit: transit, sunrise: sunrise, sunset: sunset} = solar_time,
           tomorrow_solar_time}} <-
           SolarTime.find_pair_solar_time(
             date,
             coordinate,
             calculation_parameter
           ),
         {:ok, sunrise_time} <-
           SunriseTime.find(sunrise, date, calculation_parameter, coordinate),
         {:ok, dhuhr_time} <- DhuhrTime.find(transit, date, calculation_parameter, coordinate),
         {:ok, asr_time} <- AsrTime.find(solar_time, date, calculation_parameter),
         {:ok, tomorrow_sunrise} <-
           SunriseTime.find_naive_time(tomorrow_solar_time.sunrise, tomorrow),
         {:ok, sunset_time} <- SunsetTime.find(sunset, date, calculation_parameter, coordinate),
         {:ok, night} <- calculate_night(tomorrow_sunrise, sunset_time),
         {:ok, isha_time} <-
           IshaTime.find(solar_time, sunset_time, night, latitude, date, calculation_parameter),
         {:ok, fajr_time} <-
           FajrTime.find(solar_time, sunrise_time, night, latitude, date, calculation_parameter),
         {:ok, maghrib_time} <-
           MaghribTime.find(solar_time, sunset_time, isha_time, date, calculation_parameter) do
      %__MODULE__{
        fajr: fajr_time |> FajrTime.adjust(calculation_parameter),
        sunrise: sunrise_time |> SunriseTime.adjust(calculation_parameter),
        dhuhr: dhuhr_time |> DhuhrTime.adjust(calculation_parameter),
        asr: asr_time |> AsrTime.adjust(calculation_parameter),
        sunset: sunset_time |> SunsetTime.adjust(calculation_parameter),
        maghrib: maghrib_time |> MaghribTime.adjust(calculation_parameter),
        isha: isha_time |> IshaTime.adjust(calculation_parameter)
      }
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc """
  Returns prayer time for prayer name

      ## Examples

      iex> prayer_time = %Azan.PrayerTime{
      ...>               asr: ~U[2022-10-01 05:51:00Z],
      ...>               dhuhr: ~U[2022-10-01 02:35:00Z],
      ...>               fajr: ~U[2022-09-30 19:10:00Z],
      ...>               isha: ~U[2022-10-01 09:43:00Z],
      ...>               maghrib: ~U[2022-10-01 08:28:00Z],
      ...>               sunrise: ~U[2022-09-30 20:35:00Z],
      ...>               sunset: ~U[2022-10-01 08:25:00Z]
      ...>             }
      iex> prayer_time |> PrayerTime.time_for_prayer(:fajr)
      ~U[2022-09-30 19:10:00Z]
  """
  @spec time_for_prayer(PrayerTime.t(), atom()) :: DateTime.t()
  def time_for_prayer(%__MODULE__{} = prayer_time, prayer_name) do
    prayer_time
    |> Map.get(prayer_name)
  end

  def current_prayer(
        %__MODULE__{
          fajr: fajr,
          sunrise: sunrise,
          dhuhr: dhuhr,
          asr: asr,
          maghrib: maghrib,
          isha: isha
        },
        time
      ) do
    fajr_epoch = fajr |> Timex.to_unix()
    sunrise_epoch = sunrise |> Timex.to_unix()
    dhuhr_epoch = dhuhr |> Timex.to_unix()
    asr_epoch = asr |> Timex.to_unix()
    maghrib_epoch = maghrib |> Timex.to_unix()
    isha_epoch = isha |> Timex.to_unix()

    case time |> Timex.to_unix() do
      x when x >= isha_epoch -> :isha
      x when x >= maghrib_epoch -> :maghrib
      x when x >= asr_epoch -> :asr
      x when x >= dhuhr_epoch -> :dhuhr
      x when x >= sunrise_epoch -> :sunrise
      x when x >= fajr_epoch -> :fajr
      _ -> :none
    end
  end

  def next_prayer(
        %__MODULE__{
          fajr: fajr,
          sunrise: sunrise,
          dhuhr: dhuhr,
          asr: asr,
          maghrib: maghrib,
          isha: isha
        },
        time
      ) do
    fajr_epoch = fajr |> Timex.to_unix()
    sunrise_epoch = sunrise |> Timex.to_unix()
    dhuhr_epoch = dhuhr |> Timex.to_unix()
    asr_epoch = asr |> Timex.to_unix()
    maghrib_epoch = maghrib |> Timex.to_unix()
    isha_epoch = isha |> Timex.to_unix()

    case time |> Timex.to_unix() do
      x when x >= isha_epoch -> :none
      x when x >= maghrib_epoch -> :isha
      x when x >= asr_epoch -> :maghrib
      x when x >= dhuhr_epoch -> :asr
      x when x >= sunrise_epoch -> :dhuhr
      x when x >= fajr_epoch -> :sunrise
      _ -> :fajr
    end
  end

  def rounded_time(prayer_time, adjustment, rounding) do
    prayer_time
    |> Timex.shift(minutes: adjustment)
    |> DateUtils.rounded_minute(rounding)
  end

  def calculate_night({:error, _}, _sunset_time), do: {:error, :invalid}
  def calculate_night(_tomorrow_sunrise, {:error, _}), do: {:error, :invalid}

  def calculate_night(tomorrow_sunrise, sunset_time) do
    with {:ok, tomorrow_sunrice_epoch} <-
           tomorrow_sunrise |> Timex.to_unix() |> DateUtils.wrap_timex_error(),
         {:ok, sunset_epoch} <- sunset_time |> Timex.to_unix() |> DateUtils.wrap_timex_error() do
      {:ok, tomorrow_sunrice_epoch - sunset_epoch}
    else
      error -> error
    end
  end
end