diff --git a/CHANGELOG.md b/CHANGELOG.md index df39b0c..ee8658e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ **Note that ex_cldr_calendars_persian is now supported on Elixir 1.12 and later** +## Cldr_Calendars_Persian v1.1.0 + +This is the changelog for ex_cldr_calendars_persian v1.1.01 released on July 6th, 2024. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_calendars_persian/tags) + +### Enhancements + +* Re-implemented `Cldr.Calendar.Persian` using `use Cldr.Calendar.Behaviour`. The code is easier to understand and, as a byproduct, any new callbacks are generally automatically taken care of. + ## Cldr_Calendars_Persian v1.0.1 This is the changelog for ex_cldr_calendars_persian v1.0.1 released on September 9th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_calendars_persian/tags) diff --git a/lib/cldr/calendar/persian.ex b/lib/cldr/calendar/persian.ex index cef9cad..4fbfea1 100644 --- a/lib/cldr/calendar/persian.ex +++ b/lib/cldr/calendar/persian.ex @@ -13,425 +13,58 @@ defmodule Cldr.Calendar.Persian do the Hegira of Muhammad from Mecca to Medina in 622 CE). """ - @behaviour Calendar - @behaviour Cldr.Calendar - @type year :: -9999..-1 | 1..9999 - @type month :: 1..12 - @type day :: 1..31 - - @quarters_in_year 4 - @months_in_year 12 - @months_in_quarter 3 - @days_in_week 7 + use Cldr.Calendar.Behaviour, + epoch: ~D[0622-03-20 Cldr.Calendar.Julian], + cldr_calendar_type: :persian @mean_tropical_year 365.24219 @doc """ - Defines the CLDR calendar type for this calendar. - - This type is used in support of `Cldr.Calendar. - localize/3`. - - """ - @impl true - def cldr_calendar_type do - :persian - end - - @doc """ - Identifies that this calendar is month based. - """ - @impl true - def calendar_base do - :month - end - - @epoch Cldr.Calendar.Julian.date_to_iso_days(622, 3, 19) - def epoch do - @epoch - end - - @doc """ - Determines if the date given is valid according to - this calendar. - - """ - @impl true - @months_with_30_days 7..11 - def valid_date?(_year, month, day) when month in @months_with_30_days and day in 1..30 do - true - end - - @months_with_31_days 1..6 - def valid_date?(_year, month, day) when month in @months_with_31_days and day in 1..31 do - true - end - - def valid_date?(year, 12, 30) do - if leap_year?(year), do: true, else: false - end - - def valid_date?(_year, 12, day) when day in 1..29 do - true - end - - def valid_date?(_year, _month, _day) do - false - end - - @doc """ - Calculates the year and era from the given `year`. - The ISO calendar has two eras: the current era which - starts in year 1 and is defined as era "1". And a - second era for those years less than 1 defined as - era "0". - - """ - @spec year_of_era(year) :: {year, era :: 0..1} - def year_of_era(year) when year > 0 do - {year, 1} - end - - def year_of_era(year) when year < 0 do - {abs(year), 0} - end - - @spec year_of_era(year, month, day) :: {year, era :: 0..1} - @impl Calendar - def year_of_era(year, _month, _day) do - year_of_era(year) - end - - @doc """ - Returns the calendar year as displayed - on rendered calendars. - - """ - @spec calendar_year(year, month, day) :: Calendar.year() - @impl Cldr.Calendar - def calendar_year(year, _month, _day) do - year - end - - @doc """ - Returns the related gregorain year as displayed - on rendered calendars. - - """ - @spec related_gregorian_year(year, month, day) :: Calendar.year() - @impl Cldr.Calendar - def related_gregorian_year(year, _month, _day) do - iso_days = date_to_iso_days(year, 1, 1) - {year, _month, _day} = Cldr.Calendar.Gregorian.date_from_iso_days(iso_days) - year - end - - @doc """ - Returns the extended year as displayed - on rendered calendars. - - """ - @spec extended_year(year, month, day) :: Calendar.year() - @impl Cldr.Calendar - def extended_year(year, _month, _day) do - year - end - - @doc """ - Returns the cyclic year as displayed - on rendered calendars. - - """ - @spec cyclic_year(year, month, day) :: Calendar.year() - @impl Cldr.Calendar - def cyclic_year(year, _month, _day) do - year - end - - @doc """ - Calculates the quarter of the year from the given `year`, `month`, and `day`. - It is an integer from 1 to 4. - - """ - @spec quarter_of_year(year, month, day) :: 1..4 - @impl true - def quarter_of_year(_year, month, _day) do - Float.ceil(month / @months_in_quarter) - |> trunc - end - - @doc """ - Calculates the month of the year from the given `year`, `month`, and `day`. - It is an integer from 1 to 12. - - """ - @spec month_of_year(year, month, day) :: month - @impl true - def month_of_year(_year, month, _day) do - month - end - - @doc """ - Calculates the week of the year from the given `year`, `month`, and `day`. - It is an integer from 1 to 53. - - """ - @spec week_of_year(year, month, day) :: {:error, :not_defined} - @impl true - def week_of_year(_year, _month, _day) do - {:error, :not_defined} - end - - @doc """ - Calculates the ISO week of the year from the given `year`, `month`, and `day`. - It is an integer from 1 to 53. - - """ - @spec iso_week_of_year(year, month, day) :: {:error, :not_defined} - @impl true - def iso_week_of_year(_year, _month, _day) do - {:error, :not_defined} - end - - @doc """ - Calculates the week of the year from the given `year`, `month`, and `day`. - It is an integer from 1 to 53. - - """ - @spec week_of_month(year, month, day) :: {pos_integer(), pos_integer()} | {:error, :not_defined} - @impl true - def week_of_month(_year, _month, _day) do - {:error, :not_defined} - end - - @doc """ - Calculates the day and era from the given `year`, `month`, and `day`. - - """ - @spec day_of_era(year, month, day) :: {day :: pos_integer(), era :: 0..1} - @impl true - def day_of_era(year, month, day) do - {_, era} = year_of_era(year) - days = date_to_iso_days(year, month, day) - {days + epoch(), era} - end - - @doc """ - Calculates the day of the year from the given `year`, `month`, and `day`. - - """ - @spec day_of_year(year, month, day) :: 1..366 - @impl true - def day_of_year(year, month, day) do - first_day = date_to_iso_days(year, 1, 1) - this_day = date_to_iso_days(year, month, day) - this_day - first_day + 1 - end - - @epoch_day_of_week 6 - - if Code.ensure_loaded?(Date) && function_exported?(Date, :day_of_week, 2) do - @last_day_of_week 5 - - @spec day_of_week(year, month, day, :default | atom()) :: - {Calendar.day_of_week(), first_day_of_week :: non_neg_integer(), - last_day_of_week :: non_neg_integer()} - - @impl true - def day_of_week(year, month, day, :default) do - days = date_to_iso_days(year, month, day) - days_after_saturday = rem(days, 7) - day = Cldr.Math.amod(days_after_saturday + @epoch_day_of_week, @days_in_week) - - {day, @epoch_day_of_week, @last_day_of_week} - end - else - @spec day_of_week(year, month, day) :: 1..7 - - @impl true - def day_of_week(year, month, day) do - days = date_to_iso_days(year, month, day) - days_after_saturday = rem(days, 7) - Cldr.Math.amod(days_after_saturday + @epoch_day_of_week, @days_in_week) - end - end - - @doc """ - Calculates the number of period in a given `year`. A period - corresponds to a month in month-based calendars and - a week in week-based calendars.. - - """ - @impl true - def periods_in_year(_year) do - @months_in_year - end - - @impl true - def weeks_in_year(_year) do - {:error, :not_defined} - end - - @doc """ - Returns the number days in a given year. - - """ - @impl true - def days_in_year(year) do - if leap_year?(year), do: 366, else: 365 - end - - @doc """ - Returns how many days there are in the given year-month. - - """ - @spec days_in_month(year, month) :: 29..31 - @impl true - - def days_in_month(year, 12) do - if leap_year?(year), do: 30, else: 29 - end - - def days_in_month(_year, month) when month in @months_with_30_days do - 30 - end - - def days_in_month(_year, month) when month in @months_with_31_days do - 31 - end - - @doc """ - Returns how many days there are in the given month. - - Must be implemented in derived calendars because - we cannot know what the calendar format is. - - """ - @spec days_in_month(Calendar.month) :: Calendar.month() | {:ambiguous, Range.t() | [pos_integer()]} | {:error, :undefined} - @impl true - - def days_in_month(_month) do - {:error, :undefined} - end - - @doc """ - Returns the number days in a a week. - - """ - def days_in_week do - @days_in_week - end - - @doc """ - Returns a `Date.Range.t` representing - a given year. - - """ - @impl true - def year(year) do - last_month = months_in_year(year) - days_in_last_month = days_in_month(year, last_month) - - with {:ok, start_date} <- Date.new(year, 1, 1, __MODULE__), - {:ok, end_date} <- Date.new(year, last_month, days_in_last_month, __MODULE__) do - Date.range(start_date, end_date) - end - end - - @doc """ - Returns a `Date.Range.t` representing - a given quarter of a year. - - """ - @impl true - def quarter(year, quarter) do - months_in_quarter = div(months_in_year(year), @quarters_in_year) - starting_month = months_in_quarter * (quarter - 1) + 1 - starting_day = 1 - - ending_month = starting_month + months_in_quarter - 1 - ending_day = days_in_month(year, ending_month) - - with {:ok, start_date} <- Date.new(year, starting_month, starting_day, __MODULE__), - {:ok, end_date} <- Date.new(year, ending_month, ending_day, __MODULE__) do - Date.range(start_date, end_date) - end - end + Returns if the given year is a leap year. - @doc """ - Returns a `Date.Range.t` representing - a given month of a year. + Since this calendar is observational we + calculate the start of successive years + and then calcualate the difference in + days to determine if its a leap year. """ + @spec leap_year?(Calendar.year) :: boolean() @impl true - def month(year, month) do - starting_day = 1 - ending_day = days_in_month(year, month) - - with {:ok, start_date} <- Date.new(year, month, starting_day, __MODULE__), - {:ok, end_date} <- Date.new(year, month, ending_day, __MODULE__) do - Date.range(start_date, end_date) - end + def leap_year?(year) do + new_year = date_to_iso_days(year, 1, 1) + next_year = date_to_iso_days(year + 1, 1, 1) + next_year - new_year == 366 end @doc """ - Returns a `Date.Range.t` representing - a given week of a year. + Returns the number of days since the calendar + epoch for a given `year-month-day` """ - @impl true - def week(_year, _week) do - {:error, :not_defined} + def date_to_iso_days(year, month, day) do + new_year = + new_year_on_or_before(epoch() + 180 + + :math.floor(@mean_tropical_year * if(0 < year, do: year - 1, else: year)) |> trunc) + new_year - 1 + if(month <= 7, do: 31 * (month - 1), else: 30 * (month - 1) + 6) + day end @doc """ - Adds an `increment` number of `date_part`s - to a `year-month-day`. - - `date_part` can be `:quarters` - or`:months`. + Returns a `{year, month, day}` calculated from + the number of `iso_days`. """ - @impl true - def plus(year, month, day, date_part, increment, options \\ []) - - def plus(year, month, day, :quarters, quarters, options) do - months = quarters * @months_in_quarter - plus(year, month, day, :months, months, options) - end - - def plus(year, month, day, :months, months, options) do - months_in_year = months_in_year(year) - {year_increment, new_month} = Cldr.Math.div_amod(month + months, months_in_year) - new_year = year + year_increment - - new_day = - if Keyword.get(options, :coerce, false) do - max_new_day = days_in_month(new_year, new_month) - min(day, max_new_day) - else - day - end - - {new_year, new_month, new_day} - end - - @doc """ - Returns if the given year is a leap year. + def date_from_iso_days(iso_days) do + new_year_iso_days = new_year_on_or_before(iso_days) + y = round((new_year_iso_days - epoch()) / @mean_tropical_year) + 1 + year = if y > 0, do: y, else: y - 1 + day_of_year = 1 + iso_days - date_to_iso_days(year, 1, 1) - Since this calendar is observational we - calculate the start of successive years - and then calcualate the difference in - days to determine if its a leap year. + month = if day_of_year <= 186, do: ceil(day_of_year / 31), + else: :math.ceil((day_of_year - 6) / 30) - """ - @spec leap_year?(year) :: boolean() - @impl true - def leap_year?(year) do - new_year = date_to_iso_days(year, 1, 1) - next_year = date_to_iso_days(year + 1, 1, 1) - next_year - new_year == 366 + day = iso_days - date_to_iso_days(year, month, 1) + 1 + {year, trunc(month), trunc(day)} end @doc """ @@ -470,36 +103,6 @@ defmodule Cldr.Calendar.Persian do Astro.equinox(year, :march) end - @doc """ - Returns the number of days since the calendar - epoch for a given `year-month-day` - - """ - def date_to_iso_days(year, month, day) do - new_year = - new_year_on_or_before(epoch() + 180 + - :math.floor(@mean_tropical_year * if(0 < year, do: year - 1, else: year)) |> trunc) - new_year - 1 + if(month <= 7, do: 31 * (month - 1), else: 30 * (month - 1) + 6) + day - end - - @doc """ - Returns a `{year, month, day}` calculated from - the number of `iso_days`. - - """ - def date_from_iso_days(iso_days) do - new_year_iso_days = new_year_on_or_before(iso_days) - y = round((new_year_iso_days - epoch()) / @mean_tropical_year) + 1 - year = if y > 0, do: y, else: y - 1 - day_of_year = 1 + iso_days - date_to_iso_days(year, 1, 1) - - month = if day_of_year <= 186, do: ceil(day_of_year / 31), - else: :math.ceil((day_of_year - 6) / 30) - - day = iso_days - date_to_iso_days(year, month, 1) + 1 - {year, trunc(month), trunc(day)} - end - def new_year_on_or_before(iso_days) when is_integer(iso_days) do {year, month, day} = Cldr.Calendar.Gregorian.date_from_iso_days(iso_days) {:ok, date} = Date.new(year, month, day) @@ -515,126 +118,4 @@ defmodule Cldr.Calendar.Persian do Cldr.Calendar.Gregorian.date_to_iso_days(new_year.year, new_year.month, new_year.day) end - @doc """ - Returns the `t:Calendar.iso_days/0` format of the specified date. - - """ - @impl true - @spec naive_datetime_to_iso_days( - Calendar.year(), - Calendar.month(), - Calendar.day(), - Calendar.hour(), - Calendar.minute(), - Calendar.second(), - Calendar.microsecond() - ) :: Calendar.iso_days() - - def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do - {date_to_iso_days(year, month, day), time_to_day_fraction(hour, minute, second, microsecond)} - end - - @doc """ - Converts the `t:Calendar.iso_days/0` format to the datetime format specified by this calendar. - - """ - @spec naive_datetime_from_iso_days(Calendar.iso_days()) :: { - Calendar.year(), - Calendar.month(), - Calendar.day(), - Calendar.hour(), - Calendar.minute(), - Calendar.second(), - Calendar.microsecond() - } - @impl true - def naive_datetime_from_iso_days({days, day_fraction}) do - {year, month, day} = date_from_iso_days(days) - {hour, minute, second, microsecond} = time_from_day_fraction(day_fraction) - {year, month, day, hour, minute, second, microsecond} - end - - @doc false - @impl Calendar - defdelegate day_rollover_relative_to_midnight_utc, to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate months_in_year(year), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate time_from_day_fraction(day_fraction), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate time_to_day_fraction(hour, minute, second, microsecond), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate parse_date(date_string), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate parse_time(time_string), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate parse_utc_datetime(dt_string), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate parse_naive_datetime(dt_string), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate date_to_string(year, month, day), to: Calendar.ISO - - if Code.ensure_loaded?(Calendar.ISO) && function_exported?(Calendar.ISO, :iso_days_to_beginning_of_day, 1) do - @doc false - @impl Calendar - defdelegate iso_days_to_beginning_of_day(iso_days), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate iso_days_to_end_of_day(iso_days), to: Calendar.ISO - end - - @doc false - @impl Calendar - defdelegate datetime_to_string( - year, - month, - day, - hour, - minute, - second, - microsecond, - time_zone, - zone_abbr, - utc_offset, - std_offset - ), - to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate naive_datetime_to_string( - year, - month, - day, - hour, - minute, - second, - microsecond - ), - to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate time_to_string(hour, minute, second, microsecond), to: Calendar.ISO - - @doc false - @impl Calendar - defdelegate valid_time?(hour, minute, second, microsecond), to: Calendar.ISO end diff --git a/mix.exs b/mix.exs index e280344..079b766 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Cldr.Calendars.Persian.MixProject do use Mix.Project - @version "1.0.1" + @version "1.1.0" def project do [