The ambiguous DateTime is about daylight summer time, which is a separate issue from invalid dates. That’s because shift should be considered a shift on the wall clock instead of shifting the data in a contiguous line. This implies two things:
- You can land on invalid datetimes caused by datetime shifts in a timezone
- You can land on a date that does not really exist
For the second one, which was the one @kip mentioned, I suggest handling it with as a rounding operation, so we can either round :up or :down. I would pick :down as the default so you always land within the same month. Note this applies for quarter/month/year shifting.
For example, adding one day to Feb 28th is always March 1st. No invalid date is built in the process. Adding one month to Jan 31st should be Feb 28th (unless you have a leap year).
This further introduces complication related to commutative property of those operators. For example:
~D[2022-01-31] + 1 month + 1 month == ~D[2022-03-28]
~D[2022-01-31] + 2 month == ~D[2022-03-31]
That’s why shifting operations are often done with durations, because you need to express the whole operation in one tackle. For example, consider the difference between:
~D[2020-02-29] + 1 year + 1 month == ~D[2021-03-28]
~D[2020-02-29] + 1 year and 1 month == ~D[2021-03-29]
However, things get a bit trickier once you consider 1 year and -1 day. Do you remove the day from the resolved date or from the invalid date? E.g. which one is the desired result?
~D[2020-02-29] + 1 year and -1 day == ~D[2021-02-27]
~D[2020-02-29] + 1 year and -1 day == ~D[2021-02-28]
I think it should be the first. So my proposal would be to start with:
NaiveDateTime.shift(naive_date_time, shift_options) :: {:ok, naive_date_time}
shift_options is a keyword list with year, quarter, month, week, day, and round. shift_options will be applied on the order of highest to lower (i.e. the order defined above). Once you apply any of year/quarter/month, you need to round to a valid date. Then add week/day as wall clock shifts too.
We could support hour/minute/second too, but because we are strictly talking about wall-clock shifts, they have to be implemented as wall-clock shifts instead of operations on iso days, which is slightly annoying.
Besides the contiguous time interpretation existing in add
today, another difference between this and the add
function is that we will consider the definition of duration/interval specific to your calendar while add
always adds a unit of value specific to the gregorian calendar.
I would also suggest starting with the implementation for NaiveDateTime, and then move to DateTime and Date next. If you agree @kip, a pull request is welcome!