Calendars & Calendar Conversions

Ok, that was going to be my follow up point. :slight_smile: It seems when encoding times between different calendars we would lose precision anyway because the days are slightly longer or shorter. So the “leap seconds base” is only useful when encoding in the same calendar. But, if it is the same calendar, we don’t need to encode the time. :smiley:

Perfect. This clarifies it. So would it make sense to rather add two functions to the Calendar behaviour? One that converts a Date to integer and another that converts the Time to a fraction? There is no need to convert the time if you are interested only on the date. /cc @kip

Almost. There is however, one important caveat to note: Some calendars start the next day at sunrise, some at noon, some at sunset and some at midnight. This means that conversion from one date to another might not be unambiguous, as a date in Calendar A might overlap with two dates of Calendar B.
Thus, I think that the date_to_rata_die should return an integer as well as a fraction.

I am not entirely sure yet how date (without time) conversion could be handled gracefully; we might want to return a list of possibilities. An alternate approach could be to restrict date (without time) conversions to calendars that have the same day-rollover point; meaning that if a developer wants to convert to another, he has to make a choice as for what time-of-day is used. This is similar to how NaiveDateTime is restricted w.r.t DateTime.

So I propose:

  • @callback Calendar.datetime_to_rata_die(DateTime.t) :: {integer, float}
  • @callback Calendar.time_to_day_fraction(Time.t, options) :: float. Returns a time divided by the total fraction of day. options is a keyword list that might be used to specify day properties that are not stored in the Time struct itself. (such as: should we treat the time as a time on a leap day).
  • @callback Calendar.date_to_rata_die(Date.t) :: {integer, float}
  • @callback Calendar.day_beginning_relative_to_midnight() :: float, returns 0.0 if the day rolls over begins at midnight, 0.25 if the day rolls over at sunrise, 0.5 if at noon, etc.

Ooooooh, good point.

About the calendar functions, I will leave it up to @kip, but note the Calendar functions should not receive structs. Otherwise we have cyclic dependencies between those which, although they are fine in Elixir, it is usually not a good practice.

@josevalim another thing that caught my eye was there are callbacks like Calendar.date_to_string/3 and Calendar.datetime_to_string/11. Why there is no callbacks to get date from string? I mean, what to do to parse a string to a calendar implementation?

Sorry if this is irrelevant.

Most of the questions to why Calendar in Elixir does not do something is likely because there was not a discussion about including such features. :slight_smile: We could discuss parsing but that would be a separate discussion.

1 Like

@josevalim, @qqwy, sorry for being a bit quiet last 24 hours, a few other things got in the way but I’ve been following and on board with the discussion. Will be done in a couple of days after I update some tests around the leap second boundaries.

Yes, my current version is {days_since_epoch, float_fraction_of_day} as a tuple, not struct, in order to keep it simple and follow José’s advice.

I will have an updated PR for review in a couple of days that lev


Beautiful, thank you and please take your time. :slight_smile:

@kip wonderful! I look forward to your Pull Request :smiley: . And yes, take your time. It is not like the calendars are going anywhere in the meantime ;-).

(Humans truly are peculiar creatures, being able to discuss for hours about something that only takes a second :grin:)

1 Like

2 posts were merged into an existing topic: Ex_calendar


Btw, shall we spawn a new thread for discussing ex_calendar? I would like to keep this one focused on the improvements to Elixir calendar itself. :slight_smile:


agree, ping @AstonJ to split threads

1 Like

@Eiji @josevalim I have split the posts that are relevant to ex_calendar to its own topic. :slight_smile:


Yes and no. As we count in days and ‘day fractions’ (my previous post was an explanation of how such a day fraction could be stored in an integer format that would be precise enough to handle leap second conversions), to calculate the difference between two dates (in days + day fractions) will not need to handle leap seconds.

A date in Elixir (%Date{}) does not have fractions, so how would you be able to use fractions in the calculation?

but note the Calendar functions should not receive structs. Otherwise we have cyclic dependencies between those which, although they are fine in Elixir, it is usually not a good practice.

How about receiving maps that would be valid structs (e.g. Date, NaiveDateTime or DateTime )?

Then all alternative calendars could have functions for to_iso and from_iso for each type Date, NaiveDateTime and DateTime that would receive/return these maps. That would make comparisons simple. You just convert to ISO and compare.

1 Like

In cases of a %Date{}, we count in complete days. In cases of a %DateTime{}, we count in fractions. The problem when converting a date in one calendar to another, is that you do not know when on the day it is, and as the roll-over point between days varies between calendars (common ones are sunrise, noon, sunset and midnight), this extra information is needed if you want to unambiguously convert. In the cases of converting a date (and not a datetime) there might be more than one possible result day.

Yes. The problem with to_iso and from_iso, is that all other calendars will need to work with the complexities of the current ISO 8601 calendar, which should not at all have to be necessary if you want to compare between two non-ISO8061 calendar moments. As soon as you not only work with dates (side note: you already run into a problem there, as one thing that is unspecified in ISO8601 is what happens with dates before 1582), is that all other calendars need to handle the problem of the leap second. (And as mentioned before, ambiguity arises when only handling dates.)

Exactly this, ‘not needing to work with leap seconds’, as well as the rigorous effort that Dershowitz and Rheingold have made in their work to specify existing calendars using the Julian Day system was the reason to create the new internal format.

Besides, if you’d write your own conversion from another (non-trivial) calendar to ISO8601, it is very (arguably, the most) natural to first convert this date to a number of days and then convert this to ISO8601-format.
Thus, if we’d convert between Calendar A and Calendar B, and the only thing we have is from_iso and to_iso, this is what would happen:

date in calendar a
|> number of days.
|> to_iso
|> from_iso
|> number of days
|> date in calendar b.

Clearly there is duplicated effort (both in computational complexity and in programming complexity) here.

I think this is the correct approach :slight_smile:!


For dates before 1582, that is specified here: Which follows Erlang’s approach. So that is one less concern.

Do we want alternative calendars to be able to convert to Calendar.ISO? If so, the libraries have to concern themselves with the Gregorian rules. I don’t see how you can get around that by introducing a different intermediate format.

1 Like

We want Calendar.ISO to be convertable to and from a number in days, and Calendar.Julian, Calendar.HybridGregorian, Calendar.Hebrew, Calendar.Chinese, Calendar.ISOWeekDate etc. to be convertable to a number in days. Now these other calendar implementations do not need to know about Calendar.ISO’s design choices and inner workings at all.

1 Like

Hello everyone :slight_smile:

Can we have an update about the progress of this issue?


To whom it may concern: I’ve started a new Pull Request that implements the conclusions of our discussion so far.


And after a lot of discussions, changes, more discussions and more changes, the Pull Request (which became quite the giantess) has been merged!

There will be some more clean-up happening after, most notably Calendar still depends on :calendar which means dat date/times before the ISO8601 ‘year 0’ are not supported, which could be changed to a custom implementation.

But the main work is done. We can start building libraries that implement other calendars, and convert them to and from each of the different formats now!

:sunglasses: I am very happy.


That was’t an easy task.
Nice job. Thank you very much :heart:

I think after this elixir has one of richest and strongest calendar systems :sunglasses: