bitwalker / timex

A complete date/time library for Elixir projects.
https://hexdocs.pm/timex
MIT License
1.76k stars 379 forks source link

could_not_resolve_timezone for America/Santiago September 8th 2019 #555

Closed silva96 closed 3 years ago

silva96 commented 5 years ago

I updated to 3.6.1 hoping the commit (https://github.com/bitwalker/timex/commit/ab03f307fd2204a869183a9d957967bdf2eef8b1) that fixes #514 would solve the issue, but it doesn't.

What I need to do is have the DateTime for the very beginning of the day of the 8th of september in America/Santiago timezone. This day, there is a change from CLT to CLST (summer time, daylight savings ends exactly at 23.59.59.999999 and time goes to 01 am instead of 00 am.)

Having the following example, I would expect to have valid dates in both cases (07-september-2019 and 08-september-2019, but the latter fails with :could_not_resolve_timezone

ok_date = "2019-09-07"
Timex.parse!(ok_date, "%Y-%m-%d", :strftime) |> Timex.to_datetime("America/Santiago")

#DateTime<2019-09-07 00:00:00-04:00 -04 America/Santiago>

not_ok_date = "2019-09-08"
Timex.parse!(not_ok_date, "%Y-%m-%d", :strftime) |> Timex.to_datetime("America/Santiago")

{:error,
 {:could_not_resolve_timezone,
  "America/Santiago", 63735120000,
  :wall}}

In ruby/rails works perfectly fine.

ok_date = "2019-09-07"
ActiveSupport::TimeZone.new('America/Santiago').parse(ok_date)
=> Sat, 07 Sep 2019 00:00:00 -04 -04:00

not_ok_date = "2019-09-08"
ActiveSupport::TimeZone.new('America/Santiago').parse(not_ok_date)
=> Sun, 08 Sep 2019 01:00:00 -03 -03:00

More over, substracting minutes from the beginning of conflicting day gives totally wrong date:

ok_date = "2019-09-07 00:00:00"        
Timex.parse!(ok_date, "%Y-%m-%d %H:%M:%M", :strftime)  
|> Timex.to_datetime("America/Santiago") 
|> Timex.shift(minutes: -10)

#DateTime<2019-09-06 23:50:00-04:00 -04 America/Santiago>

not_ok_date = "2019-09-08 01:00:00" # the day begins at 1 am, thats OK

# going 10 minutes before goes 50 mintues in the future.

Timex.parse!(not_ok_date, "%Y-%m-%d %H:%M:%M", :strftime)  
|> Timex.to_datetime("America/Santiago") 
|> Timex.shift(minutes: -10)

#DateTime<2019-09-08 01:50:00-03:00 -03 America/Santiago>

# but going forward is fine

Timex.parse!(not_ok_date, "%Y-%m-%d %H:%M:%M", :strftime)  
|> Timex.to_datetime("America/Santiago") 
|> Timex.shift(minutes: 10)

#DateTime<2019-09-08 01:10:00-03:00 -03 America/Santiago>
silva96 commented 5 years ago

More insights

End of day of 4th of april 2020 in America/Santiago fails, gives 22:00 -03 instead of 23.59.59 -03 Beginning of day of 5th of april 2020 in America/Santiago is ok in timex though.

left_date = Timex.to_datetime({{2020,4,4}, {23,59,0}}, "America/Santiago") # this is day at 23:59:59.999 -03:00, time goes back to 23:00:00 -04:00
next_day = Timex.to_datetime({{2020,4,5}, {5,0,0}}, "America/Santiago")  #arbitrary non-conflictive datetime in conflictive day
Timex.end_of_day(left_date)
#DateTime<2020-04-04 22:00:00-03:00 -03 America/Santiago>
Timex.beginning_of_day(next_day)
#DateTime<2020-04-05 00:00:00-04:00 -04 America/Santiago>

In ruby/rails it works.

left_date = "2020-04-04 23:59:00"
next_day = "2020-04-05 05:00:00"

ActiveSupport::TimeZone.new('America/Santiago').parse(left_date).end_of_day
=> Sat, 04 Apr 2020 23:59:59 -03 -03:00
# correct result, different from timex

ActiveSupport::TimeZone.new('America/Santiago').parse(next_day).beginning_of_day
 => Sun, 05 Apr 2020 00:00:00 -04 -04:00
# same result as in timex
silva96 commented 5 years ago

For april 4 at 22.59 -03, times in timex and in apple clock match

image

at 23.59 -03, times in timex and in apple clock match too, because nothing happened yet (regarding time offset change)

Captura de pantalla 2020-04-04 a la(s) 23 59 53

after a minute, clocks should go backwards to 23.00 but with offset -04

which apple clock handles correctly but timex doesn't, timex gives 00:00 -04

Captura de pantalla 2020-04-04 a la(s) 23 00 15

bbassett commented 5 years ago

I have just run into this issue as well, for the duration of exactly one, the Pacific/Easter timezone when called with Timex.Timezone.get/1 was broken.

you can replicate it with the following:

iex(24)> Timex.Timezone.get("Pacific/Easter", {{2019, 9, 7}, {22, 10, 0}})
{:error, {:could_not_resolve_timezone, "Pacific/Easter", 63735113400, :wall}}

we had an error for exactly one hour, which led me down this rabbit hole, and as it has happened with at least two time zones at different times, i'm sure there's an underlying bug.

chungwong commented 5 years ago

I am experiencing a similar issue. any datetime between 02:00:00 to 02:59:59 will throw the same error.

Timex.parse!("2019-10-06T02:00:00.765Z", "{ISO:Extended:Z}") |> Timex.local
{:error, {:could_not_resolve_timezone, "Australia/Sydney", 63737546400, :wall}}

Update 1: I found if Timezone in OS is set to UTC, it will not cause any error. If Timezone is set to something different, it will cause Timex to throw the error. Tested both on CentOS and Debian

jamiesunderland commented 3 years ago

We hit this bug in production today and had a lot of issues for 1 hour. I really hope this gets fixed eventually.

silva96 commented 3 years ago

Each year a new victim :D

bbassett commented 3 years ago

Just to chime back in on this. There actually isn't a bug. The problem occurs when you try to resolve a time that actually doesn't exist in a specific timezone. I remember digging in deep.

the time doesn't exist because during daylight savings times, different regions skip hours, so the time inside of that hour for that TZ is invalid.

bottom line is: you should confirm that when you resolve a time it returns a struct, rather than an error tuple.

i'd recommend @bitwalker closes this issue

bitwalker commented 3 years ago

There is a better solution to helping this actually, and I’m finishing up the changes along with a variety of other fixes; should be pushed by EOW

chungwong commented 3 years ago

Just to chime back in on this. There actually isn't a bug. The problem occurs when you try to resolve a time that actually doesn't exist in a specific timezone. I remember digging in deep.

the time doesn't exist because during daylight savings times, different regions skip hours, so the time inside of that hour for that TZ is invalid.

bottom line is: you should confirm that when you resolve a time it returns a struct, rather than an error tuple.

i'd recommend @bitwalker closes this issue

In my opinion it is a bug and a serious one too. When I simply want a datetime and all I get during that 1 hour of timezone change, is an error. Even though I catch the error tuple, then what I do with it. There is no way for me to get a timestamp anyway.

DunyaKokoschka commented 3 years ago

Just to chime back in on this. There actually isn't a bug. The problem occurs when you try to resolve a time that actually doesn't exist in a specific timezone. I remember digging in deep.

the time doesn't exist because during daylight savings times, different regions skip hours, so the time inside of that hour for that TZ is invalid.

bottom line is: you should confirm that when you resolve a time it returns a struct, rather than an error tuple.

i'd recommend @bitwalker closes this issue

There is actually a bug. You can check here for instructions on how to reproduce it: https://github.com/bitwalker/timex/issues/625#issuecomment-799680070 That is converting a UTC time to a time in a timezone. This should always work (excluding some edge cases). UTC time refers to an unambiguous point in time. So if I could travel to a particular point in UTC time and then looked at a clock in that timezone I should always see a time (it exists) and I should only see 1 time (it is unambiguous). However, going in the other direction has problems but I'm not complaining about that.

bitwalker commented 3 years ago

This is fixed in 3.7

jamiesunderland commented 3 years ago

@bitwalker thank you so much for the quick turn around here!

nonrational commented 3 years ago

Thanks for looking into this @bitwalker. I'm seeing the issue as described in https://github.com/bitwalker/timex/issues/587 with 3.7.1

{:ok, before_dst, _} = DateTime.from_iso8601("2021-03-14T01:00:00Z")
%Timex.TimezoneInfo{} = Timex.timezone("America/New_York", before_dst)

{:ok, at_dst, _} = DateTime.from_iso8601("2021-03-14T02:00:00Z")
{:error, {:could_not_resolve_timezone, "America/New_York", 63782906400, :wall}} = Timex.timezone("America/New_York", at_dst)

Edit: It looks like Timex.Timezone.resolve/3 returns what I expect when given :utc as the third arg.

iex(1)> {:ok, two_am_utc, _} = DateTime.from_iso8601("2021-03-14T02:00:00Z")                                                                
{:ok, ~U[2021-03-14 02:00:00Z], 0}

iex(2)> Timex.Timezone.resolve("America/New_York", Timex.to_gregorian_seconds(two_am_utc), :utc)                                            
#<TimezoneInfo(America/New_York - EST (-05:00:00))>

iex(3)> Timex.Timezone.resolve("America/New_York", Timex.to_gregorian_seconds(two_am_utc), :wall)
{:error, {:could_not_resolve_timezone, "America/New_York", 63782906400, :wall}}

Why does Timex.timezone/2 require the use of the default :wall instead of :utc ?

bitwalker commented 3 years ago

@nonrational I’m not sure why you would use timezone/2 vs Timex.to_datetime/2 or Timex.Timezone.convert/2, both of which are more appropriate for getting a DateTime in a desired timezone. Is there a particular reason you are using that function directly?

Also, can you try with 3.7.2?

nonrational commented 3 years ago

I'm using Timex.timezone/2 to look up the full %Timex.TimezoneInfo{} as of a given UTC datetime for a named timezone (e.g. "America/New_York" -> %Timex.TimezoneInfo{abbreviation: "EDT"}).

Will try with 3.7.2!

bitwalker commented 3 years ago

As of 3.7.2 you are better off calling ‘Timex.Timezone.get/2` with a NaiveDateTime if you want a TimezoneInfo that reflects the wall clock time in a given zone, give that a shot instead.

nonrational commented 3 years ago

Timex.Timezone.get/2 works exactly as expected. Thank you!

fSergio101 commented 3 years ago

Hello guys, I've experienced this issue last Sunday when the DST happened in Spain (Using v3.6.1). Today I've just reproduced it doing the following

then I got


** (FunctionClauseError) no function clause matching in Timex.Timezone.total_offset/1

    The following arguments were given to Timex.Timezone.total_offset/1:

        # 1
        {:error, {:could_not_resolve_timezone, "Europe/Madrid", 63784117369, :wall}}

    Attempted function clauses (showing 1 out of 1):

        def total_offset(%Timex.TimezoneInfo{offset_std: std, offset_utc: utc})

    (timex) lib/timezone/timezone.ex:447: Timex.Timezone.total_offset/1
Timex.Timezone.get("Europe/Madrid", NaiveDateTime.utc_now())

and I got again the same error:


** (FunctionClauseError) no function clause matching in Timex.Timezone.total_offset/1

    The following arguments were given to Timex.Timezone.total_offset/1:

        # 1
        {:error, {:could_not_resolve_timezone, "Europe/Madrid", 63784117369, :wall}}

    Attempted function clauses (showing 1 out of 1):

        def total_offset(%Timex.TimezoneInfo{offset_std: std, offset_utc: utc})

    (timex) lib/timezone/timezone.ex:447: Timex.Timezone.total_offset/1

has this issue already been fixed? and it's me missing something??

chungwong commented 3 years ago

Unfortunately Timex introduced a broken(a serious one) change.

https://github.com/bitwalker/timex/issues/651

fSergio101 commented 3 years ago

Which is te relation between the given example and the breaking change you are pointing? I', using in the example a NaiveDateTime that is supposed to be agnostic from timezones. 🤔 So AFAIK there should be no relation. Moreover, this behaviour is just transient and after one hour it backs to the right one.

Unfortunately Timex introduced a broken(a serious one) change.

651