Skip to content

Commit c9f75ec

Browse files
Custom zone formatting support (#1377)
* Substitute timezone name for fixed offset timezones * Support custom zone formatting --------- Co-authored-by: Isaac Cambron <[email protected]>
1 parent 5628d48 commit c9f75ec

File tree

2 files changed

+110
-18
lines changed

2 files changed

+110
-18
lines changed

Diff for: src/impl/locale.js

+42-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { padStart, roundTo, hasRelative } from "./util.js";
1+
import { padStart, roundTo, hasRelative, formatOffset } from "./util.js";
22
import * as English from "./english.js";
33
import Settings from "../settings.js";
44
import DateTime from "../datetime.js";
@@ -197,9 +197,13 @@ class PolyNumberFormatter {
197197
class PolyDateFormatter {
198198
constructor(dt, intl, opts) {
199199
this.opts = opts;
200+
this.originalZone = undefined;
200201

201202
let z = undefined;
202-
if (dt.zone.isUniversal) {
203+
if (this.opts.timeZone) {
204+
// Don't apply any workarounds if a timeZone is explicitly provided in opts
205+
this.dt = dt;
206+
} else if (dt.zone.type === "fixed") {
203207
// UTC-8 or Etc/UTC-8 are not part of tzdata, only Etc/GMT+8 and the like.
204208
// That is why fixed-offset TZ is set to that unless it is:
205209
// 1. Representing offset 0 when UTC is used to maintain previous behavior and does not become GMT.
@@ -212,25 +216,23 @@ class PolyDateFormatter {
212216
z = offsetZ;
213217
this.dt = dt;
214218
} else {
215-
// Not all fixed-offset zones like Etc/+4:30 are present in tzdata.
216-
// So we have to make do. Two cases:
217-
// 1. The format options tell us to show the zone. We can't do that, so the best
218-
// we can do is format the date in UTC.
219-
// 2. The format options don't tell us to show the zone. Then we can adjust them
220-
// the time and tell the formatter to show it to us in UTC, so that the time is right
221-
// and the bad zone doesn't show up.
219+
// Not all fixed-offset zones like Etc/+4:30 are present in tzdata so
220+
// we manually apply the offset and substitute the zone as needed.
222221
z = "UTC";
223-
if (opts.timeZoneName) {
224-
this.dt = dt;
225-
} else {
226-
this.dt = dt.offset === 0 ? dt : DateTime.fromMillis(dt.ts + dt.offset * 60 * 1000);
227-
}
222+
this.dt = dt.offset === 0 ? dt : dt.setZone("UTC").plus({ minutes: dt.offset });
223+
this.originalZone = dt.zone;
228224
}
229225
} else if (dt.zone.type === "system") {
230226
this.dt = dt;
231-
} else {
227+
} else if (dt.zone.type === "iana") {
232228
this.dt = dt;
233229
z = dt.zone.name;
230+
} else {
231+
// Custom zones can have any offset / offsetName so we just manually
232+
// apply the offset and substitute the zone as needed.
233+
z = "UTC";
234+
this.dt = dt.setZone("UTC").plus({ minutes: dt.offset });
235+
this.originalZone = dt.zone;
234236
}
235237

236238
const intlOpts = { ...this.opts };
@@ -239,11 +241,35 @@ class PolyDateFormatter {
239241
}
240242

241243
format() {
244+
if (this.originalZone) {
245+
// If we have to substitute in the actual zone name, we have to use
246+
// formatToParts so that the timezone can be replaced.
247+
return this.formatToParts()
248+
.map(({ value }) => value)
249+
.join("");
250+
}
242251
return this.dtf.format(this.dt.toJSDate());
243252
}
244253

245254
formatToParts() {
246-
return this.dtf.formatToParts(this.dt.toJSDate());
255+
const parts = this.dtf.formatToParts(this.dt.toJSDate());
256+
if (this.originalZone) {
257+
return parts.map((part) => {
258+
if (part.type === "timeZoneName") {
259+
const offsetName = this.originalZone.offsetName(this.dt.ts, {
260+
locale: this.dt.locale,
261+
format: this.opts.timeZoneName,
262+
});
263+
return {
264+
...part,
265+
value: offsetName,
266+
};
267+
} else {
268+
return part;
269+
}
270+
});
271+
}
272+
return parts;
247273
}
248274

249275
resolvedOptions() {

Diff for: test/datetime/format.test.js

+68-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* global test expect */
22

3-
import { DateTime } from "../../src/luxon";
3+
import { DateTime, Zone, FixedOffsetZone } from "../../src/luxon";
44

55
const dtMaker = () =>
66
DateTime.fromObject(
@@ -20,6 +20,50 @@ const dtMaker = () =>
2020
dt = dtMaker(),
2121
invalid = DateTime.invalid("because");
2222

23+
class CustomZone extends Zone {
24+
constructor(name, offset) {
25+
super();
26+
this._name = name;
27+
this._offset = offset;
28+
}
29+
30+
get isUniversal() {
31+
return true;
32+
}
33+
34+
get isValid() {
35+
return true;
36+
}
37+
38+
get name() {
39+
return this._name;
40+
}
41+
42+
get type() {
43+
return "custom";
44+
}
45+
46+
equals(zone) {
47+
return zone instanceof CustomZone && zone._name === this._name && zone._offset === this._offset;
48+
}
49+
50+
offset(_ms) {
51+
return this._offset;
52+
}
53+
54+
offsetName(_ms, { format }) {
55+
if (format === "short") {
56+
return this._name.substring(0, 4);
57+
} else {
58+
return this._name;
59+
}
60+
}
61+
62+
formatOffset(...args) {
63+
return FixedOffsetZone.prototype.formatOffset(...args);
64+
}
65+
}
66+
2367
//------
2468
// #toJSON()
2569
//------
@@ -398,7 +442,29 @@ test("DateTime#toLocaleString() shows things with UTC if fixed-offset zone with
398442

399443
test("DateTime#toLocaleString() does the best it can with unsupported fixed-offset zone when showing the zone", () => {
400444
expect(dt.setZone("UTC+4:30").toLocaleString(DateTime.DATETIME_FULL)).toBe(
401-
"May 25, 1982 at 9:23 AM UTC"
445+
"May 25, 1982 at 1:53 PM UTC+4:30"
446+
);
447+
});
448+
449+
test("DateTime#toLocaleString() does the best it can with unsupported fixed-offset zone with timeStyle full", () => {
450+
expect(dt.setZone("UTC+4:30").toLocaleString({ timeStyle: "full" })).toBe("1:53:54 PM UTC+4:30");
451+
});
452+
453+
test("DateTime#toLocaleString() shows things in the right custom zone", () => {
454+
expect(dt.setZone(new CustomZone("CUSTOM", 30)).toLocaleString(DateTime.DATETIME_SHORT)).toBe(
455+
"5/25/1982, 9:53 AM"
456+
);
457+
});
458+
459+
test("DateTime#toLocaleString() shows things in the right custom zone when showing the zone", () => {
460+
expect(dt.setZone(new CustomZone("CUSTOM", 30)).toLocaleString(DateTime.DATETIME_FULL)).toBe(
461+
"May 25, 1982 at 9:53 AM CUST"
462+
);
463+
});
464+
465+
test("DateTime#toLocaleString() shows things in the right custom zone with timeStyle full", () => {
466+
expect(dt.setZone(new CustomZone("CUSTOM", 30)).toLocaleString({ timeStyle: "full" })).toBe(
467+
"9:53:54 AM CUSTOM"
402468
);
403469
});
404470

0 commit comments

Comments
 (0)