ariebovenberg / whenever

⏰ Modern datetime library for Python
https://whenever.rtfd.io
MIT License
864 stars 13 forks source link

How do I get an Instant from a SystemDateTime #146

Closed spacemanspiff2007 closed 3 months ago

spacemanspiff2007 commented 3 months ago

Before 0.6 I called LocalSystemDateTime(...).as_utc() to get an UTCDateTime. Now UTCDateTime Is called Instant and LocalSystemDateTime is called SystemDateTime, however it's unclear how I can get it to normalize. I thought the Idea is to normalize timestamps to Instant and do all calculations there. Is this a misunderstanding from my side?

ariebovenberg commented 3 months ago

Hi @spacemanspiff2007, thanks for reaching out. Let me see if I understand your use case correctly:

if:

then: starting with SystemDateTime and using .instant() is the way to go.

# example: local system set to Europe/Amsterdam
>>> my_dt = SystemDateTime(2023, 4, 1, 12)
SystemDateTime(2023-04-01T12:00:00+02:00)
>>> my_dt.instant()
Instant(2023-04-01T10:00:00Z)

I thought the Idea is to normalize timestamps to Instant and do all calculations there.

In general, yes—if your doing calculations in "exact" units like hours, minutes, seconds etc. If you'd like to do calendar arithmetic (like adding months, calendar days) you need to consider more carefully: you may be better off doing arithmetic on the .date() first.


The Instant is the "normalized" type. It strips all concepts of local time away. The old UTCDateTime still kept concepts like "months" which didn't make sense.

edit: clarifications

spacemanspiff2007 commented 3 months ago

Thank you for your quick and thorough reply. Yes - my goal is to do "exact" calculations so thank you for the confirmation that I am on the right track. I missed the function because it does not seem to align with the other names: to_tz, to_system_to and to_fixed_offset. Imho it would make sense to rename it as to_instant - what do you think?

Edit: Can you elaborate why is it not possible to replace parts of the time any more? If I want to have the next full second it's now only possible like this:

now = Instant.now()
target = (now + TimeDelta(seconds=1)).subtract(nanoseconds=now.to_system_tz().nanosecond)
ariebovenberg commented 3 months ago

@spacemanspiff2007 rounding an Instant is indeed a missing functionality! I'll add a .round() method for this purpose soon, but I'll have to give the API some thought.

About your suggestion: I had named instant() to be consistent with local(), but probably they should both be to_instant() and to_local() to avoid confusion.


BTW: A less hacky workaround for you until then. Still ugly, but handles nanoseconds properly.

now = Instant.now()
target = Instant.from_timestamp(math.ceil(now.timestamp_nanos() / 1_000_000_000))
ariebovenberg commented 3 months ago

@spacemanspiff2007 follow-up question that would help me out: What is the reason in particular you need to round an Instant? Is this because you're converting it to a string for another system? Or does your functionality depend on whole seconds?

spacemanspiff2007 commented 3 months ago

I am reworking EAScheduler - an easy to use asyncio scheduler - which lets you run coros/functions at specific points in time. It's mostly used by HABApp, a smart home rule engine which works with MQTT and/or openHAB. One option is e.g. to run something at e.g. sunrise or sunset. Since sometimes these timestamps are logged or further propagated it's unnecessary and imho not very pretty to have anything below seconds because it makes the timestamps hard to read. So I am trying to round the calculated time stamps to the second. The other use case is testing where I do some calculations which should return the next full second starting from now. Obviously I can work around both issues by rounding the SystemDateTime and then converting the result to Instant or rework my tests so that they work different. It's just something I stumbled when going from 0.5 to 0.6.2.

Another thing I realized is that Instant.add does not take days as an argument any more. I understand that months is ambiguous so removing that makes sense. Have you removed days because of the leap second? The SkippedTime and RepeatedTime Exceptions are also hard to find. May I suggest appending an Error suffix so it would be SkippedTimeError and RepeatedTimeError?

ariebovenberg commented 3 months ago

Thanks for explaining, that helps. Rounding methods should be added to the other datetimes as well, I think. In fact, I probably think it'd be best to do this rounding before converting to Instant. This is because Instant (in theory) doesn't have a concept of a 'whole second'. It just identifies a moment on the timeline, and 'whole second' is only determined by local human interpretation. For example, historically sub-second UTC offsets exist (even though TZDB and whenever don't support them): this would mean rounding a second in that location would lead to a different result than rounding in another.


Relating to your suggestion about exception names: While I'm personally not a fan of redundant suffixes, I have to admit I'm in the minority here when it comes to exceptions. I'll probably bundle this with a bunch of other renames in the next release.

edit: I've created an issue to track naming discussions here: https://github.com/ariebovenberg/whenever/issues/151

ariebovenberg commented 3 months ago

I'm closing this issue since the original question has been answered.

Relevant follow-ups:

spacemanspiff2007 commented 3 months ago

I probably think it'd be best to do this rounding before converting to Instant.

I agree! That's how I'm doing it now and it seems to be the most straightforward and logic implementation.