Repository: moment/luxon (js)

Issue:
hasSame order matters / timezones are not working with hasSame
I noticed that when I compared two DateTime objects with hasSame the ordering of the objects returned different results. It seems to only happen when at least one of the objects has a timezone defined.

I managed to reproduce it, let the code speak for itself:

```
// With zones
const first = DateTime.fromISO("2019-10-02T07:24:04.339+03:00", {
  zone: "Europe/Helsinki"
});
const second = DateTime.fromISO("2019-10-02T00:00:00.000-05:00", {
  zone: "America/Chicago"
});

console.log(first.hasSame(second, "day")); // true
console.log(second.hasSame(first, "day")); // false
console.log(first.toISO()); // 2019-10-02T07:24:04.339+03:00
console.log(second.toISO()); // 2019-10-02T00:00:00.000-05:00

// No zones
const third = DateTime.fromISO("2019-10-02T07:24:04.339+03:00");
const fourth = DateTime.fromISO("2019-10-02T00:00:00.000-05:00");

console.log(third.hasSame(fourth, "day")); // true
console.log(fourth.hasSame(third, "day")); // true
console.log(first.toISO()); // 2019-10-02T07:24:04.339+03:00
console.log(second.toISO()); // 2019-10-02T08:00:00.000+03:00
```

Pull request:
Fix hasSame unit comparison
This is an attempt to resolve #584

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..dca9e456
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,24 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ""
+labels: bug
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Please share a minimal code example that triggers the problem:
+
+**Actual vs Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Desktop (please complete the following information):**
+
+- OS: [e.g. iOS]
+- Browser [e.g. Chrome 84, safari 14.0]
+- Luxon version [e.g. 1.25.0]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..34981977
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,18 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ""
+labels: enhancement
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/README.md b/README.md
index 44e59ba6..84bda4f3 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 [![MIT License][license-image]][license] [![Build Status][travis-image]][travis-url] [![NPM version][npm-version-image]][npm-url] [![Coverage Status][test-coverage-image]][test-coverage-url] [![Doc coverage][doc-coverage-image]][doc-url] [![PRs welcome][contributing-image]][contributing-url]
 
-Luxon is a library for working with dates and times in Javascript.
+Luxon is a library for working with dates and times in JavaScript.
 
 ```js
 DateTime.now().setZone("America/New_York").minus({ weeks: 1 }).endOf("day").toISO();
diff --git a/docs/math.md b/docs/math.md
index 1a08c9a6..ee932767 100644
--- a/docs/math.md
+++ b/docs/math.md
@@ -35,7 +35,7 @@ These units use time math:
  * **Minutes** are always 60 seconds
  * **Seconds** are always 1000 milliseconds
 
-Don't worry about leap seconds. Javascript and most other programming environments don't account for them; they just happen as abrupt, invisible changes to the underlying system's time.
+Don't worry about leap seconds. JavaScript and most other programming environments don't account for them; they just happen as abrupt, invisible changes to the underlying system's time.
 
 ### How to think about calendar math
 
@@ -108,9 +108,8 @@ d1.toMillis() === d2.toMillis() // are d1 and d2 the same instant in time?
 You may also use `#hasSame` to make more subtle comparisons:
 
 ```js
-d1.hasSame(d2, 'millisecond'); // equivalent to `+d1 === +d2`
-d1.hasSame(d2, 'minute');       // both DateTimes are in the same minute (and hour, day, month, etc)
-d1.hasSame(d2, 'year');         // etc
+d1.hasSame(d2, 'year');   // both DateTimes have the same calendar year
+d1.hasSame(d2, 'day');    // both DateTimes have the same calendar day (which implies they also have the same calendar year and month)
 ```
 
 Note that these are checking against the calendar. For example, if `d1` is in 2017, calling `hasSame` with "year" asks if d2 is also in 2017, not whether the DateTimes within a year of each other. For that, you'd need `diff` (see below).
