Skip to content

Conversation

Qqwy
Copy link
Contributor

@Qqwy Qqwy commented Mar 5, 2017

This Pull Request is a follow-up from @kipcole9 's PR #5603 and the discussion in the Calendars & Calendar Conversions topic.

The current state of this PR is mainly to rekindle the discussions about this subject: There still are a few choices we must make before this functionality can be included into Elixir.


This checklist will be kept updated as the discussion in this topic evolves.

Checklist:

  • The types Calendar.day_fraction :: {non_neg_integer, pos_integer, extra: %{}} and Calendar.rata_die :: {integer, day_fraction, extra :: %{}} that are used as internal format to convert between times and datetimes, respectively. An in-day fraction is used to ensure that precision is always high enough.
  • The callbacks Calendar.datetime_to_rata_die(year, month, day, hour, minute, second, millisecond, time_zone, zone_abbr, utc_offset, std_offset) and Calendar.datetime_from_rata_die(rata_die).
  • The callbacks Calendar.time_to_day_fraction(hour, min, second, millisecond) and Calendar.time_from_day_fraction(day_fraction).
  • A DateTime.compare/2 function that works across calendars by using datetime_to_rata_die instead of to_unix as is now the case.
  • A DateTime.diff/2 function that returns the difference between two datetimes (across calendars) in the Calendar.rata_die format.
  • Change functions in new version of Calendar to work with bare arguments/tuples instead of structs to prevent cyclic dependencies.
  • Make sure that the timekeeping types in Calendar are also overspecified (just like the datekeeping types already are).
  • Add a :calendar field to %Time{}.
  • Alter functions that create %Time{} structs to include the new :calendar field.
  • Add @callback day_rollover_relative_to_midnight_utc() :: day_fraction to the Calendar behaviour, which is used to check if Dates and NaiveDateTimes (rather than only DateTimes) can be converted between two calendars.
  • Change from_erl, from_iso8601, from_unix, utc_now, utc_today and their bang-variants to versions that take a second optional 'target calendar' argument (defaulting to Calendar.ISO).
    • DateTime
    • Date
    • Time
    • NaiveDateTime
  • Change the to_* so they dispatch on their containing calendar's datetime_to_rata_die, before converting to the external format.
    • DateTime
    • Date
    • Time
    • NaiveDateTime
  • Mechanism to prevent bogus times/dates: valid_date?, valid_time?, valid_datetime?, valid_naive_datetime?.
  • Timezone information only handled by DateTime. (see DateTime.apply_tz_offset)
  • Documentation for Date.compare, Time.compare, DateTime.compare, NaiveDateTime.compare that explain when you can/cannot compare across calendars.
  • ~~~Alter the Rata Die format to contain a map with 'extra' data that can be used to keep metadata during the conversion if the source and target calendar both understand the given options.~~~
  • Expansive tests for the above, including another Calendar (visible during testing only) to convert between.
  • fixed specs for the altered and new functions.

To be discussed:

  • Should we include a second (simple!) Calendar implementation to be able to test/explain (doctests) conversions properly?
    • -> In tests only.
  • How is checked that a newly-created (Naive)(Date)(Time)-struct is valid? Add valid_datetime? etc. to the Calendar behaviour?
    • -> valid_time?, valid_date?, valid_datetime?, valid_naive_datetime?.
  • Who is responsible for handling timezones: DateTime or the Calendar? Or a new Chronology module?
    • -> Deferred to a later time when we have empirical evidence; keep this in DateTime for now.

This Pull Request will add the following to Elixir:

(This list is superseded by above checklist)

  • The types Calendar.day_fraction :: {non_neg_integer, pos_integer} and Calendar.rata_die :: {integer, day_fraction} that are used as internal format to convert between times and datetimes, respectively. An in-day fraction is used to ensure that precision is always high enough.
  • The callbacks Calendar.datetime_to_rata_die(datetime) and Calendar.datetime_from_rata_die(rata_die).
  • The callbacks Calendar.time_to_day_fraction(time) and Calendar.time_from_day_fraction(day_fraction). Note that these are not used by other code yet, as the %Time{} struct does not contain Calendar information. (See below).
  • A DateTime.compare/2 function that works across calendars by using datetime_to_rata_die instead of to_unix as is now the case.
  • A DateTime.diff/2 function that returns the difference between two datetimes (across calendars) in the Calendar.rata_die format.
  • Doctests for the above.

