diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f44a6b202..273e2ed69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,13 +2,12 @@ name: Test on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build-and-test: - runs-on: ubuntu-latest env: @@ -18,18 +17,18 @@ jobs: strategy: matrix: - node-version: [19.7.0] + node-version: [20.5.1] steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm ci - - run: npm run build - - run: npm run format-check - - run: npm run test - - run: npm run site - - run: bash <(curl -s https://codecov.io/bash) + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: npm ci + - run: npm run build + - run: npm run format-check + - run: npm run test + - run: npm run site + - run: bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d362bddf..891ac3834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +# 3.4.2 (2023-08-26) + +- Fixes regression from 3.4.1 (#1493) + # 3.4.1 (2023-08-23) - Fixes for regressions from 3.4.0 (#1482 and #1488) diff --git a/package-lock.json b/package-lock.json index 37bffaf88..479e6ce03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "luxon", - "version": "3.4.1", + "version": "3.4.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "luxon", - "version": "3.4.1", + "version": "3.4.2", "license": "MIT", "devDependencies": { "@babel/core": "^7.18.6", @@ -9022,9 +9022,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -10077,9 +10077,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -11094,9 +11094,9 @@ } }, "node_modules/node-environment-flags/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -11162,9 +11162,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -12342,9 +12342,9 @@ "dev": true }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -20369,9 +20369,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -21065,9 +21065,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -21736,9 +21736,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -21786,9 +21786,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -22636,9 +22636,9 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "serialize-javascript": { diff --git a/package.json b/package.json index 423fd19c5..7ddaf7342 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "luxon", - "version": "3.4.1", + "version": "3.4.2", "description": "Immutable date wrapper", "author": "Isaac Cambron", "keywords": [ diff --git a/scripts/test b/scripts/test index 7eea7fedd..def0897a9 100755 --- a/scripts/test +++ b/scripts/test @@ -1,2 +1,2 @@ #!/usr/bin/env bash -TZ="America/New_York" NODE_ICU_DATA="$(pwd)/node_modules/full-icu" LANG=en_US.utf8 npm run test +TZ="America/New_York" LANG=en_US.utf8 npm run test \ No newline at end of file diff --git a/src/duration.js b/src/duration.js index 76a57b276..b725b59e7 100644 --- a/src/duration.js +++ b/src/duration.js @@ -10,7 +10,6 @@ import { isUndefined, normalizeObject, roundTo, - signedFloor, } from "./impl/util.js"; import Settings from "./settings.js"; import DateTime from "./datetime.js"; @@ -127,27 +126,46 @@ function clone(dur, alts, clear = false) { return new Duration(conf); } -// this is needed since in some test cases it would return 0.9999999999999999 instead of 1 -function removePrecisionIssue(a) { - return Math.trunc(a * 1e3) / 1e3; -} - -// NB: mutates parameters -function convert(matrix, fromMap, fromUnit, toMap, toUnit) { - const conv = matrix[toUnit][fromUnit]; - const raw = fromMap[fromUnit] / conv; - const added = signedFloor(raw); - - toMap[toUnit] = removePrecisionIssue(toMap[toUnit] + added); - fromMap[fromUnit] = removePrecisionIssue(fromMap[fromUnit] - added * conv); +function durationToMillis(matrix, vals) { + let sum = vals.milliseconds ?? 0; + for (const unit of reverseUnits.slice(1)) { + if (vals[unit]) { + sum += vals[unit] * matrix[unit]["milliseconds"]; + } + } + return sum; } // NB: mutates parameters function normalizeValues(matrix, vals) { + // the logic below assumes the overall value of the duration is positive + // if this is not the case, factor is used to make it so + const factor = durationToMillis(matrix, vals) < 0 ? -1 : 1; + reverseUnits.reduce((previous, current) => { if (!isUndefined(vals[current])) { if (previous) { - convert(matrix, vals, previous, vals, current); + const previousVal = vals[previous] * factor; + const conv = matrix[current][previous]; + + // if (previousVal < 0): + // lower order unit is negative (e.g. { years: 2, days: -2 }) + // normalize this by reducing the higher order unit by the appropriate amount + // and increasing the lower order unit + // this can never make the higher order unit negative, because this function only operates + // on positive durations, so the amount of time represented by the lower order unit cannot + // be larger than the higher order unit + // else: + // lower order unit is positive (e.g. { years: 2, days: 450 } or { years: -2, days: 450 }) + // in this case we attempt to convert as much as possible from the lower order unit into + // the higher order one + // + // Math.floor takes care of both of these cases, rounding away from 0 + // if previousVal < 0 it makes the absolute value larger + // if previousVal >= it makes the absolute value smaller + const rollUp = Math.floor(previousVal / conv); + vals[current] += rollUp * factor; + vals[previous] -= rollUp * conv * factor; } return current; } else { @@ -581,13 +599,7 @@ export default class Duration { toMillis() { if (!this.isValid) return NaN; - let sum = this.values.milliseconds ?? 0; - for (let unit of reverseUnits.slice(1)) { - if (this.values[unit]) { - sum += this.values[unit] * this.matrix[unit]["milliseconds"]; - } - } - return sum; + return durationToMillis(this.matrix, this.values); } /** @@ -697,18 +709,22 @@ export default class Duration { /** * Reduce this Duration to its canonical representation in its current units. + * Assuming the overall value of the Duration is positive, this means: + * - excessive values for lower-order units are converted to higher order units (if possible, see first and second example) + * - negative lower-order units are converted to higher order units (there must be such a higher order unit, otherwise + * the overall value would be negative, see second example) + * + * If the overall value is negative, the result of this method is equivalent to `this.negate().normalize().negate()`. * @example Duration.fromObject({ years: 2, days: 5000 }).normalize().toObject() //=> { years: 15, days: 255 } + * @example Duration.fromObject({ days: 5000 }).normalize().toObject() //=> { days: 5000 } * @example Duration.fromObject({ hours: 12, minutes: -45 }).normalize().toObject() //=> { hours: 11, minutes: 15 } * @return {Duration} */ normalize() { if (!this.isValid) return this; const vals = this.toObject(); - if (this.valueOf() >= 0) { - normalizeValues(this.matrix, vals); - return clone(this, { values: vals }, true); - } - return this.negate().normalize().negate(); + normalizeValues(this.matrix, vals); + return clone(this, { values: vals }, true); } /** @@ -758,16 +774,12 @@ export default class Duration { own += vals[k]; } + // only keep the integer part for now in the hopes of putting any decimal part + // into a smaller unit later const i = Math.trunc(own); built[k] = i; accumulated[k] = (own * 1000 - i * 1000) / 1000; - // plus anything further down the chain that should be rolled up in to this - for (const down in vals) { - if (orderedUnits.indexOf(down) > orderedUnits.indexOf(k)) { - convert(this.matrix, vals, down, built, k); - } - } // otherwise, keep it in the wings to boil it later } else if (isNumber(vals[k])) { accumulated[k] = vals[k]; @@ -783,7 +795,8 @@ export default class Duration { } } - return clone(this, { values: built }, true).normalize(); + normalizeValues(this.matrix, built); + return clone(this, { values: built }, true); } /** diff --git a/src/impl/util.js b/src/impl/util.js index b934b1053..51f756ea4 100644 --- a/src/impl/util.js +++ b/src/impl/util.js @@ -124,10 +124,6 @@ export function parseMillis(fraction) { } } -export function signedFloor(number) { - return number > 0 ? Math.floor(number) : Math.ceil(number); -} - export function roundTo(number, digits, towardZero = false) { const factor = 10 ** digits, rounder = towardZero ? Math.trunc : Math.round; diff --git a/src/luxon.js b/src/luxon.js index 496bd2fbb..f21d8e234 100644 --- a/src/luxon.js +++ b/src/luxon.js @@ -9,7 +9,7 @@ import InvalidZone from "./zones/invalidZone.js"; import SystemZone from "./zones/systemZone.js"; import Settings from "./settings.js"; -const VERSION = "3.4.1"; +const VERSION = "3.4.2"; export { VERSION, diff --git a/src/package.json b/src/package.json index 2412649c4..eaa6d1dfa 100644 --- a/src/package.json +++ b/src/package.json @@ -1,4 +1,4 @@ { "type": "module", - "version": "3.4.1" + "version": "3.4.2" } diff --git a/test/datetime/format.test.js b/test/datetime/format.test.js index 66ef19e5b..d598a38f7 100644 --- a/test/datetime/format.test.js +++ b/test/datetime/format.test.js @@ -426,17 +426,17 @@ test("DateTime#toLocaleString() returns something different for invalid DateTime test("DateTime#toLocaleString() shows things in the right IANA zone", () => { expect(dt.setZone("America/New_York").toLocaleString(DateTime.DATETIME_SHORT)).toBe( - "5/25/1982, 5:23 AM" + "5/25/1982, 5:23 AM" ); }); test("DateTime#toLocaleString() shows things in the right fixed-offset zone", () => { - expect(dt.setZone("UTC-8").toLocaleString(DateTime.DATETIME_SHORT)).toBe("5/25/1982, 1:23 AM"); + expect(dt.setZone("UTC-8").toLocaleString(DateTime.DATETIME_SHORT)).toBe("5/25/1982, 1:23 AM"); }); test("DateTime#toLocaleString() shows things in the right fixed-offset zone when showing the zone", () => { expect(dt.setZone("UTC-8").toLocaleString(DateTime.DATETIME_FULL)).toBe( - "May 25, 1982 at 1:23 AM GMT-8" + "May 25, 1982 at 1:23 AM GMT-8" ); }); @@ -503,7 +503,7 @@ test("DateTime#toLocaleString() accepts a zone even when the zone is set", () => timeZoneName: "short", timeZone: "America/Los_Angeles", }) - ).toBe("2:23 AM PDT"); + ).toBe("2:23 AM PDT"); }); //------ diff --git a/test/datetime/toFormat.test.js b/test/datetime/toFormat.test.js index f73b5aa0f..7f5b20b7e 100644 --- a/test/datetime/toFormat.test.js +++ b/test/datetime/toFormat.test.js @@ -3,20 +3,20 @@ import { DateTime } from "../../src/luxon"; const dt = DateTime.fromObject( - { - year: 1982, - month: 5, - day: 25, - hour: 9, - minute: 23, - second: 54, - millisecond: 123, - }, - { - zone: "utc", - } - ), - ny = dt.setZone("America/New_York", { keepLocalTime: true }); + { + year: 1982, + month: 5, + day: 25, + hour: 9, + minute: 23, + second: 54, + millisecond: 123, + }, + { + zone: "utc", + } +); +const ny = dt.setZone("America/New_York", { keepLocalTime: true }); //------ // #toFormat() @@ -414,16 +414,16 @@ test("DateTime#toFormat('TTT') returns a medium time representation", () => { }); test("DateTime#toFormat('f') returns a short date/time representation without seconds", () => { - expect(dt.toFormat("f")).toBe("5/25/1982, 9:23 AM"); - expect(dt.set({ hour: 13 }).toFormat("f")).toBe("5/25/1982, 1:23 PM"); + expect(dt.toFormat("f").replace(/\s+/g, " ")).toBe("5/25/1982, 9:23 AM"); + expect(dt.set({ hour: 13 }).toFormat("f").replace(/\s+/g, " ")).toBe("5/25/1982, 1:23 PM"); expect(dt.reconfigure({ locale: "fr" }).toFormat("f")).toBe("25/05/1982 09:23"); expect(dt.set({ hour: 13 }).reconfigure({ locale: "fr" }).toFormat("f")).toBe("25/05/1982 13:23"); }); test("DateTime#toFormat('ff') returns a medium date/time representation without seconds", () => { - expect(dt.toFormat("ff")).toBe("May 25, 1982, 9:23 AM"); - expect(dt.set({ hour: 13 }).toFormat("ff")).toBe("May 25, 1982, 1:23 PM"); - expect(dt.set({ month: 8 }).toFormat("ff")).toBe("Aug 25, 1982, 9:23 AM"); + expect(dt.toFormat("ff").replace(/\s+/g, " ")).toBe("May 25, 1982, 9:23 AM"); + expect(dt.set({ hour: 13 }).toFormat("ff").replace(/\s+/g, " ")).toBe("May 25, 1982, 1:23 PM"); + expect(dt.set({ month: 8 }).toFormat("ff").replace(/\s+/g, " ")).toBe("Aug 25, 1982, 9:23 AM"); expect(dt.reconfigure({ locale: "fr" }).toFormat("ff")).toBe("25 mai 1982, 09:23"); expect(dt.set({ month: 2 }).reconfigure({ locale: "fr" }).toFormat("ff")).toBe( "25 févr. 1982, 09:23" @@ -434,9 +434,9 @@ test("DateTime#toFormat('ff') returns a medium date/time representation without }); test("DateTime#toFormat('fff') returns a medium date/time representation without seconds", () => { - expect(ny.toFormat("fff")).toBe("May 25, 1982 at 9:23 AM EDT"); - expect(ny.set({ hour: 13 }).toFormat("fff")).toBe("May 25, 1982 at 1:23 PM EDT"); - expect(ny.set({ month: 8 }).toFormat("fff")).toBe("August 25, 1982 at 9:23 AM EDT"); + expect(ny.toFormat("fff")).toBe("May 25, 1982 at 9:23 AM EDT"); + expect(ny.set({ hour: 13 }).toFormat("fff")).toBe("May 25, 1982 at 1:23 PM EDT"); + expect(ny.set({ month: 8 }).toFormat("fff")).toBe("August 25, 1982 at 9:23 AM EDT"); expect(ny.reconfigure({ locale: "fr" }).toFormat("fff")).toBe("25 mai 1982 à 09:23 UTC−4"); expect(ny.set({ month: 2 }).reconfigure({ locale: "fr" }).toFormat("fff")).toBe( "25 février 1982 à 09:23 UTC−5" @@ -447,12 +447,12 @@ test("DateTime#toFormat('fff') returns a medium date/time representation without }); test("DateTime#toFormat('ffff') returns a long date/time representation without seconds", () => { - expect(ny.toFormat("ffff")).toBe("Tuesday, May 25, 1982 at 9:23 AM Eastern Daylight Time"); + expect(ny.toFormat("ffff")).toBe("Tuesday, May 25, 1982 at 9:23 AM Eastern Daylight Time"); expect(ny.set({ hour: 13 }).toFormat("ffff")).toBe( - "Tuesday, May 25, 1982 at 1:23 PM Eastern Daylight Time" + "Tuesday, May 25, 1982 at 1:23 PM Eastern Daylight Time" ); expect(ny.set({ month: 2 }).toFormat("ffff")).toBe( - "Thursday, February 25, 1982 at 9:23 AM Eastern Standard Time" + "Thursday, February 25, 1982 at 9:23 AM Eastern Standard Time" ); expect(ny.reconfigure({ locale: "fr" }).toFormat("ffff")).toBe( "mardi 25 mai 1982 à 09:23 heure d’été de l’Est nord-américain" @@ -466,8 +466,8 @@ test("DateTime#toFormat('ffff') returns a long date/time representation without }); test("DateTime#toFormat('F') returns a short date/time representation with seconds", () => { - expect(dt.toFormat("F")).toBe("5/25/1982, 9:23:54 AM"); - expect(dt.set({ hour: 13 }).toFormat("F")).toBe("5/25/1982, 1:23:54 PM"); + expect(dt.toFormat("F").replace(/\s+/g, " ")).toBe("5/25/1982, 9:23:54 AM"); + expect(dt.set({ hour: 13 }).toFormat("F").replace(/\s+/g, " ")).toBe("5/25/1982, 1:23:54 PM"); expect(dt.reconfigure({ locale: "fr" }).toFormat("F")).toBe("25/05/1982 09:23:54"); expect(dt.set({ hour: 13 }).reconfigure({ locale: "fr" }).toFormat("F")).toBe( "25/05/1982 13:23:54" @@ -475,9 +475,9 @@ test("DateTime#toFormat('F') returns a short date/time representation with secon }); test("DateTime#toFormat('FF') returns a medium date/time representation with seconds", () => { - expect(dt.toFormat("FF")).toBe("May 25, 1982, 9:23:54 AM"); - expect(dt.set({ hour: 13 }).toFormat("FF")).toBe("May 25, 1982, 1:23:54 PM"); - expect(dt.set({ month: 8 }).toFormat("FF")).toBe("Aug 25, 1982, 9:23:54 AM"); + expect(dt.toFormat("FF").replace(/\s+/g, " ")).toBe("May 25, 1982, 9:23:54 AM"); + expect(dt.set({ hour: 13 }).toFormat("FF").replace(/\s+/g, " ")).toBe("May 25, 1982, 1:23:54 PM"); + expect(dt.set({ month: 8 }).toFormat("FF").replace(/\s+/g, " ")).toBe("Aug 25, 1982, 9:23:54 AM"); expect(dt.reconfigure({ locale: "fr" }).toFormat("FF")).toBe("25 mai 1982, 09:23:54"); expect(dt.set({ month: 2 }).reconfigure({ locale: "fr" }).toFormat("FF")).toBe( "25 févr. 1982, 09:23:54" @@ -488,9 +488,9 @@ test("DateTime#toFormat('FF') returns a medium date/time representation with sec }); test("DateTime#toFormat('FFF') returns a medium date/time representation without seconds", () => { - expect(ny.toFormat("FFF")).toBe("May 25, 1982 at 9:23:54 AM EDT"); - expect(ny.set({ hour: 13 }).toFormat("FFF")).toBe("May 25, 1982 at 1:23:54 PM EDT"); - expect(ny.set({ month: 8 }).toFormat("FFF")).toBe("August 25, 1982 at 9:23:54 AM EDT"); + expect(ny.toFormat("FFF")).toBe("May 25, 1982 at 9:23:54 AM EDT"); + expect(ny.set({ hour: 13 }).toFormat("FFF")).toBe("May 25, 1982 at 1:23:54 PM EDT"); + expect(ny.set({ month: 8 }).toFormat("FFF")).toBe("August 25, 1982 at 9:23:54 AM EDT"); expect(ny.reconfigure({ locale: "fr" }).toFormat("FFF")).toBe("25 mai 1982 à 9:23:54 UTC−4"); expect(ny.set({ month: 2 }).reconfigure({ locale: "fr" }).toFormat("FFF")).toBe( "25 février 1982 à 9:23:54 UTC−5" @@ -501,12 +501,12 @@ test("DateTime#toFormat('FFF') returns a medium date/time representation without }); test("DateTime#toFormat('FFFF') returns a long date/time representation without seconds", () => { - expect(ny.toFormat("FFFF")).toBe("Tuesday, May 25, 1982 at 9:23:54 AM Eastern Daylight Time"); + expect(ny.toFormat("FFFF")).toBe("Tuesday, May 25, 1982 at 9:23:54 AM Eastern Daylight Time"); expect(ny.set({ hour: 13 }).toFormat("FFFF")).toBe( - "Tuesday, May 25, 1982 at 1:23:54 PM Eastern Daylight Time" + "Tuesday, May 25, 1982 at 1:23:54 PM Eastern Daylight Time" ); expect(ny.set({ month: 2 }).toFormat("FFFF")).toBe( - "Thursday, February 25, 1982 at 9:23:54 AM Eastern Standard Time" + "Thursday, February 25, 1982 at 9:23:54 AM Eastern Standard Time" ); expect(ny.reconfigure({ locale: "fr" }).toFormat("FFFF")).toBe( "mardi 25 mai 1982 à 9:23:54 heure d’été de l’Est nord-américain"