Packages
ex_cldr_calendars
1.14.1
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.1
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
1.26.4
1.26.3
1.26.2
1.26.1
1.26.0
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
retired
1.23.1
1.23.0
1.22.1
1.22.0
1.21.0
1.20.0
1.19.0
1.18.1
1.18.0
1.17.3
1.17.2
1.17.1
1.17.0
1.17.0-rc.3
1.17.0-rc.2
1.17.0-rc.1
1.17.0-rc.0
1.16.0
1.15.3
1.15.2
1.15.1
1.15.0
1.14.1
1.14.0
1.13.0
1.13.0-rc.1
1.13.0-rc.0
1.12.1
1.12.0
1.11.0
1.11.0-rc.0
1.10.1
1.10.0
1.9.0
1.8.1
1.8.0
1.8.0-rc.0
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.0
retired
1.1.0
retired
1.0.0
retired
0.9.0
retired
0.8.0
retired
0.7.0
retired
0.6.0
retired
0.5.0
retired
0.4.1
0.4.0
retired
0.3.0
retired
0.2.0
retired
0.1.0
retired
Localized month- and week-based calendars and calendar functions based upon CLDR data.
Current section
Files
Jump to
Current section
Files
lib/cldr/calendar/base/week.ex
defmodule Cldr.Calendar.Base.Week do
@moduledoc false
alias Cldr.Calendar.Config
alias Cldr.Calendar.Base.Month
alias Calendar.ISO
alias Cldr.Math
@days_in_week 7
@weeks_in_quarter 13
@months_in_quarter 3
@months_in_year 12
@weeks_in_long_year 53
@weeks_in_normal_year 52
@quarters_in_year 4
defmacro __using__(options \\ []) do
quote bind_quoted: [options: options] do
@options options
@before_compile Cldr.Calendar.Compiler.Week
end
end
def valid_date?(year, week, day, config) do
week <= weeks_in_year(year, config) and day in 1..days_in_week()
end
def year_of_era(year, config) do
{_, year} = Cldr.Calendar.start_end_gregorian_years(year, config)
Calendar.ISO.year_of_era(year)
end
# Quarters are 13 weeks but if there
# are 53 weeks in a year then 4th
# quarter is longer
def quarter_of_year(_year, @weeks_in_long_year, _day, _config) do
4
end
def quarter_of_year(_year, week, _day, _config) do
div(week - 1, @weeks_in_quarter) + 1
end
def month_of_year(_year, @weeks_in_long_year, _day, _config) do
+12
end
def month_of_year(year, week, day, %Config{weeks_in_month: weeks_in_month} = config) do
quarter = quarter_of_year(year, week, day, config)
months_in_prior_quarters = (quarter - 1) * @months_in_quarter
week_in_quarter = Math.amod(week, @weeks_in_quarter)
[m1, m2, _m3] = weeks_in_month
month_in_quarter =
cond do
week_in_quarter <= m1 -> 1
week_in_quarter <= m1 + m2 -> 2
true -> 3
end
months_in_prior_quarters + month_in_quarter
end
def week_of_year(year, week, _day, _config) do
{year, week}
end
def iso_week_of_year(year, week, day, config) do
with {:ok, date} <- Date.new(year, week, day, config.calendar) do
{:ok, %{year: year, month: month, day: day}} = Date.convert(date, Cldr.Calendar.Gregorian)
Cldr.Calendar.Gregorian.iso_week_of_year(year, month, day)
end
end
def week_of_month(year, week, day, config) do
month = month_of_year(year, week, day, config)
%Date.Range{first: first} = month(year, month, config)
{month, week - first.month + 1}
end
def day_of_era(year, week, day, config) do
with {:ok, date} <- Date.new(year, week, day, config.calendar) do
{:ok, %{year: year, month: month, day: day}} = Date.convert(date, Calendar.ISO)
Calendar.ISO.day_of_era(year, month, day)
end
end
def day_of_year(year, week, day, config) do
start_of_year = first_gregorian_day_of_year(year, config)
this_day = first_gregorian_day_of_year(year, config) + week_to_days(week) + day
this_day - start_of_year + 1
end
def day_of_week(_year, _week, day, %{first_or_last: :first} = config) do
first_day = config.day_of_week
Math.amod(first_day + day - 1, days_in_week())
end
def day_of_week(_year, _week, day, %{first_or_last: :last} = config) do
last_day = config.day_of_week
Math.amod(last_day + day, days_in_week())
end
def months_in_year(year, _config) do
Calendar.ISO.months_in_year(year)
end
def weeks_in_year(year, config) do
if long_year?(year, config), do: weeks_in_long_year(), else: weeks_in_normal_year()
end
def weeks_in_long_year do
@weeks_in_long_year
end
def weeks_in_normal_year do
@weeks_in_normal_year
end
def weeks_in_quarter(year, quarter, config) do
if quarter == @quarters_in_year && long_year?(year, config) do
@weeks_in_quarter + 1
else
@weeks_in_quarter
end
end
def days_in_year(year, config) do
if long_year?(year, config) do
@weeks_in_long_year * @days_in_week
else
@weeks_in_normal_year * @days_in_week
end
end
def days_in_month(year, @months_in_year, config) do
%Config{weeks_in_month: [_, _, weeks_in_last_month]} = config
weeks = if long_year?(year, config), do: weeks_in_last_month + 1, else: weeks_in_last_month
weeks * days_in_week()
end
def days_in_month(_year, month, config) do
%Config{weeks_in_month: weeks_in_month} = config
month_in_quarter = Math.amod(rem(month, @months_in_quarter), @months_in_quarter)
Enum.at(weeks_in_month, month_in_quarter - 1) * days_in_week()
end
def days_in_week do
@days_in_week
end
def days_in_week(_year, _week) do
@days_in_week
end
def year(year, config) do
with {:ok, first_day} <- Date.new(year, 1, 1, config.calendar),
{:ok, last_day} <-
Date.new(year, weeks_in_year(year, config), days_in_week(), config.calendar) do
Date.range(first_day, last_day)
end
end
def quarter(year, quarter, config) do
starting_week = (quarter - 1) * @weeks_in_quarter + 1
ending_week = starting_week + weeks_in_quarter(year, quarter, config) - 1
with {:ok, first_day} <- Date.new(year, starting_week, 1, config.calendar),
{:ok, last_day} <- Date.new(year, ending_week, days_in_week(), config.calendar) do
Date.range(first_day, last_day)
end
end
def month(year, month, %{weeks_in_month: weeks_in_month} = config) do
months_prior_in_quarter = rem(month - 1, @months_in_quarter)
prior_quarters = Month.quarter_of_year(year, month, 1, config) - 1
quarter_weeks_prior = prior_quarters * @weeks_in_quarter
weeks_prior_in_quarter =
weeks_in_month
|> Enum.take(months_prior_in_quarter)
|> Enum.sum()
weeks_in_month =
Enum.at(weeks_in_month, months_prior_in_quarter) +
maybe_extra_week_for_long_year(year, month, config)
first_week = quarter_weeks_prior + weeks_prior_in_quarter + 1
last_week = first_week + weeks_in_month - 1
{:ok, start_of_month} = Date.new(year, first_week, 1, config.calendar)
{:ok, end_of_month} = Date.new(year, last_week, days_in_week(), config.calendar)
Date.range(start_of_month, end_of_month)
end
def week(year, week, config) do
with {:ok, first_day} <- Date.new(year, week, 1, config.calendar),
{:ok, last_day} <- Date.new(year, week, days_in_week(), config.calendar) do
Date.range(first_day, last_day)
end
end
def plus(year, week, day, config, :years, years, options) do
new_year = year + years
coerce? = Keyword.get(options, :coerce, false)
{new_week, new_day} = Cldr.Calendar.month_day(new_year, week, day, config.calendar, coerce?)
{new_year, new_week, new_day}
end
def plus(year, week, day, config, :quarters, quarters, options) do
plus(year, week, day, config, :months, quarters * @months_in_quarter, options)
end
def plus(year, week, day, _config, :months, 0, _options) do
{year, week, day}
end
# When adding a month the process is
# 1. Capture the day of the month
# 2. Get the first day of that month
# 3. Add the number of days in that month leaving us
# with the first day of the next month
# 4. Add back the day of the month, potentially
# coercing the last day of the month
def plus(year, week, day, config, :months, 1 = n, options) do
day_of_month = day_of_month(year, week, day, config)
{year, week, day} = first_day_of_month(year, week, day, config)
month_of_year = month_of_year(year, week, day, config)
month_in_quarter = month_in_quarter(week, config)
weeks_to_add =
slice_weeks(month_in_quarter - 1, n, config) +
maybe_extra_week_for_long_year(year, month_of_year, config)
{year_increment, week} = Cldr.Math.div_mod(week + weeks_to_add, weeks_in_year(year, config))
add_days(year + year_increment, week, day, day_of_month - 1, config, options)
end
# WHen substracting a month the process is
# 1. Work out the day of the month of the provided date
# 2. Get the last day of the month
# 3. Subtract the number of days in the month, leaving us with
# the last day of the previous month
# 4. Get the first day of the previous month
# 5. Add back the day of the month capture earlier - potentialy coercing
# the last day of the month
def plus(year, week, day, config, :months, -1 = n, options) do
day_of_month = day_of_month(year, week, day, config)
{year, week, day} = last_day_of_month(year, week, day, config)
month_of_year = month_of_year(year, week, day, config)
month_in_quarter = month_in_quarter(week, config)
weeks_to_sub =
slice_weeks(month_in_quarter - 1, abs(n), config) +
maybe_extra_week_for_long_year(year, month_of_year, config)
week = week - weeks_to_sub
# IO.puts " Proposed week of previous month: #{week}"
{year, week} =
if week < 1 do
{year - 1, weeks_in_year(year - 1, config) - week}
else
{year, week}
end
{year, week, day} = first_day_of_month(year, week, day, config)
add_days(year, week, day, day_of_month - 1, config, options)
end
# Accounting for long years is complex. So for now adding and
# subtracting months is done one month at a time. Therefore
# performance for large additions/subsctractions will be poor.
def plus(year, week, day, config, :months, months, options) when abs(months) > 1 do
increment = if months > 0, do: 1, else: -1
original_day_of_month = day_of_month(year, week, day, config)
{year, week, day} =
Enum.reduce(1..abs(months), {year, week, day}, fn _i, {year, week, day} ->
plus(year, week, day, config, :months, increment, coerce: true)
end)
# Now reconcile the original date of the month with the
# previously calculated day of the month.
proposed_day_of_month = day_of_month(year, week, day, config)
days_difference = original_day_of_month - proposed_day_of_month
month = month_of_year(year, week, day, config)
if days_difference < 0 && original_day_of_month <= days_in_month(year, month, config) do
add_days(year, month, day, days_difference, config, options)
else
{year, week, day}
end
end
def add(naive_datetime, :year, step) do
%{year: year, month: month, day: day} = Cldr.Calendar.plus(naive_datetime, :years, step)
%NaiveDateTime{naive_datetime | year: year, month: month, day: day}
end
def day_of_month(year, week, day, config) do
first_day = first_day_of_month(year, week, day, config)
date_diff({year, week, day}, first_day, config) + 1
end
def date_diff({y2, w2, d2}, {y1, w1, d1}, config) do
date_to_iso_days(y2, w2, d2, config) - date_to_iso_days(y1, w1, d1, config)
end
def add_days(year, week, day, days, config, options \\ [])
def add_days(year, week, day, 0, _config, _options) do
{year, week, day}
end
def add_days(year, week, day, days, config, options) do
days_to_add =
if Keyword.get(options, :coerce) do
month = month_of_year(year, week, day, config)
min(days_in_month(year, month, config) - 1, days)
else
days
end
iso_days = date_to_iso_days(year, week, day, config) + days_to_add
date_from_iso_days(iso_days, config)
end
def slice_weeks(from, n, %{weeks_in_month: weeks_in_month}) do
{_, weeks} = Enum.split(weeks_in_month, from)
weeks
|> Enum.take(n)
|> Enum.sum()
end
def first_day_of_month(year, week, day, config) do
quarters = quarter_of_year(year, week, day, config) - 1
month_in_quarter = month_in_quarter(week, config) - 1
week = quarters * @weeks_in_quarter + weeks_from_months(month_in_quarter, config) + 1
{year, week, 1}
end
def last_day_of_month(year, week, day, config) do
{year, week, day} = first_day_of_month(year, week, day, config)
month_of_year = month_of_year(year, week, day, config)
days_in_month = days_in_month(year, month_of_year, config)
add_days(year, week, day, days_in_month - 1, config)
end
def month_in_quarter(week, _config) when week == @weeks_in_long_year do
@months_in_quarter
end
def month_in_quarter(week, config) do
week_in_quarter = Cldr.Math.amod(week, @weeks_in_quarter)
month_from_weeks(week_in_quarter, config)
end
def weeks_from_months(0, _config) do
0
end
# When months is positive we just sum the first n members of the
# weeks_in_month list.
def weeks_from_months(months, %{weeks_in_month: weeks_in_month}) when months > 0 do
weeks_in_month
|> Enum.take(months)
|> Enum.sum()
end
# When months is negative
# TODO May not be correct
def weeks_from_months(months, %{weeks_in_month: weeks_in_month}) when months < 0 do
{_, weeks_in_month} = List.pop_at(weeks_in_month, -1)
weeks_in_month
|> Enum.take(months)
|> Enum.sum()
end
# For weeks <= 13
def month_from_weeks(weeks, %Config{weeks_in_month: [m1, m2, m3]})
when weeks <= @weeks_in_quarter do
cond do
weeks <= m1 -> 1
weeks <= m1 + m2 -> 2
weeks <= m1 + m2 + m3 -> 3
end
end
def sign(number) when number < 0, do: -1
def sign(_number), do: +1
def first_gregorian_day_of_year(year, %Config{first_or_last: :first} = config) do
{year, _} = Cldr.Calendar.start_end_gregorian_years(year, config)
%{
month_of_year: first_month,
day_of_week: first_day,
min_days_in_first_week: min_days
} = config
iso_days = ISO.date_to_iso_days(year, first_month, min_days)
day_of_week = Cldr.Calendar.iso_days_to_day_of_week(iso_days)
# The iso_days calulation is the last possible first day of the first week
# All starting days are less than or equal to this day
if first_day > day_of_week do
iso_days + (first_day - days_in_week() - day_of_week)
else
iso_days - (day_of_week - first_day)
end
end
def first_gregorian_day_of_year(year, %Config{first_or_last: :last} = config) do
last_gregorian_day_of_year(year - 1, config) + 1
end
def last_gregorian_day_of_year(year, %Config{first_or_last: :first} = config) do
first_gregorian_day_of_year(year + 1, config) - 1
end
def last_gregorian_day_of_year(year, %Config{first_or_last: :last} = config) do
{_, year} = Cldr.Calendar.start_end_gregorian_years(year, config)
%{
month_of_year: last_month,
day_of_week: last_day,
min_days_in_first_week: min_days
} = config
days_in_last_month = ISO.days_in_month(year, last_month)
iso_days = ISO.date_to_iso_days(year, last_month, days_in_last_month - min_days)
day_of_week = Cldr.Calendar.iso_days_to_day_of_week(iso_days)
if last_day <= day_of_week do
iso_days - (day_of_week - last_day) + days_in_week()
else
iso_days - (day_of_week - last_day)
end
end
def long_year?(year, %Config{} = config) do
first_day = first_gregorian_day_of_year(year, config)
last_day = last_gregorian_day_of_year(year, config)
days_in_year = last_day - first_day + 1
div(days_in_year, days_in_week()) == @weeks_in_long_year
end
def date_to_iso_days(year, week, day, config) do
{days, _day_fraction} = naive_datetime_to_iso_days(year, week, day, 0, 0, 0, {0, 6}, config)
days
end
def date_from_iso_days(iso_day_number, config) do
{year, week, day, _, _, _, _} = naive_datetime_from_iso_days({iso_day_number, {0, 6}}, config)
{year, week, day}
end
def date_to_string(year, week, day) do
"#{year}-W#{lpad(week)}-#{day}"
end
def naive_datetime_from_iso_days({days, day_fraction}, config) do
{year, _month, _day} = Calendar.ISO.date_from_iso_days(days)
first_day = first_gregorian_day_of_year(year, config)
{year, first_day} =
cond do
first_day > days ->
{year - 1, first_gregorian_day_of_year(year - 1, config)}
days - first_day + 1 > config.calendar.days_in_year(year) ->
{year + 1, first_gregorian_day_of_year(year + 1, config)}
true ->
{year, first_day}
end
day_of_year = days - first_day + 1
week = trunc(Float.ceil(day_of_year / days_in_week()))
day = day_of_year - (week - 1) * days_in_week()
{hour, minute, second, microsecond} = Calendar.ISO.time_from_day_fraction(day_fraction)
{year, week, day, hour, minute, second, microsecond}
end
def naive_datetime_to_iso_days(year, week, day, hour, minute, second, microsecond, config) do
days = first_gregorian_day_of_year(year, config) + week_to_days(week) + day - 1
day_fraction = Calendar.ISO.time_to_day_fraction(hour, minute, second, microsecond)
{days, day_fraction}
end
def datetime_to_string(
year,
month,
day,
hour,
minute,
second,
microsecond,
time_zone,
zone_abbr,
utc_offset,
std_offset
) do
date_to_string(year, month, day) <>
" " <>
Calendar.ISO.time_to_string(hour, minute, second, microsecond) <>
Cldr.Calendar.offset_to_string(utc_offset, std_offset, time_zone) <>
Cldr.Calendar.zone_to_string(utc_offset, std_offset, zone_abbr, time_zone)
end
def naive_datetime_to_string(
year,
month,
day,
hour,
minute,
second,
microsecond
) do
date_to_string(year, month, day) <>
" " <>
Calendar.ISO.time_to_string(hour, minute, second, microsecond)
end
defp lpad(week) when week < 10 do
"0#{week}"
end
defp lpad(week) do
week
end
defp week_to_days(week) do
(week - 1) * days_in_week()
end
defp maybe_extra_week_for_long_year(year, @months_in_year, config) do
if long_year?(year, config), do: 1, else: 0
end
defp maybe_extra_week_for_long_year(_year, _month, _config) do
0
end
end