Repository: moment/luxon
Pull request #1400 (2023-03-21 04:58:23)

SQL time support trim milliseconds
A more convenient way to get no milliseconds of sql time.

linux-foundation-easycla[bot]:  <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1400/#/?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><a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1400/#/?version=2' target='_blank'>:x:</a> - login: @dreak / name: Derek . The commit (e51aec9cbdd43b63cbd516d852b27ade3d23f538) is not authorized under a signed CLA. <a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/19772872/47118129/1400/#/?version=2' target='_blank'>Please click here to be authorized</a>. 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>

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce44a522..ec2b3ef2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -154,7 +154,7 @@ See [Upgrading section](https://moment.github.io/luxon/#/upgrading?id=_1x-to-20)
 
 ## 1.22.0 (2020-01-26)
 
- * Fix setZone's handling of pre-1970 dates with milisecond components
+ * Fix setZone's handling of pre-1970 dates with millisecond components
  * Fix keepLocalTime for large jumps near the target zone's DST
  * Fix cache perf for toRelative()
 
@@ -286,7 +286,7 @@ See [Upgrading section](https://moment.github.io/luxon/#/upgrading?id=_1x-to-20)
 ## 1.11.2
 
  * Fixed handling of some characters in fromFormat literal sections
- * Hanlde string values in object arguments to DateTime methods
+ * Handle string values in object arguments to DateTime methods
  * Fixed toRelativeCalendar's handling of zones in the base date
 
 ## 1.11.1
@@ -369,7 +369,7 @@ Removal accidentally-introduced runtime dependency
 ## 1.4.1
 
 - Empty diffs now have all the asked-for units in them, set at 0
-- Duration operations perserve the superset of units
+- Duration operations preserve the superset of units
 
 ## 1.4.0
 
@@ -464,7 +464,7 @@ Documentation fixes
 ## 0.5.0
 
 - `isBefore()` returns true for the end of the interval, consistent with being half-open
-- `zoneName` now rturns `null` for invalid DateTimes
+- `zoneName` now returns `null` for invalid DateTimes
 - Added quarter support
 - Adding a month to Jan 31 gives Feb 28/29
 
diff --git a/README.md b/README.md
index 5e7a7302..3fcacd7d 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ See [contributing](CONTRIBUTING.md).
 
 ![Phasers to stun][phasers-image]
 
-[license-image]: http://img.shields.io/badge/license-MIT-blue.svg
+[license-image]: https://img.shields.io/badge/license-MIT-blue.svg
 [license]: LICENSE.md
 
 [github-action-image]: https://github.com/moment/luxon/actions/workflows/test.yml/badge.svg
diff --git a/docs/moment.md b/docs/moment.md
index 08d9b02f..1e61794a 100644
--- a/docs/moment.md
+++ b/docs/moment.md
@@ -1,6 +1,6 @@
 # For Moment users
 
-Luxon borrows lots of ideas from [Moment.js](http://momentjs.com), but there are a lot of differences too. This document clarifies what they are.
+Luxon borrows lots of ideas from [Moment.js](https://momentjs.com), but there are a lot of differences too. This document clarifies what they are.
 
 ## Immutability
 
diff --git a/src/datetime.js b/src/datetime.js
index 5120b056..7b717005 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -224,14 +224,14 @@ function toISOTime(
   if (extended) {
     c += ":";
     c += padStart(o.c.minute);
-    if (o.c.second !== 0 || !suppressSeconds) {
+    if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {
       c += ":";
     }
   } else {
     c += padStart(o.c.minute);
   }
 
-  if (o.c.second !== 0 || !suppressSeconds) {
+  if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {
     c += padStart(o.c.second);
 
     if (o.c.millisecond !== 0 || !suppressMilliseconds) {
@@ -1243,6 +1243,43 @@ export default class DateTime {
     }
   }
 
+  /**
+   * Get those DateTimes which have the same local time as this DateTime, but a different offset from UTC
+   * in this DateTime's zone. During DST changes local time can be ambiguous, for example
+   * `2023-10-29T02:30:00` in `Europe/Berlin` can have offset `+01:00` or `+02:00`.
+   * This method will return both possible DateTimes if this DateTime's local time is ambiguous.
+   * @returns {DateTime[]}
+   */
+  getPossibleOffsets() {
+    if (!this.isValid || this.isOffsetFixed) {
+      return [this];
+    }
+    const dayMs = 86400000;
+    const minuteMs = 60000;
+    const localTS = objToLocalTS(this.c);
+    const oEarlier = this.zone.offset(localTS - dayMs);
+    const oLater = this.zone.offset(localTS + dayMs);
+
+    const o1 = this.zone.offset(localTS - oEarlier * minuteMs);
+    const o2 = this.zone.offset(localTS - oLater * minuteMs);
+    if (o1 === o2) {
+      return [this];
+    }
+    const ts1 = localTS - o1 * minuteMs;
+    const ts2 = localTS - o2 * minuteMs;
+    const c1 = tsToObj(ts1, o1);
+    const c2 = tsToObj(ts2, o2);
+    if (
+      c1.hour === c2.hour &&
+      c1.minute === c2.minute &&
+      c1.second === c2.second &&
+      c1.millisecond === c2.millisecond
+    ) {
+      return [clone(this, { ts: ts1 }), clone(this, { ts: ts2 })];
+    }
+    return [this];
+  }
+
   /**
    * Returns true if this DateTime is in a leap year, false otherwise
    * @example DateTime.local(2016).isInLeapYear //=> true
@@ -1726,14 +1763,25 @@ export default class DateTime {
    * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset.
    * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'
    * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00'
+   * @param {boolean} [opts.includeMilliseconds=true] - include the milliseconds
    * @example DateTime.utc().toSQL() //=> '05:15:16.345'
    * @example DateTime.now().toSQL() //=> '05:15:16.345 -04:00'
    * @example DateTime.now().toSQL({ includeOffset: false }) //=> '05:15:16.345'
    * @example DateTime.now().toSQL({ includeZone: false }) //=> '05:15:16.345 America/New_York'
+   * @example DateTime.now().toSQL({ includeMilliseconds: false}) //=> '05:15:16'
    * @return {string}
    */
-  toSQLTime({ includeOffset = true, includeZone = false, includeOffsetSpace = true } = {}) {
-    let fmt = "HH:mm:ss.SSS";
+  toSQLTime({
+    includeOffset = true,
+    includeZone = false,
+    includeOffsetSpace = true,
+    includeMilliseconds = true,
+  } = {}) {
+    let fmt = "HH:mm:ss";
+
+    if (includeMilliseconds) {
+      fmt += ".SSS";
+    }
 
     if (includeZone || includeOffset) {
       if (includeOffsetSpace) {
@@ -1755,10 +1803,12 @@ export default class DateTime {
    * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset.
    * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'
    * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00'
+   * @param {boolean} [opts.includeMilliseconds=true] - include the milliseconds
    * @example DateTime.utc(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 Z'
    * @example DateTime.local(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 -04:00'
    * @example DateTime.local(2014, 7, 13).toSQL({ includeOffset: false }) //=> '2014-07-13 00:00:00.000'
    * @example DateTime.local(2014, 7, 13).toSQL({ includeZone: true }) //=> '2014-07-13 00:00:00.000 America/New_York'
+   * @example DateTime.utc(2014, 7, 13).toSQL({ includeMilliseconds: false}) //=> '2014-07-13 00:00:00 Z'
    * @return {string}
    */
   toSQL(opts = {}) {
diff --git a/src/duration.js b/src/duration.js
index fc512b6e..72f67fb4 100644
--- a/src/duration.js
+++ b/src/duration.js
@@ -12,6 +12,7 @@ import {
   roundTo,
 } from "./impl/util.js";
 import Settings from "./settings.js";
+import DateTime from "./datetime.js";
 
 const INVALID = "Invalid Duration";
 
@@ -548,26 +549,11 @@ export default class Duration {
       includePrefix: false,
       format: "extended",
       ...opts,
+      includeOffset: false,
     };
 
-    const value = this.shiftTo("hours", "minutes", "seconds", "milliseconds");
-
-    let fmt = opts.format === "basic" ? "hhmm" : "hh:mm";
-
-    if (!opts.suppressSeconds || value.seconds !== 0 || value.milliseconds !== 0) {
-      fmt += opts.format === "basic" ? "ss" : ":ss";
-      if (!opts.suppressMilliseconds || value.milliseconds !== 0) {
-        fmt += ".SSS";
-      }
-    }
-
-    let str = value.toFormat(fmt);
-
-    if (opts.includePrefix) {
-      str = "T" + str;
-    }
-
-    return str;
+    const dateTime = DateTime.fromMillis(millis, { zone: "UTC" });
+    return dateTime.toISOTime(opts);
   }
 
   /**
diff --git a/src/impl/formatter.js b/src/impl/formatter.js
index 7fe6891f..0b874102 100644
--- a/src/impl/formatter.js
+++ b/src/impl/formatter.js
@@ -101,24 +101,25 @@ export default class Formatter {
     return df.format();
   }
 
-  formatDateTime(dt, opts = {}) {
-    const df = this.loc.dtFormatter(dt, { ...this.opts, ...opts });
-    return df.format();
+  dtFormatter(dt, opts = {}) {
+    return this.loc.dtFormatter(dt, { ...this.opts, ...opts });
+  }
+
+  formatDateTime(dt, opts) {
+    return this.dtFormatter(dt, opts).format();
   }
 
-  formatDateTimeParts(dt, opts = {}) {
-    const df = this.loc.dtFormatter(dt, { ...this.opts, ...opts });
-    return df.formatToParts();
+  formatDateTimeParts(dt, opts) {
+    return this.dtFormatter(dt, opts).formatToParts();
   }
 
-  formatInterval(interval, opts = {}) {
-    const df = this.loc.dtFormatter(interval.start, { ...this.opts, ...opts });
+  formatInterval(interval, opts) {
+    const df = this.dtFormatter(interval.start, opts);
     return df.dtf.formatRange(interval.start.toJSDate(), interval.end.toJSDate());
   }
 
-  resolvedOptions(dt, opts = {}) {
-    const df = this.loc.dtFormatter(dt, { ...this.opts, ...opts });
-    return df.resolvedOptions();
+  resolvedOptions(dt, opts) {
+    return this.dtFormatter(dt, opts).resolvedOptions();
   }
 
   num(n, p = 0) {
@@ -173,7 +174,7 @@ export default class Formatter {
       era = (length) =>
         knownEnglish ? English.eraForDateTime(dt, length) : string({ era: length }, "era"),
       tokenToString = (token) => {
-        // Where possible: http://cldr.unicode.org/translation/date-time-1/date-time#TOC-Standalone-vs.-Format-Styles
+        // Where possible: https://cldr.unicode.org/translation/date-time/date-time-symbols
         switch (token) {
           // ms
           case "S":
diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js
index 5a913b0f..19008d29 100644
--- a/src/impl/tokenParser.js
+++ b/src/impl/tokenParser.js
@@ -219,10 +219,14 @@ const partTypeStyleToTokenVal = {
   },
   dayperiod: "a",
   dayPeriod: "a",
-  hour: {
+  hour12: {
     numeric: "h",
     "2-digit": "hh",
   },
+  hour24: {
+    numeric: "H",
+    "2-digit": "HH",
+  },
   minute: {
     numeric: "m",
     "2-digit": "mm",
@@ -237,7 +241,7 @@ const partTypeStyleToTokenVal = {
   },
 };
 
-function tokenForPart(part, formatOpts) {
+function tokenForPart(part, formatOpts, resolvedOpts) {
   const { type, value } = part;
 
   if (type === "literal") {
@@ -250,7 +254,26 @@ function tokenForPart(part, formatOpts) {
 
   const style = formatOpts[type];
 
-  let val = partTypeStyleToTokenVal[type];
+  // The user might have explicitly specified hour12 or hourCycle
+  // if so, respect their decision
+  // if not, refer back to the resolvedOpts, which are based on the locale
+  let actualType = type;
+  if (type === "hour") {
+    if (formatOpts.hour12 != null) {
+      actualType = formatOpts.hour12 ? "hour12" : "hour24";
+    } else if (formatOpts.hourCycle != null) {
+      if (formatOpts.hourCycle === "h11" || formatOpts.hourCycle === "h12") {
+        actualType = "hour12";
+      } else {
+        actualType = "hour24";
+      }
+    } else {
+      // tokens only differentiate between 24 hours or not,
+      // so we do not need to check hourCycle here, which is less supported anyways
+      actualType = resolvedOpts.hour12 ? "hour12" : "hour24";
+    }
+  }
+  let val = partTypeStyleToTokenVal[actualType];
   if (typeof val === "object") {
     val = val[style];
   }
@@ -439,6 +462,8 @@ export function formatOptsToTokens(formatOpts, locale) {
   }
 
   const formatter = Formatter.create(locale, formatOpts);
-  const parts = formatter.formatDateTimeParts(getDummyDateTime());
-  return parts.map((p) => tokenForPart(p, formatOpts));
+  const df = formatter.dtFormatter(getDummyDateTime());
+  const parts = df.formatToParts();
+  const resolvedOpts = df.resolvedOptions();
+  return parts.map((p) => tokenForPart(p, formatOpts, resolvedOpts));
 }
diff --git a/src/impl/util.js b/src/impl/util.js
index 81bffc6e..51f756ea 100644
--- a/src/impl/util.js
+++ b/src/impl/util.js
@@ -151,7 +151,7 @@ export function daysInMonth(year, month) {
   }
 }
 
-// covert a calendar object to a local timestamp (epoch, but with the offset baked in)
+// convert a calendar object to a local timestamp (epoch, but with the offset baked in)
 export function objToLocalTS(obj) {
   let d = Date.UTC(
     obj.year,
diff --git a/src/settings.js b/src/settings.js
index e119e97c..a36c371d 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -112,10 +112,10 @@ export default class Settings {
   /**
    * Set the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century.
    * @type {number}
-   * @example Settings.twoDigitCutoffYear = 0 // cut-off year is 0, so all 'yy' are interpretted as current century
+   * @example Settings.twoDigitCutoffYear = 0 // cut-off year is 0, so all 'yy' are interpreted as current century
    * @example Settings.twoDigitCutoffYear = 50 // '49' -> 1949; '50' -> 2050
-   * @example Settings.twoDigitCutoffYear = 1950 // interpretted as 50
-   * @example Settings.twoDigitCutoffYear = 2050 // ALSO interpretted as 50
+   * @example Settings.twoDigitCutoffYear = 1950 // interpreted as 50
+   * @example Settings.twoDigitCutoffYear = 2050 // ALSO interpreted as 50
    */
   static set twoDigitCutoffYear(cutoffYear) {
     twoDigitCutoffYear = cutoffYear % 100;


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 062b845f..f44a6b20 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,9 +21,9 @@ jobs:
         node-version: [19.7.0]
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v2
+      uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'npm'
diff --git a/test/datetime/format.test.js b/test/datetime/format.test.js
index 3f584444..83247fa2 100644
--- a/test/datetime/format.test.js
+++ b/test/datetime/format.test.js
@@ -78,7 +78,7 @@ test("DateTime#toISO() shows 'Z' for UTC", () => {
   expect(dt.toISO()).toBe("1982-05-25T09:23:54.123Z");
 });
 
-test("DateTime#toISO() shows the offset, unless explicitely asked", () => {
+test("DateTime#toISO() shows the offset, unless explicitly 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");
@@ -96,6 +96,12 @@ test("DateTime#toISO() suppresses [milli]seconds", () => {
   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");
+
+  const suppressOnlySeconds = { suppressSeconds: true };
+  expect(dt.set({ seconds: 0 }).toISO(suppressOnlySeconds)).toBe("1982-05-25T09:23:00.123Z");
+  expect(dt.set({ seconds: 0, milliseconds: 0 }).toISO(suppressOnlySeconds)).toBe(
+    "1982-05-25T09:23Z"
+  );
 });
 
 test("DateTime#toISO() returns null for invalid DateTimes", () => {
@@ -327,6 +333,13 @@ test("DateTime#toSQLTime() returns null for invalid DateTimes", () => {
   expect(invalid.toSQLTime()).toBe(null);
 });
 
+test("DateTime#toSQLTime() returns SQL time without milliseconds", () => {
+  expect(dt.toUTC().toSQLTime({ includeMilliseconds: false })).toBe("09:23:54 Z");
+  expect(dt.setZone("America/New_York").toSQLTime({ includeMilliseconds: false })).toBe(
+    "05:23:54 -04:00"
+  );
+});
+
 //------
 // #toSQL()
 //------
@@ -360,6 +373,13 @@ test("DateTime#toSQL() returns null for invalid DateTimes", () => {
   expect(invalid.toSQL()).toBe(null);
 });
 
+test("DateTime#toSQL() returns SQL date time without milliseconds", () => {
+  expect(dt.toUTC().toSQL({ includeMilliseconds: false })).toBe("1982-05-25 09:23:54 Z");
+  expect(dt.setZone("America/New_York").toSQL({ includeMilliseconds: false })).toBe(
+    "1982-05-25 05:23:54 -04:00"
+  );
+});
+
 //------
 // #toString()
 //-------
diff --git a/test/datetime/invalid.test.js b/test/datetime/invalid.test.js
index ded6690d..a776e185 100644
--- a/test/datetime/invalid.test.js
+++ b/test/datetime/invalid.test.js
@@ -32,7 +32,7 @@ test("Invalid DateTimes tell you why", () => {
   expect(organic3.invalidReason).toBe("unit out of range");
 });
 
-test("Invalid DateTimes can provide an extented explanation", () => {
+test("Invalid DateTimes can provide an extended explanation", () => {
   expect(organic1.invalidExplanation).toBe(
     "you specified 13 (of type number) as a month, which is invalid"
   );
diff --git a/test/datetime/relative.test.js b/test/datetime/relative.test.js
index f3a4c5ab..397a759f 100644
--- a/test/datetime/relative.test.js
+++ b/test/datetime/relative.test.js
@@ -180,7 +180,7 @@ test("DateTime#toRelativeCalendar works down through the units for different zon
   expect(target3.toRelativeCalendar(options)).toBe("in 3 days");
 });
 
-test("DateTime#toRelative works down through the units for diffrent zone than local", () => {
+test("DateTime#toRelative works down through the units for different zone than local", () => {
   const base = DateTime.now().setZone(`UTC+3`);
 
   expect(base.plus({ minutes: 65 }).toRelative()).toBe("in 1 hour");
diff --git a/test/datetime/tokenParse.test.js b/test/datetime/tokenParse.test.js
index 9ba0d162..4025821d 100644
--- a/test/datetime/tokenParse.test.js
+++ b/test/datetime/tokenParse.test.js
@@ -822,7 +822,7 @@ test("DateTime.fromFormat validates weekdays", () => {
   expect(dt.isValid).toBe(false);
 });
 
-test("DateTime.fromFormat containg special regex token", () => {
+test("DateTime.fromFormat containing special regex token", () => {
   const ianaFormat = "yyyy-MM-dd'T'HH-mm[z]";
   const dt = DateTime.fromFormat("2019-01-14T11-30[Indian/Maldives]", ianaFormat, {
     setZone: true,
@@ -1186,6 +1186,17 @@ test("DateTime.parseFormatForOpts returns a parsing format", () => {
   const format = DateTime.parseFormatForOpts("");
   expect(format).toBeNull();
 });
+test("DateTime.parseFormatForOpts respects the hour cycle", () => {
+  const enFormat = DateTime.parseFormatForOpts(DateTime.TIME_SIMPLE, { locale: "en-US" });
+  expect(enFormat).toEqual("h:m a");
+
+  const deFormat = DateTime.parseFormatForOpts(DateTime.TIME_SIMPLE, { locale: "de-DE" });
+  expect(deFormat).toEqual("H:m");
+});
+test("DateTime.parseFormatForOpts respects the hour cycle when forced by the options", () => {
+  const format = DateTime.parseFormatForOpts(DateTime.TIME_24_SIMPLE, { locale: "en-US" });
+  expect(format).toEqual("H:m");
+});
 
 //------
 // .expandFormat
@@ -1200,3 +1211,16 @@ test("DateTime.expandFormat works with other locales", () => {
   const format = DateTime.expandFormat("D", { locale: "en-gb" });
   expect(format).toBe("d/M/yyyyy");
 });
+
+test("DateTime.expandFormat respects the hour cycle", () => {
+  const enFormat = DateTime.expandFormat("t", { locale: "en-US" });
+  expect(enFormat).toBe("h:m a");
+
+  const deFormat = DateTime.expandFormat("t", { locale: "de-DE" });
+  expect(deFormat).toBe("H:m");
+});
+
+test("DateTime.expandFormat respects the hour cycle when forced by the macro token", () => {
+  const format = DateTime.expandFormat("T", { locale: "en-US" });
+  expect(format).toBe("H:m");
+});
diff --git a/test/datetime/zone.test.js b/test/datetime/zone.test.js
index 66620daf..2d68f34a 100644
--- a/test/datetime/zone.test.js
+++ b/test/datetime/zone.test.js
@@ -242,6 +242,31 @@ test("DateTime#isInDST() returns true for 1974 whole year in USA- from January 6
   expect(zoned.set({ year: 1974, month: 10, day: 27 }).isInDST).toBe(false);
 });
 
+//------
+// #getPossibleOffsets()
+//------
+test("DateTime#getPossibleOffsets() returns the same DateTime for fixed zones", () => {
+  const fixedZoned = dt().setZone("+02:00");
+  const possibleOffsets = fixedZoned.getPossibleOffsets();
+  expect(possibleOffsets).toHaveLength(1);
+  expect(possibleOffsets[0]).toBe(fixedZoned);
+});
+
+test("DateTime#getPossibleOffsets() returns the same DateTime when not at an ambiguous local time", () => {
+  const zoned = DateTime.fromISO("2023-01-01T15:00", { zone: "Europe/Berlin" });
+  const possibleOffsets = zoned.getPossibleOffsets();
+  expect(possibleOffsets).toHaveLength(1);
+  expect(possibleOffsets[0]).toBe(zoned);
+});
+
+test("DateTime#getPossibleOffsets() returns the possible DateTimes when at an ambiguous local time", () => {
+  const zoned = DateTime.fromISO("2023-10-29T02:30:00+01:00", { zone: "Europe/Berlin" });
+  const possibleOffsets = zoned.getPossibleOffsets();
+  expect(possibleOffsets).toHaveLength(2);
+  expect(possibleOffsets[0].toISO()).toBe("2023-10-29T02:30:00.000+02:00");
+  expect(possibleOffsets[1].toISO()).toBe("2023-10-29T02:30:00.000+01:00");
+});
+
 //------
 // #invalid
 //------
diff --git a/test/duration/format.test.js b/test/duration/format.test.js
index abcf4f4c..408660f8 100644
--- a/test/duration/format.test.js
+++ b/test/duration/format.test.js
@@ -117,6 +117,12 @@ test("Duration#toISOTime returns null if the value is outside the range of one d
   expect(Duration.fromObject({ milliseconds: -1 }).toISOTime()).toBe(null);
 });
 
+test("Duration#toISOTime is not influenced by the locale", () => {
+  expect(Duration.fromObject({ hours: 3, minutes: 10 }, { locale: "ar-QA" }).toISOTime()).toBe(
+    "03:10:00.000"
+  );
+});
+
 //------
 // #toMillis()
 //------