Reputation: 37398
How do I get a list of "time zones" from NodaTime such that I can make a UI like the below for my users to choose from?
I want to show the UTC offset and then the appropriate cities/countries/locations. It doesn't need to be exactly like the below, but you know, something close.
DateTimeZone
doesn't have a name property, and ToString()
ing produces duplicates (from the list of Ids
from IDateTimeZoneProvider
).
I see you can go from ~countries to zones, with TzdbDateTimeZoneSource.Default.ZoneLocations
, but thats also not exactly what I'm looking for. I can see how I can cobble these two data sources together, but this feels like a solved problem I shouldn't be reinventing.
Upvotes: 6
Views: 4255
Reputation: 6281
I needed a list of canonical zone ids and their offsets from UTC so users could set their timezone in a drop-down list.
It's a bit dirty, but the code below will generate the ids and offsets. Clearly, you have to choose what offset to use, so to keep it the same all year, this ignores daylight-saving.
Important to note that the offsets are only for display, to help a user select the zone id - they can't be used for anything.
private static IEnumerable<(string Id, TimeSpan Offset)> GetCanonicalOffsets(DateTime utcNow)
{
if (utcNow.Kind != DateTimeKind.Utc) throw new ArgumentException($"DateTimeKind must be Utc.", nameof(utcNow));
var nextJanuary = Instant.FromUtc(utcNow.Year + 1, 1, 1, 0, 0);
var nextJune = Instant.FromUtc(utcNow.Month < 6 ? utcNow.Year : utcNow.Year + 1, 6, 1, 0, 0);
var result = TzdbDateTimeZoneSource.Default
.GetIds()
.Aggregate(
new HashSet<string>(),
(canonicalIds, id) =>
{
// Find the canonical zone id, as the list includes aliases:
var canonicalId = TzdbDateTimeZoneSource.Default.CanonicalIdMap[id];
// Deduplicate, as `Add` will ignore duplicates:
canonicalIds.Add(canonicalId);
return canonicalIds;
})
.Where(id =>
{
// Filter out zones we don't want to display:
return !id.StartsWith("Etc/") && id.Contains('/');
})
.Select(id =>
{
var dtz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(id);
return (Id: dtz.Id, Offset: GetOffset().ToTimeSpan());
// Try to find the offset from UTC when daylight-saving is not in force.
Offset GetOffset()
{
var january = new ZonedDateTime(nextJanuary, dtz);
if (!january.IsDaylightSavingTime())
{
return january.Offset;
}
var june = new ZonedDateTime(nextJune, dtz);
if (!june.IsDaylightSavingTime())
{
return june.Offset;
}
// Edge case:
// Zone is probably on permanent daylight savings.
// Subtract an hour to get most likely UTC offset.
return june.Offset - Offset.FromHours(1);
}
})
.OrderBy(x => x.Offset)
.ThenBy(x => x.Id);
return result;
}
Then to format the results for the drop-down list my code was similar to:
var listItems = GetCanonicalOffsets()
.Select(timeZone =>
{
var sign = timeZone.Offset < TimeSpan.Zero ? "-" : "+";
var text = $"UTC{sign}{timeZone.Offset:hh\\:mm} {timeZone.Id}";
return new ListItem(text, timeZone.Id);
});
Upvotes: 1
Reputation: 241808
You can get a list of display names and their corresponding IANA time zone ids, suitable for building a dropdown in the way you described, using my TimeZoneNames library. The resulting IDs are compatible with NodaTime's TZDB provider.
// You can either hardcode the language (ex: "en-US"), or get it from .NET globalization:
var languageCode = CultureInfo.CurrentUICulture.Name;
// Then get the names, as a list of key/value pairs
var list = TZNames.GetDisplayNames(languageCode, useIanaZoneIds: true);
// Use them as you wish. For example:
foreach (var name in list)
{
Console.WriteLine($"{name.Key} = \"{name.Value}\"");
}
Output (truncated):
Etc/GMT+12 = "(UTC-12:00) International Date Line West"
Etc/GMT+11 = "(UTC-11:00) Coordinated Universal Time-11"
America/Adak = "(UTC-10:00) Aleutian Islands"
Pacific/Honolulu = "(UTC-10:00) Hawaii"
Pacific/Marquesas = "(UTC-09:30) Marquesas Islands"
America/Anchorage = "(UTC-09:00) Alaska"
Etc/GMT+9 = "(UTC-09:00) Coordinated Universal Time-09"
America/Tijuana = "(UTC-08:00) Baja California"
Etc/GMT+8 = "(UTC-08:00) Coordinated Universal Time-08"
America/Los_Angeles = "(UTC-08:00) Pacific Time (US & Canada)"
America/Phoenix = "(UTC-07:00) Arizona"
America/Chihuahua = "(UTC-07:00) Chihuahua, La Paz, Mazatlan"
America/Denver = "(UTC-07:00) Mountain Time (US & Canada)"
America/Guatemala = "(UTC-06:00) Central America"
America/Chicago = "(UTC-06:00) Central Time (US & Canada)"
Pacific/Easter = "(UTC-06:00) Easter Island"
...
The display names are sourced from Windows language packs. The IDs are translated from Windows to IANA through CLDR. If you want Windows IDs instead, you can set useIanaZoneIds
to false
(or omit it).
See also Methods for listing time zones and the Acknowledgements in the TimeZoneNames docs.
Upvotes: 7
Reputation: 799
You can consider using GeoTimeZone Nuget Package to get the IANA timezone id by location i.e latitude and longitude for example
// using coordinates for a place in London use GeoTimeZone Library
string tz = GeoTimeZone.TimeZoneLookup.GetTimeZone(50.4372, -3.5559).Result; // Europe/London
DateTimeZone dateTimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(tz);
//You can get the UTC timeoffset at any instant possibly like this
Offset offset = dateTimeZone
.GetUtcOffset(SystemClock.Instance.GetCurrentInstant());
Console.WriteLine(offset); //+01
Upvotes: 1
Reputation: 1503090
Noda Time doesn't currently provide user-oriented strings for time zones, no.
The best source of data for that is CLDR. We have a long-standing issue for this, but unfortunately it's fundamentally tricky. At some point I'd like to get back to it, but I haven't found time yet :(
You can use the Onism.Cldr project to access CLDR data. You'll need to understand how the CLDR data works in two respects though:
Apologies that the answer at the moment is really just "No, there's nothing out of the box" - but that's the reality :(
Upvotes: 6