Repository: moment/luxon (js)

Issue:
fromISO with 10 digit milliseconds
Seems when parse milliseconds long than 9 decimals its returned as invalid.
**good**: `2020-04-03T11:41:19.539000000`
**invalid**: `2020-04-03T11:41:19.5390000001`
```
var date = DateTime.fromISO('2020-04-03T11:41:19.5390000001')
// returns false
console.log(date.invalid)
```
[jsFiddle Here](https://jsfiddle.net/hxe5762t/1/)

_Originally posted by @cmcleese in https://github.com/moment/luxon/issues/632#issuecomment-608405403_

Pull request description:
Support arbitrary precision in ISO milliseconds. Fixes #757
Simply remove the 9 digit limit in the regexp

Patch:
diff --git a/docs/formatting.md b/docs/formatting.md
index 414f38ba..1e59a884 100644
--- a/docs/formatting.md
+++ b/docs/formatting.md
@@ -133,7 +133,7 @@ The supported tokens are described in the table below.
 
 ### Intl
 
-All of the strings (e.g. month names and weekday names) are internationalized by introspecting strings generated by the Intl API. Thus they exact strings you get are implementation-specific.
+All of the strings (e.g. month names and weekday names) are internationalized by introspecting strings generated by the Intl API. Thus the exact strings you get are implementation-specific.
 
 ```js
 DateTime.fromISO('2014-08-06T13:07:04.054')
@@ -239,4 +239,4 @@ The macro options available correspond one-to-one with the preset formats define
 | FFF              |              | verbose localized date and time with seconds                   | August 6, 2014, 1:07:04 PM EDT                              |
 | FFFF             |              | extra verbose localized date and time with seconds             | Wednesday, August 6, 2014, 1:07:04 PM Eastern Daylight Time |
 | X                |              | unix timestamp in seconds                                      | 1407287224                                                  |
-| x                |              | unix timestamp in milliseconds                                 | 1407287224054                                              |
+| x                |              | unix timestamp in milliseconds                                 | 1407287224054                                               |
diff --git a/package-lock.json b/package-lock.json
index 679878e5..37ededf6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2374,195 +2374,6 @@
         "@babel/types": "^7.7.0",
         "eslint-visitor-keys": "^1.0.0",
         "resolve": "^1.12.0"
-      },
-      "dependencies": {
-        "@babel/generator": {
-          "version": "7.8.4",
-          "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz",
-          "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==",
-          "dev": true,
-          "requires": {
-            "@babel/types": "^7.8.3",
-            "jsesc": "^2.5.1",
-            "lodash": "^4.17.13",
-            "source-map": "^0.5.0"
-          }
-        },
-        "@babel/helper-function-name": {
-          "version": "7.8.3",
-          "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
-          "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
-          "dev": true,
-          "requires": {
-            "@babel/helper-get-function-arity": "^7.8.3",
-            "@babel/template": "^7.8.3",
-            "@babel/types": "^7.8.3"
-          }
-        },
-        "@babel/helper-get-function-arity": {
-          "version": "7.8.3",
-          "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
-          "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
-          "dev": true,
-          "requires": {
-            "@babel/types": "^7.8.3"
-          }
-        },
-        "@babel/helper-split-export-declaration": {
-          "version": "7.8.3",
-          "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz",
-          "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==",
-          "dev": true,
-          "requires": {
-            "@babel/types": "^7.8.3"
-          }
-        },
-        "@babel/highlight": {
-          "version": "7.8.3",
-          "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
-          "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
-          "dev": true,
-          "requires": {
-            "chalk": "^2.0.0",
-            "esutils": "^2.0.2",
-            "js-tokens": "^4.0.0"
-          }
-        },
-        "@babel/parser": {
-          "version": "7.8.4",
-          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
-          "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
-          "dev": true
-        },
-        "@babel/template": {
-          "version": "7.8.3",
-          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
-          "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.8.3",
-            "@babel/parser": "^7.8.3",
-            "@babel/types": "^7.8.3"
-          },
-          "dependencies": {
-            "@babel/code-frame": {
-              "version": "7.8.3",
-              "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
-              "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
-              "dev": true,
-              "requires": {
-                "@babel/highlight": "^7.8.3"
-              }
-            }
-          }
-        },
-        "@babel/traverse": {
-          "version": "7.8.4",
-          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz",
-          "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.8.3",
-            "@babel/generator": "^7.8.4",
-            "@babel/helper-function-name": "^7.8.3",
-            "@babel/helper-split-export-declaration": "^7.8.3",
-            "@babel/parser": "^7.8.4",
-            "@babel/types": "^7.8.3",
-            "debug": "^4.1.0",
-            "globals": "^11.1.0",
-            "lodash": "^4.17.13"
-          },
-          "dependencies": {
-            "@babel/code-frame": {
-              "version": "7.8.3",
-              "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
-              "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
-              "dev": true,
-              "requires": {
-                "@babel/highlight": "^7.8.3"
-              }
-            }
-          }
-        },
-        "@babel/types": {
-          "version": "7.8.3",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
-          "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
-          "dev": true,
-          "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.13",
-            "to-fast-properties": "^2.0.0"
-          }
-        },
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
-          "requires": {
-            "color-convert": "^1.9.0"
-          }
-        },
-        "chalk": {
-          "version": "2.4.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^3.2.1",
-            "escape-string-regexp": "^1.0.5",
-            "supports-color": "^5.3.0"
-          }
-        },
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "dev": true,
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "globals": {
-          "version": "11.12.0",
-          "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-          "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
-          "dev": true
-        },
-        "js-tokens": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-          "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-          "dev": true
-        },
-        "jsesc": {
-          "version": "2.5.2",
-          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-          "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-          "dev": true
-        },
-        "ms": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        },
-        "to-fast-properties": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-          "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
-          "dev": true
-        }
       }
     },
     "babel-generator": {
@@ -4485,9 +4296,9 @@
       }
     },
     "eslint-plugin-babel": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.0.tgz",
-      "integrity": "sha512-HPuNzSPE75O+SnxHIafbW5QB45r2w78fxqwK3HmjqIUoPfPzVrq6rD+CINU3yzoDSzEhUkX07VUphbF73Lth/w==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz",
+      "integrity": "sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==",
       "dev": true,
       "requires": {
         "eslint-rule-composer": "^0.3.0"
@@ -9078,9 +8889,9 @@
       }
     },
     "platform": {
-      "version": "1.3.5",
-      "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz",
-      "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==",
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+      "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
       "dev": true
     },
     "please-upgrade-node": {
diff --git a/src/impl/regexParser.js b/src/impl/regexParser.js
index 62a244ef..74ba737c 100644
--- a/src/impl/regexParser.js
+++ b/src/impl/regexParser.js
@@ -66,7 +66,7 @@ function simpleParse(...keys) {
 
 // ISO and SQL parsing
 const offsetRegex = /(?:(Z)|([+-]\d\d)(?::?(\d\d))?)/,
-  isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,9}))?)?)?/,
+  isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/,
   isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${offsetRegex.source}?`),
   isoTimeExtensionRegex = RegExp(`(?:T${isoTimeRegex.source})?`),
   isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/,
