Repository: moment/luxon
Pull request #1023 (2021-09-02 14:39:02)

Add test cases for day-of-the-month parsing
This commit adds two positive test cases for parsing a fixed- and variable- length day-of-the-month, as well as a negative test cases which detects an `Invalid.reason: "unparsable"` when attempting to parse unpadded day-of-the-month with a fixed-length format.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 96554b31..b01cdc70 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,7 @@ Once you're sure your bugfix or feature makes sense for Luxon, make sure you tak
 1.  Be sure you run `npm run format` before you commit. Note this will modify your source files to line up with the style guidelines.
 1.  Make sure you add or ESDoc annotations appropriately. You can run `npm run docs` to generate the HTML for them. They land in the `build/docs` directory. This also builds the markdown files in `/docs` into the guide on the Luxon website.
 1.  To test Luxon in your browser, run `npm run site` and then open `build/demo/global.html`. You can access Luxon classes in the console like `window.luxon.DateTime`.
-1.  To test in Node, run `npm run build` and then run something like `var DateTime = require('./build/cjs/luxon').DateTime`.
+1.  To test in Node, run `npm run build` and then run something like `var DateTime = require('./build/cjs-browser/luxon').DateTime`.
 
 Luxon uses [Husky](https://github.com/typicode/husky) to run the formatter on your code as a pre-commit hook. You should still run `npm run format` yourself to catch other issues, but this hook will help prevent you from failing the build with a trivial formatting error.
 
diff --git a/docs/formatting.md b/docs/formatting.md
index d5b9aec1..7eb1dd61 100644
--- a/docs/formatting.md
+++ b/docs/formatting.md
@@ -85,28 +85,29 @@ dt.toLocaleString(newFormat); //=>  'Thursday, 4/20/2017'
 
 Here's the full set of provided presets using the October 14, 1983 at `13:30:23` as an example.
 
-| Name                        | Description                                                        | Example in en_US                                           | Example in fr                                            |
-| --------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------- |
-| DATE_SHORT                  | short date                                                         | 10/14/1983                                                 | 14/10/1983                                               |
-| DATE_MED                    | abbreviated date                                                   | Oct 14, 1983                                               | 14 oct. 1983                                             |
-| DATE_FULL                   | full date                                                          | October 14, 1983                                           | 14 octobre 1983                                          |
-| DATE_HUGE                   | full date with weekday                                             | Tuesday, October 14, 1983                                  | vendredi 14 octobre 1983                                 |
-| TIME_SIMPLE                 | time                                                               | 1:30 PM                                                    | 13:30                                                    |
-| TIME_WITH_SECONDS           | time with seconds                                                  | 1:30:23 PM                                                 | 13:30:23                                                 |
-| TIME_WITH_SHORT_OFFSET      | time with seconds and abbreviated named offset                     | 1:30:23 PM EDT                                             | 13:30:23 UTC−4                                           |
-| TIME_WITH_LONG_OFFSET       | time with seconds and full named offset                            | 1:30:23 PM Eastern Daylight Time                           | 13:30:23 heure d’été de l’Est                            |
-| TIME_24_SIMPLE              | 24-hour time                                                       | 13:30                                                      | 13:30                                                    |
-| TIME_24_WITH_SECONDS        | 24-hour time with seconds                                          | 13:30:23                                                   | 13:30:23                                                 |
-| TIME_24_WITH_SHORT_OFFSET   | 24-hour time with seconds and abbreviated named offset             | 13:30:23 EDT                                               | 13:30:23 UTC−4                                           |
-| TIME_24_WITH_LONG_OFFSET    | 24-hour time with seconds and full named offset                    | 13:30:23 Eastern Daylight Time                             | 13:30:23 heure d’été de l’Est                            |
-| DATETIME_SHORT              | short date & time                                                  | 10/14/1983, 1:30 PM                                        | 14/10/1983 à 13:30                                       |
-| DATETIME_MED                | abbreviated date & time                                            | Oct 14, 1983, 1:30 PM                                      | 14 oct. 1983 à 13:30                                     |
-| DATETIME_FULL               | full date and time with abbreviated named offset                   | October 14, 1983, 1:30 PM EDT                              | 14 octobre 1983 à 13:30 UTC−4                            |
-| DATETIME_HUGE               | full date and time with weekday and full named offset              | Friday, October 14, 1983, 1:30 PM Eastern Daylight Time    | vendredi 14 octobre 1983 à 13:30 heure d’été de l’Est    |
-| DATETIME_SHORT_WITH_SECONDS | short date & time with seconds                                     | 10/14/1983, 1:30:23 PM                                     | 14/10/1983 à 13:30:23                                    |
-| DATETIME_MED_WITH_SECONDS   | abbreviated date & time with seconds                               | Oct 14, 1983, 1:30:23 PM                                   | 14 oct. 1983 à 13:30:23                                  |
-| DATETIME_FULL_WITH_SECONDS  | full date and time with abbreviated named offset with seconds      | October 14, 1983, 1:30:23 PM EDT                           | 14 octobre 1983 à 13:30:23 UTC−4                         |
-| DATETIME_HUGE_WITH_SECONDS  | full date and time with weekday and full named offset with seconds | Friday, October 14, 1983, 1:30:23 PM Eastern Daylight Time | vendredi 14 octobre 1983 à 13:30:23 heure d’été de l’Est |
+| Name                         | Description                                                        | Example in en_US                                             | Example in fr                                              |
+| ---------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------------------- |
+| `DATE_SHORT`                 | short date                                                         | `10/14/1983`                                                 | `14/10/1983`                                               |
+| `DATE_MED`                   | abbreviated date                                                   | `Oct 14, 1983`                                               | `14 oct. 1983`                                             |
+| `DATE_MED_WITH_WEEKDAY`      | abbreviated date with abbreviated weekday                          | `Fri, Oct 14, 1983`                                          | `ven. 14 oct. 1983`                                             |
+| `DATE_FULL`                  | full date                                                          | `October 14, 1983`                                           | `14 octobre 1983`                                          |
+| `DATE_HUGE`                  | full date with weekday                                             | `Friday, October 14, 1983`                                   | `vendredi 14 octobre 1983`                                 |
+| `TIME_SIMPLE`                | time                                                               | `1:30 PM`                                                    | `13:30`                                                    |
+| `TIME_WITH_SECONDS`          | time with seconds                                                  | `1:30:23 PM`                                                 | `13:30:23`                                                 |
+| `TIME_WITH_SHORT_OFFSET`     | time with seconds and abbreviated named offset                     | `1:30:23 PM EDT`                                             | `13:30:23 UTC−4`                                           |
+| `TIME_WITH_LONG_OFFSET`      | time with seconds and full named offset                            | `1:30:23 PM Eastern Daylight Time`                           | `13:30:23 heure d’été de l’Est`                            |
+| `TIME_24_SIMPLE`             | 24-hour time                                                       | `13:30`                                                      | `13:30`                                                    |
+| `TIME_24_WITH_SECONDS`       | 24-hour time with seconds                                          | `13:30:23`                                                   | `13:30:23`                                                 |
+| `TIME_24_WITH_SHORT_OFFSET`  | 24-hour time with seconds and abbreviated named offset             | `13:30:23 EDT`                                               | `13:30:23 UTC−4`                                           |
+| `TIME_24_WITH_LONG_OFFSET`   | 24-hour time with seconds and full named offset                    | `13:30:23 Eastern Daylight Time`                             | `13:30:23 heure d’été de l’Est`                            |
+| `DATETIME_SHORT`             | short date & time                                                  | `10/14/1983, 1:30 PM`                                        | `14/10/1983 à 13:30`                                       |
+| `DATETIME_MED`               | abbreviated date & time                                            | `Oct 14, 1983, 1:30 PM`                                      | `14 oct. 1983 à 13:30`                                     |
+| `DATETIME_FULL`              | full date and time with abbreviated named offset                   | `October 14, 1983, 1:30 PM EDT`                              | `14 octobre 1983 à 13:30 UTC−4`                            |
+| `DATETIME_HUGE`              | full date and time with weekday and full named offset              | `Friday, October 14, 1983, 1:30 PM Eastern Daylight Time`    | `vendredi 14 octobre 1983 à 13:30 heure d’été de l’Est`    |
+| `DATETIME_SHORT_WITH_SECONDS`| short date & time with seconds                                     | `10/14/1983, 1:30:23 PM`                                     | `14/10/1983 à 13:30:23`                                    |
+| `DATETIME_MED_WITH_SECONDS`  | abbreviated date & time with seconds                               | `Oct 14, 1983, 1:30:23 PM`                                   | `14 oct. 1983 à 13:30:23`                                  |
+| `DATETIME_FULL_WITH_SECONDS` | full date and time with abbreviated named offset with seconds      | `October 14, 1983, 1:30:23 PM EDT`                           | `14 octobre 1983 à 13:30:23 UTC−4`                         |
+| `DATETIME_HUGE_WITH_SECONDS` | full date and time with weekday and full named offset with seconds | `Friday, October 14, 1983, 1:30:23 PM Eastern Daylight Time` | `vendredi 14 octobre 1983 à 13:30:23 heure d’été de l’Est` |
 
 ### Intl
 
@@ -179,6 +180,8 @@ The macro options available correspond one-to-one with the preset formats define
 | S                |              | millisecond, no padding                                        | `54`                                                          |
 | SSS              |              | millisecond, padded to 3                                       | `054`                                                         |
 | u                |              | fractional seconds, functionally identical to SSS              | `054`                                                         |
+| uu               |              | fractional seconds, between 0 and 99, padded to 2              | `05`                                                          |
+| uuu              |              | fractional seconds, between 0 and 9                            | `0`                                                           |
 | s                |              | second, no padding                                             | `4`                                                           |
 | ss               |              | second, padded to 2 padding                                    | `04`                                                          |
 | m                |              | minute, no padding                                             | `7`                                                           |
diff --git a/docs/math.md b/docs/math.md
index 0366af5f..2dd72e8f 100644
--- a/docs/math.md
+++ b/docs/math.md
@@ -211,7 +211,7 @@ These are always true and you can roll them up and down with consistency (e.g. `
 These should match your intuition and for most purposes they work well. But they're not just wrong; they're not even self-consistent:
 
 ```js
-dur.shiftTo('months').shiftTo('days').as('years') //=> 0.9863013698630136
+Duration.fromObject({ years:1 }).shiftTo('months').shiftTo('days').as('years') //=> 0.9863013698630136
 ```
 
 This is because 12 * 30 != 365. These errors can be annoying, but they can also cause significant issues if the errors accumulate:
diff --git a/docs/moment.md b/docs/moment.md
index e41d443d..524fef64 100644
--- a/docs/moment.md
+++ b/docs/moment.md
@@ -27,7 +27,7 @@ This happens because the `plus` method returns a new instance, leaving `d1` unmo
 1. Months in Luxon are 1-indexed instead of 0-indexed like in Moment and the native Date type.
 1. Localizations and time zones are implemented by the native Intl API (or a polyfill of it), instead of by the library itself.
 1. Luxon has both a Duration type and an Interval type. The Interval type is like Twix.
-1. Luxon lacks the relative time features of Moment and won't support it until the required [facilities](https://github.com/tc39/proposal-intl-relative-time) are provided by the browser.
+1. Luxon only supports [relative time features](#humanization) if the required [Intl.RelativeTimeFormat](https://github.com/tc39/proposal-intl-relative-time) APIs are provided by the browser or polyfills.
 
 ## Other API style differences
 
diff --git a/docs/parsing.md b/docs/parsing.md
index 25210517..5540f63a 100644
--- a/docs/parsing.md
+++ b/docs/parsing.md
@@ -83,7 +83,7 @@ Both methods accept the same options, which allow you to specify a timezone, cal
 
 ### JS Date Object
 
-A native JS `Date` object can be converted into a `DateTime` using [fromJSDate](../class/src/datetime.js~DateTime.html#static-method-fromJSDate).
+A native JS `Date` object can be converted into a `DateTime` using `DateTime.fromJSDate`.
 
 An optional `zone` parameter can be provided to set the zone on the resulting object.
 
@@ -184,6 +184,8 @@ Because Luxon was able to parse the string without difficulty, the output is a l
 | S                |              | millisecond, no padding                                        | `54`                        |
 | SSS              |              | millisecond, padded to 3                                       | `054`                       |
 | u                |              | fractional seconds, (5 is a half second, 54 is slightly more)  | `54`                        |
+| uu               |              | fractional seconds, (one or two digits)                        | `05`                        |
+| uuu              |              | fractional seconds, (only one digit)                           | `5`                         |
 | s                |              | second, no padding                                             | `4`                         |
 | ss               |              | second, padded to 2 padding                                    | `04`                        |
 | m                |              | minute, no padding                                             | `7`                         |
diff --git a/src/impl/formatter.js b/src/impl/formatter.js
index 4463781a..38200c85 100644
--- a/src/impl/formatter.js
+++ b/src/impl/formatter.js
@@ -179,6 +179,11 @@ export default class Formatter {
             return this.num(dt.second);
           case "ss":
             return this.num(dt.second, 2);
+          // fractional seconds
+          case "uu":
+            return this.num(Math.floor(dt.millisecond / 10), 2);
+          case "uuu":
+            return this.num(Math.floor(dt.millisecond / 100));
           // minutes
           case "m":
             return this.num(dt.minute);
diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js
index 6b31541d..e5a1270e 100644
--- a/src/impl/tokenParser.js
+++ b/src/impl/tokenParser.js
@@ -141,6 +141,10 @@ function unitForToken(token, loc) {
           return intUnit(three);
         case "u":
           return simple(oneToNine);
+        case "uu":
+          return simple(oneOrTwo);
+        case "uuu":
+          return intUnit(one);
         // meridiem
         case "a":
           return oneOf(loc.meridiems(), 0);
diff --git a/src/zones/IANAZone.js b/src/zones/IANAZone.js
index 960c3fc6..5975598a 100644
--- a/src/zones/IANAZone.js
+++ b/src/zones/IANAZone.js
@@ -97,6 +97,9 @@ export default class IANAZone extends Zone {
    * @return {boolean}
    */
   static isValidZone(zone) {
+    if (!zone) {
+      return false;
+    }
     try {
       new Intl.DateTimeFormat("en-US", { timeZone: zone }).format();
       return true;


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c29f8f80..a2694ceb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,7 +18,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [15.x]
+        node-version: [16.x]
 
     steps:
     - uses: actions/checkout@v2
diff --git a/test/datetime/toFormat.test.js b/test/datetime/toFormat.test.js
index 1d76e384..116a71e5 100644
--- a/test/datetime/toFormat.test.js
+++ b/test/datetime/toFormat.test.js
@@ -35,6 +35,18 @@ test("DateTime#toFormat('u') returns fractional seconds", () => {
   expect(dt.set({ millisecond: 80 }).toFormat("u")).toBe("080"); // I think this is OK
 });
 
+test("DateTime#toFormat('uu') returns fractional seconds as two digits", () => {
+  expect(dt.toFormat("uu")).toBe("12");
+  expect(dt.set({ millisecond: 82 }).toFormat("uu")).toBe("08");
+  expect(dt.set({ millisecond: 789 }).toFormat("uu")).toBe("78");
+});
+
+test("DateTime#toFormat('uuu') returns fractional seconds as one digit", () => {
+  expect(dt.toFormat("uuu")).toBe("1");
+  expect(dt.set({ millisecond: 82 }).toFormat("uuu")).toBe("0");
+  expect(dt.set({ millisecond: 789 }).toFormat("uuu")).toBe("7");
+});
+
 test("DateTime#toFormat('S') returns the millisecond", () => {
   expect(dt.toFormat("S")).toBe("123");
   expect(dt.reconfigure({ locale: "bn" }).toFormat("S")).toBe("১২৩");
diff --git a/test/datetime/tokenParse.test.js b/test/datetime/tokenParse.test.js
index 08708fc2..327222c0 100644
--- a/test/datetime/tokenParse.test.js
+++ b/test/datetime/tokenParse.test.js
@@ -18,6 +18,12 @@ test("DateTime.fromFormat() parses basic times", () => {
   expect(i.millisecond).toBe(445);
 });
 
+test("DateTime.fromFormat() yields Invalid reason 'unparseable' for incompatible formats", () => {
+  const i = DateTime.fromFormat("Mar 3, 2020", "MMM dd, yyyy");
+  expect(i.invalid).not.toBeNull;
+  expect(i.invalid.reason).toEqual("unparsable");
+});
+
 test("DateTime.fromFormat() parses with variable-length input", () => {
   let i = DateTime.fromFormat("1982/05/03 09:07:05.004", "y/M/d H:m:s.S");
   expect(i.year).toBe(1982);
@@ -171,6 +177,14 @@ test("DateTime.fromFormat() parses fractional seconds", () => {
   expect(DateTime.fromFormat("003", "u").millisecond).toBe(3);
   expect(DateTime.fromFormat("1234", "u").millisecond).toBe(123);
   expect(DateTime.fromFormat("1235", "u").millisecond).toBe(123);
+
+  expect(DateTime.fromFormat("1", "uu").millisecond).toBe(100);
+  expect(DateTime.fromFormat("12", "uu").millisecond).toBe(120);
+  expect(DateTime.fromFormat("02", "uu").millisecond).toBe(20);
+  expect(DateTime.fromFormat("-33", "uu").isValid).toBe(false);
+
+  expect(DateTime.fromFormat("1", "uuu").millisecond).toBe(100);
+  expect(DateTime.fromFormat("-2", "uuu").isValid).toBe(false);
 });
 
 test("DateTime.fromFormat() parses weekdays", () => {
@@ -195,6 +209,22 @@ test("DateTime.fromFormat() parses eras", () => {
   expect(dt.year).toEqual(-206);
 });
 
+test("DateTime.fromFormat() parses variable-length days", () => {
+  let i = DateTime.fromFormat("Mar 3, 2020", "MMM d, yyyy");
+  expect(i.day).toBe(3);
+
+  i = DateTime.fromFormat("Mar 13, 2020", "MMM d, yyyy");
+  expect(i.day).toBe(13);
+});
+
+test("DateTime.fromFormat() parses fixed-length days", () => {
+  let i = DateTime.fromFormat("Mar 03, 2020", "MMM dd, yyyy");
+  expect(i.day).toBe(3);
+
+  i = DateTime.fromFormat("Mar 13, 2020", "MMM dd, yyyy");
+  expect(i.day).toBe(13);
+});
+
 test("DateTime.fromFormat() parses standalone month names", () => {
   let i = DateTime.fromFormat("May 25 1982", "LLLL dd yyyy");
   expect(i.year).toBe(1982);
diff --git a/test/zones/IANA.test.js b/test/zones/IANA.test.js
index 81d9138e..62b420ce 100644
--- a/test/zones/IANA.test.js
+++ b/test/zones/IANA.test.js
@@ -29,6 +29,9 @@ test("IANAZone.isValidZone", () => {
   expect(IANAZone.isValidZone("Fantasia/Castle")).toBe(false);
   expect(IANAZone.isValidZone("Sport~~blorp")).toBe(false);
   expect(IANAZone.isValidZone("")).toBe(false);
+  expect(IANAZone.isValidZone(undefined)).toBe(false);
+  expect(IANAZone.isValidZone(null)).toBe(false);
+  expect(IANAZone.isValidZone(4)).toBe(false);
 });
 
 test("IANAZone.parseGMTOffset returns a number for a valid input", () => {