diff --git a/docs/tour.md b/docs/tour.md
index d1328822..98d73bb7 100644
--- a/docs/tour.md
+++ b/docs/tour.md
@@ -1,6 +1,6 @@
 # A quick tour
 
-Luxon is a library that makes it easier to work with dates and times in Javascript. If you want, add and subtract them, format and parse them, ask them hard questions, and so on, Luxon provides a much easier and comprehensive interface than the native types it wraps. We're going to talk about the most immediately useful subset of that interface.
+Luxon is a library that makes it easier to work with dates and times in JavaScript. If you want, add and subtract them, format and parse them, ask them hard questions, and so on, Luxon provides a much easier and comprehensive interface than the native types it wraps. We're going to talk about the most immediately useful subset of that interface.
 
 This is going to be a bit brisk, but keep in mind that the API docs are comprehensive, so if you want to know more, feel free to [dive into them](../identifiers.html).
 
@@ -12,7 +12,7 @@ The most important class in Luxon is [DateTime](../class/src/datetime.js~DateTim
 var 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. 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/package-lock.json b/package-lock.json
index ad3d762d..14de6a9a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "luxon",
-  "version": "1.24.1",
+  "version": "1.25.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -8398,9 +8398,9 @@
       }
     },
     "node-fetch": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
-      "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==",
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+      "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
       "dev": true
     },
     "node-int64": {
@@ -10613,19 +10613,26 @@
         "is-typedarray": "^1.0.0"
       }
     },
