Skip to content

Commit dd8a792

Browse files
Change timezone API (#1022)
* add js timezone parsing * Switch icu4x and ecma to use timezones * fix timezone calculation time * fixes * extract * add changelog * Fixes * fixes * Fixes as per review * rev version to major * support all timezones * Fixes as per review? --------- Co-authored-by: Robert Bastian <[email protected]>
1 parent 7ebcd82 commit dd8a792

File tree

12 files changed

+320
-59
lines changed

12 files changed

+320
-59
lines changed

pkgs/intl4x/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.14.0
2+
3+
- Change timezone API, adding a dependency on package:timezone.
4+
15
## 0.13.2
26

37
- Add `withEra`.

pkgs/intl4x/example/bin/example.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import 'package:intl4x/datetime_format.dart';
66
import 'package:intl4x/intl4x.dart';
77

88
void main(List<String> arguments) {
9-
const timeZone = TimeZone(name: 'Europe/Paris', offset: Duration(hours: 2));
10-
final dateTime = DateTime.parse('2024-07-01T08:50:07Z');
9+
final timeZone = 'Europe/Paris';
10+
final dateTime = DateTime.parse('2024-07-01T08:50:07');
1111

1212
print(Intl().locale.toString());
1313

pkgs/intl4x/lib/src/datetime_format/datetime_format_ecma.dart

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import 'dart:js_interop';
66

7+
import 'package:collection/collection.dart' show IterableExtension;
8+
79
import '../locale/locale.dart';
810
import '../options.dart';
911
import 'datetime_format_impl.dart';
@@ -21,7 +23,7 @@ class DateTimeJSOptions {
2123
final TimeStyle? hour;
2224
final TimeStyle? minute;
2325
final TimeStyle? second;
24-
final TimeZone? timeZone;
26+
final String? timeZone;
2527
final TimeZoneType? timeZoneType;
2628
final Style? weekday;
2729

@@ -44,7 +46,7 @@ class DateTimeJSOptions {
4446
TimeStyle? hour,
4547
TimeStyle? minute,
4648
TimeStyle? second,
47-
TimeZone? timeZone,
49+
String? timeZone,
4850
TimeZoneType? timeZoneType,
4951
Style? weekday,
5052
}) => DateTimeJSOptions(
@@ -122,7 +124,7 @@ class FormatterZonedECMA extends FormatterZonedImpl {
122124
static DateTimeFormat createDateTimeFormat(
123125
FormatterECMA formatter,
124126
TimeZoneType timeZoneType,
125-
TimeZone timeZone,
127+
String timeZone,
126128
) {
127129
final localeJS = [formatter.locale.toLanguageTag().toJS].toJS;
128130
return DateTimeFormat(
@@ -140,12 +142,36 @@ class FormatterZonedECMA extends FormatterZonedImpl {
140142
}
141143

142144
@override
143-
String formatInternal(DateTime datetime, TimeZone timeZone) =>
144-
createDateTimeFormat(
145+
String formatInternal(DateTime datetime, String timeZone) {
146+
try {
147+
// ECMA will interpret this as UTC time and convert it
148+
// into the time zone, we need to invert that change.
149+
final adjustedDateTime = datetime.subtract(
150+
offsetForTimeZone(datetime, timeZone),
151+
);
152+
return createDateTimeFormat(
145153
formatter,
146154
timeZoneType,
147155
timeZone,
148-
).format(datetime.subtract(timeZone.offset).jsUtc);
156+
).format(adjustedDateTime.jsUtc);
157+
} catch (e) {
158+
// Unknown timezone. Format with UTC and append '+?'
159+
// to construct a localized 'UTC+?'
160+
final parts = createDateTimeFormat(
161+
formatter,
162+
TimeZoneType.shortOffset,
163+
'UTC',
164+
).formatToParts(datetime.jsUtc).toDart;
165+
return parts
166+
.map(Part._)
167+
.map(
168+
(part) => part.isTimezoneName
169+
? '${part.value.split('+')[0]}+?'
170+
: part.value,
171+
)
172+
.join();
173+
}
174+
}
149175
}
150176

151177
class _DateTimeFormatECMA extends DateTimeFormatImpl {
@@ -365,15 +391,63 @@ extension type Date._(JSObject _) implements JSObject {
365391
);
366392
}
367393

394+
Duration offsetForTimeZone(DateTime datetime, String iana) {
395+
final timeZoneName = DateTimeFormat(
396+
['en'.toJS].toJS,
397+
{'timeZoneName': TimeZoneType.longOffset.name, 'timeZone': iana}.jsify()!,
398+
).timeZoneName(datetime.js);
399+
return parseTimeZoneOffset(timeZoneName);
400+
}
401+
368402
@JS('Intl.DateTimeFormat')
369403
extension type DateTimeFormat._(JSObject _) implements JSObject {
370404
external factory DateTimeFormat([JSArray<JSString> locale, JSAny options]);
371-
external String format(JSAny num);
405+
external String format(Date num);
372406

373407
external static JSArray<JSString> supportedLocalesOf(
374408
JSArray listOfLocales, [
375409
JSAny options,
376410
]);
411+
412+
external JSArray<JSObject> formatToParts(JSAny num);
413+
414+
String? timeZoneName(Date date) {
415+
final timezoneNameObject = formatToParts(
416+
date,
417+
).toDart.map(Part._).firstWhereOrNull((part) => part.isTimezoneName);
418+
return timezoneNameObject?.value;
419+
}
420+
}
421+
422+
@JS()
423+
extension type Part._(JSObject _) implements JSObject {
424+
external String get type;
425+
external String get value;
426+
427+
bool get isTimezoneName => type == 'timeZoneName';
428+
}
429+
430+
final _offsetRegex = RegExp(
431+
r'([+\-\u2212])(\d{2}):?(\d{2})(?:(?::?)(\d{2}))?$',
432+
);
433+
434+
Duration parseTimeZoneOffset(String? offsetString) {
435+
if (offsetString == null || offsetString == 'UTC' || offsetString == 'GMT') {
436+
return Duration.zero;
437+
}
438+
439+
final Match? match = _offsetRegex.firstMatch(offsetString);
440+
441+
if (match == null) {
442+
throw ArgumentError('Invalid time zone offset format: "$offsetString"');
443+
}
444+
445+
final sign = (match.group(1)! == '-' || match.group(1)! == '\u2212') ? -1 : 1;
446+
final hours = int.parse(match.group(2)!);
447+
final minutes = int.parse(match.group(3)!);
448+
final seconds = int.parse(match.group(4) ?? '0');
449+
450+
return Duration(hours: hours, minutes: minutes, seconds: seconds) * sign;
377451
}
378452

379453
extension on DateTime {
@@ -397,7 +471,7 @@ extension on DateTimeFormatOptions {
397471
if (dayPeriod != null) 'dayPeriod': dayPeriod!.name,
398472
if (numberingSystem != null) 'numberingSystem': numberingSystem!.name,
399473
if (options.timeZone != null) ...{
400-
'timeZone': options.timeZone!.name,
474+
'timeZone': options.timeZone,
401475
'timeZoneName': options.timeZoneType!.name,
402476
},
403477
if (clockstyle != null) ...{

pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ abstract class FormatterZonedImpl extends ZonedDateTimeFormatter {
7070
final DateTimeFormatImpl _impl;
7171
final TimeZoneType timeZoneType;
7272

73-
String formatInternal(DateTime datetime, TimeZone timeZone);
73+
String formatInternal(DateTime datetime, String timeZone);
7474

7575
FormatterZonedImpl(this._impl, this.timeZoneType);
7676

7777
@override
78-
String format(DateTime datetime, TimeZone timeZone) {
78+
String format(DateTime datetime, String timeZone) {
7979
if (isInTest) {
8080
return '$datetime//${_impl.locale}';
8181
} else {
@@ -96,8 +96,8 @@ sealed class DateTimeFormatter {
9696
ZonedDateTimeFormatter withTimeZoneLongGeneric();
9797
}
9898

99-
/// A base class for formatters that can format a [DateTime] and [TimeZone] into
100-
/// a string.
99+
/// A base class for formatters that can format a [DateTime] and time zone
100+
/// string into a string.
101101
sealed class ZonedDateTimeFormatter {
102-
String format(DateTime datetime, TimeZone timeZone);
102+
String format(DateTime datetime, String timeZone);
103103
}

pkgs/intl4x/lib/src/datetime_format/datetime_format_options.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,6 @@ enum TimeStyle {
125125
const TimeStyle([this._jsName]);
126126
}
127127

128-
final class TimeZone {
129-
final String name;
130-
final Duration offset;
131-
132-
const TimeZone({required this.name, required this.offset});
133-
}
134-
135128
enum TimeZoneType {
136129
/// Example: `Pacific Standard Time`
137130
long,

pkgs/intl4x/lib/src/datetime_format/icu4x/date_formatter.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,9 @@ class DateFormatterZonedX extends FormatterZonedImpl {
166166
super(dateFormatter.impl, TimeZoneType.longGeneric);
167167

168168
@override
169-
String formatInternal(DateTime datetime, TimeZone timeZone) {
170-
final utcOffset = icu.UtcOffset.fromSeconds(timeZone.offset.inSeconds);
169+
String formatInternal(DateTime datetime, String timeZone) {
170+
final timeZoneX = timeZoneToX(timeZone, datetime);
171171
final (isoDate, time) = datetime.toX;
172-
final timeZoneX = icu.IanaParser()
173-
.parse(timeZone.name)
174-
.withOffset(utcOffset)
175-
.atDateTimeIso(isoDate, time);
176172

177173
return formatter.formatIso(isoDate, timeZoneX);
178174
}

pkgs/intl4x/lib/src/datetime_format/icu4x/date_time_formatter.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,9 @@ class DateTimeFormatterZonedX extends FormatterZonedImpl {
137137
super(dateFormatter.impl, TimeZoneType.longGeneric);
138138

139139
@override
140-
String formatInternal(DateTime datetime, TimeZone timeZone) {
141-
final utcOffset = icu.UtcOffset.fromSeconds(timeZone.offset.inSeconds);
140+
String formatInternal(DateTime datetime, String timeZone) {
141+
final timeZoneX = timeZoneToX(timeZone, datetime);
142142
final (isoDate, time) = datetime.toX;
143-
final timeZoneX = icu.IanaParser()
144-
.parse(timeZone.name)
145-
.withOffset(utcOffset)
146-
.atDateTimeIso(isoDate, time);
147143

148144
return formatter.formatIso(isoDate, time, timeZoneX);
149145
}

pkgs/intl4x/lib/src/datetime_format/icu4x/datetime_format_4x.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:icu4x/icu4x.dart' as icu;
6+
import 'package:timezone/data/latest_all.dart' show initializeTimeZones;
7+
import 'package:timezone/timezone.dart' show TZDateTime, timeZoneDatabase;
68

79
import '../../../datetime_format.dart';
810
import '../../locale/locale.dart';
@@ -211,6 +213,33 @@ extension on DateTimeFormatOptions {
211213
}
212214
}
213215

216+
icu.TimeZoneInfo timeZoneToX(String timeZone, DateTime datetime) {
217+
if (!timeZoneDatabase.isInitialized) {
218+
initializeTimeZones();
219+
}
220+
final location = timeZoneDatabase.locations[timeZone];
221+
final timeZoneX = location != null
222+
? icu.IanaParser()
223+
.parse(timeZone)
224+
.withOffset(
225+
icu.UtcOffset.fromSeconds(
226+
TZDateTime(
227+
location,
228+
datetime.year,
229+
datetime.month,
230+
datetime.day,
231+
datetime.hour,
232+
datetime.minute,
233+
datetime.second,
234+
datetime.millisecond,
235+
datetime.microsecond,
236+
).timeZoneOffset.inSeconds,
237+
),
238+
)
239+
: icu.TimeZone.unknown().withoutOffset();
240+
return timeZoneX;
241+
}
242+
214243
icu.Locale setLocaleExtensions(
215244
icu.Locale locale,
216245
DateTimeFormatOptions options,

pkgs/intl4x/lib/src/datetime_format/icu4x/time_formatter.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,9 @@ class TimeFormatterZonedX extends FormatterZonedImpl {
119119
super(timeFormatter.impl, TimeZoneType.longGeneric);
120120

121121
@override
122-
String formatInternal(DateTime datetime, TimeZone timeZone) {
123-
final utcOffset = icu.UtcOffset.fromSeconds(timeZone.offset.inSeconds);
122+
String formatInternal(DateTime datetime, String timeZone) {
123+
final timeZoneX = timeZoneToX(timeZone, datetime);
124124
final (isoDate, time) = datetime.toX;
125-
final timeZoneX = icu.IanaParser()
126-
.parse(timeZone.name)
127-
.withOffset(utcOffset)
128-
.atDateTimeIso(isoDate, time);
129125
return formatter.format(time, timeZoneX);
130126
}
131127
}

pkgs/intl4x/pubspec.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: intl4x
22
description: >-
33
A lightweight modular library for internationalization (i18n) functionality.
4-
version: 0.13.2
4+
version: 0.14.0
55
repository: https://github.com/dart-lang/i18n/tree/main/pkgs/intl4x
66
issue_tracker: https://github.com/dart-lang/i18n/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aintl4x
77

@@ -20,13 +20,14 @@ environment:
2020
sdk: ^3.9.3
2121

2222
dependencies:
23+
collection: ^1.19.1
2324
hooks: ^0.20.4
2425
icu4x: ^2.1.0-dev.0
2526
meta: ^1.17.0
2627
record_use: ^0.4.2
28+
timezone: ^0.10.1
2729

2830
dev_dependencies:
2931
args: ^2.7.0
30-
collection: ^1.19.1
3132
dart_flutter_team_lints: ^3.5.2
3233
test: ^1.26.3

0 commit comments

Comments
 (0)