lib/cldr/calendar/sigils.ex

defmodule Cldr.Calendar.Sigils do
  @moduledoc """
  Implements the `~d` sigils to produce
  dates, datetimes and naive datetimes.

  """

  alias Cldr.Config

  @doc """
  Implements a ~d sigil for expressing dates.

  Dates can be expressed in the following formats:

  * `~d[yyyy-mm-dd]` which produces a date in the `Cldr.Calendar.Gregorian` calendar
  * `~d[yyyy-Wmm-dd]` which produces a date in the `Cldr.Calendar.ISOWeek` calendar
  * `~d[yyyy-mm-dd calendar]` which produces a date in the given month-based calendar
  * `~d[yyyy-Wmm-dd calendar]` which produces a date in the given week-based calendar
  * `~d[yyyy-mm-dd C.E Julian]` which produces a date in the Cldr.Calendar.Julian calendar
  * `~d[yyyy-mm-dd B.C.E Julian]` which produces a date in the Cldr.Calendar.Julian calendar

  ## Examples

      iex> import Cldr.Calendar.Sigils
      iex> ~d[2019-01-01 Gregorian]
      ~d[2019-01-01 Gregorian]
      iex> ~d[2019-W01-01]
      ~d[2019-W01-1 ISOWeek]

  """
  defmacro sigil_d({:<<>>, _, [string]}, modifiers) do
    do_sigil_d(string, modifiers)
    |> Macro.escape()
  end

  def do_sigil_d(<<year::bytes-4, "-", month::bytes-2, "-", day::bytes-2>>, _) do
    to_date(year, month, day, Cldr.Calendar.Gregorian)
  end

  def do_sigil_d(<<year::bytes-4, "-", month::bytes-2, "-", day::bytes-2, " C.E. Julian">>, _) do
    to_date(year, month, day, Cldr.Calendar.Julian)
  end

  def do_sigil_d(<<year::bytes-4, "-", month::bytes-2, "-", day::bytes-2, " B.C.E. Julian">>, _) do
    to_date("-" <> year, month, day, Calendar.Julian)
  end

  def do_sigil_d(
        <<year::bytes-4, "-", month::bytes-2, "-", day::bytes-2, " ", calendar::binary>>,
        _
      ) do
    to_date(year, month, day, calendar)
  end

  def do_sigil_d(
        <<"-", year::bytes-4, "-", month::bytes-2, "-", day::bytes-2, " ", calendar::binary>>,
        _
      ) do
    to_date("-" <> year, month, day, calendar)
  end

  def do_sigil_d(<<year::bytes-4, "-W", month::bytes-2, "-", day::bytes-2>>, _) do
    to_date(year, month, day, Cldr.Calendar.ISOWeek)
  end

  def do_sigil_d(<<year::bytes-4, "-W", month::bytes-2, "-", day::bytes-1>>, _) do
    to_date(year, month, day, Cldr.Calendar.ISOWeek)
  end

  def do_sigil_d(
        <<year::bytes-4, "-W", month::bytes-2, "-", day::bytes-2, " ", calendar::binary>>,
        _
      ) do
    to_date(year, month, day, calendar)
  end

  def do_sigil_d(
        <<year::bytes-4, "-W", month::bytes-2, "-", day::bytes-1, " ", calendar::binary>>,
        _
      ) do
    to_date(year, month, day, calendar)
  end

  defp to_date(year, month, day, calendar) do
    [year, month, day] = Enum.map([year, month, day], &String.to_integer/1)

    with {:ok, calendar} <- calendar_from_binary(calendar),
         {:ok, date} <- Date.new(year, month, day, calendar) do
      date
    end
  end

  defp calendar_from_binary(calendar) do
    inbuilt_calendar(calendar) ||
      fiscal_calendar(calendar) ||
      user_calendar(calendar) ||
      calendar_error(calendar)
  end

  defp inbuilt_calendar(calendar) do
    calendar = Module.concat(Cldr.Calendar, calendar)
    get_calendar(calendar)
  end

  defp fiscal_calendar(calendar) do
    calendar = Module.concat(Cldr.Calendar.FiscalYear, calendar)
    get_calendar(calendar)
  end

  defp user_calendar(calendar) do
    Module.concat("Elixir", calendar)
    |> get_calendar
  end

  defp get_calendar(calendar) do
    if Config.ensure_compiled?(calendar) and function_exported?(calendar, :cldr_calendar_type, 0) do
      {:ok, calendar}
    else
      nil
    end
  end

  defp calendar_error(calendar) do
    {:error, {Cldr.UnknownCalendarError, calendar}}
  end
end