josevalim

josevalim

Creator of Elixir

How to support multiple week calendars in Elixir?

@kip said:

And then @josevalim said:

Which @kip finally replied:

I will continue the discussion below.

First 10 of 15 Posts Switch mode

josevalim

josevalim OP

Creator of Elixir

That’s a good point. I can think of cases this would be an issue but I can’t come up with anything concrete. Do you have any? Shall we explore this a bit further?

Here is a slightly more complete proposal. Imagine you have a data-structure called %WeekDay{}. It has fields such as year, week, and day of week. It also has a “week_calendar” that knows how to convert it to iso days so you can convert it back to a Date or even a DateTime with timezone (you can embed timezones on the Weekday data structure if you want to or even have NaiveWeekDay without tz and WeekDay with tz).

Does this make your life better or worse? I guess though what you are implementing here is exactly a Date*, except you are using the month as week and the day as day of week. Although it may still be worth exploring if it makes computations easier or harder.

They all represent the same point in time but they are different representations. In your case, they are the same representation. So I guess my point here is that it is weird to change calendars when they have the exact same underlying representation.

kip

kip

ex_cldr Core Team

José, I appreciate the opportunity to work this through, it’s been vexing me for a while.

Each calendar as a concrete implementation (reasoning)

Convertability

One of the reasons I came to the idea that each variation is its own calendar is because conversion between calendars becomes straight forward using the existing Calendar module functions.

As an example, lets say I’m using a 445 calendar. It has exactly the same notion of year, month and day. Except that the start/end of the year is a different Gregorian date and the month numbers don’t align either. Neither do the week days. For financial reporting purposes I would use my 445 calendar. But of course I also want to know what those periods are in the Gregorian/ISO calendar. Using date_to_iso_days/1 and iso_days_to_date I can convert between the two in a very straight forward manner. I don’t need to use another api to manage representations.

Encapsulation

As we discussed earlier, but keeping all the calendar configuration in the relevant Calendar module, a calendar now easily conforms to the standard Date, Time and DateTime structs which is in line with a developers expectations. Although the date/time/datetime functions only require structural compatibility, i think the developer experience is simplified if the principle of one module == one calendar is kept.

Implementation details

No doubt that there is plenty of room to optimise implementation for several classes of calendars, including 445 style, and calendars that have different min_days and first_day_of_week.

My thinking was something like this:

Configuration

defmodule MyCalendars.SomeCalendar do
  use MyCalendars.Gregorian, min_days: 4, first_day: 1
end

defmodule MyCalendars.RetailCalendar do
  use MyCalendars.Calendar445, type: :454, ends: {:last, 7, 7}
end

By no means the exact API but it will be reasonably straight forward to encapsulate the generalised version of these typical calendar types in a concrete fashion.

Module generation

One additional challenge with this approach is runtime configuration. As one example, a locale_string can actually specify the desired calendar as defined in BCP47. So assume we have an accept-language header with a language-tag it might look like this:

Accept-Language: he-IL-u-ca-hebrew-tz-jeruslm

Represents Hebrew as spoken in Israel, using the traditional Hebrew calendar, and in the “Asia/Jerusalem” time zone as identified in the tz database.

The ca=hebrew is the calendar configuration and it could be retail or cisco or whatever is interpreted by the serving system. In this case, a module needs to exist and it might need to be generated at runtime. I am not sure the optimal way to do this.

josevalim

josevalim OP

Creator of Elixir

Interesting! So the representation that you are keeping in Date/Time is actually the year of week and month of week? The other question is: what would those calendars return for week_of_year? Just the information in the struct?

EDIT: Meanwhile I have removed week_of_year from the Calendar callbacks because I am really uncertain it is the best way to go. If using calendars is the best way to model week-based calendars, then having week_of_year is like having japanese_era as a Calendar callback.

I do like this. :+1:

Qqwy

Qqwy

TypeCheck Core Team

I haven’t had the time yet to read through this discussion in much detail, but I do want to say that I really like that we are having this discussion :heart_eyes:!

I will post an in-depth post later today or tomorrow.

kip

kip

ex_cldr Core Team