Important to note:

  • Dates are by design not convertible across calendars. If you want to convert a date, you'll need to convert it to a DateTime (and thus pick some reference time to use). The reference time you pick matters, as not all calendars roll over to the next day on midnight (some do at noon, others at dusk, etc). Without specifying a reference time, the result might therefore be ambiguous. What reference time to pick is an explicit choice that the developer should make, in my opinion.
  • NaiveDateTimes are not convertible for exactly the same reason: They will only be unambiguous if their timezone is specified.
  • This PR explicitly does not add all the behaviour that @kipcole9 ´s PR had. Iterating over days or other ranges of time etc. would be wonderful, but embellishment is only useful once we've agreed on the base.

Other things I've stumbled upon and would like to note:

  • In the current Elixir, Time does not contain nor use Calendar information at all. I think it would be a good idea to add this (staying backwards compatible of course), because this implicit, hard-coded dependency on it using Calendar.ISO underwater means that constructing functions like NaiveDateTime.new(date, time) do not make sense when used for other calendars. More importantly, it will make it possible to construct and compare times across calendars, etc, as Calendar can then dispatch to the proper Calendar behaviour based on the :calendar field of the map passed to its functions.
  • The implementation of Calendar.ISO's datetime_to_rata_die and datetime_from_rata_die do not handle leap seconds, as there is no way for Elixir to know about them. The only possibility for a calendar that does support leap seconds that I can think of, is a 3rd party package that reads its info from the :tzdata package. Calendar.ISO's conversions will be off by at most 86400/864001th of a day during a leap second day. (And will be correct again after that day).
  • Functions like NaiveDateTime.compare(left, right) and Date.compare(left, right) do not care if the calendars of left and right are the same. This is something that should be fixed (regardless of whatever happens to the rest of this Pull Request). (EDIT: Date.compare/2, DateTime.compare/2, NaiveDateTime.compare/2 happily compare across calendars. #5842 was made)
  • There are multiple other functions in Date, Time and DateTime that implicitly depend on a Calendar.ISO format being used, such as from_erl, to_erl, to_unix, etc. This means that these modules are taking care of three different concerns right now. For instance, DateTime is:
    • the module containing functions that only work on ISO DateTimes.
    • the module containing functions that should work with any DateTimes, regardless of struct used (any map goes as long as it contains the expected fields).
    • The module being responsible for the creation and manipulation of the %DateTime{} struct.

We might therefore want to split these modules, into e.g. DateTime and Calendar.ISO.DateTime. But this will be somewhat problematic in terms of backwards compatibility. Any ideas?


All help and feedback is greatly welcome! :-)

@josevalim
Copy link
Member

Thank you @Qqwy!

This is a large PR, so I will first talk about the base problems and then we can discuss specifics as we move forward.

  • Implementation wise, you have added a cyclic dependency between Calendar.ISO and DateTime. The functions in Calendar.ISO are not meant to work on any of the Calendar structs.

  • You said: "Time does not contain nor use Calendar information at all". However, given that time is always represented with hour, minute and seconds, that already restricts which kinds of calendars can we support. Is there any calendar that uses hour, minute and seconds that does not align with the Calendar.ISO definition? Otherwise it doesn't make any sense to support a calendar field if we can only support Calendar.ISO types anyway.

  • You said: "Dates are by design not convertible across calendars.". That considerably limits the use of rata die then. Why should we add a calendar field to time if we need date, time and timezone to properly rely on rata die?

Finally, you are implementing the conversion to gregorian epoch... couldn't you use the functions in :calendar for that? Also, please make sure you indent code snippets in documentation with four spaces.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 6, 2017

Implementation wise, you have added a cyclic dependency between Calendar.ISO and DateTime. The functions in Calendar.ISO are not meant to work on any of the Calendar structs.

