Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Interval.toLocaleString() #1320

Merged
merged 2 commits into from Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/impl/formatter.js
Expand Up @@ -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();
Expand Down
40 changes: 35 additions & 5 deletions src/interval.js
Expand Up @@ -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";

Expand Down Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = " – " } = {}) {
Expand Down
160 changes: 160 additions & 0 deletions test/interval/format.test.js
Expand Up @@ -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()
//------
Expand Down