Repository: moment/luxon
Pull request #1122 (2022-01-20 20:43:29)

Update Duration.negate() to not negate zeros
Fixes https://github.com/moment/luxon/issues/1050

Appears when diffing two dates and the second date is larger. We negate all units, leading to confusing values such as `-0` for units with no difference.


Patch:
diff --git a/src/datetime.js b/src/datetime.js
index 46e8e117..47acf7a7 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -1678,17 +1678,20 @@ export default class DateTime {
    * @param {Object} opts - options
    * @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'
    * @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'
    * @return {string}
    */
-  toSQLTime({ includeOffset = true, includeZone = false } = {}) {
+  toSQLTime({ includeOffset = true, includeZone = false, includeOffsetSpace = true } = {}) {
     let fmt = "HH:mm:ss.SSS";
 
     if (includeZone || includeOffset) {
-      fmt += " ";
+      if (includeOffsetSpace) {
+        fmt += " ";
+      }
       if (includeZone) {
         fmt += "z";
       } else if (includeOffset) {
@@ -1704,6 +1707,7 @@ export default class DateTime {
    * @param {Object} opts - options
    * @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'
    * @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'
@@ -1749,6 +1753,14 @@ export default class DateTime {
   toSeconds() {
     return this.isValid ? this.ts / 1000 : NaN;
   }
+  
+  /**
+   * Returns the epoch seconds (as a whole number) of this DateTime.
+   * @return {number}
+   */
+  toUnixInteger() {
+    return this.isValid ? Math.floor(this.ts / 1000) : NaN;
+  }
 
   /**
    * Returns an ISO 8601 representation of this DateTime appropriate for use in JSON.
diff --git a/src/duration.js b/src/duration.js
index 5db8ddd8..b4bbbafc 100644
--- a/src/duration.js
+++ b/src/duration.js
@@ -766,7 +766,7 @@ export default class Duration {
     if (!this.isValid) return this;
     const negated = {};
     for (const k of Object.keys(this.values)) {
-      negated[k] = -this.values[k];
+      negated[k] = this.values[k] === 0 ? 0 : -this.values[k];
     }
     return clone(this, { values: negated }, true);
   }


Test patch:
diff --git a/test/datetime/diff.test.js b/test/datetime/diff.test.js
index 40bff295..3f0eeba2 100644
--- a/test/datetime/diff.test.js
+++ b/test/datetime/diff.test.js
@@ -146,6 +146,25 @@ test("DateTime#diff sets all its units to 0 if the duration is empty", () => {
   expect(t.diff(t, "days").toObject()).toEqual({ days: 0 });
 });
 
+test("DateTime#diff sets units to 0 if second object is slightly larger", () => {
+  expect(
+    diffObjs(
+      { year: 2017, month: 6, day: 26, hour: 21, minute: 1, second: 2, millisecond: 1 },
+      { year: 2017, month: 6, day: 26, hour: 21, minute: 1, second: 2, millisecond: 2 },
+      ["years", "months", "weeks", "days", "hours", "minutes", "seconds", "milliseconds"]
+    )
+  ).toEqual({
+    years: 0,
+    months: 0,
+    weeks: 0,
+    days: 0,
+    hours: 0,
+    minutes: 0,
+    seconds: 0,
+    milliseconds: -1,
+  });
+});
+
 test("DateTime#diff puts fractional parts in the lowest order unit", () => {
   expect(
     diffObjs({ year: 2017, month: 7, day: 14 }, { year: 2016, month: 6, day: 16 }, [
diff --git a/test/datetime/format.test.js b/test/datetime/format.test.js
index 2d4dd550..5cc76553 100644
--- a/test/datetime/format.test.js
+++ b/test/datetime/format.test.js
@@ -243,6 +243,12 @@ test("DateTime#toSQLTime() accepts an includeOffset option", () => {
   expect(dt.setZone("America/New_York").toSQLTime({ includeOffset: false })).toBe("05:23:54.123");
 });
 
+test("DateTime#toSQLTime() accepts an includeOffsetSpace option", () => {
+  expect(dt.setZone("America/New_York").toSQLTime({ includeOffsetSpace: false })).toBe(
+    "05:23:54.123-04:00"
+  );
+});
+
 test("DateTime#toSQLTime() accepts an includeZone option", () => {
   expect(dt.toUTC().toSQLTime({ includeZone: true })).toBe("09:23:54.123 UTC");
   expect(dt.setZone("America/New_York").toSQLTime({ includeZone: true })).toBe(
@@ -263,6 +269,12 @@ test("DateTime#toSQL() returns SQL date time", () => {
   expect(dt.setZone("America/New_York").toSQL()).toBe("1982-05-25 05:23:54.123 -04:00");
 });
 
+test("DateTime#toSQL() accepts space option", () => {
+  expect(dt.setZone("America/New_York").toSQL({ includeOffsetSpace: false })).toBe(
+    "1982-05-25 05:23:54.123-04:00"
+  );
+});
+
 test("DateTime#toSQL() accepts an includeOffset option", () => {
   expect(dt.toUTC().toSQL({ includeOffset: false })).toBe("1982-05-25 09:23:54.123");
   expect(dt.setZone("America/New_York").toSQL({ includeOffset: false })).toBe(