Astutely observed. What happens on e.g. https://github.com/Qqwy/elixir/blob/a5ad3a77dc3d1dae6de91a9dd875d1904b60e9da/lib/elixir/lib/calendar/iso.ex#L81 is indeed wrong; and (hard-coded) assumes that DateTime and NaiveDateTime follow Calendar.ISO implicitly. This should be fixed, although I am not entirely sure in what way (Build the structures manually instead?).

However, isn't the general problem that Date, DateTime and Time are not supposed to refer to Calendar.ISO, but rather to their own stored calendar module? I think this is a core issue that arises from the fact that these modules currently have multiple responsibilities (a between-calendar reusable struct, helpers to create Calendar.ISO (Date)(Time)s, conversion between Erlang <-> Calendar.ISO (Date)(Time)s).

You said: "Time does not contain nor use Calendar information at all". However, given that time is always represented with hour, minute and seconds, that already restricts which kinds of calendars can we support. Is there any calendar that uses hour, minute and seconds that does not align with the Calendar.ISO definition? Otherwise it doesn't make any sense to support a calendar field if we can only support Calendar.ISO types anyway.

The issue is that time is not always represented as hour, minute and seconds. Even if we restrict it to only h/m/s-like calendars, the amount of hours, minutes and seconds per day is not the same across calendars, and the start-of-day moment also is not always the same. Examples: The Hebrew calendar, the Hindu calendar, the Chinese calendar.

We can convert between times by using a day fraction as intermediate structure, which is always relative to midnight. There is not a problem here, because even if we 'roll over' to a different day in the target calendar when converting, this day information is discarded as we're only interested in the time.

However, when using dates, it is important to know when on the dates you are looking at, exactly because the different moments when a day starts. Time is cyclic, while dates are on a line.

You said: "Dates are by design not convertible across calendars.". That considerably limits the use of rata die then. Why should we add a calendar field to time if we need date, time and timezone to properly rely on rata die?

It does not really limits the use of Rata Die. The idea is that if you want to convert dates, you'll need to pick a moment, say midnight, as reference time for the conversion. For calendars that use the same start-of-day moment, whatever time you pick does not matter as the result will always be unambiguous. For calendars that differ, however, the picked time moment is important.

We could choose to create a function that converts dates by implicitly using 'midnight' as reference time, but this somewhat goes against the Explicit nature of Elixir.

I'll draw a nice picture that explains why conversions between times and datetimes is possible, while conversions between dates is not better than words can.

Finally, you are implementing the conversion to gregorian epoch... couldn't you use the functions in :calendar for that?

Embarrassingly I have not at all thought to look in :calendar as to not re-invent the wheel. Indeed, :calendar.date_to_gregorian_days and :calendar.gregorian_days_to_date will make the current code a lot more concise (and fast).

Also, please make sure you indent code snippets in documentation with four spaces.

Don't worry; the documentation could be improved semantically as well, I'm sure. I wanted to get this out quickly as to resume the discussion. My inner perfectionist will resolve these in due time 😋 .

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 6, 2017

Here are the images to show the difference between converting dates, times and datetimes:

converting_between_dates

As can be seen, converting dates cannot be done: Converting 2016-01-12 (Gregorian) to the Hebrew Calendar could be either 2 Shvat 5776 or 3 Shvat 5776, depending on if the moment of the day is before/after sunset.
The same problem occurs when going the other way around, as when only knowing the Hebrew date, we do not know if this is before or after midnight.

However, if we do know the time as well, we can convert, as is the case when converting 2016-01-15T20:00:00Z to 6 Shvar 5776 (1 hour, 0 halakim)

converting_between_times
On the other hand, converting between times is possible, as time runs cyclical. Here, 08:30 (UTC) is converted into (7 hours, 270 halakim) in the Hebrew calendar.

Copy link
Contributor

@alisinabh alisinabh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Qqwy
Well done

Only one thing caught my eye. I think maybe it was left by mistake.

Thank you
Best wishes ❤️

@@ -1337,6 +1376,12 @@ defmodule NaiveDateTime do

## Helpers

defp discard_timezone_info(naive_datetime) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function ever used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are completely correct. This indeed is a function that is not used anymore in the current version. I'll remove it.