-    "uglify-js": {
-      "version": "3.9.0",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.0.tgz",
-      "integrity": "sha512-j5wNQBWaql8gr06dOUrfaohHlscboQZ9B8sNsoK5o4sBjm7Ht9dxSbrMXyktQpA16Acaij8AcoozteaPYZON0g==",
+    "uglify-es": {
+      "version": "3.3.9",
+      "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
+      "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
       "dev": true,
       "requires": {
-        "commander": "~2.20.3"
+        "commander": "~2.13.0",
+        "source-map": "~0.6.1"
       },
       "dependencies": {
         "commander": {
-          "version": "2.20.3",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+          "version": "2.13.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
+          "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
           "dev": true
         }
       }
diff --git a/package.json b/package.json
index 73d994bb..e55d9086 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,7 @@
     "rollup": "^1.32.1",
     "rollup-plugin-babel": "^4.4.0",
     "rollup-plugin-babel-minify": "^6.2.0",
-    "uglify-js": "^3.9.0"
+    "uglify-es": "^3.3.9"
   },
   "main": "build/node/luxon.js",
   "module": "src/luxon.js",
diff --git a/scripts/release b/scripts/release
index 43bd6d88..df80bcc3 100755
--- a/scripts/release
+++ b/scripts/release
@@ -1,4 +1,6 @@
 #!/usr/bin/env bash
+./scripts/version || exit $?
+
 npm run build
 npm run docs
 npm run site
diff --git a/scripts/tag b/scripts/tag
index 724baae2..9f19d5f0 100755
--- a/scripts/tag
+++ b/scripts/tag
@@ -1,10 +1,7 @@
 #!/usr/bin/env bash
+./scripts/version || exit $?
 
 PACKAGE_VERSION=$(node -p "require('./package.json').version")
 
-if [ $(git tag -l "$PACKAGE_VERSION") ]; then
-  echo "tag already exists"
-else
-  git tag $PACKAGE_VERSION
-  git push origin master $PACKAGE_VERSION 
-fi
+git tag $PACKAGE_VERSION
+git push origin master $PACKAGE_VERSION
diff --git a/scripts/version b/scripts/version
new file mode 100755
index 00000000..1dd81a94
--- /dev/null
+++ b/scripts/version
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+abort() {
+  echo "ERROR: $1"
+  exit 1
+}
+
+PACKAGE_VERSION=$(node -p "require('./package.json').version")
+PACKAGE_LOCK_VERSION=$(node -p "require('./package-lock.json').version")
+EXPORTED_VERSION=$(grep -oP "(?<=const VERSION = \").*(?=\";)" ./src/luxon.js)
+
+if [ $PACKAGE_VERSION != $PACKAGE_LOCK_VERSION ]; then
+  abort "package-lock.json's version differs from package.json's"
+elif [ $PACKAGE_VERSION != $EXPORTED_VERSION ]; then
+  abort "exported version differs from package.json's"
+elif [ $(git tag -l "$PACKAGE_VERSION") ]; then
+  abort "tag already exists"
+fi
diff --git a/site/index.html b/site/index.html
index 93042b8d..6890ecf6 100644
--- a/site/index.html
+++ b/site/index.html
@@ -40,7 +40,7 @@
       <h1 class="display-1">Luxon</h1>
 
       <p class="lead">
-        A powerful, modern, and friendly wrapper for Javascript dates and times.
+        A powerful, modern, and friendly wrapper for JavaScript dates and times.
       </p>
 
       <pre>
diff --git a/src/datetime.js b/src/datetime.js
index 856f8c73..0b3664c7 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -531,8 +531,8 @@ export default class DateTime {
   }
 
   /**
-   * Create a DateTime from a Javascript Date object. Uses the default zone.
-   * @param {Date} date - a Javascript Date object
+   * Create a DateTime from a JavaScript Date object. Uses the default zone.
+   * @param {Date} date - a JavaScript Date object
    * @param {Object} options - configuration options for the DateTime
    * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into
    * @return {DateTime}
@@ -605,7 +605,7 @@ export default class DateTime {
   }
 
   /**
-   * Create a DateTime from a Javascript object with keys like 'year' and 'hour' with reasonable defaults.
+   * Create a DateTime from a JavaScript object with keys like 'year' and 'hour' with reasonable defaults.
    * @param {Object} obj - the object to create the DateTime from
    * @param {number} obj.year - a year, such as 1987
    * @param {number} obj.month - a month, 1-12
@@ -1389,6 +1389,7 @@ export default class DateTime {
    * @param {string} unit - The unit to go to the beginning of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'.
    * @example DateTime.local(2014, 3, 3).startOf('month').toISODate(); //=> '2014-03-01'
    * @example DateTime.local(2014, 3, 3).startOf('year').toISODate(); //=> '2014-01-01'
+   * @example DateTime.local(2014, 3, 3).startOf('week').toISODate(); //=> '2014-03-03', weeks always start on Mondays
    * @example DateTime.local(2014, 3, 3, 5, 30).startOf('day').toISOTime(); //=> '00:00.000-05:00'
    * @example DateTime.local(2014, 3, 3, 5, 30).startOf('hour').toISOTime(); //=> '05:00:00.000-05:00'
    * @return {DateTime}
@@ -1440,6 +1441,7 @@ export default class DateTime {
    * @param {string} unit - The unit to go to the end of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'.
    * @example DateTime.local(2014, 3, 3).endOf('month').toISO(); //=> '2014-03-31T23:59:59.999-05:00'
    * @example DateTime.local(2014, 3, 3).endOf('year').toISO(); //=> '2014-12-31T23:59:59.999-05:00'
+   * @example DateTime.local(2014, 3, 3).endOf('week').toISO(); // => '2014-03-09T23:59:59.999-05:00', weeks start on Mondays
    * @example DateTime.local(2014, 3, 3, 5, 30).endOf('day').toISO(); //=> '2014-03-03T23:59:59.999-05:00'
    * @example DateTime.local(2014, 3, 3, 5, 30).endOf('hour').toISO(); //=> '2014-03-03T05:59:59.999-05:00'
    * @return {DateTime}
@@ -1707,7 +1709,7 @@ export default class DateTime {
   }
 
   /**
-   * Returns a Javascript object with this DateTime's year, month, day, and so on.
+   * Returns a JavaScript object with this DateTime's year, month, day, and so on.
    * @param opts - options for generating the object
    * @param {boolean} [opts.includeConfig=false] - include configuration attributes in the output
    * @example DateTime.now().toObject() //=> { year: 2017, month: 4, day: 22, hour: 20, minute: 49, second: 42, millisecond: 268 }
@@ -1727,7 +1729,7 @@ export default class DateTime {
   }
 
   /**
-   * Returns a Javascript Date equivalent to this DateTime.
+   * Returns a JavaScript Date equivalent to this DateTime.
    * @return {Date}
    */
   toJSDate() {
@@ -1795,20 +1797,20 @@ export default class DateTime {
   }
 
   /**
-   * Return whether this DateTime is in the same unit of time as another 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 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 both the same calendar day
+   * @example DateTime.now().hasSame(otherDT, 'day'); //~> true if otherDT is in the same current calendar day
    * @return {boolean}
    */
   hasSame(otherDateTime, unit) {
     if (!this.isValid) return false;
-    if (unit === "millisecond") {
-      return this.valueOf() === otherDateTime.valueOf();
-    } else {
-      const inputMs = otherDateTime.valueOf();
-      return this.startOf(unit) <= inputMs && inputMs <= this.endOf(unit);
-    }
+
+    const inputMs = otherDateTime.valueOf();
+    const otherZoneDateTime = this.setZone(otherDateTime.zone, { keepLocalTime: true });
+    return otherZoneDateTime.startOf(unit) <= inputMs && inputMs <= otherZoneDateTime.endOf(unit);
   }
 
   /**
diff --git a/src/duration.js b/src/duration.js
index 3c6889a6..9bddefe8 100644
--- a/src/duration.js
+++ b/src/duration.js
@@ -216,7 +216,7 @@ export default class Duration {
   }
 
   /**
-   * Create a Duration from a Javascript object with keys like 'years' and 'hours.
+   * Create a Duration from a JavaScript object with keys like 'years' and 'hours.
    * If this object is empty then a zero milliseconds duration is returned.
    * @param {Object} obj - the object to create the DateTime from
    * @param {number} obj.years
@@ -383,7 +383,7 @@ export default class Duration {
   }
 
   /**
-   * Returns a Javascript object with this Duration's values.
+   * Returns a JavaScript object with this Duration's values.
    * @param opts - options for generating the object
    * @param {boolean} [opts.includeConfig=false] - include configuration attributes in the output
    * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toObject() //=> { years: 1, days: 6, seconds: 2 }
@@ -762,8 +762,14 @@ export default class Duration {
       return false;
     }
 
+    function eq(v1, v2) {
+      // Consider 0 and undefined as equal
+      if (v1 === undefined || v1 === 0) return v2 === undefined || v2 === 0;
+      return v1 === v2;
+    }
+
     for (const u of orderedUnits) {
-      if (this.values[u] !== other.values[u]) {
+      if (!eq(this.values[u], other.values[u])) {
         return false;
       }
     }
diff --git a/src/luxon.js b/src/luxon.js
index 31b97387..f84bb770 100644
--- a/src/luxon.js
+++ b/src/luxon.js
@@ -9,7 +9,10 @@ import InvalidZone from "./zones/invalidZone.js";
 import LocalZone from "./zones/localZone.js";
 import Settings from "./settings.js";
 
+const VERSION = "1.25.0";
+
 export {
+  VERSION,
   DateTime,
   Duration,
   Interval,
diff --git a/src/zones/localZone.js b/src/zones/localZone.js
index b8cbcdc2..dae9b2a3 100644
--- a/src/zones/localZone.js
+++ b/src/zones/localZone.js
@@ -4,7 +4,7 @@ import Zone from "../zone.js";
 let singleton = null;
 
 /**
- * Represents the local zone for this Javascript environment.
+ * Represents the local zone for this JavaScript environment.
  * @implements {Zone}
  */
 export default class LocalZone extends Zone {
diff --git a/tasks/build.js b/tasks/build.js
index 775bc52a..e5643a04 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -5,7 +5,7 @@ const rollup = require("rollup"),
   rollupMinify = require("rollup-plugin-babel-minify"),
   rollupNode = require("@rollup/plugin-node-resolve"),
   rollupCommonJS = require("@rollup/plugin-commonjs"),
-  UglifyJS = require("uglify-js"),
+  UglifyES = require("uglify-es"),
   fs = require("fs");
 
 // For some reason, the minifier is currently producing total giberrish, at least for the global build.
@@ -103,7 +103,7 @@ async function buildLibrary(dest, opts) {
 
   if (opts.minify && !TRUST_MINIFY) {
     const code = fs.readFileSync(`build/${dest}/luxon.js`, "utf8"),
-      ugly = UglifyJS.minify(code, {
+      ugly = UglifyES.minify(code, {
         toplevel: !opts.global,
         output: {
           comments: false
@@ -175,7 +175,8 @@ async function cjsBrowser() {
 async function es6() {
   await buildLibrary("es6", {
     format: "es",
-    compile: false
+    compile: false,
+    minify: true
   });
 }
 


Test patch:
diff --git a/test/datetime/misc.test.js b/test/datetime/misc.test.js
index 5c4bc783..3074c1fa 100644
--- a/test/datetime/misc.test.js
+++ b/test/datetime/misc.test.js
@@ -21,6 +21,34 @@ test("DateTime#hasSame() checks the unit", () => {
   expect(dt.hasSame(dt.plus({ days: 1 }), "days")).toBe(false);
 });
 
+test("DateTime#hasSame() checks high-order units", () => {
+  const dt1 = DateTime.fromISO("2001-02-03");
+  const dt2 = DateTime.fromISO("2001-05-03");
+  expect(dt1.hasSame(dt2, "year")).toBe(true);
+  expect(dt1.hasSame(dt2, "month")).toBe(false);
+  // Even when days are equal, return false when a higher-order unit differs.
+  expect(dt1.hasSame(dt2, "day")).toBe(false);
+});
+
+// #584
+test("DateTime#hasSame() ignores time offsets and is symmetric", () => {
+  const d1 = DateTime.fromISO("2019-10-02T01:02:03.045+03:00", {
+    zone: "Europe/Helsinki"
+  });
+  const d2 = DateTime.fromISO("2019-10-02T01:02:03.045-05:00", {
+    zone: "America/Chicago"
+  });
+
+  expect(d1.hasSame(d2, "day")).toBe(true);
+  expect(d2.hasSame(d1, "day")).toBe(true);
+  expect(d1.hasSame(d2, "hour")).toBe(true);
+  expect(d2.hasSame(d1, "hour")).toBe(true);
+  expect(d1.hasSame(d2, "second")).toBe(true);
+  expect(d2.hasSame(d1, "second")).toBe(true);
+  expect(d1.hasSame(d2, "millisecond")).toBe(true);
+  expect(d2.hasSame(d1, "millisecond")).toBe(true);
+});
+
 test("DateTime#hasSame() returns false for invalid DateTimes", () => {
   const dt = DateTime.now(),
     invalid = DateTime.invalid("because");
diff --git a/test/duration/equality.test.js b/test/duration/equality.test.js
index 6d22dfca..bfbd50de 100644
--- a/test/duration/equality.test.js
+++ b/test/duration/equality.test.js
@@ -18,6 +18,14 @@ test("equals identically constructed but one has sting type values", () => {
   expect(l1.equals(l2)).toBe(true);
 });
 
+// #809
+test("equals with extra zero units", () => {
+  const l1 = Duration.fromObject({ years: 5, days: 6 }),
+    l2 = Duration.fromObject({ years: 5, days: 6, minutes: 0, seconds: -0 });
+  expect(l1.equals(l2)).toBe(true);
+  expect(l2.equals(l1)).toBe(true);
+});
+
 test("does not equal an invalid duration", () => {
   const l1 = Duration.fromObject({ years: 5, days: 6 }),
     l2 = Duration.invalid("because");