srawlins / timezone

Time zone database and time zone aware DateTime object for Dart.
BSD 2-Clause "Simplified" License
101 stars 52 forks source link

Location with the name "Etc/UTC" doesn't exist #176

Open bartekpacia opened 1 year ago

bartekpacia commented 1 year ago

That's how I initialize timezone data in my app:

import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/data/latest.dart' as tz_data;
import 'package:timezone/timezone.dart' as tz;

void main() {
  runApp(const ExampleApp());

  setUpTimezone();
}

Future<void> setUpTimezone() async {
  tz_data.initializeTimeZones();
  final timezone = await FlutterTimezone.getLocalTimezone();
  final location = tz.getLocation(timezone);
  tz.setLocalLocation(location);
}

When I run this code on Android Lollipop emulator (API level 21), the following exception is thrown:

flutter : The following LocationNotFoundException was thrown running a test:
07-03 13:26:09.688  3569  3588 I flutter : Location with the name "Etc/UTC" doesn't exist
07-03 13:26:09.688  3569  3588 I flutter : When the exception was thrown, this was the stack:
07-03 13:26:09.688  3569  3588 I flutter : #0      LocationDatabase.get (package:timezone/src/location_database.dart:40:7)
07-03 13:26:09.688  3569  3588 I flutter : #1      getLocation (package:timezone/src/env.dart:36:20)
07-03 13:26:09.688  3569  3588 I flutter : #2      setUpTimezone (package:example/main.dart:22:23)

I'd be grateful for any support or even an idea how this could be mitigated.

jamesncl commented 1 year ago

Okay so this comes up a lot, so thought I would take the time to give a full answer here. Maybe someone can add this to the docs as away of explanation. It took me a while to figure this all out.

The problem is that this package doesn't support many timezone abbreviations such as Etc/UTC, which is what the flutter_timezone package sometimes returns. This is a longstanding issue which won't be fixed, because this is intentional: abbreviations like this are ambiguous.

The problem is that timezones are really, really complicated, and to deal with them you have to understand a bit of this complexity. I'm not an expert, but I think the problem is that Etc/UTC isn't actually a proper timezone, it's a 'fixed offset'. A timezone is a whole history of what a particular group of people decided how they would handle time, daylight savings, etc. and the historical changes made over time. For example, during World War II the UK went 2 hours ahead at daylight saving time instead of the normal 1 hour, to save fuel. To be accurate with time, you have to take all these little historical details into account, because if you have a date from that time during World War II you could end up being an hour out if you don't.

Etc/UTC (and all other abbreviations with ETC in them) is not a history of timekeeping for a particular country, it's just a fixed number (0 hours offset from UTC). This means, depending on exactly where on earth you're talking about, converting a date from Etc/UTC might give you different results.

Most of the time, the timezone we get back from FlutterTimezone.getLocalTimezone() is a proper timezone, like 'Europe/London'. But occasionally, it will return an ambiguous abbreviation. This is unfortunate, and is a result of the underlying device-specific implementation. For Android, this is what FlutterTimezone does under the hood:

    private fun getLocalTimezone(): String {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            ZoneId.systemDefault().id
        } else {
            TimeZone.getDefault().id
        }
    }

And if we [look at the docs for TimeZone.getDefault()](https://developer.android.com/reference/java/util/TimeZone#getDefault()) (which is what ZoneId.systemDefault() also calls under the hood) it says:

Gets the default TimeZone for this host. The source of the default TimeZone may vary with implementation.

"may vary with implementation" basically means we have to deal with all kinds of garbage results, eg from old devices, SDKs, emulators etc. So, unfortunately this is just a problem we have to deal with ourselves. The docs state this on the front page:

We don't provide any functions to get locations by time zone abbreviations because of the ambiguities.

Alphabetic time zone abbreviations should not be used as unique identifiers for UTC offsets as they are ambiguous in practice. For example, "EST" denotes 5 hours behind UTC in English-speaking North America, but it denotes 10 or 11 hours ahead of UTC in Australia; and French-speaking North Americans prefer "HNE" to "EST".

The tz database

Ideally you should only pass unambiguous abbreviations, or full timezone identifiers. For example see the list in Wikipedia: the full timezone identifier is in the 'TZ identifier' column. Notice how the ETC abbreviations don't have proper timezone identifiers.

As a workaround, we have to manually assign these ambiguous timezones to a best guess. For example:

import 'package:timezone/data/latest_all.dart' as tzdatabase;
import 'package:timezone/timezone.dart' as tz;

...

    tzdatabase.initializeTimeZones();
    String localTimezone = await FlutterTimezone.getLocalTimezone();

    if(localTimezone == "Etc/UTC") {
      localTimezone = "??????"; <- replace this with a valid timezone identifier
    }
    tz.setLocalLocation(tz.getLocation(localTimezone));

    // then use tz.local whenever you need the local timezone

Alternatively you could try setting the timezone on the emulator, or trying a different emulator image which returns a valid timezone.

Also note the use of import 'package:timezone/data/latest_all.dart' as tzdatabase; here: importing the ALL version of the timezone database will include some old / invalid / deprecated timezones such as Etc/GMT+5 but will increase the load time. Again, this is explained in the docs.

See this old closed issue for more discussion.

bartekpacia commented 1 year ago

Thank you very much @jamesncl for such a thorough explanation. Looks like I unknowingly stepped into an uncanny valley of ambiguity :D

Should I close the issue or leave it open for future wanderers?

And again, I really appreciate your comment. Thank you!

thomasatbloom commented 8 months ago

Stumbled across this issue here with my own app. Using the all database does indeed fix it, but the page on pub.dev also says:

default: doesn't contain deprecated and historical zones with some exceptions like "US/Eastern" and "Etc/UTC"; this is about 75% the size of the all database.

I would assume then that Etc/UTC is supported.