Repository: moment/luxon
Pull request #1499 (2023-09-05 15:19:47)

Added some JS doc for time zones
Had to look at the sources to get an idea of what the IANA zones and the UTC zones are really doing. Maybe this helps.

linux-foundation-easycla[bot]:  <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1499/#/?version=2"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-missing-id.svg" alt="CLA Missing ID" align="left" height="28" width="328"></a> <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1499/#/?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> :x: The email address for the commit (35ff59507a4af0fd4a40f43b96944dd65554b4af) is not linked to the GitHub account, preventing the EasyCLA check. Consult <a href='https://confluence.linuxfoundation.org/pages/viewpage.action?pageId=86641302' target='_blank'>this Help Article</a> and <a href='https://help.github.com/en/github/committing-changes-to-your-project/why-are-my-commits-linked-to-the-wrong-user' target='_blank'>GitHub Help</a> to resolve. (To view the commit's email address, add .patch at the end of this PR page's URL.) 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>
ptandler: @icambron I added some documentation that I was missing when working with Luxon. Might be helpful for others as well.

I just amended the commit such that it uses my github email address and the CLA check now succeeded.

Patch:
diff --git a/benchmarks/datetime.js b/benchmarks/datetime.js
index 39d6bf07..f4dffda7 100644
--- a/benchmarks/datetime.js
+++ b/benchmarks/datetime.js
@@ -8,8 +8,10 @@ function runDateTimeSuite() {
 
     const dt = DateTime.now();
 
+    const formatParser = DateTime.buildFormatParser("yyyy/MM/dd HH:mm:ss.SSS");
+
     suite
-      .add("DateTime.local", () => {
+      .add("DateTime.now", () => {
         DateTime.now();
       })
       .add("DateTime.fromObject with locale", () => {
@@ -18,6 +20,9 @@ function runDateTimeSuite() {
       .add("DateTime.local with numbers", () => {
         DateTime.local(2017, 5, 15);
       })
+      .add("DateTime.local with numbers and zone", () => {
+        DateTime.local(2017, 5, 15, 11, 7, 35, { zone: "America/New_York" });
+      })
       .add("DateTime.fromISO", () => {
         DateTime.fromISO("1982-05-25T09:10:11.445Z");
       })
@@ -32,6 +37,14 @@ function runDateTimeSuite() {
           zone: "America/Los_Angeles",
         });
       })
+      .add("DateTime.fromFormatParser", () => {
+        DateTime.fromFormatParser("1982/05/25 09:10:11.445", formatParser);
+      })
+      .add("DateTime.fromFormatParser with zone", () => {
+        DateTime.fromFormatParser("1982/05/25 09:10:11.445", formatParser, {
+          zone: "America/Los_Angeles",
+        });
+      })
       .add("DateTime#setZone", () => {
         dt.setZone("America/Los_Angeles");
       })
diff --git a/docs/formatting.md b/docs/formatting.md
index 6ad02c3e..b266520c 100644
--- a/docs/formatting.md
+++ b/docs/formatting.md
@@ -86,27 +86,28 @@ 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_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`                                     |
+| 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_MED_WITH_WEEKDAY`  | abbreviated date & time with abbreviated weekday                   | `Fri, Oct 14, 1983, 1:30 PM`                                   | `ven. 14 oct. 1983 à 13:30`                                |
 | `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_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 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` |
 
diff --git a/site/index.html b/site/index.html
index d44cfb86..98f9527a 100644
--- a/site/index.html
+++ b/site/index.html
@@ -26,6 +26,7 @@
   <script src="https://unpkg.com/docsify/lib/plugins/search.min.js"></script>
   <script src="global/luxon.js"></script>
   <script>
+    var DateTime = luxon.DateTime;
     console.log(
       "You can try Luxon right here using the `luxon` global, like `luxon.DateTime.now()`."
     );
diff --git a/src/datetime.js b/src/datetime.js
index 9472cb53..5314a9b3 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -28,6 +28,7 @@ import {
   explainFromTokens,
   formatOptsToTokens,
   expandMacroTokens,
+  TokenParser,
 } from "./impl/tokenParser.js";
 import {
   gregorianToWeek,
@@ -373,8 +374,12 @@ function normalizeUnitWithLocalWeeks(unit) {
 // but doesn't do any validation, makes a bunch of assumptions about what units
 // are present, and so on.
 function quickDT(obj, opts) {
-  const zone = normalizeZone(opts.zone, Settings.defaultZone),
-    loc = Locale.fromObject(opts),
+  const zone = normalizeZone(opts.zone, Settings.defaultZone);
+  if (!zone.isValid) {
+    return DateTime.invalid(unsupportedZone(zone));
+  }
+
+  const loc = Locale.fromObject(opts),
     tsNow = Settings.now();
 
   let ts, o;
@@ -487,7 +492,9 @@ export default class DateTime {
       if (unchanged) {
         [c, o] = [config.old.c, config.old.o];
       } else {
-        const ot = zone.offset(this.ts);
+        // If an offset has been passed and we have not been called from
+        // clone(), we can trust it and avoid the offset calculation.
+        const ot = isNumber(config.o) && !config.old ? config.o : zone.offset(this.ts);
         c = tsToObj(this.ts, ot);
         invalid = Number.isNaN(c.year) ? new Invalid("invalid input") : null;
         c = invalid ? null : c;
@@ -2229,6 +2236,74 @@ export default class DateTime {
     return DateTime.fromFormatExplain(text, fmt, options);
   }
 
+  /**
+   * Build a parser for `fmt` using the given locale. This parser can be passed
+   * to {@link DateTime.fromFormatParser} to a parse a date in this format. This
+   * can be used to optimize cases where many dates need to be parsed in a
+   * specific format.
+   *
+   * @param {String} fmt - the format the string is expected to be in (see
+   * description)
+   * @param {Object} options - options used to set locale and numberingSystem
+   * for parser
+   * @returns {TokenParser} - opaque object to be used
+   */
+  static buildFormatParser(fmt, options = {}) {
+    const { locale = null, numberingSystem = null } = options,
+      localeToUse = Locale.fromOpts({
+        locale,
+        numberingSystem,
+        defaultToEN: true,
+      });
+    return new TokenParser(localeToUse, fmt);
+  }
+
+  /**
+   * Create a DateTime from an input string and format parser.
+   *
+   * The format parser must have been created with the same locale as this call.
+   *
+   * @param {String} text - the string to parse
+   * @param {TokenParser} formatParser - parser from {@link DateTime.buildFormatParser}
+   * @param {Object} opts - options taken by fromFormat()
+   * @returns {DateTime}
+   */
+  static fromFormatParser(text, formatParser, opts = {}) {
+    if (isUndefined(text) || isUndefined(formatParser)) {
+      throw new InvalidArgumentError(
+        "fromFormatParser requires an input string and a format parser"
+      );
+    }
+    const { locale = null, numberingSystem = null } = opts,
+      localeToUse = Locale.fromOpts({
+        locale,
+        numberingSystem,
+        defaultToEN: true,
+      });
+
+    if (!localeToUse.equals(formatParser.locale)) {
+      throw new InvalidArgumentError(
+        `fromFormatParser called with a locale of ${localeToUse}, ` +
+          `but the format parser was created for ${formatParser.locale}`
+      );
+    }
+
+    const { result, zone, specificOffset, invalidReason } = formatParser.explainFromTokens(text);
+
+    if (invalidReason) {
+      return DateTime.invalid(invalidReason);
+    } else {
+      return parseDataToDateTime(
+        result,
+        zone,
+        opts,
+        `format ${formatParser.format}`,
+        text,
+        specificOffset
+      );
+    }
+  }
+
   // FORMAT PRESETS
 
   /**
diff --git a/src/impl/digits.js b/src/impl/digits.js
index 6ac1bae0..c8c68af0 100644
--- a/src/impl/digits.js
+++ b/src/impl/digits.js
@@ -70,6 +70,21 @@ export function parseDigits(str) {
   }
 }
 
+// cache of {numberingSystem: {append: regex}}
+let digitRegexCache = {};
+export function resetDigitRegexCache() {
+  digitRegexCache = {};
+}
+
 export function digitRegex({ numberingSystem }, append = "") {
-  return new RegExp(`${numberingSystems[numberingSystem || "latn"]}${append}`);
+  const ns = numberingSystem || "latn";
+
+  if (!digitRegexCache[ns]) {
+    digitRegexCache[ns] = {};
+  }
+  if (!digitRegexCache[ns][append]) {
+    digitRegexCache[ns][append] = new RegExp(`${numberingSystems[ns]}${append}`);
+  }
+
+  return digitRegexCache[ns][append];
 }
diff --git a/src/impl/locale.js b/src/impl/locale.js
index f1caf149..93c489b0 100644
--- a/src/impl/locale.js
+++ b/src/impl/locale.js
@@ -340,7 +340,7 @@ export default class Locale {
 
   static create(locale, numberingSystem, outputCalendar, weekSettings, defaultToEN = false) {
     const specifiedLocale = locale || Settings.defaultLocale;
-    // the system locale is useful for human readable strings but annoying for parsing/formatting known formats
+    // the system locale is useful for human-readable strings but annoying for parsing/formatting known formats
     const localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale());
     const numberingSystemR = numberingSystem || Settings.defaultNumberingSystem;
     const outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;
@@ -539,4 +539,8 @@ export default class Locale {
       this.outputCalendar === other.outputCalendar
     );
   }
+
+  toString() {
+    return `Locale(${this.locale}, ${this.numberingSystem}, ${this.outputCalendar})`;
+  }
 }
diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js
index 8dd38f37..48a7595e 100644
--- a/src/impl/tokenParser.js
+++ b/src/impl/tokenParser.js
@@ -432,27 +432,59 @@ export function expandMacroTokens(tokens, locale) {
  * @private
  */
 
-export function explainFromTokens(locale, input, format) {
-  const tokens = expandMacroTokens(Formatter.parseFormat(format), locale),
-    units = tokens.map((t) => unitForToken(t, locale)),
-    disqualifyingUnit = units.find((t) => t.invalidReason);
+export class TokenParser {
+  constructor(locale, format) {
+    this.locale = locale;
+    this.format = format;
+    this.tokens = expandMacroTokens(Formatter.parseFormat(format), locale);
+    this.units = this.tokens.map((t) => unitForToken(t, locale));
+    this.disqualifyingUnit = this.units.find((t) => t.invalidReason);
+
+    if (!this.disqualifyingUnit) {
+      const [regexString, handlers] = buildRegex(this.units);
+      this.regex = RegExp(regexString, "i");
+      this.handlers = handlers;
+    }
+  }
 
-  if (disqualifyingUnit) {
-    return { input, tokens, invalidReason: disqualifyingUnit.invalidReason };
-  } else {
-    const [regexString, handlers] = buildRegex(units),
-      regex = RegExp(regexString, "i"),
-      [rawMatches, matches] = match(input, regex, handlers),
-      [result, zone, specificOffset] = matches
-        ? dateTimeFromMatches(matches)
-        : [null, null, undefined];
-    if (hasOwnProperty(matches, "a") && hasOwnProperty(matches, "H")) {
-      throw new ConflictingSpecificationError(
-        "Can't include meridiem when specifying 24-hour format"
-      );
+  explainFromTokens(input) {
+    if (!this.isValid) {
+      return { input, tokens: this.tokens, invalidReason: this.invalidReason };
+    } else {
+      const [rawMatches, matches] = match(input, this.regex, this.handlers),
+        [result, zone, specificOffset] = matches
+          ? dateTimeFromMatches(matches)
+          : [null, null, undefined];
+      if (hasOwnProperty(matches, "a") && hasOwnProperty(matches, "H")) {
+        throw new ConflictingSpecificationError(
+          "Can't include meridiem when specifying 24-hour format"
+        );
+      }
+      return {
+        input,
+        tokens: this.tokens,
+        regex: this.regex,
+        rawMatches,
+        matches,
+        result,
+        zone,
+        specificOffset,
+      };
     }
-    return { input, tokens, regex, rawMatches, matches, result, zone, specificOffset };
   }
+
+  get isValid() {
+    return !this.disqualifyingUnit;
+  }
+
+  get invalidReason() {
+    return this.disqualifyingUnit ? this.disqualifyingUnit.invalidReason : null;
+  }
+}
+
+export function explainFromTokens(locale, input, format) {
+  const parser = new TokenParser(locale, format);
+  return parser.explainFromTokens(input);
 }
 
 export function parseFromTokens(locale, input, format) {
diff --git a/src/impl/util.js b/src/impl/util.js
index 40519244..a9b548c3 100644
--- a/src/impl/util.js
+++ b/src/impl/util.js
@@ -287,6 +287,13 @@ export function normalizeObject(obj, normalizer) {
   return normalized;
 }
 
+/**
+ * Returns the offset's value as a string
+ * @param {number} ts - Epoch milliseconds for which to get the offset
+ * @param {string} format - What style of offset to return.
+ *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
+ * @return {string}
+ */
 export function formatOffset(offset, format) {
   const hours = Math.trunc(Math.abs(offset / 60)),
     minutes = Math.trunc(Math.abs(offset % 60)),
diff --git a/src/interval.js b/src/interval.js
index dfb58d06..6073d028 100644
--- a/src/interval.js
+++ b/src/interval.js
@@ -405,7 +405,7 @@ export default class Interval {
   }
 
   /**
-   * Return whether this Interval engulfs the start and end of the specified Interval.
+   * Returns true if this Interval fully contains the specified Interval, specifically if the intersect (of this Interval and the other Interval) is equal to the other Interval; false otherwise.
    * @param {Interval} other
    * @return {boolean}
    */
diff --git a/src/settings.js b/src/settings.js
index 462e71e6..f109708d 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -4,6 +4,7 @@ import Locale from "./impl/locale.js";
 
 import { normalizeZone } from "./impl/zoneUtil.js";
 import { validateWeekSettings } from "./impl/util.js";
+import { resetDigitRegexCache } from "./impl/digits.js";
 
 let now = () => Date.now(),
   defaultZone = "system",
@@ -171,5 +172,6 @@ export default class Settings {
   static resetCaches() {
     Locale.resetCache();
     IANAZone.resetCache();
+    resetDigitRegexCache();
   }
 }
diff --git a/src/zone.js b/src/zone.js
index cec0e4fd..f91a02ee 100644
--- a/src/zone.js
+++ b/src/zone.js
@@ -22,6 +22,12 @@ export default class Zone {
     throw new ZoneIsAbstractError();
   }
 
+  /**
+   * The IANA name of this zone.
+   * Defaults to `name` if not overwritten by a subclass.
+   * @abstract
+   * @type {string}
+   */
   get ianaName() {
     return this.name;
   }
diff --git a/src/zones/IANAZone.js b/src/zones/IANAZone.js
index ad594513..ae66f45a 100644
--- a/src/zones/IANAZone.js
+++ b/src/zones/IANAZone.js
@@ -83,7 +83,7 @@ export default class IANAZone extends Zone {
    * @param {string} s - The string to check validity on
    * @example IANAZone.isValidSpecifier("America/New_York") //=> true
    * @example IANAZone.isValidSpecifier("Sport~~blorp") //=> false
-   * @deprecated This method returns false for some valid IANA names. Use isValidZone instead.
+   * @deprecated For backward compatibility, this forwards to isValidZone, better use `isValidZone()` directly instead.
    * @return {boolean}
    */
   static isValidSpecifier(s) {
@@ -118,32 +118,65 @@ export default class IANAZone extends Zone {
     this.valid = IANAZone.isValidZone(name);
   }
 
-  /** @override **/
+  /**
+   * The type of zone. `iana` for all instances of `IANAZone`.
+   * @override
+   * @type {string}
+   */
   get type() {
     return "iana";
   }
 
-  /** @override **/
+  /**
+   * The name of this zone (i.e. the IANA zone name).
+   * @override
+   * @type {string}
+   */
   get name() {
     return this.zoneName;
   }
 
-  /** @override **/
+  /**
+   * Returns whether the offset is known to be fixed for the whole year:
+   * Always returns false for all IANA zones.
+   * @override
+   * @type {boolean}
+   */
   get isUniversal() {
     return false;
   }
 
-  /** @override **/
+  /**
+   * Returns the offset's common name (such as EST) at the specified timestamp
+   * @override
+   * @param {number} ts - Epoch milliseconds for which to get the name
+   * @param {Object} opts - Options to affect the format
+   * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'.
+   * @param {string} opts.locale - What locale to return the offset name in.
+   * @return {string}
+   */
   offsetName(ts, { format, locale }) {
     return parseZoneInfo(ts, format, locale, this.name);
   }
 
-  /** @override **/
+  /**
+   * Returns the offset's value as a string
+   * @override
+   * @param {number} ts - Epoch milliseconds for which to get the offset
+   * @param {string} format - What style of offset to return.
+   *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
+   * @return {string}
+   */
   formatOffset(ts, format) {
     return formatOffset(this.offset(ts), format);
   }
 
-  /** @override **/
+  /**
+   * Return the offset in minutes for this zone at the specified timestamp.
+   * @override
+   * @param {number} ts - Epoch milliseconds for which to compute the offset
+   * @return {number}
+   */
   offset(ts) {
     const date = new Date(ts);
 
@@ -177,12 +210,21 @@ export default class IANAZone extends Zone {
     return (asUTC - asTS) / (60 * 1000);
   }
 
-  /** @override **/
+  /**
+   * Return whether this Zone is equal to another zone
+   * @override
+   * @param {Zone} otherZone - the zone to compare
+   * @return {boolean}
+   */
   equals(otherZone) {
     return otherZone.type === "iana" && otherZone.name === this.name;
   }
 
-  /** @override **/
+  /**
+   * Return whether this Zone is valid.
+   * @override
+   * @type {boolean}
+   */
   get isValid() {
     return this.valid;
   }
diff --git a/src/zones/fixedOffsetZone.js b/src/zones/fixedOffsetZone.js
index dcfa25a1..b1ab4ab1 100644
--- a/src/zones/fixedOffsetZone.js
+++ b/src/zones/fixedOffsetZone.js
@@ -52,16 +52,31 @@ export default class FixedOffsetZone extends Zone {
     this.fixed = offset;
   }
 
-  /** @override **/
+  /**
+   * The type of zone. `fixed` for all instances of `FixedOffsetZone`.
+   * @override
+   * @type {string}
+   */
   get type() {
     return "fixed";
   }
 
-  /** @override **/
+  /**
+   * The name of this zone.
+   * All fixed zones' names always start with "UTC" (plus optional offset)
+   * @override
+   * @type {string}
+   */
   get name() {
     return this.fixed === 0 ? "UTC" : `UTC${formatOffset(this.fixed, "narrow")}`;
   }
 
+  /**
+   * The IANA name of this zone, i.e. `Etc/UTC` or `Etc/GMT+/-nn`
+   *
+   * @override
+   * @type {string}
+   */
   get ianaName() {
     if (this.fixed === 0) {
       return "Etc/UTC";
@@ -70,32 +85,65 @@ export default class FixedOffsetZone extends Zone {
     }
   }
 
-  /** @override **/
+  /**
+   * Returns the offset's common name at the specified timestamp.
+   *
+   * For fixed offset zones this equals to the zone name.
+   * @override
+   */
   offsetName() {
     return this.name;
   }
 
-  /** @override **/
+  /**
+   * Returns the offset's value as a string
+   * @override
+   * @param {number} ts - Epoch milliseconds for which to get the offset
+   * @param {string} format - What style of offset to return.
+   *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
+   * @return {string}
+   */
   formatOffset(ts, format) {
     return formatOffset(this.fixed, format);
   }
 
-  /** @override **/
+  /**
+   * Returns whether the offset is known to be fixed for the whole year:
+   * Always returns true for all fixed offset zones.
+   * @override
+   * @type {boolean}
+   */
   get isUniversal() {
     return true;
   }
 
-  /** @override **/
+  /**
+   * Return the offset in minutes for this zone at the specified timestamp.
+   *
+   * For fixed offset zones, this is constant and does not depend on a timestamp.
+   * @override
+   * @return {number}
+   */
   offset() {
     return this.fixed;
   }
 
-  /** @override **/
+  /**
+   * Return whether this Zone is equal to another zone (i.e. also fixed and same offset)
+   * @override
+   * @param {Zone} otherZone - the zone to compare
+   * @return {boolean}
+   */
   equals(otherZone) {
     return otherZone.type === "fixed" && otherZone.fixed === this.fixed;
   }
 
-  /** @override **/
+  /**
+   * Return whether this Zone is valid:
+   * All fixed offset zones are valid.
+   * @override
+   * @type {boolean}
+   */
   get isValid() {
     return true;
   }


Test patch:
diff --git a/test/datetime/invalid.test.js b/test/datetime/invalid.test.js
index d13fb864..2fed5bbd 100644
--- a/test/datetime/invalid.test.js
+++ b/test/datetime/invalid.test.js
@@ -5,7 +5,11 @@ import { DateTime, Settings } from "../../src/luxon";
 const organic1 = DateTime.utc(2014, 13, 33),
   // not an actual Wednesday
   organic2 = DateTime.fromObject({ weekday: 3, year: 1982, month: 5, day: 25 }, { zone: "UTC" }),
-  organic3 = DateTime.fromObject({ year: 1982, month: 5, day: 25, hour: 27 });
+  organic3 = DateTime.fromObject({ year: 1982, month: 5, day: 25, hour: 27 }),
+  organic4 = DateTime.fromObject(
+    { year: 1982, month: 5, day: 25, hour: 2 },
+    { zone: "America/Lasers" }
+  );
 
 test("Explicitly invalid dates are invalid", () => {
   const dt = DateTime.invalid("just because", "seriously, just because");
@@ -22,14 +26,28 @@ test("Invalid creations are invalid", () => {
 
 test("invalid zones result in invalid dates", () => {
   expect(DateTime.now().setZone("America/Lasers").isValid).toBe(false);
+  expect(DateTime.now().setZone("America/Lasers").invalidReason).toBe("unsupported zone");
+
   expect(DateTime.local({ zone: "America/Lasers" }).isValid).toBe(false);
+  expect(DateTime.local({ zone: "America/Lasers" }).invalidReason).toBe("unsupported zone");
+
+  expect(DateTime.local(1982, { zone: "America/Lasers" }).isValid).toBe(false);
+  expect(DateTime.local(1982, { zone: "America/Lasers" }).invalidReason).toBe("unsupported zone");
+
   expect(DateTime.fromJSDate(new Date(), { zone: "America/Lasers" }).isValid).toBe(false);
+  expect(DateTime.fromJSDate(new Date(), { zone: "America/Lasers" }).invalidReason).toBe(
+    "unsupported zone"
+  );
+
+  expect(DateTime.fromMillis(0, { zone: "America/Lasers" }).isValid).toBe(false);
+  expect(DateTime.fromMillis(0, { zone: "America/Lasers" }).invalidReason).toBe("unsupported zone");
 });
 
 test("Invalid DateTimes tell you why", () => {
   expect(organic1.invalidReason).toBe("unit out of range");
   expect(organic2.invalidReason).toBe("mismatched weekday");
   expect(organic3.invalidReason).toBe("unit out of range");
+  expect(organic4.invalidReason).toBe("unsupported zone");
 });
 
 test("Invalid DateTimes can provide an extended explanation", () => {
diff --git a/test/datetime/proto.test.js b/test/datetime/proto.test.js
index bbe132dc..e3c7bd03 100644
--- a/test/datetime/proto.test.js
+++ b/test/datetime/proto.test.js
@@ -4,6 +4,8 @@ import { DateTime } from "../../src/luxon";
 test("DateTime prototype properties should not throw when accessed", () => {
   const d = DateTime.now();
   expect(() =>
-    Object.getOwnPropertyNames(d.__proto__).forEach((name) => d.__proto__[name])
+    Object.getOwnPropertyNames(Object.getPrototypeOf(d)).forEach(
+      (name) => Object.getPrototypeOf(d)[name]
+    )
   ).not.toThrow();
 });
diff --git a/test/datetime/tokenParse.test.js b/test/datetime/tokenParse.test.js
index 4025821d..8b5c6a8d 100644
--- a/test/datetime/tokenParse.test.js
+++ b/test/datetime/tokenParse.test.js
@@ -1224,3 +1224,28 @@ test("DateTime.expandFormat respects the hour cycle when forced by the macro tok
   const format = DateTime.expandFormat("T", { locale: "en-US" });
   expect(format).toBe("H:m");
 });
+
+//------
+// .fromFormatParser
+//-------
+
+test("DateTime.fromFormatParser behaves equivalently to DateTime.fromFormat", () => {
+  const dateTimeStr = "1982/05/25 09:10:11.445";
+  const format = "yyyy/MM/dd HH:mm:ss.SSS";
+  const formatParser = DateTime.buildFormatParser(format);
+  const ff1 = DateTime.fromFormat(dateTimeStr, format),
+    ffP1 = DateTime.fromFormatParser(dateTimeStr, formatParser);
+
+  expect(ffP1).toEqual(ff1);
+  expect(ffP1.isValid).toBe(true);
+});
+
+test("DateTime.fromFormatParser throws error when used with a different locale than it was created with", () => {
+  const format = "yyyy/MM/dd HH:mm:ss.SSS";
+  const formatParser = DateTime.buildFormatParser(format, { locale: "es-ES" });
+  expect(() =>
+    DateTime.fromFormatParser("1982/05/25 09:10:11.445", formatParser, { locale: "es-MX" })
+  ).toThrowError(
+    "fromFormatParser called with a locale of Locale(es-MX, null, null), but the format parser was created for Locale(es-ES, null, null)"
+  );
+});
diff --git a/test/duration/proto.test.js b/test/duration/proto.test.js
index aba74c4f..2b424db0 100644
--- a/test/duration/proto.test.js
+++ b/test/duration/proto.test.js
@@ -3,6 +3,8 @@ import { Duration } from "../../src/luxon";
 test("Duration prototype properties should not throw when addressed", () => {
   const d = Duration.fromObject({ hours: 1 });
   expect(() =>
-    Object.getOwnPropertyNames(d.__proto__).forEach((name) => d.__proto__[name])
+    Object.getOwnPropertyNames(Object.getPrototypeOf(d)).forEach(
+      (name) => Object.getPrototypeOf(d)[name]
+    )
   ).not.toThrow();
 });
diff --git a/test/interval/proto.test.js b/test/interval/proto.test.js
index bcc3e9b3..831d86ef 100644
--- a/test/interval/proto.test.js
+++ b/test/interval/proto.test.js
@@ -4,6 +4,8 @@ import { DateTime } from "../../src/luxon";
 test("Interval prototype properties should not throw when addressed", () => {
   const i = DateTime.fromISO("2018-01-01").until(DateTime.fromISO("2018-01-02"));
   expect(() =>
-    Object.getOwnPropertyNames(i.__proto__).forEach((name) => i.__proto__[name])
+    Object.getOwnPropertyNames(Object.getPrototypeOf(i)).forEach(
+      (name) => Object.getPrototypeOf(i)[name]
+    )
   ).not.toThrow();
 });