diff --git a/docs/install.md b/docs/install.md index f29a875bf..13da769ea 100644 --- a/docs/install.md +++ b/docs/install.md @@ -66,8 +66,8 @@ requirejs(["luxon"], function(luxon) { ## ES6 -- [Download full](https://moment.github.io/luxon/es6/luxon.js) -- [Download minified](https://moment.github.io/luxon/es6/luxon.min.js) +- [Download full](https://moment.github.io/luxon/es6/luxon.mjs) +- [Download minified](https://moment.github.io/luxon/es6/luxon.min.mjs) ```js import { DateTime } from "luxon"; diff --git a/package.json b/package.json index f80269b17..ed1f34b9c 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "uglify-js": "^3.13.10" }, "main": "build/node/luxon.js", - "module": "src/luxon.js", + "module": "build/es6/luxon.mjs", "browser": "build/cjs-browser/luxon.js", "jsdelivr": "build/global/luxon.min.js", "unpkg": "build/global/luxon.min.js", diff --git a/site/plugins/dark-theme-toggle.css b/site/plugins/dark-theme-toggle.css index 463a79174..11129d4f6 100644 --- a/site/plugins/dark-theme-toggle.css +++ b/site/plugins/dark-theme-toggle.css @@ -1,4 +1,4 @@ -#docsify-dark-theme-toggle { +.docsify-dark-theme-toggle { position: absolute; display: inline-block; width: 52px; @@ -13,30 +13,38 @@ .sidebar > .app-name { position: relative; } -.app-name > #docsify-dark-theme-toggle { +.app-name > .docsify-dark-theme-toggle { margin-right: 1rem; margin-top: 0; right: 0; left: unset; + transition: opacity 250ms ease, visibility 250ms steps(1, jump-both); + opacity: 0; + visibility: hidden; +} +.sticky .app-name > .docsify-dark-theme-toggle { + opacity: 1; + visibility: visible; } -#docsify-dark-theme-toggle::before, #docsify-dark-theme-toggle::after { + +.docsify-dark-theme-toggle::before, .docsify-dark-theme-toggle::after { position: absolute; top: 0.1em; font-size: 16px; transition: opacity 0.3s; } -#docsify-dark-theme-toggle::before { +.docsify-dark-theme-toggle::before { content: "🌙"; left: 0.1em; opacity: 0; z-index: 1; } -#docsify-dark-theme-toggle::after { +.docsify-dark-theme-toggle::after { content: "🌞"; right: 0.1em; opacity: 1; } -#docsify-dark-theme-toggle > span { +.docsify-dark-theme-toggle > span { position: absolute; top: 0; left: 0; @@ -46,7 +54,7 @@ border-radius: 28px; transition: background-color 0.3s; } -#docsify-dark-theme-toggle > span::before { +.docsify-dark-theme-toggle > span::before { content: ""; position: absolute; height: 22px; @@ -58,15 +66,15 @@ border-radius: 50%; transition: transform 0.3s; } -.dark #docsify-dark-theme-toggle::after { +.dark .docsify-dark-theme-toggle::after { opacity: 0; } -.dark #docsify-dark-theme-toggle::before { +.dark .docsify-dark-theme-toggle::before { opacity: 1; } -.dark #docsify-dark-theme-toggle > span { +.dark .docsify-dark-theme-toggle > span { background-color: var(--theme-color, #ea6f5a); } -.dark #docsify-dark-theme-toggle > span::before { +.dark .docsify-dark-theme-toggle > span::before { transform: translateX(24px); -} \ No newline at end of file +} diff --git a/site/plugins/dark-theme-toggle.js b/site/plugins/dark-theme-toggle.js index 13c2e68e9..e6f74cfff 100644 --- a/site/plugins/dark-theme-toggle.js +++ b/site/plugins/dark-theme-toggle.js @@ -3,7 +3,6 @@ const TOGGLE_ID = "docsify-dark-theme-toggle", dom = Docsify.dom, darkThemeStyleSheet = dom.find('link[href$="dark.css"]'), - toggleEl = dom.create("div", ""), applyTheme = (swap = false) => { const isDark = Boolean(swap ^ (localStorage[TOGGLE_ID] == "true")); localStorage[TOGGLE_ID] = isDark; @@ -11,10 +10,18 @@ dom.toggleClass(dom.body, isDark ? "add" : "remove", "dark"); }; localStorage[TOGGLE_ID] ??= matchMedia("(prefers-color-scheme: dark)").matches; - toggleEl.id = TOGGLE_ID; - dom.on(toggleEl, "click", () => applyTheme(true)); hook.init(applyTheme); - hook.doneEach(() => dom.before(dom.find(".cover.show, .sidebar > .app-name"), toggleEl)); + hook.doneEach(() => { + dom.findAll(".cover.show, .sidebar > .app-name").forEach((target) => { + let toggleEl = dom.find(target, `.${TOGGLE_ID}`); + if (null == toggleEl) { + toggleEl = dom.create("div", ""); + toggleEl.className = TOGGLE_ID; + dom.on(toggleEl, "click", () => applyTheme(true)); + } + dom.before(target, toggleEl); + }); + }); }; $docsify ??= {}; $docsify.plugins = [...($docsify.plugins ?? []), darkThemeTogglePlugin]; diff --git a/src/datetime.js b/src/datetime.js index 3ac0d5f8c..a528c76e7 100644 --- a/src/datetime.js +++ b/src/datetime.js @@ -60,6 +60,7 @@ function unsupportedZone(zone) { // we cache week data on the DT object and this intermediates the cache /** + * @ignore * @param {DateTime} dt */ function possiblyCachedWeekData(dt) { @@ -70,6 +71,7 @@ function possiblyCachedWeekData(dt) { } /** + * @ignore * @param {DateTime} dt */ function possiblyCachedLocalWeekData(dt) { @@ -93,6 +95,7 @@ function clone(inst, alts) { o: inst.o, loc: inst.loc, invalid: inst.invalid, + wasHole: inst.wasHole, }; return new DateTime({ ...current, ...alts, old: current }); } @@ -108,7 +111,7 @@ function fixOffset(localTS, o, tz) { // If so, offset didn't change and we're done if (o === o2) { - return [utcGuess, o]; + return [utcGuess, o, false]; } // If not, change the ts by the difference in the offset @@ -117,11 +120,11 @@ function fixOffset(localTS, o, tz) { // If that gives us the local time we want, we're done const o3 = tz.offset(utcGuess); if (o2 === o3) { - return [utcGuess, o2]; + return [utcGuess, o2, false]; } // If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time - return [localTS - Math.min(o2, o3) * 60 * 1000, Math.max(o2, o3)]; + return [localTS - Math.min(o2, o3) * 60 * 1000, Math.max(o2, o3), true]; } // convert an epoch timestamp into a calendar object with the given offset @@ -173,7 +176,7 @@ function adjustTime(inst, dur) { }).as("milliseconds"), localTS = objToLocalTS(c); - let [ts, o] = fixOffset(localTS, oPre, inst.zone); + let [ts, o, wasHole] = fixOffset(localTS, oPre, inst.zone); if (millisToAdd !== 0) { ts += millisToAdd; @@ -181,7 +184,7 @@ function adjustTime(inst, dur) { o = inst.zone.offset(ts); } - return { ts, o }; + return { ts, o, wasHole }; } // helper useful in turning the results of parsing into real dates @@ -410,6 +413,7 @@ function normalizeUnitWithLocalWeeks(unit) { /** * @param {Zone} zone * @return {number} + * @ignore */ function guessOffsetForZone(zone) { if (zoneOffsetTs === undefined) { @@ -441,7 +445,7 @@ function quickDT(obj, opts) { const loc = Locale.fromObject(opts); - let ts, o; + let ts, o, wasHole; // assume we have the higher-order units if (!isUndefined(obj.year)) { @@ -457,12 +461,12 @@ function quickDT(obj, opts) { } const offsetProvis = guessOffsetForZone(zone); - [ts, o] = objToTS(obj, offsetProvis, zone); + [ts, o, wasHole] = objToTS(obj, offsetProvis, zone); } else { ts = Settings.now(); } - return new DateTime({ ts, zone, loc, o }); + return new DateTime({ ts, zone, loc, o, wasHole }); } function diffRelative(start, end, opts) { @@ -510,6 +514,7 @@ function lastOpts(argList) { /** * Timestamp to use for cached zone offset guesses (exposed for test) + * @ignore */ let zoneOffsetTs; /** @@ -517,6 +522,8 @@ let zoneOffsetTs; * * This optimizes quickDT via guessOffsetForZone to avoid repeated calls of * zone.offset(). + * + * @ignore */ const zoneOffsetGuessCache = new Map(); @@ -535,7 +542,7 @@ const zoneOffsetGuessCache = new Map(); * {@link DateTime#day}, {@link DateTime#hour}, {@link DateTime#minute}, {@link DateTime#second}, {@link DateTime#millisecond} accessors. * * **Week calendar**: For ISO week calendar attributes, see the {@link DateTime#weekYear}, {@link DateTime#weekNumber}, and {@link DateTime#weekday} accessors. * * **Configuration** See the {@link DateTime#locale} and {@link DateTime#numberingSystem} accessors. - * * **Transformation**: To transform the DateTime into other DateTimes, use {@link DateTime#set}, {@link DateTime#reconfigure}, {@link DateTime#setZone}, {@link DateTime#setLocale}, {@link DateTime.plus}, {@link DateTime#minus}, {@link DateTime#endOf}, {@link DateTime#startOf}, {@link DateTime#toUTC}, and {@link DateTime#toLocal}. + * * **Transformation**: To transform the DateTime into other DateTimes, use {@link DateTime#set}, {@link DateTime#reconfigure}, {@link DateTime#setZone}, {@link DateTime#setLocale}, {@link DateTime#plus}, {@link DateTime#minus}, {@link DateTime#endOf}, {@link DateTime#startOf}, {@link DateTime#toUTC}, and {@link DateTime#toLocal}. * * **Output**: To convert the DateTime to other representations, use the {@link DateTime#toRelative}, {@link DateTime#toRelativeCalendar}, {@link DateTime#toJSON}, {@link DateTime#toISO}, {@link DateTime#toHTTP}, {@link DateTime#toObject}, {@link DateTime#toRFC2822}, {@link DateTime#toString}, {@link DateTime#toLocaleString}, {@link DateTime#toFormat}, {@link DateTime#toMillis} and {@link DateTime#toJSDate}. * * There's plenty others documented below. In addition, for more information on subtler topics like internationalization, time zones, alternative calendars, validity, and so on, see the external documentation. @@ -556,13 +563,14 @@ export default class DateTime { */ this.ts = isUndefined(config.ts) ? Settings.now() : config.ts; - let c = null, - o = null; + let c = null; + let o = null; if (!invalid) { const unchanged = config.old && config.old.ts === this.ts && config.old.zone.equals(zone); if (unchanged) { - [c, o] = [config.old.c, config.old.o]; + c = config.old.c; + o = config.old.o; } else { // If an offset has been passed and we have not been called from // clone(), we can trust it and avoid the offset calculation. @@ -598,9 +606,12 @@ export default class DateTime { * @access private */ this.c = c; + /** * @access private */ + this._wasHole = config.wasHole || false; + this.o = o; /** * @access private @@ -873,16 +884,18 @@ export default class DateTime { // compute the actual time const gregorian = useWeekData - ? weekToGregorian(normalized, minDaysInFirstWeek, startOfWeek) - : containsOrdinal - ? ordinalToGregorian(normalized) - : normalized, - [tsFinal, offsetFinal] = objToTS(gregorian, offsetProvis, zoneToUse), + ? weekToGregorian(normalized, minDaysInFirstWeek, startOfWeek) + : containsOrdinal + ? ordinalToGregorian(normalized) + : normalized; + + const [tsFinal, offsetFinal, wasHole] = objToTS(gregorian, offsetProvis, zoneToUse), inst = new DateTime({ ts: tsFinal, zone: zoneToUse, o: offsetFinal, loc, + wasHole, }); // gregorian data + weekday serves only to validate @@ -1168,6 +1181,19 @@ export default class DateTime { return this.isValid ? this.zone.name : null; } + /** + * Whether this DateTime was created from a "hole time" that can exist during DST due to the + * clocks moving forward. + * + * @example DateTime.local(2017, 3, 12, 2).wasHole; //=> true + * @example DateTime.local(2017, 3, 12, 4).wasHole; //=> false + * + * @return {boolean} + */ + get wasHole() { + return this._wasHole; + } + /** * Get the year * @example DateTime.local(2017, 5, 25).year //=> 2017 @@ -1571,23 +1597,32 @@ export default class DateTime { return DateTime.invalid(unsupportedZone(zone)); } else { let newTS = this.ts; + let wasHole = false; if (keepLocalTime || keepCalendarTime) { const offsetGuess = zone.offset(this.ts); const asObj = this.toObject(); - [newTS] = objToTS(asObj, offsetGuess, zone); + [newTS, , wasHole] = objToTS(asObj, offsetGuess, zone); } - return clone(this, { ts: newTS, zone }); + return clone(this, { ts: newTS, zone, wasHole }); } } /** - * "Set" the locale, numberingSystem, or outputCalendar. Returns a newly-constructed DateTime. + * "Set" the locale, numberingSystem, outputCalendar, or weekSettings. Returns a newly-constructed DateTime. * @param {Object} properties - the properties to set + * @param {string} [properties.locale] - the locale to set + * @param {string} [properties.numberingSystem] - the numbering system to set + * @param {string} [properties.outputCalendar] - the output calendar to set + * @param {Object} [properties.weekSettings] - the week settings to set + * @param {number} [properties.weekSettings.firstDay] - the first day of the week (1-7, Monday-Sunday) + * @param {number} [properties.weekSettings.minimalDays] - the minimum number of days in the first week + * @param {number[]} [properties.weekSettings.weekend] - the weekend days * @example DateTime.local(2017, 5, 25).reconfigure({ locale: 'en-GB' }) + * @example DateTime.local(2017, 5, 25).reconfigure({ weekSettings: { firstDay: 1 } }) * @return {DateTime} */ - reconfigure({ locale, numberingSystem, outputCalendar } = {}) { - const loc = this.loc.clone({ locale, numberingSystem, outputCalendar }); + reconfigure({ locale, numberingSystem, outputCalendar, weekSettings } = {}) { + const loc = this.loc.clone({ locale, numberingSystem, outputCalendar, weekSettings }); return clone(this, { loc }); } @@ -1659,8 +1694,8 @@ export default class DateTime { } } - const [ts, o] = objToTS(mixed, this.o, this.zone); - return clone(this, { ts, o }); + const [ts, o, wasHole] = objToTS(mixed, this.o, this.zone); + return clone(this, { ts, o, wasHole }); } /** diff --git a/src/impl/conversions.js b/src/impl/conversions.js index 4c7d1170e..417611b9a 100644 --- a/src/impl/conversions.js +++ b/src/impl/conversions.js @@ -112,6 +112,7 @@ export function ordinalToGregorian(ordinalData) { * If so, validates that they are not mixed with ISO week units and then copies them to the normal week unit properties. * Modifies obj in-place! * @param obj the object values + * @ignore */ export function usesLocalWeekValues(obj, loc) { const hasLocaleWeekData = diff --git a/src/impl/regexParser.js b/src/impl/regexParser.js index c39dd2082..807a42ea2 100644 --- a/src/impl/regexParser.js +++ b/src/impl/regexParser.js @@ -75,7 +75,7 @@ const isoTimeExtensionRegex = RegExp(`(?:[Tt]${isoTimeRegex.source})?`); const isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/; const isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/; const isoOrdinalRegex = /(\d{4})-?(\d{3})/; -const extractISOWeekData = simpleParse("weekYear", "weekNumber", "weekDay"); +const extractISOWeekData = simpleParse("weekYear", "weekNumber", "weekday"); const extractISOOrdinalData = simpleParse("year", "ordinal"); const sqlYmdRegex = /(\d{4})-(\d\d)-(\d\d)/; // dumbed-down version of the ISO one const sqlTimeRegex = RegExp( @@ -305,7 +305,7 @@ const extractPartialIsoIntervalEndDate = simpleParse( "month", "day", "weekNumber", - "weekDay", + "weekday", "ordinal" ); diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js index 0190efc5f..7a2196c7e 100644 --- a/src/impl/tokenParser.js +++ b/src/impl/tokenParser.js @@ -52,6 +52,7 @@ function escapeToken(value) { /** * @param token * @param {Locale} loc + * @ignore */ function unitForToken(token, loc) { const one = digitRegex(loc), diff --git a/src/impl/util.js b/src/impl/util.js index 916425020..32a5d9d72 100644 --- a/src/impl/util.js +++ b/src/impl/util.js @@ -307,6 +307,7 @@ export function normalizeObject(obj, normalizer) { * @param {string} format - What style of offset to return. * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively * @return {string} + * @ignore */ export function formatOffset(offset, format) { const hours = Math.trunc(Math.abs(offset / 60)), diff --git a/src/interval.js b/src/interval.js index 1cdb6b118..83ab504ff 100644 --- a/src/interval.js +++ b/src/interval.js @@ -589,6 +589,9 @@ export default class Interval { * 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. + * + * Requires support for Intl.DateTimeFormat.prototype.formatRange(). + * * @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. diff --git a/test/datetime/dst.test.js b/test/datetime/dst.test.js index 8ab5d77ab..16d5a7867 100644 --- a/test/datetime/dst.test.js +++ b/test/datetime/dst.test.js @@ -15,6 +15,7 @@ for (const [name, local] of Object.entries(dateTimeConstructors)) { const d = local(2017, 3, 12, 2); expect(d.hour).toBe(3); expect(d.offset).toBe(-4 * 60); + expect(d.wasHole).toBe(true); }); if (name == "fromObject") { @@ -190,3 +191,59 @@ describe("DateTime.local() with offset caching", () => { } } }); + +describe("DateTime maintains the wasHole setting properly", () => { + test("is false by default", () => { + expect(DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 4 }).wasHole).toBe(false); + }); + + test("is set on hole times", () => { + expect(DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 2 }).wasHole).toBe(true); + }); + + test("is set on hole times with DateTime.local", () => { + expect(DateTime.local(2017, 3, 12, 2).wasHole).toBe(true); + }); + + test("is set on hole times with DateTime.fromISO", () => { + expect(DateTime.fromISO("2017-03-12T02:00:00").wasHole).toBe(true); + }); + + test("is false when setting to non-hole", () => { + const fromHole = DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 2 }); + expect(fromHole.set({ hour: 4 }).wasHole).toBe(false); + }); + + test("is true when setting hole-time on time from hole", () => { + const dt = DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 2 }); + expect(dt.wasHole).toBe(true); + expect(dt.set({ hour: 2 }).wasHole).toBe(true); + }); + + test("is true when setting hole-time on time from non-hole", () => { + const dt = DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 4 }); + expect(dt.wasHole).toBe(false); + expect(dt.set({ hour: 2 }).wasHole).toBe(true); + }); + + test("is dropped on math", () => { + expect( + DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 2 }).plus({ hours: 2 }).wasHole + ).toBe(false); + }); + + test("is kept on reconfigure", () => { + expect( + DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 2 }).reconfigure({ + locale: "es-ES", + }).wasHole + ).toBe(true); + }); + + test("is dropped on rezoning", () => { + expect( + DateTime.fromObject({ year: 2017, month: 3, day: 12, hour: 2 }).setZone("Europe/London") + .wasHole + ).toBe(false); + }); +}); diff --git a/test/datetime/reconfigure.test.js b/test/datetime/reconfigure.test.js index 3801419c7..50fa77bf7 100644 --- a/test/datetime/reconfigure.test.js +++ b/test/datetime/reconfigure.test.js @@ -41,3 +41,21 @@ test("DateTime#reconfigure() with no arguments no opts", () => { expect(recon.numberingSystem).toBe("beng"); expect(recon.outputCalendar).toBe("coptic"); }); + +test("DateTime#reconfigure() sets the weekSettings", () => { + const original = DateTime.local(2022, 1, 4, { locale: "en-US" }); + const recon = original.reconfigure({ + weekSettings: { firstDay: 6, minimalDays: 1, weekend: [1, 2] }, + }); + expect(recon.startOf("week", { useLocaleWeeks: true }).weekday).toBe(6); +}); + +test("DateTime#reconfigure() preserves weekSettings when setting other options", () => { + const original = DateTime.local(2022, 1, 4, { + locale: "en-US", + weekSettings: { firstDay: 3, minimalDays: 1, weekend: [] }, + }); + const recon = original.reconfigure({ locale: "de-DE" }); + expect(recon.locale).toBe("de-DE"); + expect(recon.startOf("week", { useLocaleWeeks: true }).weekday).toBe(3); +});