Repository: moment/luxon
Pull request #1427 (2023-04-30 21:35:36)

Fix: Remove "TTT" from valid parsing tokens
One-liner change. Spent a while trying to figure out why I couldn't use fromFormat with `TTT`, and ended up being not supported by the function even though it's in the parsing table of valid tokens, which may be confusing, albeit also already stating that `Luxon also doesn't support the "macro" tokens that include offset names, such as "ttt" and "FFFF"`.

linux-foundation-easycla[bot]:  <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1427/#/?version=2"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-not-signed.svg" alt="CLA Not Signed" align="left" height="28" width="328"></a><br/><br /><ul><li><a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1427/#/?version=2' target='_blank'>:x:</a> - login: @6ixfalls / name: Six . The commit (3b7394052279791fee06004453495b5109b00ec5) is not authorized under a signed CLA. <a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1427/#/?version=2' target='_blank'>Please click here to be authorized</a>. For further assistance with EasyCLA, <a href='https://jira.linuxfoundation.org/servicedesk/customer/portal/4' target='_blank'>please submit a support request ticket</a>.</li></ul>

Patch:
diff --git a/docs/math.md b/docs/math.md
index 6934c51a..4b21b0d3 100644
--- a/docs/math.md
+++ b/docs/math.md
@@ -23,7 +23,7 @@ More generally we can differentiate two modes of math:
 
 These units use calendar math:
 
- * **Years** vary because of years.
+ * **Years** vary because of leap years.
  * **Months** vary because they're just different lengths.
  * **Days** vary because DST transitions mean some days are 23 or 25 hours long.
  * **Quarters** are always three months, but months vary in length so quarters do too.
diff --git a/docs/parsing.md b/docs/parsing.md
index 2b2607a1..f3b98493 100644
--- a/docs/parsing.md
+++ b/docs/parsing.md
@@ -235,7 +235,6 @@ Because Luxon was able to parse the string without difficulty, the output is a l
 | tt               |              | localized time with seconds                                       | `1:07:04 PM`                |
 | T                |              | localized 24-hour time                                            | `13:07`                     |
 | TT               |              | localized 24-hour time with seconds                               | `13:07:04`                  |
-| TTT              |              | localized 24-hour time with seconds and abbreviated offset        | `13:07:04 EDT`              |
 | f                |              | short localized date and time                                     | `8/6/2014, 1:07 PM`         |
 | ff               |              | less short localized date and time                                | `Aug 6, 2014, 1:07 PM`      |
 | F                |              | short localized date and time with seconds                        | `8/6/2014, 1:07:04 PM`      |
diff --git a/docs/zones.md b/docs/zones.md
index 6b21262a..60638397 100644
--- a/docs/zones.md
+++ b/docs/zones.md
@@ -276,14 +276,14 @@ start.plus({ hours: 24 }).hour; //=> 11, DST pushed forward an hour
 By default, Luxon creates DateTimes in the system's zone. However, you can override this behavior globally:
 
 ```js
-Settings.defaultZoneName = "Asia/Tokyo";
+Settings.defaultZone = "Asia/Tokyo";
 DateTime.local().zoneName; //=> 'Asia/Tokyo'
 
-Settings.defaultZoneName = "utc";
+Settings.defaultZone = "utc";
 DateTime.local().zoneName; //=> 'UTC'
 
 // you can reset by setting to 'system'
 
-Settings.defaultZoneName = "system";
+Settings.defaultZone = "system";
 DateTime.local().zoneName; //=> 'America/New_York'
 ```
diff --git a/src/datetime.js b/src/datetime.js
index 90279094..40c1e6ae 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -894,7 +894,7 @@ export default class DateTime {
 
   /**
    * Create an invalid DateTime.
-   * @param {DateTime} reason - simple string of why this DateTime is invalid. Should not contain parameters or anything else data-dependent
+   * @param {string} reason - simple string of why this DateTime is invalid. Should not contain parameters or anything else data-dependent.
    * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information
    * @return {DateTime}
    */
diff --git a/src/duration.js b/src/duration.js
index 72f67fb4..49033a47 100644
--- a/src/duration.js
+++ b/src/duration.js
@@ -126,20 +126,19 @@ function clone(dur, alts, clear = false) {
   return new Duration(conf);
 }
 
-function antiTrunc(n) {
-  return n < 0 ? Math.floor(n) : Math.ceil(n);
+// 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],
     raw = fromMap[fromUnit] / conv,
-    sameSign = Math.sign(raw) === Math.sign(toMap[toUnit]),
-    // ok, so this is wild, but see the matrix in the tests
-    added =
-      !sameSign && toMap[toUnit] !== 0 && Math.abs(raw) <= 1 ? antiTrunc(raw) : Math.trunc(raw);
-  toMap[toUnit] += added;
-  fromMap[fromUnit] -= added * conv;
+    added = Math.floor(raw);
+
+  toMap[toUnit] = removePrecisionIssue(toMap[toUnit] + added);
+  fromMap[fromUnit] = removePrecisionIssue(fromMap[fromUnit] - added * conv);
 }
 
 // NB: mutates parameters
@@ -577,7 +576,13 @@ export default class Duration {
    * @return {number}
    */
   toMillis() {
-    return this.as("milliseconds");
+    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;
   }
 
   /**
@@ -694,8 +699,11 @@ export default class Duration {
   normalize() {
     if (!this.isValid) return this;
     const vals = this.toObject();
-    normalizeValues(this.matrix, vals);
-    return clone(this, { values: vals }, true);
+    if (this.valueOf() >= 0) {
+      normalizeValues(this.matrix, vals);
+      return clone(this, { values: vals }, true);
+    }
+    return this.negate().normalize().negate();
   }
 
   /**
diff --git a/src/impl/locale.js b/src/impl/locale.js
index a1dcd33b..fe7dd57d 100644
--- a/src/impl/locale.js
+++ b/src/impl/locale.js
@@ -120,7 +120,7 @@ function intlConfigString(localeStr, numberingSystem, outputCalendar) {
 function mapMonths(f) {
   const ms = [];
   for (let i = 1; i <= 12; i++) {
-    const dt = DateTime.utc(2016, i, 1);
+    const dt = DateTime.utc(2009, i, 1);
     ms.push(f(dt));
   }
   return ms;
@@ -135,8 +135,8 @@ function mapWeekdays(f) {
   return ms;
 }
 
-function listStuff(loc, length, defaultOK, englishFn, intlFn) {
-  const mode = loc.listingMode(defaultOK);
+function listStuff(loc, length, englishFn, intlFn) {
+  const mode = loc.listingMode();
 
   if (mode === "error") {
     return null;
@@ -388,8 +388,8 @@ export default class Locale {
     return this.clone({ ...alts, defaultToEN: false });
   }
 
-  months(length, format = false, defaultOK = true) {
-    return listStuff(this, length, defaultOK, English.months, () => {
+  months(length, format = false) {
+    return listStuff(this, length, English.months, () => {
       const intl = format ? { month: length, day: "numeric" } : { month: length },
         formatStr = format ? "format" : "standalone";
       if (!this.monthsCache[formatStr][length]) {
@@ -399,8 +399,8 @@ export default class Locale {
     });
   }
 
-  weekdays(length, format = false, defaultOK = true) {
-    return listStuff(this, length, defaultOK, English.weekdays, () => {
+  weekdays(length, format = false) {
+    return listStuff(this, length, English.weekdays, () => {
       const intl = format
           ? { weekday: length, year: "numeric", month: "long", day: "numeric" }
           : { weekday: length },
@@ -414,11 +414,10 @@ export default class Locale {
     });
   }
 
-  meridiems(defaultOK = true) {
+  meridiems() {
     return listStuff(
       this,
       undefined,
-      defaultOK,
       () => English.meridiems,
       () => {
         // In theory there could be aribitrary day periods. We're gonna assume there are exactly two
@@ -435,8 +434,8 @@ export default class Locale {
     );
   }
 
-  eras(length, defaultOK = true) {
-    return listStuff(this, length, defaultOK, English.eras, () => {
+  eras(length) {
+    return listStuff(this, length, English.eras, () => {
       const intl = { era: length };
 
       // This is problematic. Different calendars are going to define eras totally differently. What I need is the minimum set of dates
diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js
index 19008d29..8dd38f37 100644
--- a/src/impl/tokenParser.js
+++ b/src/impl/tokenParser.js
@@ -53,6 +53,10 @@ function escapeToken(value) {
   return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
 }
 
+/**
+ * @param token
+ * @param {Locale} loc
+ */
 function unitForToken(token, loc) {
   const one = digitRegex(loc),
     two = digitRegex(loc, "{2}"),
@@ -73,9 +77,9 @@ function unitForToken(token, loc) {
       switch (t.val) {
         // era
         case "G":
-          return oneOf(loc.eras("short", false), 0);
+          return oneOf(loc.eras("short"), 0);
         case "GG":
-          return oneOf(loc.eras("long", false), 0);
+          return oneOf(loc.eras("long"), 0);
         // years
         case "y":
           return intUnit(oneToSix);
@@ -93,17 +97,17 @@ function unitForToken(token, loc) {
         case "MM":
           return intUnit(two);
         case "MMM":
-          return oneOf(loc.months("short", true, false), 1);
+          return oneOf(loc.months("short", true), 1);
         case "MMMM":
-          return oneOf(loc.months("long", true, false), 1);
+          return oneOf(loc.months("long", true), 1);
         case "L":
           return intUnit(oneOrTwo);
         case "LL":
           return intUnit(two);
         case "LLL":
-          return oneOf(loc.months("short", false, false), 1);
+          return oneOf(loc.months("short", false), 1);
         case "LLLL":
-          return oneOf(loc.months("long", false, false), 1);
+          return oneOf(loc.months("long", false), 1);
         // dates
         case "d":
           return intUnit(oneOrTwo);
@@ -163,13 +167,13 @@ function unitForToken(token, loc) {
         case "c":
           return intUnit(one);
         case "EEE":
-          return oneOf(loc.weekdays("short", false, false), 1);
+          return oneOf(loc.weekdays("short", false), 1);
         case "EEEE":
-          return oneOf(loc.weekdays("long", false, false), 1);
+          return oneOf(loc.weekdays("long", false), 1);
         case "ccc":
-          return oneOf(loc.weekdays("short", true, false), 1);
+          return oneOf(loc.weekdays("short", true), 1);
         case "cccc":
-          return oneOf(loc.weekdays("long", true, false), 1);
+          return oneOf(loc.weekdays("long", true), 1);
         // offset/zone
         case "Z":
         case "ZZ":
diff --git a/src/impl/zoneUtil.js b/src/impl/zoneUtil.js
index ad389db5..c3151a71 100644
--- a/src/impl/zoneUtil.js
+++ b/src/impl/zoneUtil.js
@@ -24,7 +24,7 @@ export function normalizeZone(input, defaultZone) {
     else return FixedOffsetZone.parseSpecifier(lowered) || IANAZone.create(input);
   } else if (isNumber(input)) {
     return FixedOffsetZone.instance(input);
-  } else if (typeof input === "object" && input.offset && typeof input.offset === "number") {
+  } else if (typeof input === "object" && "offset" in input && typeof input.offset === "function") {
     // This is dumb, but the instanceof check above doesn't seem to really work
     // so we're duck checking it
     return input;


Test patch:
diff --git a/test/duration/units.test.js b/test/duration/units.test.js
index 676f4d6f..2b37fad4 100644
--- a/test/duration/units.test.js
+++ b/test/duration/units.test.js
@@ -189,6 +189,10 @@ test("Duration#normalize handles the full grid partially negative durations", ()
       { months: 0, days: -28 },
       { months: 0, days: -28 },
     ],
+    [
+      { hours: 96, minutes: 0, seconds: -10 },
+      { hours: 95, minutes: 59, seconds: 50 },
+    ],
   ];
 
   sets.forEach(([from, to]) => {
diff --git a/test/info/listers.test.js b/test/info/listers.test.js
index b54deb59..7b4597d0 100644
--- a/test/info/listers.test.js
+++ b/test/info/listers.test.js
@@ -105,6 +105,8 @@ test("Info.months respects the numbering system", () => {
 
 test("Info.months respects the calendar", () => {
   expect(Info.months("long", { locale: "en", outputCalendar: "islamic" })).toEqual([
+    "Muharram",
+    "Safar",
     "Rabiʻ I",
     "Rabiʻ II",
     "Jumada I",
@@ -115,8 +117,6 @@ test("Info.months respects the calendar", () => {
     "Shawwal",
     "Dhuʻl-Qiʻdah",
     "Dhuʻl-Hijjah",
-    "Safar",
-    "Rabiʻ I",
   ]);
 });