lib/chinese_holiday.ex

defmodule ChineseHoliday do
  @moduledoc """
  Provides utilities for handling chinese holiday related problems.
  """

  require Logger
  alias __MODULE__.Data

  @supported_years Data.get_supported_years()
  @data_updated_datetime Data.get_updated_datetime()

  @doc """
  Gets all the supported years.

  ## Examples

      iex> ChineseHoliday.get_supported_years()
      [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]

  """
  @spec get_supported_years() :: list()
  def get_supported_years(), do: @supported_years

  @doc """
  Gets the version of data.

  The version is a plain `%DateTime{}`.

  ## Examples

      iex> ChineseHoliday.get_version()
      ~U[2022-12-10 02:02:00Z]

  """
  @spec get_version() :: DateTime.t()
  def get_version(), do: @data_updated_datetime

  @doc """
  Checks if a given date is a holiday.

  Following dates will be considered as holidays:

  * dates in holiday schedules which are published by State Council of the People's Republic of China,
    aka. chinese statutory holiday.

  > To get the original information, please visit
  > [中华人民共和国中央人民政府 - 政府信息公开](http://www.gov.cn/zhengce/xxgk/index.htm),
  > and search '节假日'.

  ## Examples

      # the holiday of lunar new year
      iex> ChineseHoliday.is_holiday?(~D[2023-01-21])
      true

      # the holiday of lunar new year
      iex> ChineseHoliday.is_holiday?(~D[2023-01-27])
      true

      # it's time to work ;)
      iex> ChineseHoliday.is_holiday?(~D[2023-01-28])
      false

      # the holiday information is missing, so this function doesn't know how to handle it.
      # in this case, it will log a warning message, and return `false`.
      iex> ChineseHoliday.is_holiday?(~D[2090-01-28])
      false

  """
  @spec is_holiday?(Date.t()) :: boolean()
  def is_holiday?(%Date{year: year}) when year not in @supported_years do
    Logger.warning(fn ->
      "the holiday information of #{year} isn't provided by current version of chinese_holiday"
    end)

    false
  end

  Data.get_holidays()
  |> Enum.map(fn entry ->
    %{entry | date: Macro.escape(entry.date)}
  end)
  |> Enum.each(fn %{date: date} ->
    def is_holiday?(unquote(date)), do: true
  end)

  def is_holiday?(_), do: false

  @doc """
  Checks if a given date is a working day.

  Following dates will be considered as working days:

  * dates marked as additional working days to compensate for the long holiday break.
  * dates which are normal weekdays (monday ~ friday), and are not in the duration of any holiday.

  ## Examples

      # additional working days
      iex> ChineseHoliday.is_working_day?(~D[2023-01-28])
      true

      iex> ChineseHoliday.is_working_day?(~D[2023-01-29])
      true

      # weekdays in the duration of one holiday
      iex> ChineseHoliday.is_working_day?(~D[2023-01-27])
      false

      # normal weekdays
      iex> ChineseHoliday.is_working_day?(~D[2023-01-30])
      true

      iex> ChineseHoliday.is_working_day?(~D[2023-01-31])
      true

      # normal weekends
      iex> ChineseHoliday.is_working_day?(~D[2023-02-11])
      false

      # the working day information is missing, so this function doesn't know how to handle it.
      # in this case, it will log a warning message, and return `false`.
      iex> ChineseHoliday.is_working_day?(~D[2090-01-28])
      false

  """
  @spec is_working_day?(Date.t()) :: boolean()
  def is_working_day?(%Date{year: year}) when year not in @supported_years do
    Logger.warning(fn ->
      "the working day information of #{year} isn't provided by current version of chinese_holiday"
    end)

    false
  end

  Data.get_working_days()
  |> Enum.map(fn entry ->
    %{entry | date: Macro.escape(entry.date)}
  end)
  |> Enum.each(fn %{date: date} ->
    def is_working_day?(unquote(date)), do: true
  end)

  def is_working_day?(%Date{} = date) do
    %{year: year, month: month, day: day} = date
    weekday = :calendar.day_of_the_week(year, month, day)
    !is_holiday?(date) and weekday in 1..5
  end
end