Repository: moment/luxon
Pull request #1246 (2022-07-21 07:17:09)

Bump terser from 5.14.1 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.14.1 to 5.14.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/terser/terser/blob/master/CHANGELOG.md">terser's changelog</a>.</em></p>
<blockquote>
<h2>v5.14.2</h2>
<ul>
<li>Security fix for RegExps that should not be evaluated (regexp DDOS)</li>
<li>Source maps improvements (<a href="https://github-redirect.dependabot.com/terser/terser/issues/1211">#1211</a>)</li>
<li>Performance improvements in long property access evaluation (<a href="https://github-redirect.dependabot.com/terser/terser/issues/1213">#1213</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/terser/terser/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=terser&package-manager=npm_and_yarn&previous-version=5.14.1&new-version=5.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/moment/luxon/network/alerts).

</details>


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d453c85..da77275b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+# 3.0.2 (2022-08-28)
+
+* Lots of doc changes
+* Added DateTime.expandFormat
+* Added support for custom conversion matrices in Durations
+
 # 3.0.1 (2022-07-09)
 
 * Add DateTime.parseFormatForOpts
diff --git a/README.md b/README.md
index c636bc91..5e7a7302 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ See [contributing](CONTRIBUTING.md).
 [test-coverage-url]: https://codecov.io/gh/moment/luxon
 [test-coverage-image]: https://codecov.io/gh/moment/luxon/branch/master/graph/badge.svg
 
-[contributing-url]: https://github.com/moment/luxon/blob/master/contributing.md
+[contributing-url]: https://github.com/moment/luxon/blob/master/CONTRIBUTING.md
 [contributing-image]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
 
 [phasers-image]: https://img.shields.io/badge/phasers-stun-brightgreen.svg
diff --git a/docs/formatting.md b/docs/formatting.md
index c08ee3e1..9306d26d 100644
--- a/docs/formatting.md
+++ b/docs/formatting.md
@@ -102,12 +102,12 @@ Here's the full set of provided presets using the October 14, 1983 at `13:30:23`
 | `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_FULL`              | full date and time with abbreviated named offset                   | `October 14, 1983 at 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 at 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` |
+| `DATETIME_FULL_WITH_SECONDS` | full date and time with abbreviated named offset with seconds      | `October 14, 1983 at 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 at 1:30:23 PM Eastern Daylight Time` | `vendredi 14 octobre 1983 à 13:30:23 heure d’été de l’Est` |
 
 ### Intl
 
diff --git a/docs/parsing.md b/docs/parsing.md
index d1229ee8..d5aa70a9 100644
--- a/docs/parsing.md
+++ b/docs/parsing.md
@@ -236,3 +236,4 @@ Because Luxon was able to parse the string without difficulty, the output is a l
 | 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`      |
 | FF               |              | less short localized date and time with seconds                | `Aug 6, 2014, 1:07:04 PM`   |
+| '                |              | literal start/end, characters between are not tokenized        | `'T'`                       |
diff --git a/package-lock.json b/package-lock.json
index 5e7d9f8d..a405d439 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "luxon",
-  "version": "3.0.1",
+  "version": "3.0.3",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "luxon",
-      "version": "3.0.1",
+      "version": "3.0.3",
       "license": "MIT",
       "devDependencies": {
         "@babel/core": "^7.18.6",
@@ -17,14 +17,14 @@
         "@rollup/plugin-commonjs": "^19.0.0",
         "@rollup/plugin-node-resolve": "^13.0.0",
         "babel-jest": "^28.1.2",
-        "benchmark": "latest",
-        "codecov": "latest",
-        "documentation": "latest",
+        "benchmark": "*",
+        "codecov": "*",
+        "documentation": "*",
         "fs-extra": "^6.0.1",
         "husky": "^7.0.0",
         "jest": "^28.1.0",
         "lint-staged": "^11.0.0",
-        "prettier": "latest",
+        "prettier": "*",
         "rollup": "^2.52.7",
         "rollup-plugin-terser": "^7.0.2",
         "uglify-js": "^3.13.10"
@@ -12932,9 +12932,9 @@
       }
     },
     "node_modules/terser": {
-      "version": "5.14.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz",
-      "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==",
+      "version": "5.14.2",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
+      "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
       "dev": true,
       "dependencies": {
         "@jridgewell/source-map": "^0.3.2",
@@ -23794,9 +23794,9 @@
       }
     },
     "terser": {
-      "version": "5.14.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz",
-      "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==",
+      "version": "5.14.2",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
+      "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
       "dev": true,
       "requires": {
         "@jridgewell/source-map": "^0.3.2",
diff --git a/package.json b/package.json
index 2e9bd6eb..d72f56ce 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "luxon",
-  "version": "3.0.1",
+  "version": "3.0.3",
   "description": "Immutable date wrapper",
   "author": "Isaac Cambron",
   "keywords": [
@@ -12,7 +12,8 @@
     ".": {
       "import": "./src/luxon.js",
       "require": "./build/node/luxon.js"
-    }
+    },
+    "./package.json": "./package.json"
   },
   "scripts": {
     "build": "babel-node tasks/buildAll.js",
diff --git a/src/datetime.js b/src/datetime.js
index 6f1ea1e6..a9beffed 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -23,7 +23,12 @@ import {
 import { normalizeZone } from "./impl/zoneUtil.js";
 import diff from "./impl/diff.js";
 import { parseRFC2822Date, parseISODate, parseHTTPDate, parseSQL } from "./impl/regexParser.js";
-import { parseFromTokens, explainFromTokens, formatOptsToTokens } from "./impl/tokenParser.js";
+import {
+  parseFromTokens,
+  explainFromTokens,
+  formatOptsToTokens,
+  expandMacroTokens,
+} from "./impl/tokenParser.js";
 import {
   gregorianToWeek,
   weekToGregorian,
@@ -413,7 +418,7 @@ function lastOpts(argList) {
  *
  * Here is a brief overview of the most commonly used functionality it provides:
  *
- * * **Creation**: To create a DateTime from its components, use one of its factory class methods: {@link DateTime#local}, {@link DateTime#utc}, and (most flexibly) {@link DateTime#fromObject}. To create one from a standard string format, use {@link DateTime#fromISO}, {@link DateTime#fromHTTP}, and {@link DateTime#fromRFC2822}. To create one from a custom string format, use {@link DateTime#fromFormat}. To create one from a native JS date, use {@link DateTime#fromJSDate}.
+ * * **Creation**: To create a DateTime from its components, use one of its factory class methods: {@link DateTime.local}, {@link DateTime.utc}, and (most flexibly) {@link DateTime.fromObject}. To create one from a standard string format, use {@link DateTime.fromISO}, {@link DateTime.fromHTTP}, and {@link DateTime.fromRFC2822}. To create one from a custom string format, use {@link DateTime.fromFormat}. To create one from a native JS date, use {@link DateTime.fromJSDate}.
  * * **Gregorian calendar and time**: To examine the Gregorian properties of a DateTime individually (i.e as opposed to collectively through {@link DateTime#toObject}), use the {@link DateTime#year}, {@link DateTime#month},
  * {@link DateTime#day}, {@link DateTime#hour}, {@link DateTime#minute}, {@link DateTime#second}, {@link DateTime#millisecond} accessors.
  * * **Week calendar**: For ISO week calendar attributes, see the {@link DateTime#weekYear}, {@link DateTime#weekNumber}, and {@link DateTime#weekday} accessors.
@@ -927,6 +932,18 @@ export default class DateTime {
     return !tokenList ? null : tokenList.map((t) => (t ? t.val : null)).join("");
   }
 
+  /**
+   * Produce the the fully expanded format token for the locale
+   * Does NOT quote characters, so quoted tokens will not round trip correctly
+   * @param fmt
+   * @param localeOpts
+   * @returns {string}
+   */
+  static expandFormat(fmt, localeOpts = {}) {
+    const expanded = expandMacroTokens(Formatter.parseFormat(fmt), Locale.fromObject(localeOpts));
+    return expanded.map((t) => t.val).join("");
+  }
+
   // INFO
 
   /**
@@ -1536,8 +1553,8 @@ export default class DateTime {
    * @param {Object} opts - opts to override the configuration options on this DateTime
    * @example DateTime.now().toLocaleString(); //=> 4/20/2017
    * @example DateTime.now().setLocale('en-gb').toLocaleString(); //=> '20/04/2017'
-   * @example DateTime.now().toLocaleString({ locale: 'en-gb' }); //=> '20/04/2017'
    * @example DateTime.now().toLocaleString(DateTime.DATE_FULL); //=> 'April 20, 2017'
+   * @example DateTime.now().toLocaleString(DateTime.DATE_FULL, { locale: 'fr' }); //=> '28 août 2022'
    * @example DateTime.now().toLocaleString(DateTime.TIME_SIMPLE); //=> '11:32 AM'
    * @example DateTime.now().toLocaleString(DateTime.DATETIME_SHORT); //=> '4/20/2017, 11:32 AM'
    * @example DateTime.now().toLocaleString({ weekday: 'long', month: 'long', day: '2-digit' }); //=> 'Thursday, April 20'
@@ -1576,7 +1593,7 @@ export default class DateTime {
    * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0
    * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0
    * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'
-   * @param {boolean} [opts.extendedZone=true] - add the time zone format extension
+   * @param {boolean} [opts.extendedZone=false] - add the time zone format extension
    * @param {string} [opts.format='extended'] - choose between the basic and extended format
    * @example DateTime.utc(1983, 5, 25).toISO() //=> '1982-05-25T00:00:00.000Z'
    * @example DateTime.now().toISO() //=> '2017-04-22T20:47:05.335-04:00'
diff --git a/src/duration.js b/src/duration.js
index 362986ae..21dcf5fb 100644
--- a/src/duration.js
+++ b/src/duration.js
@@ -120,6 +120,7 @@ function clone(dur, alts, clear = false) {
     values: clear ? alts.values : { ...dur.values, ...(alts.values || {}) },
     loc: dur.loc.clone(alts.loc),
     conversionAccuracy: alts.conversionAccuracy || dur.conversionAccuracy,
+    matrix: alts.matrix || dur.matrix,
   };
   return new Duration(conf);
 }
@@ -159,7 +160,7 @@ function normalizeValues(matrix, vals) {
  *
  * Here is a brief overview of commonly used methods and getters in Duration:
  *
- * * **Creation** To create a Duration, use {@link Duration#fromMillis}, {@link Duration#fromObject}, or {@link Duration#fromISO}.
+ * * **Creation** To create a Duration, use {@link Duration.fromMillis}, {@link Duration.fromObject}, or {@link Duration.fromISO}.
  * * **Unit values** See the {@link Duration#years}, {@link Duration#months}, {@link Duration#weeks}, {@link Duration#days}, {@link Duration#hours}, {@link Duration#minutes}, {@link Duration#seconds}, {@link Duration#milliseconds} accessors.
  * * **Configuration** See  {@link Duration#locale} and {@link Duration#numberingSystem} accessors.
  * * **Transformation** To create new Durations out of old ones use {@link Duration#plus}, {@link Duration#minus}, {@link Duration#normalize}, {@link Duration#set}, {@link Duration#reconfigure}, {@link Duration#shiftTo}, and {@link Duration#negate}.
@@ -173,6 +174,12 @@ export default class Duration {
    */
   constructor(config) {
     const accurate = config.conversionAccuracy === "longterm" || false;
+    let matrix = accurate ? accurateMatrix : casualMatrix;
+
+    if (config.matrix) {
+      matrix = config.matrix;
+    }
+
     /**
      * @access private
      */
@@ -192,7 +199,7 @@ export default class Duration {
     /**
      * @access private
      */
-    this.matrix = accurate ? accurateMatrix : casualMatrix;
+    this.matrix = matrix;
     /**
      * @access private
      */
@@ -228,7 +235,8 @@ export default class Duration {
    * @param {Object} [opts=[]] - options for creating this Duration
    * @param {string} [opts.locale='en-US'] - the locale to use
    * @param {string} opts.numberingSystem - the numbering system to use
-   * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
+   * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use
+   * @param {string} [opts.matrix=Object] - the custom conversion system to use
    * @return {Duration}
    */
   static fromObject(obj, opts = {}) {
@@ -244,6 +252,7 @@ export default class Duration {
       values: normalizeObject(obj, Duration.normalizeUnit),
       loc: Locale.fromObject(opts),
       conversionAccuracy: opts.conversionAccuracy,
+      matrix: opts.matrix,
     });
   }
 
@@ -277,7 +286,8 @@ export default class Duration {
    * @param {Object} opts - options for parsing
    * @param {string} [opts.locale='en-US'] - the locale to use
    * @param {string} opts.numberingSystem - the numbering system to use
-   * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
+   * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use
+   * @param {string} [opts.matrix=Object] - the preset conversion system to use
    * @see https://en.wikipedia.org/wiki/ISO_8601#Durations
    * @example Duration.fromISO('P3Y6M1W4DT12H30M5S').toObject() //=> { years: 3, months: 6, weeks: 1, days: 4, hours: 12, minutes: 30, seconds: 5 }
    * @example Duration.fromISO('PT23H').toObject() //=> { hours: 23 }
@@ -299,7 +309,8 @@ export default class Duration {
    * @param {Object} opts - options for parsing
    * @param {string} [opts.locale='en-US'] - the locale to use
    * @param {string} opts.numberingSystem - the numbering system to use
-   * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
+   * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use
+   * @param {string} [opts.matrix=Object] - the conversion system to use
    * @see https://en.wikipedia.org/wiki/ISO_8601#Times
    * @example Duration.fromISOTime('11:22:33.444').toObject() //=> { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 }
    * @example Duration.fromISOTime('11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
@@ -659,14 +670,9 @@ export default class Duration {
    * @example dur.reconfigure({ locale: 'en-GB' })
    * @return {Duration}
    */
-  reconfigure({ locale, numberingSystem, conversionAccuracy } = {}) {
-    const loc = this.loc.clone({ locale, numberingSystem }),
-      opts = { loc };
-
-    if (conversionAccuracy) {
-      opts.conversionAccuracy = conversionAccuracy;
-    }
-
+  reconfigure({ locale, numberingSystem, conversionAccuracy, matrix } = {}) {
+    const loc = this.loc.clone({ locale, numberingSystem });
+    const opts = { loc, matrix, conversionAccuracy };
     return clone(this, opts);
   }
 
diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js
index 60344684..30674308 100644
--- a/src/impl/tokenParser.js
+++ b/src/impl/tokenParser.js
@@ -392,7 +392,7 @@ function maybeExpandMacroToken(token, locale) {
   return tokens;
 }
 
-function expandMacroTokens(tokens, locale) {
+export function expandMacroTokens(tokens, locale) {
   return Array.prototype.concat(...tokens.map((t) => maybeExpandMacroToken(t, locale)));
 }
 
diff --git a/src/interval.js b/src/interval.js
index 5c79bb77..c68a3e56 100644
--- a/src/interval.js
+++ b/src/interval.js
@@ -27,10 +27,10 @@ function validateStartEnd(start, end) {
  *
  * Here is a brief overview of the most commonly used methods and getters in Interval:
  *
- * * **Creation** To create an Interval, use {@link Interval#fromDateTimes}, {@link Interval#after}, {@link Interval#before}, or {@link Interval#fromISO}.
+ * * **Creation** To create an Interval, use {@link Interval.fromDateTimes}, {@link Interval.after}, {@link Interval.before}, or {@link Interval.fromISO}.
  * * **Accessors** Use {@link Interval#start} and {@link Interval#end} to get the start and end.
  * * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame}, {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}.
- * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval#merge}, {@link Interval#xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}.
+ * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval.merge}, {@link Interval.xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}.
  * * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs}
  * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}.
  */
diff --git a/src/luxon.js b/src/luxon.js
index caec9023..b82c7ef0 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.0.1";
+const VERSION = "3.0.3";
 
 export {
   VERSION,


Test patch:
diff --git a/test/datetime/math.test.js b/test/datetime/math.test.js
index 546c9ae4..848d5ba6 100644
--- a/test/datetime/math.test.js
+++ b/test/datetime/math.test.js
@@ -1,6 +1,7 @@
 /* global test expect */
 
 import { DateTime, Duration } from "../../src/luxon";
+import { casualMatrix } from "../../src/duration";
 
 function createDateTime() {
   return DateTime.fromObject({
diff --git a/test/datetime/tokenParse.test.js b/test/datetime/tokenParse.test.js
index d0ea1912..285e0229 100644
--- a/test/datetime/tokenParse.test.js
+++ b/test/datetime/tokenParse.test.js
@@ -1141,3 +1141,17 @@ test("DateTime.parseFormatForOpts returns a parsing format", () => {
   const format = DateTime.parseFormatForOpts("");
   expect(format).toBeNull();
 });
+
+//------
+// .expandFormat
+//-------
+//
+test("DateTime.expandFormat works with the default locale", () => {
+  const format = DateTime.expandFormat("D");
+  expect(format).toBe("M/d/yyyyy");
+});
+
+test("DateTime.expandFormat works with other locales", () => {
+  const format = DateTime.expandFormat("D", { locale: "en-gb" });
+  expect(format).toBe("d/M/yyyyy");
+});
diff --git a/test/duration/customMatrix.test.js b/test/duration/customMatrix.test.js
new file mode 100644
index 00000000..a1d06d25
--- /dev/null
+++ b/test/duration/customMatrix.test.js
@@ -0,0 +1,49 @@
+/* global test expect */
+
+import { Duration } from "../../src/luxon";
+import { casualMatrix } from "../../src/duration";
+
+const businessMatrix = {
+  ...casualMatrix,
+  months: {
+    weeks: 4,
+    days: 22,
+    hours: 22 * 7,
+    minutes: 22 * 7 * 60,
+    seconds: 22 * 7 * 60 * 60,
+    milliseconds: 22 * 7 * 60 * 60 * 1000,
+  },
+  weeks: {
+    days: 5,
+    hours: 5 * 7,
+    minutes: 5 * 7 * 60,
+    seconds: 5 * 7 * 60 * 60,
+    milliseconds: 5 * 7 * 60 * 60 * 1000,
+  },
+  days: {
+    hours: 7,
+    minutes: 7 * 60,
+    seconds: 7 * 60 * 60,
+    milliseconds: 7 * 60 * 60 * 1000,
+  },
+};
+
+const convert = (amt, from, to) =>
+  Duration.fromObject({ [from]: amt }, { matrix: businessMatrix }).as(to);
+
+test("One day is made of 7 hours", () => {
+  expect(convert(1, "days", "hours")).toBeCloseTo(7, 4);
+  expect(convert(7, "hours", "days")).toBeCloseTo(1, 4);
+});
+
+test("One and a half week is made of 7 days 3 hours and 30 minutes", () => {
+  const dur = Duration.fromObject({ weeks: 1.5 }, { matrix: businessMatrix }).shiftTo(
+    "days",
+    "hours",
+    "minutes"
+  );
+
+  expect(dur.days).toBeCloseTo(7, 4);
+  expect(dur.hours).toBeCloseTo(3, 4);
+  expect(dur.minutes).toBeCloseTo(30, 4);
+});