You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Q: What's the best way I can detect missing/ambiguous times? #1018
Sorry for not using your issue templates! I have a question I'd like some advice on, I hope this is an okay place to post it.
This is about missing/ambiguous times. I'm sure it comes up all the time (as recently as #914) so I'm not here to request this as a feature that gets added, but rather, I'd like to ask what the best strategy is for working around this?
I have a website for sharing times across timezones (https://notime.zone) and I'm doing a rewrite using Luxon (it's a lot easier to work with than moment 😄). I let people specify a datetime in my form, but I'd like to tell my users a few things:
That no such time exists if they enter a time that was skipped due to DST (e.g. 2017-03-12 02:30, US/Eastern)
That such a time is ambiguous if they enter a time that happened twice due to DST (e.g. 2017-11-05 02:30, US/Eastern)
Even better, offer them a way to specify which time they meant (if possible?)
That, when VIEWING a CONVERTED time, if the result is ambiguous, that there are 2 possible meanings
And of course I would like to outright prevent the website from displaying a time that does not exist due to DST (however, I believe this Luxon will already do this).
So as I said I understand the reasons that luxon cannot account for / deal with these scenarios as part of its standard library. What I'd like to know from you is, what are the best ways I can detect these scenarios myself? It's okay for my use-case to do a bit of poking around with nearby times to try and detect things and such and so forth, I'd just like to know the best approaches for this. Happy also to use the underlying Intl APIs if necessary.
however, I believe this Luxon will already do this
correct
The basic way to think about detecting these things is to figure out: given a local time and an offset, does the offset "tie out"? What I mean by that is: if you add the offset to the time to get a UTC time, and then compute the zones offset for that time, does it give you back the offset you plugged in?
Here's how Luxon takes a local time figures out the corresponding UTC time it will build the DateTime around (bear with me here, this is going somewhere):
We don't know the offset for this local time (we can only compute them for UTC times), but we guess one. This could be any guess, but Luxon uses the offset from now() in that zone.
We compute a "local timestamp". This is Luxon's core trick, and it's a little subtle. If you know the local time, then you can pretend it's a UTC time and get the epoch timestamp for that time. That UTC date will be a real date but off by the local offset, whatever that is. "Local timestamps" aren't a real thing -- they're a trick Luxon uses to make the native Date system not try to make its own adjustments. Here's how Luxon makes a local timestamp
You can add the guessed offset to the local timestamp, and ask the zone to compute its offset (using the Zone object, which for IANA zones uses Intl -- but you can just use the zone object). Is the offset the same as your guess? If not, the guessed offset is wrong--it didn't tie out--and needs to be adjusted by the difference. Now you know the real offset, and when you add that, you get the real UTC date.
All the logic for 3 is here. It's probably Luxon's most core function.
But--and this is the important part--notice a little lower down that function it tries to tie out the offsets a second time, and if that fails it decides its a hole time and does some hackery to advance it to after the hole. That's because adjusting the offset by the difference moves the local time over the hole and misses it from the other direction. A hole time, to Luxon, is one for which it can't find the offset that ties out.
OK, so armed with that, let's go further: what if you could guess two offsets instead? Then you could see if they both tie out. If they do, then it's an ambiguous time.
So that suggests this algorithm:
Probe around the date (say two days before and two days after) and get the offsets. Just use Luxon for this, like yourDT.plus({days: 2}).offset. These are like Luxon's guesses in the algo above, except that--assuming the offset doesn't change twice in four days--you're pretty sure at least one of them is right, or should be.
Now see if those offsets tie out for the local time you have. If neither do, you've got a hole. If both do, you have an ambiguity.
There are a few gotchas here I can think of offhand (and remember, I've never actually tried this):
What if your probes land on holes or ambiguities? I guess pick more probes, like +2 days, +3 days, -2 days, and -3 days?
As you mentioned, Luxon won't actually give you a hole time because it will advance the time for you, and the original provisional local time will be lost. So you need to capture that and construct the local timestamp before you put it into Luxon. That will actually make probing harder too...not sure what to do there.
Let me know how it goes if you try this!
P.S. that last complication makes me want to tag DateTimes with whether we advanced them over a hole time, which we do know, unlike ambiguous times.
Thanks for this big write up! I've been thinking about how this works for the last day and a half haha. After drawing some diagrams I believe I finally understand how this works.
As a result, I seem to have solved this problem! I have chucked a sample up on CodePen here.
The approach is very similar to suggested above, essentially:
Create a "local timestamp" of the time you are trying to test.
The best way I can figure out how to achieve this is to parse the input dateTime as UTC via some means (e.g. a "date object", an ISO-string, etc.)
Figure out the min and max offsets within the given timezone (e.g. for US/Eastern min = -5 hours and max = -4 hours). We will use these as our "guesses".
I am currently calculating these by min/maxing the offsets returned from every month of the year. I'm sure there are better ways to do this.
This comes with some assumptions. The core assumption is that any DST transition starts/ends on these min/max offsets. This seems to be the case already for Luxon's functionality, as a guess that's too incorrect will lead to an incorrect result. Using .now().offset guarantees the same property. Does that make sense? Luxon already rests on this assumption.
Test both of these offsets once against the local timestamp.
Neither resolve: Skipped or "Hole" time
Both resolve: Ambiguous time
One resolves: Any other normal time
I do feel now, that through some extra work I could even resolve which "occurrence" an ambiguous time is. Possibly by adding or subtracting the difference between the min/max offsets and testing the reported offset at that time. Through some very tricky UX this may ultimately allow users to specify which one they want.
Otherwise though, I am also wondering if this could end up as a feature in Luxon. If this kind of functionality was to be included, how might you see it working? Performance concerns could be alleviated by making it a function that you call such as .isSkippedTime() or .isAmbiguousTime(). Ambiguous times could potentially return an index for which occurance they are. I'd be interested in putting a PR together if you think this fits in within Luxon's API somewhere 🙂
Activity
icambron commentedon Aug 24, 2021
correct
The basic way to think about detecting these things is to figure out: given a local time and an offset, does the offset "tie out"? What I mean by that is: if you add the offset to the time to get a UTC time, and then compute the zones offset for that time, does it give you back the offset you plugged in?
Here's how Luxon takes a local time figures out the corresponding UTC time it will build the DateTime around (bear with me here, this is going somewhere):
now()in that zone.All the logic for 3 is here. It's probably Luxon's most core function.
But--and this is the important part--notice a little lower down that function it tries to tie out the offsets a second time, and if that fails it decides its a hole time and does some hackery to advance it to after the hole. That's because adjusting the offset by the difference moves the local time over the hole and misses it from the other direction. A hole time, to Luxon, is one for which it can't find the offset that ties out.
OK, so armed with that, let's go further: what if you could guess two offsets instead? Then you could see if they both tie out. If they do, then it's an ambiguous time.
So that suggests this algorithm:
yourDT.plus({days: 2}).offset. These are like Luxon's guesses in the algo above, except that--assuming the offset doesn't change twice in four days--you're pretty sure at least one of them is right, or should be.There are a few gotchas here I can think of offhand (and remember, I've never actually tried this):
Let me know how it goes if you try this!
P.S. that last complication makes me want to tag DateTimes with whether we advanced them over a hole time, which we do know, unlike ambiguous times.
peabnuts123 commentedon Aug 26, 2021
Thanks for this big write up! I've been thinking about how this works for the last day and a half haha. After drawing some diagrams I believe I finally understand how this works.
As a result, I seem to have solved this problem! I have chucked a sample up on CodePen here.
The approach is very similar to suggested above, essentially:
min = -5 hoursandmax = -4 hours). We will use these as our "guesses"..now().offsetguarantees the same property. Does that make sense? Luxon already rests on this assumption.I do feel now, that through some extra work I could even resolve which "occurrence" an ambiguous time is. Possibly by adding or subtracting the difference between the min/max offsets and testing the reported offset at that time. Through some very tricky UX this may ultimately allow users to specify which one they want.
Otherwise though, I am also wondering if this could end up as a feature in Luxon. If this kind of functionality was to be included, how might you see it working? Performance concerns could be alleviated by making it a function that you call such as
.isSkippedTime()or.isAmbiguousTime(). Ambiguous times could potentially return an index for which occurance they are. I'd be interested in putting a PR together if you think this fits in within Luxon's API somewhere 🙂icambron commentedon Aug 26, 2021
@peabnuts123 Awesome! I'll take a look soon
peabnuts123 commentedon Sep 8, 2021
😅
peabnuts123 commentedon Nov 28, 2021
@icambron this might have gotten a bit lost in your mentions since the issue is closed :)