Repository: moment/luxon
Pull request #1062 (2021-10-22 13:29:30)

Add fromDurationLike method
This PR moves the private `friendlyDuration` function to `Duration.fromDurationLike`.
`DurationLike` is a TS interface defined [here](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/22d5e137fff20a3da5ba2c4937f10731a100a530/types/luxon/src/duration.d.ts#L60)

This change was suggested in #1057

Basically, no code was added, so bundle size remains the same.

## Why this is useful
This change allows to do things like this:
```ts
type Example = {
  durationProp: Duration
}

const createExample = (duration: DurationLike): Example => {
  return {
    durationProp: Duration.fromDurationLike(duration)
  }
}

const a = createExample(1000)
const b = createExample({ seconds: 5 })

// now we can compare a.durationProp and b.durationProp
```



Patch:
diff --git a/benchmarks/datetime.js b/benchmarks/datetime.js
index dbfdb703..1fcd0faf 100644
--- a/benchmarks/datetime.js
+++ b/benchmarks/datetime.js
@@ -54,6 +54,9 @@ function runDateTimeSuite() {
       .add("DateTime#toLocaleString", () => {
         dt.toLocaleString();
       })
+      .add("DateTime#toLocaleString in utc", () => {
+        dt.toUTC().toLocaleString();
+      })
       .add("DateTime#toRelativeCalendar", () => {
         dt.toRelativeCalendar({ base: DateTime.now(), locale: "fi" });
       })
diff --git a/docs/math.md b/docs/math.md
index e15b02b9..864fb1b6 100644
--- a/docs/math.md
+++ b/docs/math.md
@@ -173,8 +173,8 @@ Finally, you can diff using multiple units:
 
 ```js
 var end = DateTime.fromISO('2017-03-13');
-var start = DateTime.fromISO('2017-02-15');
-end.diff(start, ['months', 'days']) //=> { months: 1, days: 2 }
+var start = DateTime.fromISO('2017-02-11');
+end.diff(start, ['months', 'days']).toObject() //=> { months: 1, days: 2 }
 ```
 
 ### Casual vs longterm conversion accuracy
diff --git a/docs/tour.md b/docs/tour.md
index 08e3208b..b9868c09 100644
--- a/docs/tour.md
+++ b/docs/tour.md
@@ -12,7 +12,7 @@ The most important class in Luxon is `DateTime`. A DateTime represents a specifi
 const dt = DateTime.local(2017, 5, 15, 8, 30);
 ```
 
-[DateTime.local](../class/src/datetime.js~DateTime.html#static-method-local) takes any number of arguments, all the way out to milliseconds. Underneath, this is similar to a JavaScript Date object. But we've decorated it with lots of useful methods.
+[DateTime.local](../class/src/datetime.js~DateTime.html#static-method-local) takes any number of arguments, all the way out to milliseconds (months are 1-indexed). Underneath, this is similar to a JavaScript Date object. But we've decorated it with lots of useful methods.
 
 ## Creating a DateTime
 
diff --git a/src/datetime.js b/src/datetime.js
index c476e0aa..b04d8f21 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -1,4 +1,4 @@
-import Duration, { friendlyDuration } from "./duration.js";
+import Duration from "./duration.js";
 import Interval from "./interval.js";
 import Settings from "./settings.js";
 import Info from "./info.js";
@@ -389,7 +389,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.
@@ -1248,7 +1248,7 @@ export default class DateTime {
   /**
    * "Set" the DateTime's zone to UTC. Returns a newly-constructed DateTime.
    *
-   * Equivalent to {@link DateTime.setZone}('utc')
+   * Equivalent to {@link DateTime#setZone}('utc')
    * @param {number} [offset=0] - optionally, an offset from UTC in minutes
    * @param {Object} [opts={}] - options to pass to `setZone()`
    * @return {DateTime}
@@ -1270,8 +1270,8 @@ export default class DateTime {
   /**
    * "Set" the DateTime's zone to specified zone. Returns a newly-constructed DateTime.
    *
-   * By default, the setter keeps the underlying time the same (as in, the same timestamp), but the new instance will report different local times and consider DSTs when making computations, as with {@link DateTime.plus}. You may wish to use {@link DateTime.toLocal} and {@link DateTime.toUTC} which provide simple convenience wrappers for commonly used zones.
-   * @param {string|Zone} [zone='local'] - a zone identifier. As a string, that can be any IANA zone supported by the host environment, or a fixed-offset name of the form 'UTC+3', or the strings 'local' or 'utc'. You may also supply an instance of a {@link DateTime.Zone} class.
+   * By default, the setter keeps the underlying time the same (as in, the same timestamp), but the new instance will report different local times and consider DSTs when making computations, as with {@link DateTime#plus}. You may wish to use {@link DateTime#toLocal} and {@link DateTime#toUTC} which provide simple convenience wrappers for commonly used zones.
+   * @param {string|Zone} [zone='local'] - a zone identifier. As a string, that can be any IANA zone supported by the host environment, or a fixed-offset name of the form 'UTC+3', or the strings 'local' or 'utc'. You may also supply an instance of a {@link DateTime#Zone} class.
    * @param {Object} opts - options
    * @param {boolean} [opts.keepLocalTime=false] - If true, adjust the underlying time so that the local time stays the same, but in the target zone. You should rarely need this.
    * @return {DateTime}
@@ -1316,7 +1316,7 @@ export default class DateTime {
 
   /**
    * "Set" the values of specified units. Returns a newly-constructed DateTime.
-   * You can only set units with this method; for "setting" metadata, see {@link DateTime.reconfigure} and {@link DateTime.setZone}.
+   * You can only set units with this method; for "setting" metadata, see {@link DateTime#reconfigure} and {@link DateTime#setZone}.
    * @param {Object} values - a mapping of units to numbers
    * @example dt.set({ year: 2017 })
    * @example dt.set({ hour: 8, minute: 30 })
@@ -1382,19 +1382,19 @@ export default class DateTime {
    */
   plus(duration) {
     if (!this.isValid) return this;
-    const dur = friendlyDuration(duration);
+    const dur = Duration.fromDurationLike(duration);
     return clone(this, adjustTime(this, dur));
   }
 
   /**
    * Subtract a period of time to this DateTime and return the resulting DateTime
-   * See {@link DateTime.plus}
+   * See {@link DateTime#plus}
    * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()
    @return {DateTime}
   */
   minus(duration) {
     if (!this.isValid) return this;
-    const dur = friendlyDuration(duration).negate();
+    const dur = Duration.fromDurationLike(duration).negate();
     return clone(this, adjustTime(this, dur));
   }
 
@@ -1472,7 +1472,7 @@ export default class DateTime {
 
   /**
    * Returns a string representation of this DateTime formatted according to the specified format string.
-   * **You may not want this.** See {@link DateTime.toLocaleString} for a more flexible formatting tool. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).
+   * **You may not want this.** See {@link DateTime#toLocaleString} for a more flexible formatting tool. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).
    * Defaults to en-US if no locale has been specified, regardless of the system's locale.
    * @param {string} fmt - the format string
    * @param {Object} opts - opts to override the configuration options on this DateTime
@@ -1687,7 +1687,7 @@ export default class DateTime {
   }
 
   /**
-   * Returns the epoch milliseconds of this DateTime. Alias of {@link DateTime.toMillis}
+   * Returns the epoch milliseconds of this DateTime. Alias of {@link DateTime#toMillis}
    * @return {number}
    */
   valueOf() {
@@ -1789,7 +1789,7 @@ export default class DateTime {
 
   /**
    * Return the difference between this DateTime and right now.
-   * See {@link DateTime.diff}
+   * See {@link DateTime#diff}
    * @param {string|string[]} [unit=['milliseconds']] - the unit or units units (such as 'hours' or 'days') to include in the duration
    * @param {Object} opts - options that affect the creation of the Duration
    * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
@@ -1811,7 +1811,7 @@ export default class DateTime {
   /**
    * Return whether this DateTime is in the same unit of time as another DateTime.
    * Higher-order units must also be identical for this function to return `true`.
-   * Note that time zones are **ignored** in this comparison, which compares the **local** calendar time. Use {@link DateTime.setZone} to convert one of the dates if needed.
+   * Note that time zones are **ignored** in this comparison, which compares the **local** calendar time. Use {@link DateTime#setZone} to convert one of the dates if needed.
    * @param {DateTime} otherDateTime - the other DateTime
    * @param {string} unit - the unit of time to check sameness on
    * @example DateTime.now().hasSame(otherDT, 'day'); //~> true if otherDT is in the same current calendar day
@@ -1955,7 +1955,7 @@ export default class DateTime {
   // FORMAT PRESETS
 
   /**
-   * {@link DateTime.toLocaleString} format like 10/14/1983
+   * {@link DateTime#toLocaleString} format like 10/14/1983
    * @type {Object}
    */
   static get DATE_SHORT() {
@@ -1963,7 +1963,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Oct 14, 1983'
+   * {@link DateTime#toLocaleString} format like 'Oct 14, 1983'
    * @type {Object}
    */
   static get DATE_MED() {
@@ -1971,7 +1971,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Fri, Oct 14, 1983'
+   * {@link DateTime#toLocaleString} format like 'Fri, Oct 14, 1983'
    * @type {Object}
    */
   static get DATE_MED_WITH_WEEKDAY() {
@@ -1979,7 +1979,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'October 14, 1983'
+   * {@link DateTime#toLocaleString} format like 'October 14, 1983'
    * @type {Object}
    */
   static get DATE_FULL() {
@@ -1987,7 +1987,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Tuesday, October 14, 1983'
+   * {@link DateTime#toLocaleString} format like 'Tuesday, October 14, 1983'
    * @type {Object}
    */
   static get DATE_HUGE() {
@@ -1995,7 +1995,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30 AM'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like '09:30 AM'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get TIME_SIMPLE() {
@@ -2003,7 +2003,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30:23 AM'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like '09:30:23 AM'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get TIME_WITH_SECONDS() {
@@ -2011,7 +2011,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30:23 AM EDT'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like '09:30:23 AM EDT'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get TIME_WITH_SHORT_OFFSET() {
@@ -2019,7 +2019,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30:23 AM Eastern Daylight Time'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like '09:30:23 AM Eastern Daylight Time'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get TIME_WITH_LONG_OFFSET() {
@@ -2027,7 +2027,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30', always 24-hour.
+   * {@link DateTime#toLocaleString} format like '09:30', always 24-hour.
    * @type {Object}
    */
   static get TIME_24_SIMPLE() {
@@ -2035,7 +2035,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30:23', always 24-hour.
+   * {@link DateTime#toLocaleString} format like '09:30:23', always 24-hour.
    * @type {Object}
    */
   static get TIME_24_WITH_SECONDS() {
@@ -2043,7 +2043,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30:23 EDT', always 24-hour.
+   * {@link DateTime#toLocaleString} format like '09:30:23 EDT', always 24-hour.
    * @type {Object}
    */
   static get TIME_24_WITH_SHORT_OFFSET() {
@@ -2051,7 +2051,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '09:30:23 Eastern Daylight Time', always 24-hour.
+   * {@link DateTime#toLocaleString} format like '09:30:23 Eastern Daylight Time', always 24-hour.
    * @type {Object}
    */
   static get TIME_24_WITH_LONG_OFFSET() {
@@ -2059,7 +2059,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '10/14/1983, 9:30 AM'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30 AM'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_SHORT() {
@@ -2067,7 +2067,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like '10/14/1983, 9:30:33 AM'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30:33 AM'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_SHORT_WITH_SECONDS() {
@@ -2075,7 +2075,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Oct 14, 1983, 9:30 AM'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30 AM'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_MED() {
@@ -2083,7 +2083,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Oct 14, 1983, 9:30:33 AM'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30:33 AM'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_MED_WITH_SECONDS() {
@@ -2091,7 +2091,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Fri, 14 Oct 1983, 9:30 AM'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like 'Fri, 14 Oct 1983, 9:30 AM'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_MED_WITH_WEEKDAY() {
@@ -2099,7 +2099,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'October 14, 1983, 9:30 AM EDT'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30 AM EDT'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_FULL() {
@@ -2107,7 +2107,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'October 14, 1983, 9:30:33 AM EDT'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30:33 AM EDT'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_FULL_WITH_SECONDS() {
@@ -2115,7 +2115,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Friday, October 14, 1983, 9:30 AM Eastern Daylight Time'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30 AM Eastern Daylight Time'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_HUGE() {
@@ -2123,7 +2123,7 @@ export default class DateTime {
   }
 
   /**
-   * {@link DateTime.toLocaleString} format like 'Friday, October 14, 1983, 9:30:33 AM Eastern Daylight Time'. Only 12-hour if the locale is.
+   * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30:33 AM Eastern Daylight Time'. Only 12-hour if the locale is.
    * @type {Object}
    */
   static get DATETIME_HUGE_WITH_SECONDS() {
diff --git a/src/duration.js b/src/duration.js
index 0def3d89..a0ad48cb 100644
--- a/src/duration.js
+++ b/src/duration.js
@@ -6,6 +6,7 @@ import { parseISODuration, parseISOTimeOnly } from "./impl/regexParser.js";
 import {
   asNumber,
   hasOwnProperty,
+  isInteger,
   isNumber,
   isUndefined,
   normalizeObject,
@@ -16,7 +17,7 @@ import Settings from "./settings.js";
 const INVALID = "Invalid Duration";
 
 // unit conversion constants
-const lowOrderMatrix = {
+export const lowOrderMatrix = {
     weeks: {
       days: 7,
       hours: 7 * 24,
@@ -155,11 +156,11 @@ function normalizeValues(matrix, vals) {
 }
 
 /**
- * A Duration object represents a period of time, like "2 months" or "1 day, 1 hour". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime.plus} to add a Duration object to a DateTime, producing another DateTime.
+ * A Duration object represents a period of time, like "2 months" or "1 day, 1 hour". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime#plus} to add a Duration object to a DateTime, producing another DateTime.
  *
  * 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}.
@@ -239,6 +240,7 @@ export default class Duration {
         }`
       );
     }
