From 7e5d24dcabb737e1a88be5d416b5ea78921f860c Mon Sep 17 00:00:00 2001 From: Florent Odier Date: Tue, 29 Nov 2022 05:44:54 +0100 Subject: [PATCH] Add Interval.toLocaleString() (#1320) * Add Interval.toLocaleString() --- src/impl/formatter.js | 5 ++ src/interval.js | 40 +++++++-- test/interval/format.test.js | 160 +++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 5 deletions(-) diff --git a/src/impl/formatter.js b/src/impl/formatter.js index b3486301a..4dcd6be72 100644 --- a/src/impl/formatter.js +++ b/src/impl/formatter.js @@ -108,6 +108,11 @@ export default class Formatter { return df.formatToParts(); } + formatInterval(interval, opts = {}) { + const df = this.loc.dtFormatter(interval.start, { ...this.opts, ...opts }); + return df.dtf.formatRange(interval.start.toJSDate(), interval.end.toJSDate()); + } + resolvedOptions(dt, opts = {}) { const df = this.loc.dtFormatter(dt, { ...this.opts, ...opts }); return df.resolvedOptions(); diff --git a/src/interval.js b/src/interval.js index c68a3e563..a4c177ae6 100644 --- a/src/interval.js +++ b/src/interval.js @@ -3,6 +3,8 @@ import Duration from "./duration.js"; import Settings from "./settings.js"; import { InvalidArgumentError, InvalidIntervalError } from "./errors.js"; import Invalid from "./impl/invalid.js"; +import Formatter from "./impl/formatter.js"; +import * as Formats from "./impl/formats.js"; const INVALID = "Invalid Interval"; @@ -32,7 +34,7 @@ function validateStartEnd(start, end) { * * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame}, {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}. * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval.merge}, {@link Interval.xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}. * * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs} - * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}. + * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toLocaleString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}. */ export default class Interval { /** @@ -529,6 +531,30 @@ export default class Interval { return `[${this.s.toISO()} – ${this.e.toISO()})`; } + /** + * Returns a localized string representing this Interval. Accepts the same options as the + * Intl.DateTimeFormat constructor and any presets defined by Luxon, such as + * {@link DateTime.DATE_FULL} or {@link DateTime.TIME_SIMPLE}. The exact behavior of this method + * is browser-specific, but in general it will return an appropriate representation of the + * Interval in the assigned locale. Defaults to the system's locale if no locale has been + * specified. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat + * @param {Object} [formatOpts=DateTime.DATE_SHORT] - Either a DateTime preset or + * Intl.DateTimeFormat constructor options. + * @param {Object} opts - Options to override the configuration of the start DateTime. + * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(); //=> 11/7/2022 – 11/8/2022 + * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL); //=> November 7 – 8, 2022 + * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL, { locale: 'fr-FR' }); //=> 7–8 novembre 2022 + * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString(DateTime.TIME_SIMPLE); //=> 6:00 – 8:00 PM + * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> Mon, Nov 07, 6:00 – 8:00 p + * @return {string} + */ + toLocaleString(formatOpts = Formats.DATE_SHORT, opts = {}) { + return this.isValid + ? Formatter.create(this.s.loc.clone(opts), formatOpts).formatInterval(this) + : INVALID; + } + /** * Returns an ISO 8601-compliant string representation of this Interval. * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals @@ -564,10 +590,14 @@ export default class Interval { } /** - * Returns a string representation of this Interval formatted according to the specified format string. - * @param {string} dateFormat - the format string. This string formats the start and end time. See {@link DateTime#toFormat} for details. - * @param {Object} opts - options - * @param {string} [opts.separator = ' – '] - a separator to place between the start and end representations + * Returns a string representation of this Interval formatted according to the specified format + * string. **You may not want this.** See {@link Interval#toLocaleString} for a more flexible + * formatting tool. + * @param {string} dateFormat - The format string. This string formats the start and end time. + * See {@link DateTime#toFormat} for details. + * @param {Object} opts - Options. + * @param {string} [opts.separator = ' – '] - A separator to place between the start and end + * representations. * @return {string} */ toFormat(dateFormat, { separator = " – " } = {}) { diff --git a/test/interval/format.test.js b/test/interval/format.test.js index ca4658a49..c983823b8 100644 --- a/test/interval/format.test.js +++ b/test/interval/format.test.js @@ -16,6 +16,166 @@ test("Interval#toString returns a simple range format", () => test("Interval#toString returns an unfriendly string for invalid intervals", () => expect(invalid.toString()).toBe("Invalid Interval")); +//------ +// .toLocaleString() +//------ + +test("Interval#toLocaleString defaults to the DATE_SHORT format", () => + expect(interval.toLocaleString()).toBe("5/25/1982 – 10/14/1983")); + +test("Interval#toLocaleString returns an unfriendly string for invalid intervals", () => + expect(invalid.toLocaleString()).toBe("Invalid Interval")); + +test("Interval#toLocaleString lets the locale set the numbering system", () => { + expect( + Interval.after(interval.start.reconfigure({ locale: "ja-JP" }), { hour: 2 }).toLocaleString({ + hour: "numeric", + }) + ).toBe("9時~11時"); +}); + +test("Interval#toLocaleString accepts locale settings from the start DateTime", () => { + expect( + Interval.fromDateTimes( + interval.start.reconfigure({ locale: "be" }), + interval.end + ).toLocaleString() + ).toBe("25.5.1982 – 14.10.1983"); +}); + +test("Interval#toLocaleString accepts numbering system settings from the start DateTime", () => { + expect( + Interval.fromDateTimes( + interval.start.reconfigure({ numberingSystem: "beng" }), + interval.end + ).toLocaleString() + ).toBe("৫/২৫/১৯৮২ – ১০/১৪/১৯৮৩"); +}); + +test("Interval#toLocaleString accepts ouptput calendar settings from the start DateTime", () => { + expect( + Interval.fromDateTimes( + interval.start.reconfigure({ outputCalendar: "islamic" }), + interval.end + ).toLocaleString() + ).toBe("8/2/1402 – 1/8/1404 AH"); +}); + +test("Interval#toLocaleString accepts options to the formatter", () => { + expect(interval.toLocaleString({ weekday: "short" })).toBe("Tue – Fri"); +}); + +test("Interval#toLocaleString can override the start DateTime's locale", () => { + expect( + Interval.fromDateTimes( + interval.start.reconfigure({ locale: "be" }), + interval.end + ).toLocaleString({}, { locale: "fr" }) + ).toBe("25/05/1982 – 14/10/1983"); +}); + +test("Interval#toLocaleString can override the start DateTime's numbering system", () => { + expect( + Interval.fromDateTimes( + interval.start.reconfigure({ numberingSystem: "beng" }), + interval.end + ).toLocaleString({ numberingSystem: "mong" }) + ).toBe("᠕/᠒᠕/᠑᠙᠘᠒ – ᠑᠐/᠑᠔/᠑᠙᠘᠓"); +}); + +test("Interval#toLocaleString can override the start DateTime's output calendar", () => { + expect( + Interval.fromDateTimes( + interval.start.reconfigure({ outputCalendar: "islamic" }), + interval.end + ).toLocaleString({}, { outputCalendar: "coptic" }) + ).toBe("9/17/1698 – 2/3/1700 ERA1"); +}); + +test("Interval#toLocaleString shows things in the right IANA zone", () => { + expect( + Interval.fromDateTimes( + interval.start.setZone("Australia/Melbourne"), + interval.end + ).toLocaleString(DateTime.DATETIME_SHORT) + ).toBe("5/25/1982, 7:00 PM – 10/14/1983, 11:30 PM"); +}); + +test("Interval#toLocaleString shows things in the right fixed-offset zone", () => { + expect( + Interval.fromDateTimes(interval.start.setZone("UTC-8"), interval.end).toLocaleString( + DateTime.DATETIME_SHORT + ) + ).toBe("5/25/1982, 1:00 AM – 10/14/1983, 5:30 AM"); +}); + +test("Interval#toLocaleString shows things in the right fixed-offset zone when showing the zone", () => { + expect( + Interval.fromDateTimes(interval.start.setZone("UTC-8"), interval.end).toLocaleString( + DateTime.DATETIME_FULL + ) + ).toBe("May 25, 1982 at 1:00 AM GMT-8 – October 14, 1983 at 5:30 AM GMT-8"); +}); + +test("Interval#toLocaleString shows things with UTC if fixed-offset with 0 offset is used", () => { + expect( + Interval.fromDateTimes(interval.start.setZone("UTC"), interval.end).toLocaleString( + DateTime.DATETIME_FULL + ) + ).toBe("May 25, 1982 at 9:00 AM UTC – October 14, 1983 at 1:30 PM UTC"); +}); + +test("Interval#toLocaleString does the best it can with unsupported fixed-offset zone when showing the zone", () => { + expect( + Interval.fromDateTimes(interval.start.setZone("UTC+4:30"), interval.end).toLocaleString( + DateTime.DATETIME_FULL + ) + ).toBe("May 25, 1982 at 9:00 AM UTC – October 14, 1983 at 1:30 PM UTC"); +}); + +test("Interval#toLocaleString uses locale-appropriate time formats", () => { + expect( + Interval.after(interval.start.reconfigure({ locale: "en-US" }), { hour: 2 }).toLocaleString( + DateTime.TIME_SIMPLE + ) + ).toBe("9:00 – 11:00 AM"); + expect( + Interval.after(interval.start.reconfigure({ locale: "en-US" }), { hour: 2 }).toLocaleString( + DateTime.TIME_24_SIMPLE + ) + ).toBe("09:00 – 11:00"); + + // France has 24-hour by default + expect( + Interval.after(interval.start.reconfigure({ locale: "fr" }), { hour: 2 }).toLocaleString( + DateTime.TIME_SIMPLE + ) + ).toBe("09:00 – 11:00"); + expect( + Interval.after(interval.start.reconfigure({ locale: "fr" }), { hour: 2 }).toLocaleString( + DateTime.TIME_24_SIMPLE + ) + ).toBe("09:00 – 11:00"); + + // Spain does't prefix with "0" and doesn't use spaces + expect( + Interval.after(interval.start.reconfigure({ locale: "es" }), { hour: 2 }).toLocaleString( + DateTime.TIME_SIMPLE + ) + ).toBe("9:00–11:00"); + expect( + Interval.after(interval.start.reconfigure({ locale: "es" }), { hour: 2 }).toLocaleString( + DateTime.TIME_24_SIMPLE + ) + ).toBe("9:00–11:00"); +}); + +test("Interval#toLocaleString sets the separator between days for same-month dates", () => { + expect(Interval.after(interval.start, { day: 2 }).toLocaleString(DateTime.DATE_MED)).toBe( + "May 25 – 27, 1982" + ); +}); + //------ // .toISO() //------