Glad you noticed it! 😄

Runs both `div/2` and `mod/2` on the same input, and returns a tuple with both results.
"""
@spec div_mod(integer, neg_integer | pos_integer) :: integer
def div_mod(x, y) when is_integer(x) and is_integer(y) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used in Calendar.ISO module, so it should be a private function there.

6
"""
@spec gcd(integer, integer) :: non_neg_integer
def gcd(a, 0), do: abs(a)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one too.

@josevalim
Copy link
Member

This should be fixed, although I am not entirely sure in what way (Build the structures manually instead?).

They should return tuples which DateTime will use to build its representation then.

However, isn't the general problem that Date, DateTime and Time are not supposed to refer to Calendar.ISO, but rather to their own stored calendar module?

Also yes. I would still say we should avoid the cyclic dependency. And the Calendar.ISO has always meant to be a temporary limitation (which this PR will hopefully help address).

We can convert between times by using a day fraction as intermediate structure, which is always relative to midnight.

This makes perfect sense, thank you. The image is also extremely helpful. However, according to the image, we should be able to convert from a NaiveDateTime to another NaiveDateTime in another calendar, shouldn't we? To sum up:

  1. Times can support calendar conversion. Therefore we should add a calendar field to them.
  2. Dates do not support calendar conversion. Question: is the calendar field of any use here then besides metadata purposes?
  3. NaiveDateTime should support calendar conversions. When building NaiveDateTime.new(date, time) we need to check both date and time are on the same calendar. Question: why don't we?
  4. DateTime supports calendar conversions. Question: how does timezones and calendar conversion interplay? Today you are using utc_offset and std_offset but then the timezone information is lost and we cannot convert it back to the previous timezone. Can't we simply keep the timezone information and offsets as is in the new datetime when we change calendars?

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 6, 2017

1.Times can support calendar conversion. Therefore we should add a calendar field to them.

Yes! 😄

  1. Dates do not support calendar conversion. Question: is the calendar field of any use here then besides metadata purposes?

Depending on if you want %Date{} to only be able to store Calendar.ISO values or other calendars.
It would be useful if the :calendar field were used for dates to dispatch certain functionality to the given calendar module, similar to what :__struct__ does.

There are quite probable other situations in which it would be useful as well; let's not be too quick to remove it.

  1. NaiveDateTime should support calendar conversions. When building NaiveDateTime.new(date, time) we need to check both date and time are on the same calendar. Question: why don't we?

Two NaiveDateTimes might not be in the same timezone, meaning that they are skewed by an unknown amount relative to another. Converting is thus only possible if we know the difference between their timezones.

  1. DateTime supports calendar conversions. Question: how does timezones and calendar conversion interplay? Today you are using utc_offset and std_offset but then the timezone information is lost and we cannot convert it back to the previous timezone. Can't we simply keep the timezone information and offsets as is in the new datetime when we change calendars?

The UTC-relative timezones as specified with utc_offset and std_offset only make sense w.r.t the ISO 8601 Calendar.
However, some other calendars have their own timezone-like offsets w.r.t. a given 'base' value, although this seems to be rare. There are many calendars that do not use time zones at all, or in other words, for which there is only a single timezone. Interstingly, for these, DateTime and NaiveDateTime would be the same.

Maybe an idea would be to add a 'metadata' map to the Rata Die format, in which calendars can supply meta-information that might be applicable to the conversion target calendar or not, such that things like the time zone and the requested microsecond precision can be kept iff both calendars support this.


Let me ask some questions to you and the other Elixir Core members in return:

  1. The current internal structure of the %Time{}, %Date{}, %NaiveDateTime{} and %DateTime{} structs mandates :year, :month, :day for dates, :hour, :minute, :second, :microsecond for times, (and also :utc_offset, :std_offset, :zone_name, :zone_abbr for the datetime timezones). Maintaining backwards-compatibility will make it hard to alter this, but there are cases for which this format is either insufficient or confusing (Example). Question: What should the purpose of the %Time{}, %Date{}, %NaiveDateTime{} and %DateTime{} structs? Working with any calendar by dispatching on the :calendar-field, or not?

  2. If we decide to make these structs more general, then there are certain functions that do not belong in their modules anymore. Question: Where should functions like Date.from_erl/1, and Date.leap_year? move to? Maybe Calendar.ISO.Date?

  3. If we decide to not make these structs more general, then we need a different general representation of dates/times/datetimes. I feel like Elixir Core should handle this to allow different libraries to work with a common interface. If we pick this, then: Question: How should such an interface then look?

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 6, 2017