diff --git a/src/impl/util.js b/src/impl/util.js
index 19d0c1e7..b345c694 100644
--- a/src/impl/util.js
+++ b/src/impl/util.js
@@ -263,18 +263,17 @@ export function normalizeObject(obj, normalizer, nonUnitKeys) {
 }
 
 export function formatOffset(offset, format) {
-  const hours = Math.trunc(offset / 60),
-    minutes = Math.abs(offset % 60),
-    sign = hours >= 0 && !Object.is(hours, -0) ? "+" : "-",
-    base = `${sign}${Math.abs(hours)}`;
+  const hours = Math.trunc(Math.abs(offset / 60)),
+    minutes = Math.trunc(Math.abs(offset % 60)),
+    sign = offset >= 0 ? "+" : "-";
 
   switch (format) {
     case "short":
-      return `${sign}${padStart(Math.abs(hours), 2)}:${padStart(minutes, 2)}`;
+      return `${sign}${padStart(hours, 2)}:${padStart(minutes, 2)}`;
     case "narrow":
-      return minutes > 0 ? `${base}:${minutes}` : base;
+      return `${sign}${hours}${minutes > 0 ? `:${minutes}` : ""}`;
     case "techie":
-      return `${sign}${padStart(Math.abs(hours), 2)}${padStart(minutes, 2)}`;
+      return `${sign}${padStart(hours, 2)}${padStart(minutes, 2)}`;
     default:
       throw new RangeError(`Value format ${format} is out of range for property format`);
   }
diff --git a/src/interval.js b/src/interval.js
index 51cd6b07..0fd901ad 100644
--- a/src/interval.js
+++ b/src/interval.js
@@ -134,19 +134,32 @@ export default class Interval {
   static fromISO(text, opts) {
     const [s, e] = (text || "").split("/", 2);
     if (s && e) {
-      const start = DateTime.fromISO(s, opts),
+      let start, startIsValid;
+      try {
+        start = DateTime.fromISO(s, opts);
+        startIsValid = start.isValid;
+      } catch (e) {
+        startIsValid = false;
+      }
+
+      let end, endIsValid;
+      try {
         end = DateTime.fromISO(e, opts);
+        endIsValid = end.isValid;
+      } catch (e) {
+        endIsValid = false;
+      }
 
-      if (start.isValid && end.isValid) {
+      if (startIsValid && endIsValid) {
         return Interval.fromDateTimes(start, end);
       }
 
-      if (start.isValid) {
+      if (startIsValid) {
         const dur = Duration.fromISO(e, opts);
         if (dur.isValid) {
           return Interval.after(start, dur);
         }
-      } else if (end.isValid) {
+      } else if (endIsValid) {
         const dur = Duration.fromISO(s, opts);
         if (dur.isValid) {
           return Interval.before(end, dur);


Test patch:
diff --git a/test/datetime/format.test.js b/test/datetime/format.test.js
index 5051e02e..2634b6cf 100644
--- a/test/datetime/format.test.js
+++ b/test/datetime/format.test.js
@@ -30,15 +30,39 @@ test("DateTime#toISO() shows 'Z' for UTC", () => {
   expect(dt.toISO()).toBe("1982-05-25T09:23:54.123Z");
 });
 
-test("DateTime#toISO() shows the offset", () => {
+test("DateTime#toISO() shows the offset, unless explicitely asked", () => {
   const offsetted = dt.toUTC(-6 * 60);
   expect(offsetted.toISO()).toBe("1982-05-25T03:23:54.123-06:00");
+  expect(offsetted.toISO({ includeOffset: false })).toBe("1982-05-25T03:23:54.123");
+});
+
+test("DateTime#toISO() supports the 'basic' format", () => {
+  expect(dt.toISO({ format: "basic" })).toBe("19820525T092354.123Z");
+});
+
+test("DateTime#toISO() suppresses [milli]seconds", () => {
+  const noZeroMilliseconds = { suppressMilliseconds: true };
+  expect(dt.toISO(noZeroMilliseconds)).toBe("1982-05-25T09:23:54.123Z");
+  expect(dt.set({ millisecond: 0 }).toISO(noZeroMilliseconds)).toBe("1982-05-25T09:23:54Z");
+
+  const noZeroSeconds = { suppressSeconds: true, suppressMilliseconds: true };
+  expect(dt.set({ millisecond: 0 }).toISO(noZeroSeconds)).toBe("1982-05-25T09:23:54Z");
+  expect(dt.set({ seconds: 0, milliseconds: 0 }).toISO(noZeroSeconds)).toBe("1982-05-25T09:23Z");
 });
 
 test("DateTime#toISO() returns null for invalid DateTimes", () => {
   expect(invalid.toISO()).toBe(null);
 });
 
+// #724, Firefox specific issue, offset prints as '-05:50.60000000000002'
+test("DateTime#toISO() rounds fractional timezone minute offsets", () => {
+  expect(
+    DateTime.fromMillis(-62090696591000)
+      .setZone("America/Chicago")
+      .toISO()
+  ).toBe("0002-06-04T10:26:13.000-05:50");
+});
+
 //------
 // #toISODate()
 //------
diff --git a/test/datetime/regexParse.test.js b/test/datetime/regexParse.test.js
index 6a2007f0..30a2e174 100644
--- a/test/datetime/regexParse.test.js
+++ b/test/datetime/regexParse.test.js
@@ -323,6 +323,17 @@ test("DateTime.fromISO() accepts year-month-dayThour:minute:second.millisecond",
     millisecond: 999
   });
 
+  // Support up to 20 digits
+  isSame("2016-05-25T09:24:15.12345678901234567890123456789", {
+    year: 2016,
+    month: 5,
+    day: 25,
+    hour: 9,
+    minute: 24,
+    second: 15,
+    millisecond: 123
+  });
+
   isSame("2016-05-25T09:24:15.1", {
     year: 2016,
     month: 5,
diff --git a/test/interval/parse.test.js b/test/interval/parse.test.js
index b7bad590..d2bd58cf 100644
--- a/test/interval/parse.test.js
+++ b/test/interval/parse.test.js
@@ -1,5 +1,8 @@
 /* global test expect */
 import { Interval } from "../../src/luxon";
+import Helpers from "../helpers";
+
+const withThrowOnInvalid = Helpers.setUnset("throwOnInvalid");
 
 //------
 // .fromISO()
@@ -117,6 +120,17 @@ test("Interval.fromISO accepts a zone argument", () => {
   expect(durDate.start.zoneName).toBe("Europe/Paris");
 });
 
+// #728
+test("Interval.fromISO works with Settings.throwOnInvalid", () => {
+  withThrowOnInvalid(true, () => {
+    const dateDur = Interval.fromISO("2020-06-22T17:30:00.000+02:00/PT5H30M");
+    expect(dateDur.isValid).toBe(true);
+
+    const durDate = Interval.fromISO("PT5H30M/2020-06-22T17:30:00.000+02:00");
+    expect(durDate.isValid).toBe(true);
+  });
+});
+
 const badInputs = [
   null,
   "",
diff --git a/test/zones/fixedOffset.test.js b/test/zones/fixedOffset.test.js
index 8ae5b631..82df958d 100644
--- a/test/zones/fixedOffset.test.js
+++ b/test/zones/fixedOffset.test.js
@@ -53,6 +53,16 @@ test("FixedOffsetZone.formatOffset is consistent despite the provided timestamp"
   expect(zone.formatOffset(1552280400, "techie")).toBe("-0500");
 });
 
+test("FixedOffsetZone.formatOffset prints the correct sign before the offset", () => {
+  expect(FixedOffsetZone.instance(-300).formatOffset(0, "short")).toBe("-05:00");
+  expect(FixedOffsetZone.instance(-30).formatOffset(0, "short")).toBe("-00:30");
+  // Note the negative zero results in a + sign
+  expect(FixedOffsetZone.instance(-0).formatOffset(0, "short")).toBe("+00:00");
+  expect(FixedOffsetZone.instance(0).formatOffset(0, "short")).toBe("+00:00");
+  expect(FixedOffsetZone.instance(30).formatOffset(0, "short")).toBe("+00:30");
+  expect(FixedOffsetZone.instance(300).formatOffset(0, "short")).toBe("+05:00");
+});
+
 test("FixedOffsetZone.equals requires both zones to be fixed", () => {
   expect(FixedOffsetZone.utcInstance.equals(IANAZone.create("UTC"))).toBe(false);
 });