Yes, adding the exact number of seconds (86_400) multiplied by the number of days results in this, but DateTime(dt, 5, :day) usually means the developer wants to get the very same hh:mm:ss time back.
Consider a ToDoApp (huh) example. If I am after producing a Stream of recurring events, I am surely to expect it wonât change the Time part alongside with a DST change.
I discovered this when people complained about Tempus.Slots.Stream.recurrent/3 is broken in an aforementioned way. I fixed it by reassuring Time part to stay intact in the produced stream elements, but it looks like a hack and I am positive, DateTime.add(âŠ, âŠ, :day) should not corrupt the Time anyway.
This function relies on a contiguous representation of time, ignoring the wall time and timezone changes. For example, if you add one day when there are summer time/daylight saving time changes, it will also change the time forward or backward by one hour, so the elapsed time is precisely 24 hours.
This is called out in the docs as the intended behaviour.
If you want a continuous stream of datetimes at the same hour you can use a date and Date.add and turn those dates into datetimes with a time. Though you might run into invalid/duplicate datetimes this way if a date+time combination is in a DST change switch.
The docs for the new DateTime.shift/3 landing in 1.17 addresses this specifically and offers a suggestion using NaiveDateTime. It looks like this works:
I know how to overcome it. Also, I know this behaviour is intended. I started this topic with âThis question is more about semantics,â and thatââs what all this is about.
I find it extremely confusing that adding an integer number of days to the instance of DateTime results in the change of hour field in the struct. If thatâs only me, I am good. But I am positive if you stop a hundred persons on the street to ask ânow itâs Saturday, 3PM. This night there will be a DST shift. What would be the result of adding exactly one day to now?ââ99 if not 100 of them would answer 3PM.
Please note, that the above does not apply to adding hours, let alone seconds.
The laymans opinion doesnât really help in a domain full of real life footguns a layman is usually not aware of. As mentioned you cannot add a day to a datetime while keeping the hour value the same without it being able to produce invalid datetimes. DateTime.add cannot fail though or adding :day (together with hour and minute) as a unit wouldâve been a BC breaking change. Personally I probably wouldâve even skipped :day for exactly this discussion.
If you still want to attempt this you simply need to do it using APIs of DateTime, which can return you errors around those invalid datetimes and decide for a way to handle those for your specific usecase.
E.g. the following naive datetime is invalid, as it is never observed, even though the day before and after do have an observed datetime at 02:30:00. So there needs to be a decision made if the invalid one is skipped, shifted to the last observed datetime before or the next after. Similar thing happens when shifting the other direction where a naive datetime would be observed twice, once with DST offset and once without.
iex(1)> DateTime.from_naive(~N[2024-03-31 02:30:00], "Europe/Berlin")
{:gap, #DateTime<2024-03-31 01:59:59.999999+01:00 CET Europe/Berlin>,
#DateTime<2024-03-31 03:00:00+02:00 CEST Europe/Berlin>}
That Iâd strongly appreciate, but once we already have it, we might be more adaptive to laymansâ opinion
Look, we already round days down to the nearest valid date when shifting by month in 1.17. The API is already not generic. We are already treating âadd 1 monthâ in laymanâs terms. What would be wrong with treating âadd 1 dayâ in the very same manner? Once itâs documented itâs ok, thatâs what you were saying in the first comment, right?
Fwiw, Iâd share why it made me upset. While working on Tempus, which is a library to represent time slots, I dug a lot, and the most inspiring was tempo library by @kip, which is great, but my goal was slightly different. I was more after infinite streams of intervals, and their interop, like joins, intersections, etc. I needed a way to tell if this infinite slot stream covers that particular time. Thatâs why the algebra Iâve chosen is slightly different (although similar.) Still, I wanted the clients to face as few quirks of the interval mess as possible.
And the aforementioned âadd several daysâ was literally the only thing I was not ready to (and I was ready to enter a hell, trust me.) Anyway.
YMMV: 40 out of 100 of them would also tell you that DST helps solar panels make more power, because thereâs âmore sunâ.
To @LostKobrakaiâs example, only a developer would tell you âthereâs no such time as one day after 2:30AM Saturday morning if the Sunday is the DST switchoverâ
DateTime.shift could make that decision, sure. But DateTime.add adds a certain (and fixed per amount/unit) amount of seconds to the supplied DateTime. Thatâs a different task. The issue is that it allows for :day with a constant of 86400 seconds when days are not always that long. Thatâs also the reason why :week, :month, :year werenât added to DateTime.add, but handled into the new DateTime.shift.
Not modeling leapseconds doesnât leave you in a state where you miss a (naive) datetime or run into it twice though - which is what happens if you donât handle the dst change complexities.