By the way, it might help to look at how Joda-Time, Java's de facto timekeeping implementation that supports eight calendars was implemented. Its 'key concepts' documentation is very readable, in my opinion.

What Joda-time does good in my opinion:

  • Their implementation of Partial times is a smart idea.
  • Their implementation of Periods to iterate over times is a smart idea.
  • The choices they made in the support for the eight different calendars are very reasonable.

What it lacks:

  • When working with time Instants (fixed points in time), these are always converted to UNIX timestamps without taking things like leap seconds into account. The same is true for Durations. The Rata Die is a stronger format for these tasks, as it does not need to care about leap problems.
  • It does not allow other ways of timekeeping than the 24h clock, nor does it support calendars whose days roll over at anything other than midnight.

It's also interesting to note that it thus looks like it is possible to support at least these eight calendars without rigorously changing the fields of the existing structs, and without working with non-ISO8601 timezones. Their (Date)(Time) objects use the 'decorator pattern', which is analogous of passing a :calendar field around like Elixir's (Date)(Time) structs are doing.

@josevalim
Copy link
Member

There are quite probable other situations in which it would be useful as well; let's not be too quick to remove it.

We can't remove them before 2.0 anyway. :)

Two NaiveDateTimes might not be in the same timezone, meaning that they are skewed by an unknown amount relative to another. Converting is thus only possible if we know the difference between their timezones.

I believe this assumption doesn't fit NaiveDateTime, since it has diff/2 which would fail for the same reasons. We assume they belong to the same timezone. The burden is on the user to guarantee the diff/2 operation is meaningful, hence why it is called Naive.

However, your later answer on DateTime and timezones being something specific to ISO explains why calendar conversions wouldn't work for NaiveDateTime.

Question: What should the purpose of the %Time{}, %Date{}, %NaiveDateTime{} and %DateTime{} structs? Working with any calendar by dispatching on the :calendar-field, or not?

The trade-off we took here is that we would allow to support multiple calendars as long as they fit the regular hour-minute-second and year-month-date representations. We preferred to optimize for the most common scenario, both in terms of simplicity of use and performance, at the cost of limiting some usage. A completely generic API though can still be built as a library that would use protocols instead.

Therefore we don't plan to support a more general representation. Therefore the last question is: are the current trade-offs enough to support other calendars or should we assume that we can only effectively support Calendar.ISO and consider the :calendar field obsolete? Or, to be more precise, which calendars can we effectively support with the current constraints?

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 7, 2017

Therefore we don't plan to support a more general representation. Therefore the last question is: are the current trade-offs enough to support other calendars or should we assume that we can only effectively support Calendar.ISO and consider the :calendar field obsolete? Or, to be more precise, which calendars can we effectively support with the current constraints?

I think that maybe looking at what is possible with Joda-Time might shed some light on this.
I believe we can at least support the eight calendars that Joda-Time supports, which are: Buddhist, Coptic, Ethiopic, Gregorian-Julian cutover, Gregorian, Islamic, ISO and Julian.
I also believe that this implementation could support the Hebrew, Chinese and Mayan calendars on top of that.
According to Wikipedia, this would cover all of the calendars that are currently in wide use.

I believe this assumption doesn't fit NaiveDateTime, since it has diff/2 which would fail for the same reasons. We assume they belong to the same timezone. The burden is on the user to guarantee the diff/2 operation is meaningful, hence why it is called Naive.
However, your later answer on DateTime and timezones being something specific to ISO explains why calendar conversions wouldn't work for NaiveDateTime.

Take your pick: You can still decide to put the burden on the user that the times are of the same zone or not in that case. It's possible to reason both ways here.


