Repository: moment/luxon
Pull request #1180 (2022-04-13 05:35:31)

Update math.md
- Expand explanation about leap seconds

---

See #1176.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d413dd0..9c7ec718 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## 2.3.2 (2022-04-17)
+
+* Fix timezone calculations for negative years
+* add week formatting token "w" for durations
+* fix weekday computation for years 0-100
+
+
 ## 2.3.1 (2022-02-23)
 
  * Added an `includeOffsetSpace` option to `toSQL` and `toSQLTime`
diff --git a/docs/math.md b/docs/math.md
index 864fb1b6..6934c51a 100644
--- a/docs/math.md
+++ b/docs/math.md
@@ -23,7 +23,7 @@ More generally we can differentiate two modes of math:
 
 These units use calendar math:
 
- * **Years** vary because of leap years.
+ * **Years** vary because of years.
  * **Months** vary because they're just different lengths.
  * **Days** vary because DST transitions mean some days are 23 or 25 hours long.
  * **Quarters** are always three months, but months vary in length so quarters do too.
@@ -35,7 +35,14 @@ 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.
+### Leap seconds
+
+Luxon ([as JavaScript in general](https://262.ecma-international.org/5.1/#sec-15.9.1.1)) doesn't account for [leap seconds](https://en.wikipedia.org/wiki/Leap_second); as in most programming environments, leap seconds happen as invisible changes to the underlying system's time. This can, in rare cases, cause the same second to "occur" twice from Luxon's perspective.
+
+The practical effect of leaps seconds are quite limited:
+
+1. You can't represent the leap second itself (i.e., `DateTime.utc(2016, 12, 31, 23, 59, 60).isValid` returns `false`).
+2. A Luxon `diff()` calculation that crosses a leap second will not exactly match the number of seconds that passed in the outside world. This can come up in rare situations where it matters to your application exactly what happened in the last *n* seconds. Even this is increasingly mitigated by [leap smear](https://developers.google.com/time/smear).
 
 ### How to think about calendar math
 
diff --git a/package-lock.json b/package-lock.json
index 42a4ee79..799d8252 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "luxon",
-  "version": "2.3.1",
+  "version": "2.3.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "luxon",
-      "version": "2.3.1",
+      "version": "2.3.2",
       "license": "MIT",
       "devDependencies": {
         "@babel/core": "^7.16.0",
@@ -17,9 +17,9 @@
         "@rollup/plugin-commonjs": "^19.0.0",
         "@rollup/plugin-node-resolve": "^13.0.0",
         "babel-jest": "^25.3.0",
-        "benchmark": "latest",
-        "codecov": "latest",
-        "documentation": "latest",
+        "benchmark": "*",
+        "codecov": "*",
+        "documentation": "*",
         "fs-extra": "^6.0.1",
         "husky": "^7.0.0",
         "jest": "^27.0.6",
@@ -13060,9 +13060,9 @@
       }
     },
     "node_modules/minimist": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
       "dev": true
     },
     "node_modules/minimist-options": {
@@ -27638,9 +27638,9 @@
       }
     },
     "minimist": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
       "dev": true
     },
     "minimist-options": {
diff --git a/package.json b/package.json
index e9f38f90..d975ec52 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "luxon",
-  "version": "2.3.1",
+  "version": "2.3.2",
   "description": "Immutable date wrapper",
   "author": "Isaac Cambron",
   "keywords": [
diff --git a/site/demo/demo.js b/site/demo/demo.js
index 20af69dd..b3a84bf9 100644
--- a/site/demo/demo.js
+++ b/site/demo/demo.js
@@ -19,6 +19,8 @@ function demo(luxon) {
         return "[ DateTime " + result.toISO() + " ]";
       case result instanceof Duration:
         return "[ Duration " + JSON.stringify(result.toObject()) + " ]";
+      case result instanceof Date:
+        return "[ Date " + result.toString() + " ]";
       default:
         return JSON.stringify(result);
     }
@@ -37,18 +39,23 @@ function demo(luxon) {
 
   example("Info.features()");
   example("DateTime.now()");
-  example("DateTime.local(2017, 5, 15, 17, 36)");
+  example("DateTime.now().toUnixInteger()");
+  example("DateTime.now().toJSDate()");
+  example("DateTime.utc().toISO()");
   example("DateTime.utc(2017, 5, 15, 17, 36)");
-  example("DateTime.now().toUTC()");
   example("DateTime.utc(2017, 5, 15, 17, 36).toLocal()");
+  example("DateTime.local(2017, 5, 15, 17, 36)");
+  example("DateTime.local(2017, 5, 15, 17, 36).toUTC()");
   example("DateTime.now().toObject()");
-  example("DateTime.fromObject({year: 2017, month: 5, day: 15, hour: 17, minute: 36})");
+  example("DateTime.fromObject({ year: 2017, month: 5, day: 15, hour: 17, minute: 36 })");
   example(
-    "DateTime.fromObject({year: 2017, month: 5, day: 15, hour: 17, minute: 36 }, { zone: 'America/New_York' })"
+    "DateTime.fromObject({ year: 2017, month: 5, day: 15, hour: 17, minute: 36 }, { zone: 'America/New_York' })"
   );
   example(
-    "DateTime.fromObject({year: 2017, month: 5, day: 15, hour: 17, minute: 36 }, { zone: 'Asia/Singapore' })"
+    "DateTime.fromObject({ year: 2017, month: 5, day: 15, hour: 17, minute: 36 }, { zone: 'Asia/Singapore' })"
   );
+  example("DateTime.now().setZone('America/New_York')");
+  example("DateTime.now().setZone('America/New_York').startOf('day')");
   example("DateTime.now().plus({minutes: 15, seconds: 8})");
   example("DateTime.now().plus({days: 6})");
   example("DateTime.now().minus({days: 6})");
diff --git a/src/datetime.js b/src/datetime.js
index 0c880c79..f91aff4b 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -1198,7 +1198,8 @@ export default class DateTime {
       return false;
     } else {
       return (
-        this.offset > this.set({ month: 1 }).offset || this.offset > this.set({ month: 5 }).offset
+        this.offset > this.set({ month: 1, day: 1 }).offset ||
+        this.offset > this.set({ month: 5 }).offset
       );
     }
   }
diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js
index 781ef42c..70234c41 100644
--- a/src/impl/tokenParser.js
+++ b/src/impl/tokenParser.js
@@ -13,7 +13,7 @@ function intUnit(regex, post = (i) => i) {
 }
 
 const NBSP = String.fromCharCode(160);
-const spaceOrNBSP = `( |${NBSP})`;
+const spaceOrNBSP = `[ ${NBSP}]`;
 const spaceOrNBSPRegExp = new RegExp(spaceOrNBSP, "g");
 
 function fixListRegex(s) {
diff --git a/src/luxon.js b/src/luxon.js
index 586bfa0e..02548152 100644
--- a/src/luxon.js
+++ b/src/luxon.js
@@ -9,7 +9,7 @@ import InvalidZone from "./zones/invalidZone.js";
 import SystemZone from "./zones/systemZone.js";
 import Settings from "./settings.js";
 
-const VERSION = "2.3.1";
+const VERSION = "2.3.2";
 
 export {
   VERSION,
diff --git a/src/zones/IANAZone.js b/src/zones/IANAZone.js
index ff1f0567..ad594513 100644
--- a/src/zones/IANAZone.js
+++ b/src/zones/IANAZone.js
@@ -1,8 +1,6 @@
-import { formatOffset, parseZoneInfo, isUndefined, ianaRegex, objToLocalTS } from "../impl/util.js";
+import { formatOffset, parseZoneInfo, isUndefined, objToLocalTS } from "../impl/util.js";
 import Zone from "../zone.js";
 
-const matchingRegex = RegExp(`^${ianaRegex.source}$`);
-
 let dtfCache = {};
 function makeDTF(zone) {
   if (!dtfCache[zone]) {
@@ -15,6 +13,7 @@ function makeDTF(zone) {
       hour: "2-digit",
       minute: "2-digit",
       second: "2-digit",
+      era: "short",
     });
   }
   return dtfCache[zone];
@@ -24,26 +23,29 @@ const typeToPos = {
   year: 0,
   month: 1,
   day: 2,
-  hour: 3,
-  minute: 4,
-  second: 5,
+  era: 3,
+  hour: 4,
+  minute: 5,
+  second: 6,
 };
 
 function hackyOffset(dtf, date) {
   const formatted = dtf.format(date).replace(/\u200E/g, ""),
-    parsed = /(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/.exec(formatted),
-    [, fMonth, fDay, fYear, fHour, fMinute, fSecond] = parsed;
-  return [fYear, fMonth, fDay, fHour, fMinute, fSecond];
+    parsed = /(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(formatted),
+    [, fMonth, fDay, fYear, fadOrBc, fHour, fMinute, fSecond] = parsed;
+  return [fYear, fMonth, fDay, fadOrBc, fHour, fMinute, fSecond];
 }
 
 function partsOffset(dtf, date) {
-  const formatted = dtf.formatToParts(date),
-    filled = [];
+  const formatted = dtf.formatToParts(date);
+  const filled = [];
   for (let i = 0; i < formatted.length; i++) {
-    const { type, value } = formatted[i],
-      pos = typeToPos[type];
+    const { type, value } = formatted[i];
+    const pos = typeToPos[type];
 
-    if (!isUndefined(pos)) {
+    if (type === "era") {
+      filled[pos] = value;
+    } else if (!isUndefined(pos)) {
       filled[pos] = parseInt(value, 10);
     }
   }
@@ -147,10 +149,14 @@ export default class IANAZone extends Zone {
 
     if (isNaN(date)) return NaN;
 
-    const dtf = makeDTF(this.name),
-      [year, month, day, hour, minute, second] = dtf.formatToParts
-        ? partsOffset(dtf, date)
-        : hackyOffset(dtf, date);
+    const dtf = makeDTF(this.name);
+    let [year, month, day, adOrBc, hour, minute, second] = dtf.formatToParts
+      ? partsOffset(dtf, date)
+      : hackyOffset(dtf, date);
+
+    if (adOrBc === "BC") {
+      year = -Math.abs(year) + 1;
+    }
 
     // because we're using hour12 and https://bugs.chromium.org/p/chromium/issues/detail?id=1025564&can=2&q=%2224%3A00%22%20datetimeformat
     const adjustedHour = hour === 24 ? 0 : hour;


Test patch:
diff --git a/test/datetime/zone.test.js b/test/datetime/zone.test.js
index cc5567fc..fbb7e45d 100644
--- a/test/datetime/zone.test.js
+++ b/test/datetime/zone.test.js
@@ -206,6 +206,13 @@ test("DateTime#setZone works for dates before 1970 with milliseconds", () => {
   expect(offset).toBe(-300);
 });
 
+// # 1179
+test("DateTime#setZone handles negative years", () => {
+  const dt = DateTime.fromMillis(-84753824400000).setZone("Europe/Rome");
+  expect(dt.year).toBe(-716);
+  expect(dt.offset < 60).toBe(true);
+});
+
 //------
 // #isInDST()
 //------
@@ -224,6 +231,12 @@ test("DateTime#isInDST() returns false for post-DST times", () => {
   expect(zoned.set({ month: 12 }).isInDST).toBe(false);
 });
 
+test("DateTime#isInDST() returns true for 1974 whole year in USA- from January 6th to October 27th", () => {
+  const zoned = dt().setZone("America/Los_Angeles");
+  expect(zoned.set({ year: 1974, month: 1, day: 6 }).isInDST).toBe(true);
+  expect(zoned.set({ year: 1974, month: 10, day: 27 }).isInDST).toBe(false);
+});
+
 //------
 // #invalid
 //------