From 6f2ed48421fee969e2914386d04b1a8c26be6003 Mon Sep 17 00:00:00 2001 From: Arron Zou Date: Fri, 14 Aug 2026 18:01:06 +0800 Subject: [PATCH] fix: keep calendar date when subtracting years across historical TZ change daysInMonth() used endOf('month'), which builds local 23:59:59.999. In Asia/Kuala_Lumpur that instant jumps across the 1981-12-31 UTC+7:30 to UTC+8 change, so the day becomes 1 and year/month set clamps to Dec 1. Use the UTC calendar month length instead. Fixes #3137 --- package.json | 3 ++- src/index.js | 4 +++- test/plugin/timezone.test.js | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 238ce1ef7..7e366e1fc 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "main": "dayjs.min.js", "types": "index.d.ts", "scripts": { - "test": "cross-env TZ=Pacific/Auckland npm run test-tz && cross-env TZ=Europe/London npm run test-tz && cross-env TZ=America/Whitehorse npm run test-tz && npm run test-tz && jest --coverage --coverageThreshold=\"{ \\\"global\\\": { \\\"lines\\\": 100} }\"", + "test": "cross-env TZ=Pacific/Auckland npm run test-tz && cross-env TZ=Europe/London npm run test-tz && cross-env TZ=America/Whitehorse npm run test-tz && cross-env TZ=Asia/Kuala_Lumpur npm run test-tz-kl && npm run test-tz && jest --coverage --coverageThreshold=\"{ \\\"global\\\": { \\\"lines\\\": 100} }\"", "test-tz": "date && jest test/timezone.test --coverage=false", + "test-tz-kl": "date && jest test/plugin/timezone.test.js --coverage=false", "lint": "./node_modules/.bin/eslint src/* test/* build/*", "prettier": "prettier --write \"docs/**/*.md\"", "babel": "cross-env BABEL_ENV=build babel src --out-dir esm --copy-files && node build/esm", diff --git a/src/index.js b/src/index.js index 061ade178..b7f935eec 100644 --- a/src/index.js +++ b/src/index.js @@ -389,7 +389,9 @@ class Dayjs { } daysInMonth() { - return this.endOf(C.M).$D + // Calendar length; local endOf('month') 23:59:59.999 can land on the next + // day after historical midnight offset changes (e.g. Asia/Kuala_Lumpur 1981). + return new Date(Date.UTC(this.$y, this.$M + 1, 0)).getUTCDate() } $locale() { // get locale object diff --git a/test/plugin/timezone.test.js b/test/plugin/timezone.test.js index d83a03f8a..5e5a54f85 100644 --- a/test/plugin/timezone.test.js +++ b/test/plugin/timezone.test.js @@ -352,3 +352,17 @@ describe('UTC timezone', () => { expect(dayjs2.format()).toBe(moment2.format()) }) }) + +describe('Historical timezone offset change', () => { + // Also run this file with TZ=Asia/Kuala_Lumpur (see package.json test-tz-kl). + it('subtract(years) keeps the calendar date across Malaysia 1981 UTC+7:30 to UTC+8', () => { + const parsed = dayjs('20811225', 'YYYYMMDD') + const subtracted = parsed.subtract(100, 'years') + expect(subtracted.format('YYYY-MM-DD')).toBe('1981-12-25') + expect(subtracted.date()).toBe(25) + + const fromTz = dayjs.tz('2081-12-25', 'Asia/Kuala_Lumpur').subtract(100, 'years') + expect(fromTz.format('YYYY-MM-DD')).toBe('1981-12-25') + expect(fromTz.date()).toBe(25) + }) +})