Maybe the best idea to my own question (2.) I can come up with so far is to have Date.from_erl and friends remain, but be changed to a two-argument version:
Date.from_erl(arg, calendar \\ Calendar.default_calendar), where Calendar.default_calendar returns the value of an application configuration that can be changed, with the default being Calendar.ISO. This would be really nice, because someone could then swap Calendar.ISO for another calendar that for instance could handle leap seconds by depending on tzdata, without changing any other part of the code.

Idea for changes to Elixir would then be:

  • Add a :calendar field to %Time{}.
  • Make sure that the timekeeping types in Calendar are also overspecified (just like the datekeeping types already are).
  • Possibly add an :extra field to (Naive)(Date)(Time), to allow passing around things that certain calendars might need that do not naturally fit in the already-available fields. This extra field should also be added to Calendar's callbacks like date_to_string, day_of_week, etc.
  • Alter the Rata Die format to contain a map with 'extra' data that can be used to keep metadata during the conversion if the source and target calendar both understand the given options.
  • Make sure compare and diff use Rata Die instead of only working for Calendar.ISO structures.
  • Change from_erl, to_erl, from_iso8601, to_iso8601, from_unix, to_unix, utc_now, utc_today and their bang-variants to versions that read the application's default calendar (Falling back to Calendar.ISO).

@josevalim
Copy link
Member

Take your pick: You can still decide to put the burden on the user that the times are of the same zone or not in that case. It's possible to reason both ways here.

If it requires a specific timezone information to properly convert to another calendar, then I believe we should not add it to NaiveDateTime.

About your proposed changes, let's NOT do the following:

Possibly add an :extra field to (Naive)(Date)(Time), to allow passing around things that certain calendars might need that do not naturally fit in the already-available fields. This extra field should also be added to Calendar's callbacks like date_to_string, day_of_week, etc.

Let's NOT do this because we can add a field any time if we find it truly necessary. We should be conservative when adding because it is much harder to remove.

Change from_erl, to_erl, from_iso8601, to_iso8601, from_unix, to_unix, utc_now, utc_today and their bang-variants to versions that read the application's default calendar (Falling back to Calendar.ISO).

Let's add support for custom calendar as second argument but let's not provide an application wide default (yet). Although I don't think the second argument would be necessary for the to_* functions, would they?

@josevalim
Copy link
Member

josevalim commented Mar 7, 2017

Edit: this proposal is a bad idea™. :)

About the Calendar API, can we break it into those four functions?

@callback date_to_gregorian_days(year, month, day) :: integer()
@callback gregorian_days_to_date(integer()) :: {year, month, day}

@callback time_to_day_fraction(hour, minute, seconds, microseconds, offset) :: day_fraction
@callback day_fraction_to_time(day_fraction) :: {hour, minute, second, microseconds}

@type day_fraction :: {integer(), pos_integer()}

Note the first element of day_fraction can be a negative integer (in case a negative offset is given).

I believe the above the smallest API we can require from Calendar while still allowing Time and DateTime to build on top of it (and even NaiveDateTime if we find so necessary in the future).

@josevalim
Copy link
Member

Actually, please ignore my proposal above. It won't work for datetime as we need to pass both date and time. I believe your current proposal looks good, we just need to make it accept multiple arguments and return tuples instead of the DateTime structs.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 7, 2017

Agreed on all fronts!
(and yes, the second parameter is indeed only necessary for the from_* functions)

Expect a refined implementation (including functions that work with tuples instead of maps to resolve the cyclic dependencies) that does these things on 2017-03-09 (Gregorian 😜 ).

@josevalim
Copy link
Member

It would be nicer in my opinion to have both a tuple-returning version of diff as well as a raising one, but I guess this would again be impossible because of backwards compatibility of the already-existing NativeDateTime.diff. Is there another way we could do this?

Raising the only way to go due to backwards compat, yet.

Date.compare/2's documentation:

The thing about Date.compare/2 is that it is reasonable to ask if a DateTime is at a given Date ignoring the other fields. I cannot find a good reason to convert to to_erl directly or to another calendar transparently. In this case calling DateTime.to_date first doesn't hurt IMO.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 19, 2017