+
     return new Duration({
       values: normalizeObject(obj, Duration.normalizeUnit),
       loc: Locale.fromObject(opts),
@@ -246,6 +248,30 @@ export default class Duration {
     });
   }
 
+  /**
+   * Create a Duration from DurationLike.
+   *
+   * @param {Object | number | Duration} durationLike
+   * One of:
+   * - object with keys like 'years' and 'hours'.
+   * - number representing milliseconds
+   * - Duration instance
+   * @return {Duration}
+   */
+  static fromDurationLike(durationLike) {
+    if (isNumber(durationLike)) {
+      return Duration.fromMillis(durationLike);
+    } else if (Duration.isDuration(durationLike)) {
+      return durationLike;
+    } else if (typeof durationLike === "object") {
+      return Duration.fromObject(durationLike);
+    } else {
+      throw new InvalidArgumentError(
+        `Unknown duration argument ${durationLike} of type ${typeof durationLike}`
+      );
+    }
+  }
+
   /**
    * Create a Duration from an ISO 8601 duration string.
    * @param {string} text - text to parse
@@ -379,7 +405,7 @@ export default class Duration {
    * * `y` for years
    * Notes:
    * * Add padding by repeating the token, e.g. "yy" pads the years to two digits, "hhhh" pads the hours out to four digits
-   * * The duration will be converted to the set of units in the format string using {@link Duration.shiftTo} and the Durations's conversion accuracy setting.
+   * * The duration will be converted to the set of units in the format string using {@link Duration#shiftTo} and the Durations's conversion accuracy setting.
    * @param {string} fmt - the format string
    * @param {Object} opts - options
    * @param {boolean} [opts.floor=true] - floor numerical values
@@ -530,7 +556,7 @@ export default class Duration {
   plus(duration) {
     if (!this.isValid) return this;
 
-    const dur = friendlyDuration(duration),
+    const dur = Duration.fromDurationLike(duration),
       result = {};
 
     for (const k of orderedUnits) {
@@ -550,7 +576,7 @@ export default class Duration {
   minus(duration) {
     if (!this.isValid) return this;
 
-    const dur = friendlyDuration(duration);
+    const dur = Duration.fromDurationLike(duration);
     return this.plus(dur.negate());
   }
 
@@ -841,20 +867,3 @@ export default class Duration {
     return true;
   }
 }
-
-/**
- * @private
- */
-export function friendlyDuration(durationish) {
-  if (isNumber(durationish)) {
-    return Duration.fromMillis(durationish);
-  } else if (Duration.isDuration(durationish)) {
-    return durationish;
-  } else if (typeof durationish === "object") {
-    return Duration.fromObject(durationish);
-  } else {
-    throw new InvalidArgumentError(
-      `Unknown duration argument ${durationish} of type ${typeof durationish}`
-    );
-  }
-}
diff --git a/src/impl/locale.js b/src/impl/locale.js
index 633b1f97..500a60c1 100644
--- a/src/impl/locale.js
+++ b/src/impl/locale.js
@@ -181,8 +181,7 @@ class PolyDateFormatter {
       //    - < Etc/GMT-14, > Etc/GMT+12, and 30-minute or 45-minute offsets are not part of tzdata
       const gmtOffset = -1 * (dt.offset / 60);
       const offsetZ = gmtOffset >= 0 ? `Etc/GMT+${gmtOffset}` : `Etc/GMT${gmtOffset}`;
-      const isOffsetZoneSupported = IANAZone.isValidZone(offsetZ);
-      if (dt.offset !== 0 && isOffsetZoneSupported) {
+      if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) {
         z = offsetZ;
         this.dt = dt;
       } else {
diff --git a/src/impl/regexParser.js b/src/impl/regexParser.js
index 65ba6be3..eb4fefc7 100644
--- a/src/impl/regexParser.js
+++ b/src/impl/regexParser.js
@@ -5,6 +5,7 @@ import {
   parseMillis,
   ianaRegex,
   isUndefined,
+  parseFloating,
 } from "./util.js";
 import * as English from "./english.js";
 import FixedOffsetZone from "../zones/fixedOffsetZone.js";
@@ -125,7 +126,7 @@ const isoTimeOnly = RegExp(`^T?${isoTimeBaseRegex.source}$`);
 // ISO duration parsing
 
 const isoDuration =
-  /^-?P(?:(?:(-?\d{1,9})Y)?(?:(-?\d{1,9})M)?(?:(-?\d{1,9})W)?(?:(-?\d{1,9})D)?(?:T(?:(-?\d{1,9})H)?(?:(-?\d{1,9})M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,9}))?S)?)?)$/;
+  /^-?P(?:(?:(-?\d{1,9}(?:\.\d{1,9})?)Y)?(?:(-?\d{1,9}(?:\.\d{1,9})?)M)?(?:(-?\d{1,9}(?:\.\d{1,9})?)W)?(?:(-?\d{1,9}(?:\.\d{1,9})?)D)?(?:T(?:(-?\d{1,9}(?:\.\d{1,9})?)H)?(?:(-?\d{1,9}(?:\.\d{1,9})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,9}))?S)?)?)$/;
 
 function extractISODuration(match) {
   const [s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr] =
@@ -139,13 +140,13 @@ function extractISODuration(match) {
 
   return [
     {
-      years: maybeNegate(parseInteger(yearStr)),
-      months: maybeNegate(parseInteger(monthStr)),
-      weeks: maybeNegate(parseInteger(weekStr)),
-      days: maybeNegate(parseInteger(dayStr)),
-      hours: maybeNegate(parseInteger(hourStr)),
-      minutes: maybeNegate(parseInteger(minuteStr)),
-      seconds: maybeNegate(parseInteger(secondStr), secondStr === "-0"),
+      years: maybeNegate(parseFloating(yearStr)),
+      months: maybeNegate(parseFloating(monthStr)),
+      weeks: maybeNegate(parseFloating(weekStr)),
+      days: maybeNegate(parseFloating(dayStr)),
+      hours: maybeNegate(parseFloating(hourStr)),
+      minutes: maybeNegate(parseFloating(minuteStr)),
+      seconds: maybeNegate(parseFloating(secondStr), secondStr === "-0"),
       milliseconds: maybeNegate(parseMillis(millisecondsStr), negativeSeconds),
     },
   ];
diff --git a/src/impl/util.js b/src/impl/util.js
index e25d6be8..f576c5e7 100644
--- a/src/impl/util.js
+++ b/src/impl/util.js
@@ -108,6 +108,14 @@ export function parseInteger(string) {
   }
 }
 
+export function parseFloating(string) {
+  if (isUndefined(string) || string === null || string === "") {
+    return undefined;
+  } else {
+    return parseFloat(string);
+  }
+}
+
 export function parseMillis(fraction) {
   // Return undefined (instead of 0) in these cases, where fraction is not set
   if (isUndefined(fraction) || fraction === null || fraction === "") {
diff --git a/src/info.js b/src/info.js
index 4d92035b..1f30b32f 100644
--- a/src/info.js
+++ b/src/info.js
@@ -37,7 +37,7 @@ export default class Info {
    * * If `input` is a string containing a valid time zone name, a Zone instance
    *   with that name is returned.
    * * If `input` is a string that doesn't refer to a known time zone, a Zone
-   *   instance with {@link Zone.isValid} == false is returned.
+   *   instance with {@link Zone#isValid} == false is returned.
    * * If `input is a number, a Zone instance with the specified fixed offset
    *   in minutes is returned.
    * * If `input` is `null` or `undefined`, the default zone is returned.
diff --git a/src/interval.js b/src/interval.js
index fb0e9f31..5c79bb77 100644
--- a/src/interval.js
+++ b/src/interval.js
@@ -1,5 +1,5 @@
 import DateTime, { friendlyDateTime } from "./datetime.js";
-import Duration, { friendlyDuration } from "./duration.js";
+import Duration from "./duration.js";
 import Settings from "./settings.js";
 import { InvalidArgumentError, InvalidIntervalError } from "./errors.js";
 import Invalid from "./impl/invalid.js";
@@ -27,7 +27,7 @@ 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}.
@@ -106,7 +106,7 @@ export default class Interval {
    * @return {Interval}
    */
   static after(start, duration) {
-    const dur = friendlyDuration(duration),
+    const dur = Duration.fromDurationLike(duration),
       dt = friendlyDateTime(start);
     return Interval.fromDateTimes(dt, dt.plus(dur));
   }
@@ -118,7 +118,7 @@ export default class Interval {
    * @return {Interval}
    */
   static before(end, duration) {
-    const dur = friendlyDuration(duration),
+    const dur = Duration.fromDurationLike(duration),
       dt = friendlyDateTime(end);
     return Interval.fromDateTimes(dt.minus(dur), dt);
   }
@@ -127,7 +127,7 @@ export default class Interval {
    * Create an Interval from an ISO 8601 string.
    * Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats.
    * @param {string} text - the ISO string to parse
-   * @param {Object} [opts] - options to pass {@link DateTime.fromISO} and optionally {@link Duration.fromISO}
+   * @param {Object} [opts] - options to pass {@link DateTime#fromISO} and optionally {@link Duration#fromISO}
    * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
    * @return {Interval}
    */
@@ -333,7 +333,7 @@ export default class Interval {
    * @return {Array}
    */
   splitBy(duration) {
-    const dur = friendlyDuration(duration);
+    const dur = Duration.fromDurationLike(duration);
 
     if (!this.isValid || !dur.isValid || dur.as("milliseconds") === 0) {
       return [];
@@ -555,7 +555,7 @@ export default class Interval {
    * Returns an ISO 8601-compliant string representation of time of this Interval.
    * The date components are ignored.
    * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
-   * @param {Object} opts - The same options as {@link DateTime.toISO}
+   * @param {Object} opts - The same options as {@link DateTime#toISO}
    * @return {string}
    */
   toISOTime(opts) {
@@ -565,7 +565,7 @@ export default class Interval {
 
   /**
    * Returns a string representation of this Interval formatted according to the specified format string.
-   * @param {string} dateFormat - the format string. This string formats the start and end time. See {@link DateTime.toFormat} for details.
+   * @param {string} dateFormat - the format string. This string formats the start and end time. See {@link DateTime#toFormat} for details.
    * @param {Object} opts - options
    * @param {string} [opts.separator =  ' – '] - a separator to place between the start and end representations
    * @return {string}


Test patch:
diff --git a/test/duration/create.test.js b/test/duration/create.test.js
index ec24b745..1a6cbb22 100644
--- a/test/duration/create.test.js
+++ b/test/duration/create.test.js
@@ -24,6 +24,22 @@ test("Duration.fromObject sets all the values", () => {
   expect(dur.milliseconds).toBe(7);
 });
 
+test("Duration.fromObject sets all the fractional values", () => {
+  const dur = Duration.fromObject({
+    years: 1,
+    months: 2,
+    days: 3,
+    hours: 4.5,
+  });
+  expect(dur.years).toBe(1);
+  expect(dur.months).toBe(2);
+  expect(dur.days).toBe(3);
+  expect(dur.hours).toBe(4.5);
+  expect(dur.minutes).toBe(0);
+  expect(dur.seconds).toBe(0);
+  expect(dur.milliseconds).toBe(0);
+});
+
 test("Duration.fromObject sets all the values from the object having string type values", () => {
   const dur = Duration.fromObject({
     years: "1",
@@ -90,3 +106,30 @@ test("Duration.fromObject is valid if providing options only", () => {
   expect(dur.milliseconds).toBe(0);
   expect(dur.isValid).toBe(true);
 });
+
+//------
+// .fromDurationLike()
+//-------
+
+it("Duration.fromDurationLike returns a Duration from millis", () => {
+  const dur = Duration.fromDurationLike(1000);
+  expect(dur).toBeInstanceOf(Duration);
+  expect(dur).toMatchInlineSnapshot(`"PT1S"`);
+});
+
+it("Duration.fromDurationLike returns a Duration from object", () => {
+  const dur = Duration.fromDurationLike({ hours: 1 });
+  expect(dur).toBeInstanceOf(Duration);
+  expect(dur.toObject()).toStrictEqual({ hours: 1 });
+});
+
+it("Duration.fromDurationLike returns passed Duration", () => {
+  const durFromObject = Duration.fromObject({ hours: 1 });
+  const dur = Duration.fromDurationLike(durFromObject);
+  expect(dur).toStrictEqual(durFromObject);
+});
+
+it("Duration.fromDurationLike returns passed Duration", () => {
+  expect(() => Duration.fromDurationLike("foo")).toThrow();
+  expect(() => Duration.fromDurationLike(null)).toThrow();
+});
diff --git a/test/duration/equality.test.js b/test/duration/equality.test.js
index bfbd50de..a7b7765e 100644
--- a/test/duration/equality.test.js
+++ b/test/duration/equality.test.js
@@ -12,12 +12,24 @@ test("equals identically constructed", () => {
   expect(l1.equals(l2)).toBe(true);
 });
 
-test("equals identically constructed but one has sting type values", () => {
+test("equals identically constructed with fractional values", () => {
+  const l1 = Duration.fromObject({ years: 5.5, days: 6 }),
+    l2 = Duration.fromObject({ years: 5.5, days: 6 });
+  expect(l1.equals(l2)).toBe(true);
+});
+
+test("equals identically constructed but one has string type values", () => {
   const l1 = Duration.fromObject({ years: 5, days: 6 }),
     l2 = Duration.fromObject({ years: "5", days: "6" });
   expect(l1.equals(l2)).toBe(true);
 });
 
+test("equals identically constructed but one has fractional string type values", () => {
+  const l1 = Duration.fromObject({ years: 5.5, days: 6 }),
+    l2 = Duration.fromObject({ years: "5.5", days: "6" });
+  expect(l1.equals(l2)).toBe(true);
+});
+
 // #809
 test("equals with extra zero units", () => {
   const l1 = Duration.fromObject({ years: 5, days: 6 }),
diff --git a/test/duration/format.test.js b/test/duration/format.test.js
index ff1b77dd..9a50c9d7 100644
--- a/test/duration/format.test.js
+++ b/test/duration/format.test.js
@@ -21,6 +21,20 @@ test("Duration#toISO fills out every field", () => {
   expect(dur().toISO()).toBe("P1Y2M1W3DT4H5M6.007S");
 });
 
+test("Duration#toISO fills out every field with fractional", () => {
+  const dur = Duration.fromObject({
+    years: 1.1,
+    months: 2.2,
+    weeks: 1.1,
+    days: 3.3,
+    hours: 4.4,
+    minutes: 5.5,
+    seconds: 6.6,
+    milliseconds: 7,
+  });
+  expect(dur.toISO()).toBe("P1.1Y2.2M1.1W3.3DT4.4H5.5M6.607S");
+});
+
 test("Duration#toISO creates a minimal string", () => {
   expect(Duration.fromObject({ years: 3, seconds: 45 }).toISO()).toBe("P3YT45S");
   expect(Duration.fromObject({ months: 4, seconds: 45 }).toISO()).toBe("P4MT45S");
diff --git a/test/duration/getters.test.js b/test/duration/getters.test.js
index 7b313100..192cb3a6 100644
--- a/test/duration/getters.test.js
+++ b/test/duration/getters.test.js
@@ -44,6 +44,24 @@ test("Duration#hours returns the hours", () => {
   expect(inv.hours).toBeFalsy();
 });
 
+test("Duration#hours returns the fractional hours", () => {
+  const localDur = Duration.fromObject({
+      years: 1,
+      quarters: 2,
+      months: 2,
+      days: 3,
+      hours: 4.5,
+      minutes: 5,
+      seconds: 6,
+      milliseconds: 7,
+      weeks: 8,
+    }),
+    localInv = Duration.invalid("because i say so");
+
+  expect(localDur.hours).toBe(4.5);
+  expect(localInv.hours).toBeFalsy();
+});
+
 test("Duration#minutes returns the minutes", () => {
   expect(dur.minutes).toBe(5);
   expect(inv.minutes).toBeFalsy();
diff --git a/test/duration/info.test.js b/test/duration/info.test.js
index cedb8095..350a3a51 100644
--- a/test/duration/info.test.js
+++ b/test/duration/info.test.js
@@ -6,7 +6,7 @@ const dur = Duration.fromObject(
   {
     years: 1,
     months: 2,
-    days: 3,
+    days: 3.3,
   },
   {
     locale: "fr",
@@ -22,7 +22,7 @@ test("Duration#toObject returns the object", () => {
   expect(dur.toObject()).toEqual({
     years: 1,
     months: 2,
-    days: 3,
+    days: 3.3,
   });
 });
 
@@ -31,7 +31,7 @@ test("Duration#toObject accepts a flag to return config", () => {
     {
       years: 1,
       months: 2,
-      days: 3,
+      days: 3.3,
     },
     {
       locale: "fr",
diff --git a/test/duration/math.test.js b/test/duration/math.test.js
index 21d31ba1..536afe21 100644
--- a/test/duration/math.test.js
+++ b/test/duration/math.test.js
@@ -16,6 +16,17 @@ test("Duration#plus add straightforward durations", () => {
   expect(result.milliseconds).toBe(14);
 });
 
+test("Duration#plus add fractional durations", () => {
+  const first = Duration.fromObject({ hours: 4.2, minutes: 12, seconds: 2 }),
+    second = Duration.fromObject({ hours: 1, seconds: 6.8, milliseconds: 14 }),
+    result = first.plus(second);
+
+  expect(result.hours).toBeCloseTo(5.2, 8);
+  expect(result.minutes).toBe(12);
+  expect(result.seconds).toBeCloseTo(8.8, 8);
+  expect(result.milliseconds).toBe(14);
+});
+
 test("Duration#plus noops empty druations", () => {
   const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
     second = Duration.fromObject({}),
@@ -87,6 +98,17 @@ test("Duration#minus subtracts durations", () => {
   expect(result.milliseconds).toBe(-14);
 });
 
+test("Duration#minus subtracts fractional durations", () => {
+  const first = Duration.fromObject({ hours: 4.2, minutes: 12, seconds: 2 }),
+    second = Duration.fromObject({ hours: 1, seconds: 6, milliseconds: 14 }),
+    result = first.minus(second);
+
+  expect(result.hours).toBeCloseTo(3.2, 8);
+  expect(result.minutes).toBe(12);
+  expect(result.seconds).toBe(-4);
+  expect(result.milliseconds).toBe(-14);
+});
+
 test("Duration#minus subtracts single values", () => {
   const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
     result = first.minus({ minutes: 5 });
diff --git a/test/duration/parse.test.js b/test/duration/parse.test.js
index 22b0a314..a593e936 100644
--- a/test/duration/parse.test.js
+++ b/test/duration/parse.test.js
@@ -65,6 +65,24 @@ test("Duration.fromISO can parse fractions of seconds", () => {
   });
 });
 
+test("Duration.fromISO can parse fractions", () => {
+  expect(Duration.fromISO("P1.5Y").toObject()).toEqual({
+    years: 1.5,
+  });
+  expect(Duration.fromISO("P1.5M").toObject()).toEqual({
+    months: 1.5,
+  });
+  expect(Duration.fromISO("P1.5W").toObject()).toEqual({
+    weeks: 1.5,
+  });
+  expect(Duration.fromISO("P1.5D").toObject()).toEqual({
+    days: 1.5,
+  });
+  expect(Duration.fromISO("PT9.5H").toObject()).toEqual({
+    hours: 9.5,
+  });
+});
+
 const rejects = (s) => {
   expect(Duration.fromISO(s).isValid).toBe(false);
 };
diff --git a/test/duration/set.test.js b/test/duration/set.test.js
index fd63e91e..bd20ab75 100644
--- a/test/duration/set.test.js
+++ b/test/duration/set.test.js
@@ -20,6 +20,7 @@ test("Duration#set() sets the values", () => {
   expect(dur().set({ months: 2 }).months).toBe(2);
   expect(dur().set({ days: 2 }).days).toBe(2);
   expect(dur().set({ hours: 4 }).hours).toBe(4);
+  expect(dur().set({ hours: 4.5 }).hours).toBe(4.5);
   expect(dur().set({ minutes: 16 }).minutes).toBe(16);
   expect(dur().set({ seconds: 45 }).seconds).toBe(45);
   expect(dur().set({ milliseconds: 86 }).milliseconds).toBe(86);
diff --git a/test/duration/typecheck.test.js b/test/duration/typecheck.test.js
index cbc2a7af..4a30c2a6 100644
--- a/test/duration/typecheck.test.js
+++ b/test/duration/typecheck.test.js
@@ -6,7 +6,7 @@ import { Duration } from "../../src/luxon";
 // #isDuration
 //-------
 test("Duration#isDuration return true for valid duration", () => {
-  const dur = Duration.fromObject({ hours: 1 });
+  const dur = Duration.fromObject({ hours: 1, minutes: 4.5 });
   expect(Duration.isDuration(dur)).toBe(true);
 });
 
@@ -19,6 +19,7 @@ test("Duration#isDuration return false for primitives", () => {
   expect(Duration.isDuration({})).toBe(false);
   expect(Duration.isDuration({ hours: 60 })).toBe(false);
   expect(Duration.isDuration(1)).toBe(false);
+  expect(Duration.isDuration(1.1)).toBe(false);
   expect(Duration.isDuration("")).toBe(false);
   expect(Duration.isDuration(null)).toBe(false);
   expect(Duration.isDuration()).toBe(false);
diff --git a/test/duration/units.test.js b/test/duration/units.test.js
index da35d939..9d39df35 100644
--- a/test/duration/units.test.js
+++ b/test/duration/units.test.js
@@ -46,6 +46,14 @@ test("Duration#shiftTo deconstructs decimal inputs", () => {
   expect(dur.minutes).toBeCloseTo(18, 8);
 });
 
+test("Duration#shiftTo deconstructs in cascade and tacks decimal onto the end", () => {
+  const dur = Duration.fromObject({ hours: 1.17 }).shiftTo("hours", "minutes", "seconds");
+  expect(dur.isValid).toBe(true);
+  expect(dur.hours).toBe(1);
+  expect(dur.minutes).toBe(10);
+  expect(dur.seconds).toBeCloseTo(12, 8);
+});
+
 test("Duration#shiftTo maintains invalidity", () => {
   const dur = Duration.invalid("because").shiftTo("years");
   expect(dur.isValid).toBe(false);