I’ve gone backwards and forwards on this and concluded that for any given Calendar, the year, month and day are that calendars representation. This fits for Gregorian derivatives (like 445, or US fiscal) since the notion of year, month and day remain but are offset from the Gregorian.

Where it is uncomfortable is for week-based calendars, like ISOWeek. In this case I use month to mean week. year and day retail their normal meaning but within the ISOWeek context. Its not perfect but its workable.

Every calendar we have discussed has the concept of a week (French Revolutionary has 10 days, but there rest, afair are 7 days). Maybe I’m an outlier on this. I don’t see week_of_year as valuable to only week-based calendars like ISOWeek.

For Gregorian-based calendars, the first_day and min_days determine the start of the weekly cycle. Weeks of year (and also quarter and month) are used quite a lot for scheduling and reporting (“.. my forecast for week 11 of quarter 3 is …”). I hope there may be a case to return the callback, but clearly it can be implemented outside of the behaviour.

josevalim

josevalim OP

Creator of Elixir

Why not use the week-based month here too? Since that is also specified by ISO? I am mostly curious. :slight_smile:

kip

kip

ex_cldr Core Team

Mostly because the standard for ISOWeek says:

The ISO standard does not define any association of weeks to months. A date is either expressed with a month and day-of-the-month, or with a week and day-of-the-week, never a mix.

Qqwy

Qqwy

TypeCheck Core Team

I finally had time to catch up with the discussion.


In the earlier discussion we had when talking about implementing the calendar behaviour, I noted that the concept of a week is orthogonal to the concept of days/months ❦, and arrived at the same reasoning as we did here, that when calculating with weeks, this could be modeled by a separate calendar that uses the month-field to store the week number.

If I remember correctly, this was not done at that time and we ended up including day_of_week in the Calendar behaviour; the reasoning being that it was common enough for people to want to look up the week to include it in the calendar directly.

I think this reasoning is still valid, although I do not think that we should make the Calendar behaviour itself more complex because of it.

Currently, I like the %WeekDay{}-approach the best; I think it might look as follows:

  • We introduce a new behaviour called WeekCalendar, which mainly works with %WeekDay{} (or maybe %WeekDate{}/%WeekDateTime{}-structs?)
  • We might find that there is some overlap between the WeekCalendar and Calendar behaviours (maybe we can extract this to a separate behaviour?)
  • Converting between a Calendar and a WeekCalendar is possible using the date_to_iso_days-helper in the middle.
  • We decide on some way to configure (and provide defaults?) on conversions that happen from a Calendar to a compatible WeekCalendar.
  • This conversion might be used under the hood in the implementation of e.g. day_of_week, and of course play a role in the new formatter-proposal we’re working on in a similar fashion.

I see the main advantage to introducing a new behaviour as twofold:

  1. There is a clear separation between :month and :week, week-based calendars do not have to think about how the months work (and only implement behaviour callbacks that make sense for week-related datastructures).
  2. People working with the results are able to pattern-match on the :week-field rather than a :month-field as well.

❦ Side note: Calendar-weeks are often based on a religious background, although there is research that suggests that there definitely is psychological merit for humans to having a work-leisure cycle of ~7-8 days. In any case, the relation between weeks and ‘months’ for any given calendar is either ‘very loose’ (when trying to evenly subdivide the synodic lunar month of ~29.5 days), or completely non-existent. @kip 7 day-weeks are the most common, but there are:

  • 7-day week calendars with ‘leap weeks’ to make sure every new year starts at the same day of the week again.
  • The old Roman 8-day week Nundiae (‘market week’) that was in use until the Julian calendar took over.
  • Besides the French Revolution calendar, there are a couple of ancient Egyptian and Chinese calendars with 10-day weeks.
  • A couple of places used a 5-day calendar (Iceland, Java, Korea)
  • The Maya calendar has two variants, one dividing the 260-day year in 20 13-day weeks, and one to divide the 260-day year in 18 20-day ‘months’ that were each subdivideded in 5-day weeks, and having one extra ‘monthless’ 5 day week.
  • The Soviet Union had a 5-day week (and later also a 6-day week) that was used alongside the traditional 7-day one for factory workers.
kip

kip

ex_cldr Core Team

Its been a while but I’m getting reasonable close to releasing ex_cldr_calendars. The focus has been on gregorian-based calendars that follow the gregorian month cycle with a variable starting month as well as week-based calendars that have a variable start or end day and a configurable week.

After a number of design experiments I am settled on an approach where a calendar module can be defined statically such as the calendar that is defined by the US National Retail Federation. Its quite simple:

defmodule Cldr.Calendar.NRF do
  use Cldr.Calendar.Base.Week,
    min_days: 4,
    anchor: :last,
    day: 6,
    month: 1
end

Which basically means the calendar year ends on the last saturday nearest the end of January. An example of a financial year calendar for a US corporation that elects to have its financial year finish on the last Saturday in July would be:

defmodule Cldr.Calendar.CSCO do
  use Cldr.Calendar.Base.Week,
    min_days: 5,
    anchor: :last,
    day: 6,
    month: 7
end

An example of a month-based calendar would be a financial year calendar for a country. In the US the government financial year starts in October so it is defined by:

defmodule Cldr.Calendar.US do
  use Cldr.Calendar.Base.Month,
    month: 10
end

(Australia would be month 7, the UK would be month 4).

Calendars can be generated at runtime by passing the configuration to Cldr.Calendar.get_or_create(calendar, config) or to Cldr.Calendar.get_or_create_for(territory) which will create a financial year calendar module configured for a given territory (country).

Thoughts on the Calendar behaviour

As I’ve gone through this process I have asked myself what I would want to add to the current Calendar behaviour and the answer is: very little.

  1. I would like to propose that the Inspect protocol implementation delegate to an inspect/2 function on a calendar. This would be very useful when inspecting week-based calendars, or when using (as I do) a sigil_d/2 to define dates in different calendars. For an ISOWeek calendar for example, it would - in my implementation - return ~d[2019-W12-02]ISOWeek which is a lot more meaningful that the struct dump - especially since for week based calendars one has to hijack the month field and use it instead as week.

  2. I would like to propose revising the Calendar.week_of_year/1 addition to the Calendar behaviour. Weeks are such a standard part of most modern calendars, including the ISO calendar. The behaviour today nearly every other date facet: era, year, quarter, month, day. Week is the notable exception. Reporting data based upon weeks is an intrinsic part of business activities; they are how we structure formatted calendars.

These are really the only two behaviour additions that after a few months of calendar hacking strike me as additions I’d like to see - which is a testament to the original design and the recent 1.8 additions.

Cldr.Calendar behaviour builds on the core Calendar

In my own Cldr.Calendar behaviour I definitely have other requirements that I don’t believe are relevant to the core Calendar module but may be of interest to other calendarises:

  • year/1, quarter/1, month/1, week/1 that return a Date.Range
  • week_in_year/3 that takes a date, :first_day and :min_days per the earlier discussion
  • iso_week_in_year/1 that delegates to week_in_year/3 with first_day: 1, min_days: 4
  • first_gregorian_day_in_year/2 and last_gregorian_day_in_year/2 to return what it says

I don’t see any of these belonging in the core Calendar module but they are very useful in the practical use of calendar building and calendar usage.

I still have to add spec, docs and more tests so cldr_calendars is definitely not ready for anyone but it will be in a couple of weeks or so. Then I can finally finish up version 2.0 of ex_cldr_dates_times which only has to focus on formatting (think of it as an CLDR-based formatting using the LDML formats in a localised context).

josevalim

josevalim OP

Creator of Elixir

Fantastic work @kip!

Yes, please do send a PR!

We added it to Elixir but removed it before v1.8 was released exactly because the understanding of the “week of year” is locale specific. The only other functionality that I believe to be locale specific in the current calendar is day_of_week. So it felt awkward to add week_of_year given that it would be limited in many cases.

However, my understanding is that week_of_year would help your CLDR because calendars will now be able to rely on this uniform callback. Is this correct?

Can those be implemented using the existing Calendar callbacks? Those look nice additions to Elixir but I think we would need to add at least days_in_year and days_in_week.

Where Next?

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91561 914
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
bjorng
We want to introduce a new native datatype to Erlang: native records. Although replacing all tuple records with native records is not our...
New

We're in Beta

About us Mission Statement