So in the case of Date.to_erl/1 and Time.to_erl/1 (and idem for to_iso8601/1), should these functions be restricted to only work on %Date{}s and %Time{}s respectively? This would break backwards compatibility. The only way to keep it backwards compatible while also restricting maps with higher precision would be to have a special case for Calendar.ISO:

  • For Calendar.ISO, you can pass a %NaiveDateTime{} to Date.to_erl/1
  • For other calendars, you can only pass a %Date{} directly.

This feels very weird/confusing.
I totally agree with you that it would be better if people would be forced to convert their structs explicitly before calling these functions, but implementing it while remaining backwards compatible would result in a function with what I would consider counter-intuitive behaviour.

@josevalim
Copy link
Member

josevalim commented Mar 19, 2017 via email

…to_erl, Date.to_iso8601, Time.to_iso8601.

Passing DateTimes or NaiveDateTimes to these functions directly is now deprecated.
@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 20, 2017

Deprecation warnings have been added. As the proper behaviour has been part of Elixir for as long as the Date/Time types have, a warning is shown right away. The CHANGELOG and the documentation of these functions has been updated to reflect the changes.

@Qqwy Qqwy force-pushed the calendar_rata_die branch from 9fb68e2 to 73c4142 Compare March 20, 2017 11:53
CHANGELOG.md Outdated
* [Kernel] `not left in right` is soft-deprecated in favor of `left not in right`

### 4. Deprecations

#### Elixir

* [Calendar] Calling `Date.to_erl/1`, `Date.to_iso8601/1`, `Time.to_erl/1` and `Time.to_iso8601/1` directly with a `DateTime` or `NaiveDateTime` struct is deprecated in favour of converting them to `Date`s/`Time`s first by using `DateTime.to_date/1`, `NaiveDateTime.to_date/1`, `DateTime.to_time/1` and `NaiveDateTime.to_time/1` respectively.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't change the CHANGELOG. We will do it after merging (and it will also avoid your PR getting conflicts!)

@Qqwy Qqwy force-pushed the calendar_rata_die branch from ffe5df9 to bc054b5 Compare March 20, 2017 15:35
@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 22, 2017

The only thing that is not yet done is changing Date.utc_today to work with other calendars, as I am having trouble deciding between these two:

  • Take ISO UTC date+time of *now* |> convert to Date
  • Take ISO UTC date at last midnight |> convert to Date

I am leaning towards the former of these two, because it might be counter-intuitive if DateTime.utc_now(YourCalendar) |> DateTime.to_date != Date.utc_today(YourCalendar). Do you agree?

@josevalim
Copy link
Member

@Qqwy I agree. It should be the date+time of now.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 22, 2017

I think it is time for a new review round :-)

@josevalim josevalim merged commit d017c5a into elixir-lang:master Mar 23, 2017
@josevalim
Copy link
Member

❤️ 💚 💙 💛 💜

@josevalim
Copy link
Member

Thank you so much @Qqwy, there are some minor adjustments but those can be fixed locally. The conceptual changes have all been covered. If something is missing, please send more pull requests.

If you would like to work on removing the calendar dependency, that will be welcome, as it would allow us to use also the calendar library in other projects such as elixirscript. :)

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

Wonderful! 😍

Thank you so much. It is a joy to work with you 😃 .

end
end

def convert(%{calendar: calendar} = naive_datetime, calendar) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Qqwy We are missing documentation and spec for this function, can you please send a PR?



@spec convert!(DateTime.t, Calendar.calendar) :: DateTime.t
def convert!(datetime, calendar) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Qqwy documentation is also missing here.

end
end

@spec convert!(NaiveDateTime.t, Calendar.calendar) :: NaiveDateTime.t
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Qqwy documentation is also missing here.

{:ok, result_time}
end

@spec convert!(Time.t, Calendar.calendar) :: Time.t
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Qqwy documentation is also missing here.

@Qqwy
Copy link
Contributor Author

Qqwy commented Mar 23, 2017

Yes, give me a minute!

@josevalim
Copy link
Member

@Qqwy i have added two issues and copied you on them. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants