Repository: moment/luxon
Pull request #1066 (2021-10-29 08:02:57)

perf: Use memoized zone for universal zone validation
When formatting to UTC IANAZone.isValidZone will construct a new Intl.DateTimeFormat for each call.

![luxon-isvalidzone](https://user-images.githubusercontent.com/51433/139398058-67a8cd87-2e84-4a06-8b31-630338593899.png)



Patch:
diff --git a/benchmarks/datetime.js b/benchmarks/datetime.js
index dbfdb703..1fcd0faf 100644
--- a/benchmarks/datetime.js
+++ b/benchmarks/datetime.js
@@ -54,6 +54,9 @@ function runDateTimeSuite() {
       .add("DateTime#toLocaleString", () => {
         dt.toLocaleString();
       })
+      .add("DateTime#toLocaleString in utc", () => {
+        dt.toUTC().toLocaleString();
+      })
       .add("DateTime#toRelativeCalendar", () => {
         dt.toRelativeCalendar({ base: DateTime.now(), locale: "fi" });
       })
diff --git a/docs/math.md b/docs/math.md
index e15b02b9..864fb1b6 100644
--- a/docs/math.md
+++ b/docs/math.md
@@ -173,8 +173,8 @@ Finally, you can diff using multiple units:
 
 ```js
 var end = DateTime.fromISO('2017-03-13');
-var start = DateTime.fromISO('2017-02-15');
-end.diff(start, ['months', 'days']) //=> { months: 1, days: 2 }
+var start = DateTime.fromISO('2017-02-11');
+end.diff(start, ['months', 'days']).toObject() //=> { months: 1, days: 2 }
 ```
 
 ### Casual vs longterm conversion accuracy
diff --git a/docs/tour.md b/docs/tour.md
index 08e3208b..b9868c09 100644
--- a/docs/tour.md
+++ b/docs/tour.md
@@ -12,7 +12,7 @@ The most important class in Luxon is `DateTime`. A DateTime represents a specifi
 const dt = DateTime.local(2017, 5, 15, 8, 30);
 ```
 
-[DateTime.local](../class/src/datetime.js~DateTime.html#static-method-local) takes any number of arguments, all the way out to milliseconds. Underneath, this is similar to a JavaScript Date object. But we've decorated it with lots of useful methods.
+[DateTime.local](../class/src/datetime.js~DateTime.html#static-method-local) takes any number of arguments, all the way out to milliseconds (months are 1-indexed). Underneath, this is similar to a JavaScript Date object. But we've decorated it with lots of useful methods.
 
 ## Creating a DateTime
 
diff --git a/src/duration.js b/src/duration.js
index 5372a472..60122631 100644
--- a/src/duration.js
+++ b/src/duration.js
@@ -6,6 +6,7 @@ import { parseISODuration, parseISOTimeOnly } from "./impl/regexParser.js";
 import {
   asNumber,
   hasOwnProperty,
+  isInteger,
   isNumber,
   isUndefined,
   normalizeObject,
@@ -16,7 +17,7 @@ import Settings from "./settings.js";
 const INVALID = "Invalid Duration";
 
 // unit conversion constants
-const lowOrderMatrix = {
+export const lowOrderMatrix = {
     weeks: {
       days: 7,
       hours: 7 * 24,
@@ -239,6 +240,7 @@ export default class Duration {
         }`
       );
     }
+
     return new Duration({
       values: normalizeObject(obj, Duration.normalizeUnit),
       loc: Locale.fromObject(opts),
diff --git a/src/impl/locale.js b/src/impl/locale.js
index 633b1f97..500a60c1 100644
--- a/src/impl/locale.js
+++ b/src/impl/locale.js
@@ -181,8 +181,7 @@ class PolyDateFormatter {
       //    - < Etc/GMT-14, > Etc/GMT+12, and 30-minute or 45-minute offsets are not part of tzdata
       const gmtOffset = -1 * (dt.offset / 60);
       const offsetZ = gmtOffset >= 0 ? `Etc/GMT+${gmtOffset}` : `Etc/GMT${gmtOffset}`;
-      const isOffsetZoneSupported = IANAZone.isValidZone(offsetZ);
-      if (dt.offset !== 0 && isOffsetZoneSupported) {
+      if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) {
         z = offsetZ;
         this.dt = dt;
       } else {
diff --git a/src/impl/regexParser.js b/src/impl/regexParser.js
index 65ba6be3..eb4fefc7 100644
--- a/src/impl/regexParser.js
+++ b/src/impl/regexParser.js
@@ -5,6 +5,7 @@ import {
   parseMillis,
   ianaRegex,
   isUndefined,
+  parseFloating,
 } from "./util.js";
 import * as English from "./english.js";
 import FixedOffsetZone from "../zones/fixedOffsetZone.js";
@@ -125,7 +126,7 @@ const isoTimeOnly = RegExp(`^T?${isoTimeBaseRegex.source}$`);
 // ISO duration parsing
 
 const isoDuration =
-  /^-?P(?:(?:(-?\d{1,9})Y)?(?:(-?\d{1,9})M)?(?:(-?\d{1,9})W)?(?:(-?\d{1,9})D)?(?:T(?:(-?\d{1,9})H)?(?:(-?\d{1,9})M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,9}))?S)?)?)$/;
+  /^-?P(?:(?:(-?\d{1,9}(?:\.\d{1,9})?)Y)?(?:(-?\d{1,9}(?:\.\d{1,9})?)M)?(?:(-?\d{1,9}(?:\.\d{1,9})?)W)?(?:(-?\d{1,9}(?:\.\d{1,9})?)D)?(?:T(?:(-?\d{1,9}(?:\.\d{1,9})?)H)?(?:(-?\d{1,9}(?:\.\d{1,9})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,9}))?S)?)?)$/;
 
 function extractISODuration(match) {
   const [s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr] =
@@ -139,13 +140,13 @@ function extractISODuration(match) {
 
   return [
     {
-      years: maybeNegate(parseInteger(yearStr)),
-      months: maybeNegate(parseInteger(monthStr)),
-      weeks: maybeNegate(parseInteger(weekStr)),
-      days: maybeNegate(parseInteger(dayStr)),
-      hours: maybeNegate(parseInteger(hourStr)),
-      minutes: maybeNegate(parseInteger(minuteStr)),
-      seconds: maybeNegate(parseInteger(secondStr), secondStr === "-0"),
+      years: maybeNegate(parseFloating(yearStr)),
+      months: maybeNegate(parseFloating(monthStr)),
+      weeks: maybeNegate(parseFloating(weekStr)),
+      days: maybeNegate(parseFloating(dayStr)),
+      hours: maybeNegate(parseFloating(hourStr)),
+      minutes: maybeNegate(parseFloating(minuteStr)),
+      seconds: maybeNegate(parseFloating(secondStr), secondStr === "-0"),
       milliseconds: maybeNegate(parseMillis(millisecondsStr), negativeSeconds),
     },
   ];
diff --git a/src/impl/util.js b/src/impl/util.js
index e25d6be8..f576c5e7 100644
--- a/src/impl/util.js
+++ b/src/impl/util.js
@@ -108,6 +108,14 @@ export function parseInteger(string) {
   }
 }
 
+export function parseFloating(string) {
+  if (isUndefined(string) || string === null || string === "") {
+    return undefined;
+  } else {
+    return parseFloat(string);
+  }
+}
+
 export function parseMillis(fraction) {
   // Return undefined (instead of 0) in these cases, where fraction is not set
   if (isUndefined(fraction) || fraction === null || fraction === "") {


Test patch:
diff --git a/test/duration/create.test.js b/test/duration/create.test.js
index ec24b745..a425dc6c 100644
--- a/test/duration/create.test.js
+++ b/test/duration/create.test.js
@@ -24,6 +24,22 @@ test("Duration.fromObject sets all the values", () => {
   expect(dur.milliseconds).toBe(7);
 });
 
+test("Duration.fromObject sets all the fractional values", () => {
+  const dur = Duration.fromObject({
+    years: 1,
+    months: 2,
+    days: 3,
+    hours: 4.5,
+  });
+  expect(dur.years).toBe(1);
+  expect(dur.months).toBe(2);
+  expect(dur.days).toBe(3);
+  expect(dur.hours).toBe(4.5);
+  expect(dur.minutes).toBe(0);
+  expect(dur.seconds).toBe(0);
+  expect(dur.milliseconds).toBe(0);
+});
+
 test("Duration.fromObject sets all the values from the object having string type values", () => {
   const dur = Duration.fromObject({
     years: "1",
diff --git a/test/duration/equality.test.js b/test/duration/equality.test.js
index bfbd50de..a7b7765e 100644
--- a/test/duration/equality.test.js
+++ b/test/duration/equality.test.js
@@ -12,12 +12,24 @@ test("equals identically constructed", () => {
   expect(l1.equals(l2)).toBe(true);
 });
 
-test("equals identically constructed but one has sting type values", () => {
+test("equals identically constructed with fractional values", () => {
+  const l1 = Duration.fromObject({ years: 5.5, days: 6 }),
+    l2 = Duration.fromObject({ years: 5.5, days: 6 });
+  expect(l1.equals(l2)).toBe(true);
+});
+
+test("equals identically constructed but one has string type values", () => {
   const l1 = Duration.fromObject({ years: 5, days: 6 }),
     l2 = Duration.fromObject({ years: "5", days: "6" });
   expect(l1.equals(l2)).toBe(true);
 });
 
+test("equals identically constructed but one has fractional string type values", () => {
+  const l1 = Duration.fromObject({ years: 5.5, days: 6 }),
+    l2 = Duration.fromObject({ years: "5.5", days: "6" });
+  expect(l1.equals(l2)).toBe(true);
+});
+
 // #809
 test("equals with extra zero units", () => {
   const l1 = Duration.fromObject({ years: 5, days: 6 }),
diff --git a/test/duration/format.test.js b/test/duration/format.test.js
index ff1b77dd..9a50c9d7 100644
--- a/test/duration/format.test.js
+++ b/test/duration/format.test.js
@@ -21,6 +21,20 @@ test("Duration#toISO fills out every field", () => {
   expect(dur().toISO()).toBe("P1Y2M1W3DT4H5M6.007S");
 });
 
+test("Duration#toISO fills out every field with fractional", () => {
+  const dur = Duration.fromObject({
+    years: 1.1,
+    months: 2.2,
+    weeks: 1.1,
+    days: 3.3,
+    hours: 4.4,
+    minutes: 5.5,
+    seconds: 6.6,
+    milliseconds: 7,
+  });
+  expect(dur.toISO()).toBe("P1.1Y2.2M1.1W3.3DT4.4H5.5M6.607S");
+});
+
 test("Duration#toISO creates a minimal string", () => {
   expect(Duration.fromObject({ years: 3, seconds: 45 }).toISO()).toBe("P3YT45S");
   expect(Duration.fromObject({ months: 4, seconds: 45 }).toISO()).toBe("P4MT45S");
diff --git a/test/duration/getters.test.js b/test/duration/getters.test.js
index 7b313100..192cb3a6 100644
--- a/test/duration/getters.test.js
+++ b/test/duration/getters.test.js
@@ -44,6 +44,24 @@ test("Duration#hours returns the hours", () => {
   expect(inv.hours).toBeFalsy();
 });
 
+test("Duration#hours returns the fractional hours", () => {
+  const localDur = Duration.fromObject({
+      years: 1,
+      quarters: 2,
+      months: 2,
+      days: 3,
+      hours: 4.5,
+      minutes: 5,
+      seconds: 6,
+      milliseconds: 7,
+      weeks: 8,
+    }),
+    localInv = Duration.invalid("because i say so");
+
+  expect(localDur.hours).toBe(4.5);
+  expect(localInv.hours).toBeFalsy();
+});
+
 test("Duration#minutes returns the minutes", () => {
   expect(dur.minutes).toBe(5);
   expect(inv.minutes).toBeFalsy();
diff --git a/test/duration/info.test.js b/test/duration/info.test.js
index cedb8095..350a3a51 100644
--- a/test/duration/info.test.js
+++ b/test/duration/info.test.js
@@ -6,7 +6,7 @@ const dur = Duration.fromObject(
   {
     years: 1,
     months: 2,
-    days: 3,
+    days: 3.3,
   },
   {
     locale: "fr",
@@ -22,7 +22,7 @@ test("Duration#toObject returns the object", () => {
   expect(dur.toObject()).toEqual({
     years: 1,
     months: 2,
-    days: 3,
+    days: 3.3,
   });
 });
 
@@ -31,7 +31,7 @@ test("Duration#toObject accepts a flag to return config", () => {
     {
       years: 1,
       months: 2,
-      days: 3,
+      days: 3.3,
     },
     {
       locale: "fr",
diff --git a/test/duration/math.test.js b/test/duration/math.test.js
index 21d31ba1..536afe21 100644
--- a/test/duration/math.test.js
+++ b/test/duration/math.test.js
@@ -16,6 +16,17 @@ test("Duration#plus add straightforward durations", () => {
   expect(result.milliseconds).toBe(14);
 });
 
+test("Duration#plus add fractional durations", () => {
+  const first = Duration.fromObject({ hours: 4.2, minutes: 12, seconds: 2 }),
+    second = Duration.fromObject({ hours: 1, seconds: 6.8, milliseconds: 14 }),
+    result = first.plus(second);
+
+  expect(result.hours).toBeCloseTo(5.2, 8);
+  expect(result.minutes).toBe(12);
+  expect(result.seconds).toBeCloseTo(8.8, 8);
+  expect(result.milliseconds).toBe(14);
+});
+
 test("Duration#plus noops empty druations", () => {
   const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
     second = Duration.fromObject({}),
@@ -87,6 +98,17 @@ test("Duration#minus subtracts durations", () => {
   expect(result.milliseconds).toBe(-14);
 });
 
+test("Duration#minus subtracts fractional durations", () => {
+  const first = Duration.fromObject({ hours: 4.2, minutes: 12, seconds: 2 }),
+    second = Duration.fromObject({ hours: 1, seconds: 6, milliseconds: 14 }),
+    result = first.minus(second);
+
+  expect(result.hours).toBeCloseTo(3.2, 8);
+  expect(result.minutes).toBe(12);
+  expect(result.seconds).toBe(-4);
+  expect(result.milliseconds).toBe(-14);
+});
+
 test("Duration#minus subtracts single values", () => {
   const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
     result = first.minus({ minutes: 5 });
diff --git a/test/duration/parse.test.js b/test/duration/parse.test.js
index 22b0a314..a593e936 100644
--- a/test/duration/parse.test.js
+++ b/test/duration/parse.test.js
@@ -65,6 +65,24 @@ test("Duration.fromISO can parse fractions of seconds", () => {
   });
 });
 
+test("Duration.fromISO can parse fractions", () => {
+  expect(Duration.fromISO("P1.5Y").toObject()).toEqual({
+    years: 1.5,
+  });
+  expect(Duration.fromISO("P1.5M").toObject()).toEqual({
+    months: 1.5,
+  });
+  expect(Duration.fromISO("P1.5W").toObject()).toEqual({
+    weeks: 1.5,
+  });
+  expect(Duration.fromISO("P1.5D").toObject()).toEqual({
+    days: 1.5,
+  });
+  expect(Duration.fromISO("PT9.5H").toObject()).toEqual({
+    hours: 9.5,
+  });
+});
+
 const rejects = (s) => {
   expect(Duration.fromISO(s).isValid).toBe(false);
 };
diff --git a/test/duration/set.test.js b/test/duration/set.test.js
index fd63e91e..bd20ab75 100644
--- a/test/duration/set.test.js
+++ b/test/duration/set.test.js
@@ -20,6 +20,7 @@ test("Duration#set() sets the values", () => {
   expect(dur().set({ months: 2 }).months).toBe(2);
   expect(dur().set({ days: 2 }).days).toBe(2);
   expect(dur().set({ hours: 4 }).hours).toBe(4);
+  expect(dur().set({ hours: 4.5 }).hours).toBe(4.5);
   expect(dur().set({ minutes: 16 }).minutes).toBe(16);
   expect(dur().set({ seconds: 45 }).seconds).toBe(45);
   expect(dur().set({ milliseconds: 86 }).milliseconds).toBe(86);
diff --git a/test/duration/typecheck.test.js b/test/duration/typecheck.test.js
index cbc2a7af..4a30c2a6 100644
--- a/test/duration/typecheck.test.js
+++ b/test/duration/typecheck.test.js
@@ -6,7 +6,7 @@ import { Duration } from "../../src/luxon";
 // #isDuration
 //-------
 test("Duration#isDuration return true for valid duration", () => {
-  const dur = Duration.fromObject({ hours: 1 });
+  const dur = Duration.fromObject({ hours: 1, minutes: 4.5 });
   expect(Duration.isDuration(dur)).toBe(true);
 });
 
@@ -19,6 +19,7 @@ test("Duration#isDuration return false for primitives", () => {
   expect(Duration.isDuration({})).toBe(false);
   expect(Duration.isDuration({ hours: 60 })).toBe(false);
   expect(Duration.isDuration(1)).toBe(false);
+  expect(Duration.isDuration(1.1)).toBe(false);
   expect(Duration.isDuration("")).toBe(false);
   expect(Duration.isDuration(null)).toBe(false);
   expect(Duration.isDuration()).toBe(false);
diff --git a/test/duration/units.test.js b/test/duration/units.test.js
index da35d939..9d39df35 100644
--- a/test/duration/units.test.js
+++ b/test/duration/units.test.js
@@ -46,6 +46,14 @@ test("Duration#shiftTo deconstructs decimal inputs", () => {
   expect(dur.minutes).toBeCloseTo(18, 8);
 });
 
+test("Duration#shiftTo deconstructs in cascade and tacks decimal onto the end", () => {
+  const dur = Duration.fromObject({ hours: 1.17 }).shiftTo("hours", "minutes", "seconds");
+  expect(dur.isValid).toBe(true);
+  expect(dur.hours).toBe(1);
+  expect(dur.minutes).toBe(10);
+  expect(dur.seconds).toBeCloseTo(12, 8);
+});
+
 test("Duration#shiftTo maintains invalidity", () => {
   const dur = Duration.invalid("because").shiftTo("years");
   expect(dur.isValid).toBe(false);