├── .gitignore ├── .tool-versions ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── babel.config.json ├── benchmark.ts ├── copy.mjs ├── package.json ├── playground.mjs ├── pnpm-lock.yaml ├── size.mjs ├── src ├── constants │ └── index.ts ├── date │ ├── index.d.ts │ ├── index.js │ ├── mini.d.ts │ ├── mini.js │ └── tests.ts ├── index.ts ├── tests.ts ├── tz │ ├── index.ts │ └── tests.ts ├── tzOffset │ ├── index.ts │ └── tests.ts └── tzScan │ ├── index.ts │ └── tests.ts ├── tsconfig.json ├── tsconfig.lib.json └── vitest.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.17.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning]. 5 | 6 | This change log follows the format documented in [Keep a CHANGELOG]. 7 | 8 | [semantic versioning]: http://semver.org/ 9 | [keep a changelog]: http://keepachangelog.com/ 10 | 11 | ## v1.2.0 - 2024-10-31 12 | 13 | ### Fixed 14 | 15 | - Fixed issue with `setTime` not syncing the value to the internal date resulting in incorrect behavior [#16](https://github.com/date-fns/tz/issues/16), [#24](https://github.com/date-fns/tz/issues/24). 16 | 17 | ## v1.1.2 - 2024-09-24 18 | 19 | ### Fixed 20 | 21 | - Improved compatability with FormatJS Intl polifyll [#8](https://github.com/date-fns/tz/issues/8). Thanks to [@kevin-abiera](https://github.com/kevin-abiera). 22 | 23 | ## v1.1.1 - 2024-09-23 24 | 25 | ### Fixed 26 | 27 | - Reworked DST handling to fix various bugs and edge cases. There might still be some issues, but I'm actively working on improving test coverage. 28 | 29 | ## v1.1.0 - 2024-09-22 30 | 31 | This is yet another critical bug-fix release. Thank you to all the people who sent PRs and reported their issues. Special thanks to [@huextrat](https://github.com/huextrat), [@allohamora](https://github.com/allohamora) and [@lhermann](https://github.com/lhermann). 32 | 33 | ### Fixed 34 | 35 | - [Fixed negative fractional time zones like `America/St_Johns`](https://github.com/date-fns/tz/pull/7) [@allohamora](https://github.com/allohamora). 36 | 37 | - Fixed the DST bug when creating a date in the DST transition hour. 38 | 39 | ### Added 40 | 41 | - Added support for `±HH:MM/±HHMM/±HH` time zone formats for Node.js below v22 (and other environments that has this problem) [#3](https://github.com/date-fns/tz/issues/3) 42 | 43 | ## v1.0.2 - 2024-09-14 44 | 45 | This release fixes a couple of critical bugs in the previous release. 46 | 47 | ### Fixed 48 | 49 | - Fixed UTC setters functions generation. 50 | 51 | - Create `Invalid Date` instead of throwing an error on invalid arguments. 52 | 53 | - Make all the number getters return `NaN` when the date or time zone is invalid. 54 | 55 | - Make `tzOffset` return `NaN` when the date or the time zone is invalid. 56 | 57 | ## v1.0.1 - 2024-09-13 58 | 59 | Initial version 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2024 Sasha Koss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @env TZ=Asia/Singapore pnpm exec vitest run 3 | .PHONY: test 4 | 5 | test-watch: 6 | @env TZ=Asia/Singapore pnpm exec vitest 7 | 8 | types: 9 | @pnpm exec tsc --noEmit 10 | 11 | types-watch: 12 | @pnpm exec tsc --noEmit --watch 13 | 14 | test-types: build 15 | @pnpm exec attw --pack lib 16 | 17 | build: prepare-build 18 | @pnpm exec tsc -p tsconfig.lib.json 19 | @env BABEL_ENV=esm pnpm exec babel src --config-file ./babel.config.json --source-root src --out-dir lib --extensions .js,.ts --out-file-extension .js --quiet 20 | @env BABEL_ENV=cjs pnpm exec babel src --config-file ./babel.config.json --source-root src --out-dir lib --extensions .js,.ts --out-file-extension .cjs --quiet 21 | @node copy.mjs 22 | @make build-cts 23 | 24 | build-cts: 25 | @find lib -name '*.d.ts' | while read file; do \ 26 | new_file=$${file%.d.ts}.d.cts; \ 27 | cp $$file $$new_file; \ 28 | done 29 | 30 | prepare-build: 31 | @rm -rf lib 32 | @mkdir -p lib 33 | 34 | publish: build 35 | cd lib && pnpm publish --access public 36 | 37 | publish-next: build 38 | cd lib && pnpm publish --access public --tag next 39 | 40 | link: 41 | @cd lib && pnpm link 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @date-fns/tz 2 | 3 | The package provides `Date` extensions `TZDate` and `TZDateMini` that perform all calculations in the given time zone rather than the system time zone. 4 | 5 | Using it makes [date-fns](https://github.com/date-fns/date-fns) operate in given time zone but can be also used without it. 6 | 7 | Like everything else in the date-fns ecosystem, the library is build-size aware. The smallest component, `TZDateMini,` is only `916 B`. 8 | 9 | **Need only UTC?** See [@date-fns/utc](https://github.com/date-fns/utc) that provides lighter solution. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install @date-fns/tz --save 15 | ``` 16 | 17 | ## Usage 18 | 19 | `TZDate` and `TZDateMini` have API similar to `Date`, but perform all calculations in the given time zone, which might be essential when operating across different time zones, calculating dates for users in different regions, or rendering chart or calendar component: 20 | 21 | ```ts 22 | import { TZDate } from "@date-fns/tz"; 23 | import { addHours } from "date-fns"; 24 | 25 | // Given that the system time zone is America/Los_Angeles 26 | // where DST happens at Sunday, 13 March 2022, 02:00:00 27 | 28 | // Using system time zone will produce 03:00 instead of 02:00 because of DST: 29 | const date = new Date(2022, 2, 13); 30 | addHours(date, 2).toString(); 31 | //=> 'Sun Mar 13 2022 03:00:00 GMT-0700 (Pacific Daylight Time)' 32 | 33 | // Using Asia/Singapore will provide expected 02:00: 34 | const tzDate = new TZDate(2022, 2, 13, "Asia/Singapore"); 35 | addHours(tzDate, 2).toString(); 36 | //=> 'Sun Mar 13 2022 02:00:00 GMT+0800 (Singapore Standard Time)' 37 | ``` 38 | 39 | ### Accepted time zone formats 40 | 41 | You can pass IANA time zone name ("Asia/Singapore", "America/New_York", etc.) or UTC offset ("+01:00", "-2359", or "+23"): 42 | 43 | ```ts 44 | new TZDate(2022, 2, 13, "Asia/Singapore"); 45 | 46 | new TZDate(2022, 2, 13, "+08:00"); 47 | 48 | new TZDate(2022, 2, 13, "-2359"); 49 | ``` 50 | 51 | ### Difference between `TZDate` and `TZDateMini` 52 | 53 | The main difference between `TZDate` and `TZDateMini` is the build footprint. The `TZDateMini` is `916 B`, and the `TZDate` is `1.2 kB`. While the difference is slight it might be essential in some environments and use cases. 54 | 55 | Unlike `TZDateMini` which implements only getters, setters, and `getTimezoneOffset`, `TZDate` also provides formatter functions, mirroring all original `Date` functionality: 56 | 57 | ```ts 58 | import { TZDateMini, TZDate } from "@date-fns/tz"; 59 | 60 | // TZDateMini will format date-time in the system time zone: 61 | new TZDateMini(2022, 2, 13).toString(); 62 | //=> 'Sat Mar 12 2022 16:00:00 GMT-0800 (Pacific Standard Time)' 63 | 64 | // TZDate will format date-time in the Singapore time zone, like expected: 65 | new TZDate(2022, 2, 13).toString(); 66 | //=> 'Sun Mar 13 2022 00:00:00 GMT+0800 (Singapore Standard Time)' 67 | ``` 68 | 69 | Even though `TZDate` has a complete API, developers rarely use the formatter functions outside of debugging, so we recommend you pick the more lightweight `TZDateMini` for internal use. However, in environments you don't control, i.e., when you expose the date from a library, using `TZDate` will be a safer choice. 70 | 71 | ## API 72 | 73 | - [`TZDate`](#tzdate) 74 | - [`tz`](#tz) 75 | - [`tzOffset`](#tzoffset) 76 | - [`tzScan`](#tzscan) 77 | 78 | ### `TZDate` 79 | 80 | All the `TZDate` docs are also true for `TZDateMini`. 81 | 82 | #### Constructor 83 | 84 | When creating `TZDate`, you can pass the time zone as the last argument: 85 | 86 | ```ts 87 | new TZDate(2022, 2, "Asia/Singapore"); 88 | 89 | new TZDate(timestamp, "Asia/Singapore"); 90 | 91 | new TZDate("2024-09-12T00:00:00Z", "Asia/Singapore"); 92 | ``` 93 | 94 | The constructor mirrors the original `Date` parameters except for the last time zone parameter. 95 | 96 | #### `TZDate.tz` 97 | 98 | The static `tz` function allows to construct `TZDate` instance with just a time zone: 99 | 100 | ```ts 101 | // Create now in Singapore time zone: 102 | TZDate.tz("Asia/Singapore"); 103 | 104 | // ❌ This will not work, as TZDate expects a date string: 105 | new TZDate("Asia/Singapore"); 106 | //=> Invalid Date 107 | ``` 108 | 109 | Just like the constructor, the function accepts all parameters variants: 110 | 111 | ```ts 112 | TZDate.tz("Asia/Singapore", 2022, 2); 113 | 114 | TZDate.tz("Asia/Singapore", timestamp); 115 | 116 | TZDate.tz("Asia/Singapore", "2024-09-12T00:00:00Z"); 117 | ``` 118 | 119 | #### `timeZone` 120 | 121 | The readonly `timeZone` property returns the time zone name assigned to the instance: 122 | 123 | ```ts 124 | new TZDate(2022, 2, 13, "Asia/Singapore").timeZone; 125 | // "Asia/Singapore" 126 | ``` 127 | 128 | The property might be `undefined` when created without a time zone: 129 | 130 | ```ts 131 | new TZDate().timeZone; 132 | // undefined 133 | ``` 134 | 135 | #### `withTimeZone` 136 | 137 | The `withTimeZone` method allows to create a new `TZDate` instance with a different time zone: 138 | 139 | ```ts 140 | const sg = new TZDate(2022, 2, 13, "Asia/Singapore"); 141 | const ny = sg.withTimeZone("America/New_York"); 142 | 143 | sg.toString(); 144 | //=> 'Sun Mar 13 2022 00:00:00 GMT+0800 (Singapore Standard Time)' 145 | 146 | ny.toString(); 147 | //=> 'Sat Mar 12 2022 11:00:00 GMT-0500 (Eastern Standard Time)' 148 | ``` 149 | 150 | #### `[Symbol.for("constructDateFrom")]` 151 | 152 | The `TZDate` instance also exposes a method to construct a `Date` instance in the same time zone: 153 | 154 | ```ts 155 | const sg = TZDate.tz("Asia/Singapore"); 156 | 157 | // Given that the system time zone is America/Los_Angeles 158 | 159 | const date = sg[Symbol.for("constructDateFrom")](new Date(2024, 0, 1)); 160 | 161 | date.toString(); 162 | //=> 'Mon Jan 01 2024 16:00:00 GMT+0800 (Singapore Standard Time)' 163 | ``` 164 | 165 | It's created for date-fns but can be used in any context. You can access it via `Symbol.for("constructDateFrom")` or import it from the package: 166 | 167 | ```ts 168 | import { constructFromSymbol } from "@date-fns/tz"; 169 | ``` 170 | 171 | ### `tz` 172 | 173 | The `tz` function allows to specify the context for the [date-fns] functions (**starting from date-fns@4**): 174 | 175 | ```ts 176 | import { isSameDay } from "date-fns"; 177 | import { tz } from "@date-fns/tz"; 178 | 179 | isSameDay("2024-09-09T23:00:00-04:00", "2024-09-10T10:00:00+08:00", { 180 | in: tz("Europe/Prague"), 181 | }); 182 | //=> true 183 | ``` 184 | 185 | ### `tzOffset` 186 | 187 | The `tzOffset` function allows to get the time zone UTC offset in minutes from the given time zone and a date: 188 | 189 | ```ts 190 | import { tzOffset } from "@date-fns/tz"; 191 | 192 | const date = new Date("2020-01-15T00:00:00Z"); 193 | 194 | tzOffset("Asia/Singapore", date); 195 | //=> 480 196 | 197 | tzOffset("America/New_York", date); 198 | //=> -300 199 | 200 | // Summer time: 201 | tzOffset("America/New_York", "2020-01-15T00:00:00Z"); 202 | //=> -240 203 | ``` 204 | 205 | Unlike `Date.prototype.getTimezoneOffset`, this function returns the value mirrored to the sign of the offset in the time zone. For Asia/Singapore (UTC+8), `tzOffset` returns 480, while `getTimezoneOffset` returns -480. 206 | 207 | ### `tzScan` 208 | 209 | The function scans the time zone for changes in the given interval. It returns an array of objects with the date of the change, the offset change, and the new offset: 210 | 211 | ```ts 212 | import { tzScan } from "@date-fns/tz"; 213 | 214 | tzScan("America/New_York", { 215 | start: new Date("2020-01-01T00:00:00Z"), 216 | end: new Date("2024-01-01T00:00:00Z"), 217 | }); 218 | //=> [ 219 | //=> { date: 2020-03-08T07:00:00.000Z, change: 60, offset: -240 }, 220 | //=> { date: 2020-11-01T06:00:00.000Z, change: -60, offset: -300 }, 221 | //=> { date: 2021-03-14T07:00:00.000Z, change: 60, offset: -240 }, 222 | //=> { date: 2021-11-07T06:00:00.000Z, change: -60, offset: -300 }, 223 | //=> { date: 2022-03-13T07:00:00.000Z, change: 60, offset: -240 }, 224 | //=> { date: 2022-11-06T06:00:00.000Z, change: -60, offset: -300 }, 225 | //=> { date: 2023-03-12T07:00:00.000Z, change: 60, offset: -240 }, 226 | //=> { date: 2023-11-05T06:00:00.000Z, change: -60, offset: -300 } 227 | //=> ] 228 | ``` 229 | 230 | ## Changelog 231 | 232 | See [the changelog](./CHANGELOG.md). 233 | 234 | ## License 235 | 236 | [MIT © Sasha Koss](https://kossnocorp.mit-license.org/) 237 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript"], 3 | 4 | "env": { 5 | "cjs": { 6 | "presets": [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | "targets": { "node": "current" }, 11 | "modules": "commonjs", 12 | "loose": true 13 | } 14 | ] 15 | ], 16 | 17 | "plugins": [ 18 | [ 19 | "@babel/plugin-transform-modules-commonjs", 20 | { "strict": true, "noInterop": true } 21 | ], 22 | [ 23 | "babel-plugin-replace-import-extension", 24 | { "extMapping": { ".js": ".cjs", ".ts": ".cjs" } } 25 | ] 26 | ] 27 | }, 28 | 29 | "esm": { 30 | "presets": [ 31 | [ 32 | "@babel/preset-env", 33 | { "targets": { "node": "current" }, "modules": false } 34 | ] 35 | ], 36 | 37 | "plugins": [ 38 | [ 39 | "babel-plugin-replace-import-extension", 40 | { "extMapping": { ".ts": ".js" } } 41 | ] 42 | ] 43 | } 44 | }, 45 | 46 | "ignore": [ 47 | "src/**/*.d.ts", 48 | "src/**/tests.ts", 49 | "src/tests/**/*", 50 | "src/**/tysts.ts", 51 | "src/tysts/**/*" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /benchmark.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from "tinybench"; 2 | import { tzOffset, tzScan } from "./src/index.js"; 3 | 4 | const bench = new Bench({ time: 3000 }); 5 | 6 | const duration = { 7 | start: new Date("2020-01-01T00:00:00Z"), 8 | end: new Date("2020-12-31T00:00:00Z"), 9 | }; 10 | 11 | bench.add("tzScan", () => { 12 | tzScan("America/New_York", duration); 13 | }); 14 | 15 | bench.add("tzOffset", () => { 16 | tzOffset("America/New_York", duration.start); 17 | }); 18 | 19 | await bench.warmup(); 20 | await bench.run(); 21 | 22 | console.table(bench.table()); 23 | -------------------------------------------------------------------------------- /copy.mjs: -------------------------------------------------------------------------------- 1 | import watcher from "@parcel/watcher"; 2 | import { copyFile, mkdir } from "fs/promises"; 3 | import { glob } from "glob"; 4 | import { minimatch } from "minimatch"; 5 | import { dirname, join, relative } from "path"; 6 | 7 | const watch = !!process.argv.find((arg) => arg === "--watch"); 8 | 9 | const srcRegExp = /^src\//; 10 | const patterns = ["src/**/*.d.ts", "package.json", "*.md"]; 11 | 12 | if (watch) { 13 | const debouncedCopy = debounceByArgs(copy, 100); 14 | 15 | watcher.subscribe(process.cwd(), (error, events) => { 16 | if (error) { 17 | console.error("The filesystem watcher encountered an error:"); 18 | console.error(error); 19 | process.exit(1); 20 | } 21 | 22 | events.forEach((event) => { 23 | if (event.type !== "create" && event.type !== "update") return; 24 | const path = relative(process.cwd(), event.path); 25 | if (!patterns.some((pattern) => minimatch(path, pattern))) return; 26 | debouncedCopy(path); 27 | }); 28 | }); 29 | } else { 30 | glob(patterns).then((paths) => Promise.all(paths.map(copy))); 31 | } 32 | 33 | async function copy(path) { 34 | const libPath = srcRegExp.test(path) 35 | ? path.replace(/^src/, "lib") 36 | : join("lib", path); 37 | const dir = dirname(libPath); 38 | await mkdir(dir, { recursive: true }); 39 | await copyFile(path, libPath); 40 | console.log(`Copied ${path} to ${libPath}`); 41 | } 42 | 43 | export function debounceByArgs(func, waitFor) { 44 | const timeouts = {}; 45 | 46 | return (...args) => { 47 | const argsKey = JSON.stringify(args); 48 | const later = () => { 49 | delete timeouts[argsKey]; 50 | func(...args); 51 | }; 52 | clearTimeout(timeouts[argsKey]); 53 | timeouts[argsKey] = setTimeout(later, waitFor); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@date-fns/tz", 3 | "version": "1.2.0", 4 | "description": "date-fns timezone utils", 5 | "type": "module", 6 | "main": "index.cjs", 7 | "module": "index.js", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "require": { 12 | "types": "./index.d.cts", 13 | "default": "./index.cjs" 14 | }, 15 | "import": { 16 | "types": "./index.d.ts", 17 | "default": "./index.js" 18 | } 19 | }, 20 | "./tzOffset": { 21 | "require": { 22 | "types": "./tzOffset/index.d.cts", 23 | "default": "./tzOffset/index.cjs" 24 | }, 25 | "import": { 26 | "types": "./tzOffset/index.d.ts", 27 | "default": "./tzOffset/index.js" 28 | } 29 | }, 30 | "./tzScan": { 31 | "require": { 32 | "types": "./tzScan/index.d.cts", 33 | "default": "./tzScan/index.cjs" 34 | }, 35 | "import": { 36 | "types": "./tzScan/index.d.ts", 37 | "default": "./tzScan/index.js" 38 | } 39 | }, 40 | "./date": { 41 | "require": { 42 | "types": "./date/index.d.cts", 43 | "default": "./date/index.cjs" 44 | }, 45 | "import": { 46 | "types": "./date/index.d.ts", 47 | "default": "./date/index.js" 48 | } 49 | }, 50 | "./date/mini": { 51 | "require": { 52 | "types": "./date/mini.d.cts", 53 | "default": "./date/mini.cjs" 54 | }, 55 | "import": { 56 | "types": "./date/mini.d.ts", 57 | "default": "./date/mini.js" 58 | } 59 | }, 60 | "./tz": { 61 | "require": { 62 | "types": "./tz/index.d.cts", 63 | "default": "./tz/index.cjs" 64 | }, 65 | "import": { 66 | "types": "./tz/index.d.ts", 67 | "default": "./tz/index.js" 68 | } 69 | }, 70 | "./constants": { 71 | "require": { 72 | "types": "./constants/index.d.cts", 73 | "default": "./constants/index.cjs" 74 | }, 75 | "import": { 76 | "types": "./constants/index.d.ts", 77 | "default": "./constants/index.js" 78 | } 79 | } 80 | }, 81 | "scripts": { 82 | "test": "vitest run" 83 | }, 84 | "repository": { 85 | "type": "git", 86 | "url": "git+https://github.com/date-fns/tz.git" 87 | }, 88 | "keywords": [ 89 | "date-fns", 90 | "tz", 91 | "timezones", 92 | "date", 93 | "time", 94 | "datetime" 95 | ], 96 | "author": "Sasha Koss ", 97 | "license": "MIT", 98 | "bugs": { 99 | "url": "https://github.com/date-fns/tz/issues" 100 | }, 101 | "homepage": "https://github.com/date-fns/tz#readme", 102 | "devDependencies": { 103 | "@arethetypeswrong/cli": "^0.16.2", 104 | "@babel/cli": "^7.24.1", 105 | "@babel/core": "^7.24.4", 106 | "@babel/plugin-transform-modules-commonjs": "^7.24.1", 107 | "@babel/preset-env": "^7.24.4", 108 | "@babel/preset-typescript": "^7.24.1", 109 | "@parcel/watcher": "^2.4.1", 110 | "@sinonjs/fake-timers": "^11.2.2", 111 | "@swc/core": "^1.4.13", 112 | "@types/sinonjs__fake-timers": "^8.1.5", 113 | "babel-plugin-replace-import-extension": "^1.1.4", 114 | "bytes-iec": "^3.1.1", 115 | "date-fns": "4.0.0-alpha.1", 116 | "glob": "^10.3.12", 117 | "minimatch": "^10.0.1", 118 | "picocolors": "^1.0.0", 119 | "tinybench": "^2.7.0", 120 | "typescript": "^5.5.4", 121 | "vitest": "^1.4.0" 122 | }, 123 | "packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c" 124 | } 125 | -------------------------------------------------------------------------------- /playground.mjs: -------------------------------------------------------------------------------- 1 | console.log( 2 | `###### ${ 3 | process.env.TZ || 4 | Intl.DateTimeFormat("en-US", { 5 | timeZoneName: "longGeneric", 6 | }) 7 | .format(new Date()) 8 | .split(" ") 9 | .slice(1) 10 | .join(" ") 11 | } ######` 12 | ); 13 | 14 | function handle(fn, ...args) { 15 | const date = new Date(2020, 0, 1); 16 | 17 | console.log(`=== date.${fn}(${args.join(", ")}) ===`); 18 | console.log(); 19 | print(date); 20 | console.log("->"); 21 | 22 | date[fn](...args); 23 | 24 | print(date); 25 | console.log(); 26 | } 27 | 28 | console.log(); 29 | console.log("************************************"); 30 | console.log("********** setUTCFullYear **********"); 31 | console.log("************************************"); 32 | console.log(); 33 | 34 | handle("setUTCFullYear", 2020); 35 | handle("setUTCFullYear", 2020, 0); 36 | handle("setUTCFullYear", 2020, 0, 1); 37 | 38 | handle("setUTCFullYear", 2020, 48); 39 | handle("setUTCFullYear", 2020, -8); 40 | 41 | handle("setUTCFullYear", 2020, 14, 45); 42 | handle("setUTCFullYear", 2020, -8, -60); 43 | 44 | console.log(); 45 | console.log("************************************"); 46 | console.log("************ setUTCMonth ***********"); 47 | console.log("************************************"); 48 | console.log(); 49 | 50 | handle("setUTCMonth", 1); 51 | handle("setUTCMonth", 1, 11); 52 | 53 | handle("setUTCMonth", 48); 54 | handle("setUTCMonth", -18); 55 | 56 | handle("setUTCMonth", 18, 45); 57 | handle("setUTCMonth", -18, -60); 58 | 59 | console.log(); 60 | console.log("************************************"); 61 | console.log("************ setUTCDate ************"); 62 | console.log("************************************"); 63 | console.log(); 64 | 65 | handle("setUTCDate", 11); 66 | 67 | handle("setUTCDate", 945); 68 | handle("setUTCDate", -60); 69 | 70 | console.log(); 71 | console.log("------------------------------------"); 72 | console.log("--------------- time ---------------"); 73 | console.log("------------------------------------"); 74 | console.log(); 75 | 76 | console.log(); 77 | console.log("************************************"); 78 | console.log("************ setUTCHours ***********"); 79 | console.log("************************************"); 80 | console.log(); 81 | 82 | handle("setUTCHours", 12); 83 | handle("setUTCHours", 12, 34, 56, 789); 84 | 85 | handle("setUTCHours", 30, 120, 120, 30000); 86 | handle("setUTCHours", -30, -120, -120, -30000); 87 | 88 | console.log(); 89 | console.log("************************************"); 90 | console.log("*********** setUTCMinutes **********"); 91 | console.log("************************************"); 92 | console.log(); 93 | 94 | handle("setUTCMinutes", 34); 95 | handle("setUTCMinutes", 34, 56, 789); 96 | 97 | handle("setUTCMinutes", 120, 120, 30000); 98 | handle("setUTCMinutes", -120, -120, -30000); 99 | 100 | console.log(); 101 | console.log("************************************"); 102 | console.log("*********** setUTCSeconds **********"); 103 | console.log("************************************"); 104 | console.log(); 105 | 106 | handle("setUTCSeconds", 56); 107 | handle("setUTCSeconds", 56, 789); 108 | 109 | handle("setUTCSeconds", 120, 30000); 110 | handle("setUTCSeconds", -120, -30000); 111 | 112 | console.log(); 113 | console.log("************************************"); 114 | console.log("******** setUTCMilliseconds ********"); 115 | console.log("************************************"); 116 | console.log(); 117 | 118 | handle("setUTCMilliseconds", 789); 119 | 120 | handle("setUTCMilliseconds", 30000); 121 | handle("setUTCMilliseconds", -30000); 122 | 123 | function print(date) { 124 | console.log(`${format(date)} / ${date.toISOString()} (UTC)`); 125 | } 126 | 127 | function format(date) { 128 | return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad( 129 | date.getDate() 130 | )}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad( 131 | date.getSeconds() 132 | )}.${pad(date.getMilliseconds(), 3)}${tz(date)}`; 133 | } 134 | 135 | function pad(num, length = 2) { 136 | return num.toString().padStart(length, "0"); 137 | } 138 | 139 | function tz(date) { 140 | return Intl.DateTimeFormat("en-US", { 141 | timeZoneName: "longOffset", 142 | }) 143 | .format(date) 144 | .split(" ")[1] 145 | .slice(3); 146 | } 147 | -------------------------------------------------------------------------------- /size.mjs: -------------------------------------------------------------------------------- 1 | import { minify, transform } from "@swc/core"; 2 | import { readFile } from "fs/promises"; 3 | import watcher from "@parcel/watcher"; 4 | import { relative } from "path"; 5 | import { createBrotliCompress, constants } from "node:zlib"; 6 | import { Readable } from "stream"; 7 | import bytes from "bytes-iec"; 8 | import picocolors from "picocolors"; 9 | 10 | const { blue, green, gray, red } = picocolors; 11 | 12 | const srcPath = relative(process.cwd(), process.argv[2]); 13 | const watch = !!process.argv.find((arg) => arg === "--watch"); 14 | const debouncedMeasure = debounce(measure, 50); 15 | 16 | measure(); 17 | 18 | if (watch) 19 | watcher.subscribe(process.cwd(), (error, events) => { 20 | if (error) { 21 | console.error("The filesystem watcher encountered an error:"); 22 | console.error(error); 23 | process.exit(1); 24 | } 25 | 26 | events.forEach((event) => { 27 | if (event.type !== "create" && event.type !== "update") return; 28 | const path = relative(process.cwd(), event.path); 29 | if (srcPath !== path) return; 30 | debouncedMeasure(); 31 | }); 32 | }); 33 | 34 | let lastLength; 35 | let lastSize; 36 | 37 | async function measure() { 38 | const code = await readFile(srcPath, "utf-8"); 39 | const processedCode = srcPath.endsWith(".ts") 40 | ? await transform(code, { 41 | jsc: { target: "esnext", parser: { syntax: "typescript" } }, 42 | }) 43 | : code; 44 | 45 | minify(processedCode, { 46 | compress: true, 47 | mangle: true, 48 | sourceMap: false, 49 | module: true, 50 | }) 51 | .then(({ code }) => Promise.all([code, measureSize(code)]).then([code])) 52 | .then(([code, size]) => { 53 | if (code.length === lastLength && size === lastSize) return; 54 | 55 | watch && console.clear(); 56 | console.log(`Last write: ${blue(new Date().toString())}`); 57 | console.log(""); 58 | console.log("Source code:"); 59 | console.log(""); 60 | console.log(gray(code)); 61 | console.log(""); 62 | console.log( 63 | `Length: ${blue(code.length)} ${formatDiff(code.length - lastLength)}` 64 | ); 65 | console.log(""); 66 | console.log( 67 | `Size: ${blue(bytes(size, { decimalPlaces: 3 }))} ${formatDiff( 68 | size - lastSize 69 | )}` 70 | ); 71 | console.log(""); 72 | 73 | lastLength = code.length; 74 | lastSize = size; 75 | }); 76 | } 77 | 78 | function formatDiff(diff) { 79 | if (!diff) return ""; 80 | return diff > 0 ? red(`+${diff}`) : green(diff); 81 | } 82 | 83 | function measureSize(code) { 84 | return new Promise((resolve, reject) => { 85 | let size = 0; 86 | const stream = new Readable(); 87 | stream.push(code); 88 | stream.push(null); 89 | 90 | let pipe = stream.pipe( 91 | createBrotliCompress({ 92 | params: { 93 | [constants.BROTLI_PARAM_QUALITY]: 11, // Use maximum compression quality 94 | }, 95 | }) 96 | ); 97 | 98 | pipe.on("error", reject); 99 | pipe.on("data", (buf) => (size += buf.length)); 100 | pipe.on("end", () => resolve(size)); 101 | }); 102 | } 103 | 104 | function debounce(func, waitFor) { 105 | let timeout; 106 | return (...args) => { 107 | if (timeout !== null) clearTimeout(timeout); 108 | timeout = setTimeout(() => func(...args), waitFor); 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The symbol to access the `TZDate`'s function to construct a new instance from 3 | * the provided value. It helps date-fns to inherit the time zone. 4 | */ 5 | export const constructFromSymbol = Symbol.for("constructDateFrom"); 6 | -------------------------------------------------------------------------------- /src/date/index.d.ts: -------------------------------------------------------------------------------- 1 | import { constructFromSymbol } from "../constants/index.ts"; 2 | 3 | /** 4 | * Time zone date class. It overrides original Date functions making them 5 | * to perform all the calculations in the given time zone. 6 | * 7 | * It also provides new functions useful when working with time zones. 8 | * 9 | * Combined with date-fns, it allows using the class the same way as 10 | * the original date class. 11 | * 12 | * This complete version provides formatter functions, mirroring all original 13 | * methods of the `Date` class. It's build-size-heavier than `TZDateMini` and 14 | * should be used when you need to format a string or in an environment you 15 | * don't fully control (a library). 16 | * 17 | * For the minimal version, see `TZDateMini`. 18 | */ 19 | export class TZDate extends Date { 20 | /** 21 | * Constructs a new `TZDate` instance in the system time zone. 22 | */ 23 | constructor(); 24 | 25 | /** 26 | * Constructs a new `TZDate` instance from the date time string and time zone. 27 | * 28 | * @param dateStr - Date time string to create a new instance from 29 | * @param timeZone - Time zone name (IANA or UTC offset) 30 | */ 31 | constructor(dateStr: string, timeZone?: string); 32 | 33 | /** 34 | * Constructs a new `TZDate` instance from the date object and time zone. 35 | * 36 | * @param date - Date object to create a new instance from 37 | * @param timeZone - Time zone name (IANA or UTC offset) 38 | */ 39 | constructor(date: Date, timeZone?: string); 40 | 41 | /** 42 | * Constructs a new `TZDate` instance from the Unix timestamp in milliseconds 43 | * and time zone. 44 | * 45 | * @param timestamp - Unix timestamp in milliseconds to create a new instance from 46 | * @param timeZone - Time zone name (IANA or UTC offset) 47 | */ 48 | constructor(timestamp: number, timeZone?: string); 49 | 50 | /** 51 | * Constructs a new `TZDate` instance from the year, month, and time zone. 52 | * 53 | * @param year - Year 54 | * @param month - Month (0-11) 55 | * @param timeZone - Time zone name (IANA or UTC offset) 56 | */ 57 | constructor(year: number, month: number, timeZone?: string); 58 | 59 | /** 60 | * Constructs a new `TZDate` instance from the year, month, date and time zone. 61 | * 62 | * @param year - Year 63 | * @param month - Month (0-11) 64 | * @param date - Date 65 | * @param timeZone - Time zone name (IANA or UTC offset) 66 | */ 67 | constructor(year: number, month: number, date: number, timeZone?: string); 68 | 69 | /** 70 | * Constructs a new `TZDate` instance from the year, month, date, hours 71 | * and time zone. 72 | * 73 | * @param year - Year 74 | * @param month - Month (0-11) 75 | * @param date - Date 76 | * @param hours - Hours 77 | * @param timeZone - Time zone name (IANA or UTC offset) 78 | */ 79 | constructor( 80 | year: number, 81 | month: number, 82 | date: number, 83 | hours: number, 84 | timeZone?: string 85 | ); 86 | 87 | /** 88 | * Constructs a new `TZDate` instance from the year, month, date, hours, 89 | * minutes and time zone. 90 | * 91 | * @param year - Year 92 | * @param month - Month (0-11) 93 | * @param date - Date 94 | * @param hours - Hours 95 | * @param minutes - Minutes 96 | * @param timeZone - Time zone name (IANA or UTC offset) 97 | */ 98 | constructor( 99 | year: number, 100 | month: number, 101 | date: number, 102 | hours: number, 103 | minutes: number, 104 | timeZone?: string 105 | ); 106 | 107 | /** 108 | * Constructs a new `TZDate` instance from the year, month, date, hours, 109 | * minutes, seconds and time zone. 110 | * 111 | * @param year - Year 112 | * @param month - Month (0-11) 113 | * @param date - Date 114 | * @param hours - Hours 115 | * @param minutes - Minutes 116 | * @param seconds - Seconds 117 | * @param timeZone - Time zone name (IANA or UTC offset) 118 | */ 119 | constructor( 120 | year: number, 121 | month: number, 122 | date: number, 123 | hours: number, 124 | minutes: number, 125 | seconds: number, 126 | timeZone?: string 127 | ); 128 | 129 | /** 130 | * Constructs a new `TZDate` instance from the year, month, date, hours, 131 | * minutes, seconds, milliseconds and time zone. 132 | * 133 | * @param year - Year 134 | * @param month - Month (0-11) 135 | * @param date - Date 136 | * @param hours - Hours 137 | * @param minutes - Minutes 138 | * @param seconds - Seconds 139 | * @param milliseconds - Milliseconds 140 | * @param timeZone - Time zone name (IANA or UTC offset) 141 | */ 142 | constructor( 143 | year: number, 144 | month: number, 145 | date: number, 146 | hours: number, 147 | minutes: number, 148 | seconds: number, 149 | milliseconds: number, 150 | timeZone?: string 151 | ); 152 | 153 | /** 154 | * Creates a new `TZDate` instance in the given time zone. 155 | * 156 | * @param tz - Time zone name (IANA or UTC offset) 157 | */ 158 | static tz(tz: string): TZDate; 159 | 160 | /** 161 | * Creates a new `TZDate` instance in the given time zone from the Unix 162 | * timestamp in milliseconds. 163 | * 164 | * @param tz - Time zone name (IANA or UTC offset) 165 | * @param timestamp - Unix timestamp in milliseconds 166 | */ 167 | static tz(tz: string, timestamp: number): TZDate; 168 | 169 | /** 170 | * Creates a new `TZDate` instance in the given time zone from the date time 171 | * string. 172 | * 173 | * @param tz - Time zone name (IANA or UTC offset) 174 | * @param dateStr - Date time string 175 | */ 176 | static tz(tz: string, dateStr: string): TZDate; 177 | 178 | /** 179 | * Creates a new `TZDate` instance in the given time zone from the date object. 180 | * 181 | * @param tz - Time zone name (IANA or UTC offset) 182 | * @param date - Date object 183 | */ 184 | static tz(tz: string, date: Date): TZDate; 185 | 186 | /** 187 | * Creates a new `TZDate` instance in the given time zone from the year 188 | * and month. 189 | * 190 | * @param tz - Time zone name (IANA or UTC offset) 191 | * @param year - Year 192 | * @param month - Month (0-11) 193 | */ 194 | static tz(tz: string, year: number, month: number): TZDate; 195 | 196 | /** 197 | * Creates a new `TZDate` instance in the given time zone from the year, 198 | * month and date. 199 | * 200 | * @param tz - Time zone name (IANA or UTC offset) 201 | * @param year - Year 202 | * @param month - Month (0-11) 203 | * @param date - Date 204 | */ 205 | static tz(tz: string, year: number, month: number, date: number): TZDate; 206 | 207 | /** 208 | * Creates a new `TZDate` instance in the given time zone from the year, 209 | * month, date and hours. 210 | * 211 | * @param tz - Time zone name (IANA or UTC offset) 212 | * @param year - Year 213 | * @param month - Month (0-11) 214 | * @param date - Date 215 | * @param hours - Hours 216 | */ 217 | static tz( 218 | tz: string, 219 | year: number, 220 | month: number, 221 | date: number, 222 | hours: number 223 | ): TZDate; 224 | 225 | /** 226 | * Creates a new `TZDate` instance in the given time zone from the year, 227 | * month, date, hours and minutes. 228 | * 229 | * @param tz - Time zone name (IANA or UTC offset) 230 | * @param year - Year 231 | * @param month - Month (0-11) 232 | * @param date - Date 233 | * @param hours - Hours 234 | * @param minutes - Minutes 235 | */ 236 | static tz( 237 | tz: string, 238 | year: number, 239 | month: number, 240 | date: number, 241 | hours: number, 242 | minutes: number 243 | ): TZDate; 244 | 245 | /** 246 | * Creates a new `TZDate` instance in the given time zone from the year, 247 | * month, date, hours, minutes and seconds. 248 | * 249 | * @param tz - Time zone name (IANA or UTC offset) 250 | * @param year - Year 251 | * @param month - Month (0-11) 252 | * @param date - Date 253 | * @param hours - Hours 254 | * @param minutes - Minutes 255 | * @param seconds - Seconds 256 | */ 257 | static tz( 258 | tz: string, 259 | year: number, 260 | month: number, 261 | date: number, 262 | hours: number, 263 | minutes: number, 264 | seconds: number 265 | ): TZDate; 266 | 267 | /** 268 | * Creates a new `TZDate` instance in the given time zone from the year, 269 | * month, date, hours, minutes, seconds and milliseconds. 270 | * 271 | * @param tz - Time zone name (IANA or UTC offset) 272 | * @param year - Year 273 | * @param month - Month (0-11) 274 | * @param date - Date 275 | * @param hours - Hours 276 | * @param minutes - Minutes 277 | * @param seconds - Seconds 278 | * @param milliseconds - Milliseconds 279 | */ 280 | static tz( 281 | tz: string, 282 | year: number, 283 | month: number, 284 | date: number, 285 | hours: number, 286 | minutes: number, 287 | seconds: number, 288 | milliseconds: number 289 | ): TZDate; 290 | 291 | /** 292 | * The current time zone of the date. 293 | */ 294 | readonly timeZone: string | undefined; 295 | 296 | /** 297 | * Creates a new `TZDate` instance in the given time zone. 298 | */ 299 | withTimeZone(timeZone: string): TZDate; 300 | 301 | /** 302 | * Creates a new `TZDate` instance in the current instance time zone and 303 | * the specified date time value. 304 | * 305 | * @param date - Date value to create a new instance from 306 | */ 307 | [constructFromSymbol](date: Date | number | string): TZDate; 308 | } 309 | -------------------------------------------------------------------------------- /src/date/index.js: -------------------------------------------------------------------------------- 1 | import { TZDateMini } from "./mini.js"; 2 | 3 | /** 4 | * UTC date class. It maps getters and setters to corresponding UTC methods, 5 | * forcing all calculations in the UTC time zone. 6 | * 7 | * Combined with date-fns, it allows using the class the same way as 8 | * the original date class. 9 | * 10 | * This complete version provides not only getters, setters, 11 | * and `getTimezoneOffset`, but also the formatter functions, mirroring 12 | * all original `Date` functionality. Use this version when you need to format 13 | * a string or in an environment you don't fully control (a library). 14 | * For a minimal version, see `UTCDateMini`. 15 | */ 16 | export class TZDate extends TZDateMini { 17 | //#region static 18 | 19 | static tz(tz, ...args) { 20 | return args.length ? new TZDate(...args, tz) : new TZDate(Date.now(), tz); 21 | } 22 | 23 | //#endregion 24 | 25 | //#region representation 26 | 27 | toISOString() { 28 | const [sign, hours, minutes] = this.tzComponents(); 29 | const tz = `${sign}${hours}:${minutes}`; 30 | return this.internal.toISOString().slice(0, -1) + tz; 31 | } 32 | 33 | toString() { 34 | // "Tue Aug 13 2024 07:50:19 GMT+0800 (Singapore Standard Time)"; 35 | return `${this.toDateString()} ${this.toTimeString()}`; 36 | } 37 | 38 | toDateString() { 39 | // toUTCString returns RFC 7231 ("Mon, 12 Aug 2024 23:36:08 GMT") 40 | const [day, date, month, year] = this.internal.toUTCString().split(" "); 41 | // "Tue Aug 13 2024" 42 | return `${day?.slice(0, -1) /* Remove "," */} ${month} ${date} ${year}`; 43 | } 44 | 45 | toTimeString() { 46 | // toUTCString returns RFC 7231 ("Mon, 12 Aug 2024 23:36:08 GMT") 47 | const time = this.internal.toUTCString().split(" ")[4]; 48 | const [sign, hours, minutes] = this.tzComponents(); 49 | // "07:42:23 GMT+0800 (Singapore Standard Time)" 50 | return `${time} GMT${sign}${hours}${minutes} (${tzName( 51 | this.timeZone, 52 | this 53 | )})`; 54 | } 55 | 56 | toLocaleString(locales, options) { 57 | return Date.prototype.toLocaleString.call(this, locales, { 58 | ...options, 59 | timeZone: options?.timeZone || this.timeZone, 60 | }); 61 | } 62 | 63 | toLocaleDateString(locales, options) { 64 | return Date.prototype.toLocaleDateString.call(this, locales, { 65 | ...options, 66 | timeZone: options?.timeZone || this.timeZone, 67 | }); 68 | } 69 | 70 | toLocaleTimeString(locales, options) { 71 | return Date.prototype.toLocaleTimeString.call(this, locales, { 72 | ...options, 73 | timeZone: options?.timeZone || this.timeZone, 74 | }); 75 | } 76 | 77 | //#endregion 78 | 79 | //#region private 80 | 81 | tzComponents() { 82 | const offset = this.getTimezoneOffset(); 83 | const sign = offset > 0 ? "-" : "+"; 84 | const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0"); 85 | const minutes = String(Math.abs(offset) % 60).padStart(2, "0"); 86 | return [sign, hours, minutes]; 87 | } 88 | 89 | //#endregion 90 | 91 | withTimeZone(timeZone) { 92 | return new TZDate(+this, timeZone); 93 | } 94 | 95 | //#region date-fns integration 96 | 97 | [Symbol.for("constructDateFrom")](date) { 98 | return new TZDate(+new Date(date), this.timeZone); 99 | } 100 | 101 | //#endregion 102 | } 103 | 104 | function tzName(tz, date) { 105 | return new Intl.DateTimeFormat("en-GB", { 106 | timeZone: tz, 107 | timeZoneName: "long", 108 | }) 109 | .format(date) 110 | .slice(12); 111 | } 112 | -------------------------------------------------------------------------------- /src/date/mini.d.ts: -------------------------------------------------------------------------------- 1 | import type { TZDate } from "./index.ts"; 2 | 3 | /** 4 | * Time zone date class. It overrides original Date functions making them 5 | * to perform all the calculations in the given time zone. 6 | * 7 | * It also provides new functions useful when working with time zones. 8 | * 9 | * Combined with date-fns, it allows using the class the same way as 10 | * the original date class. 11 | * 12 | * This minimal version provides complete functionality required for date-fns 13 | * and excludes build-size-heavy formatter functions. 14 | * 15 | * For the complete version, see `TZDate`. 16 | */ 17 | export const TZDateMini: typeof TZDate; 18 | -------------------------------------------------------------------------------- /src/date/mini.js: -------------------------------------------------------------------------------- 1 | import { tzOffset } from "../tzOffset/index.ts"; 2 | 3 | export class TZDateMini extends Date { 4 | //#region static 5 | 6 | constructor(...args) { 7 | super(); 8 | 9 | if (args.length > 1 && typeof args[args.length - 1] === "string") { 10 | this.timeZone = args.pop(); 11 | } 12 | 13 | this.internal = new Date(); 14 | 15 | if (isNaN(tzOffset(this.timeZone, this))) { 16 | this.setTime(NaN); 17 | } else { 18 | if (!args.length) { 19 | this.setTime(Date.now()); 20 | } else if ( 21 | typeof args[0] === "number" && 22 | (args.length === 1 || 23 | (args.length === 2 && typeof args[1] !== "number")) 24 | ) { 25 | this.setTime(args[0]); 26 | } else if (typeof args[0] === "string") { 27 | this.setTime(+new Date(args[0])); 28 | } else if (args[0] instanceof Date) { 29 | this.setTime(+args[0]); 30 | } else { 31 | this.setTime(+new Date(...args)); 32 | adjustToSystemTZ(this, NaN); 33 | syncToInternal(this); 34 | } 35 | } 36 | } 37 | 38 | static tz(tz, ...args) { 39 | return args.length 40 | ? new TZDateMini(...args, tz) 41 | : new TZDateMini(Date.now(), tz); 42 | } 43 | 44 | //#endregion 45 | 46 | //#region time zone 47 | 48 | withTimeZone(timeZone) { 49 | return new TZDateMini(+this, timeZone); 50 | } 51 | 52 | getTimezoneOffset() { 53 | return -tzOffset(this.timeZone, this); 54 | } 55 | 56 | //#endregion 57 | 58 | //#region time 59 | 60 | setTime(time) { 61 | Date.prototype.setTime.apply(this, arguments); 62 | syncToInternal(this); 63 | return +this; 64 | } 65 | 66 | //#endregion 67 | 68 | //#region date-fns integration 69 | 70 | [Symbol.for("constructDateFrom")](date) { 71 | return new TZDateMini(+new Date(date), this.timeZone); 72 | } 73 | 74 | //#endregion 75 | } 76 | 77 | // Assign getters and setters 78 | const re = /^(get|set)(?!UTC)/; 79 | Object.getOwnPropertyNames(Date.prototype).forEach((method) => { 80 | if (!re.test(method)) return; 81 | 82 | const utcMethod = method.replace(re, "$1UTC"); 83 | // Filter out methods without UTC counterparts 84 | if (!TZDateMini.prototype[utcMethod]) return; 85 | 86 | if (method.startsWith("get")) { 87 | // Delegate to internal date's UTC method 88 | TZDateMini.prototype[method] = function () { 89 | return this.internal[utcMethod](); 90 | }; 91 | } else { 92 | // Assign regular setter 93 | TZDateMini.prototype[method] = function () { 94 | Date.prototype[utcMethod].apply(this.internal, arguments); 95 | syncFromInternal(this); 96 | return +this; 97 | }; 98 | 99 | // Assign UTC setter 100 | TZDateMini.prototype[utcMethod] = function () { 101 | Date.prototype[utcMethod].apply(this, arguments); 102 | syncToInternal(this); 103 | return +this; 104 | }; 105 | } 106 | }); 107 | 108 | /** 109 | * Function syncs time to internal date, applying the time zone offset. 110 | * 111 | * @param {Date} date - Date to sync 112 | */ 113 | function syncToInternal(date) { 114 | date.internal.setTime(+date); 115 | date.internal.setUTCMinutes( 116 | date.internal.getUTCMinutes() - date.getTimezoneOffset() 117 | ); 118 | } 119 | 120 | /** 121 | * Function syncs the internal date UTC values to the date. It allows to get 122 | * accurate timestamp value. 123 | * 124 | * @param {Date} date - The date to sync 125 | */ 126 | function syncFromInternal(date) { 127 | // First we transpose the internal values 128 | Date.prototype.setFullYear.call( 129 | date, 130 | date.internal.getUTCFullYear(), 131 | date.internal.getUTCMonth(), 132 | date.internal.getUTCDate() 133 | ); 134 | Date.prototype.setHours.call( 135 | date, 136 | date.internal.getUTCHours(), 137 | date.internal.getUTCMinutes(), 138 | date.internal.getUTCSeconds(), 139 | date.internal.getUTCMilliseconds() 140 | ); 141 | 142 | // Now we have to adjust the date to the system time zone 143 | adjustToSystemTZ(date); 144 | } 145 | 146 | /** 147 | * Function adjusts the date to the system time zone. It uses the time zone 148 | * differences to calculate the offset and adjust the date. 149 | * 150 | * @param {Date} date - Date to adjust 151 | */ 152 | function adjustToSystemTZ(date) { 153 | // Save the time zone offset before all the adjustments 154 | const offset = tzOffset(date.timeZone, date); 155 | 156 | //#region System DST adjustment 157 | 158 | // The biggest problem with using the system time zone is that when we create 159 | // a date from internal values stored in UTC, the system time zone might end 160 | // up on the DST hour: 161 | // 162 | // $ TZ=America/New_York node 163 | // > new Date(2020, 2, 8, 1).toString() 164 | // 'Sun Mar 08 2020 01:00:00 GMT-0500 (Eastern Standard Time)' 165 | // > new Date(2020, 2, 8, 2).toString() 166 | // 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)' 167 | // > new Date(2020, 2, 8, 3).toString() 168 | // 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)' 169 | // > new Date(2020, 2, 8, 4).toString() 170 | // 'Sun Mar 08 2020 04:00:00 GMT-0400 (Eastern Daylight Time)' 171 | // 172 | // Here we get the same hour for both 2 and 3, because the system time zone 173 | // has DST beginning at 8 March 2020, 2 a.m. and jumps to 3 a.m. So we have 174 | // to adjust the internal date to reflect that. 175 | // 176 | // However we want to adjust only if that's the DST hour the change happenes, 177 | // not the hour where DST moves to. 178 | 179 | // We calculate the previous hour to see if the time zone offset has changed 180 | // and we have landed on the DST hour. 181 | const prevHour = new Date(+date); 182 | // We use UTC methods here as we don't want to land on the same hour again 183 | // in case of DST. 184 | prevHour.setUTCHours(prevHour.getUTCHours() - 1); 185 | 186 | // Calculate if we are on the system DST hour. 187 | const systemOffset = -new Date(+date).getTimezoneOffset(); 188 | const prevHourSystemOffset = -new Date(+prevHour).getTimezoneOffset(); 189 | const systemDSTChange = systemOffset - prevHourSystemOffset; 190 | // Detect the DST shift. System DST change will occur both on 191 | const dstShift = 192 | Date.prototype.getHours.apply(date) !== date.internal.getUTCHours(); 193 | 194 | // Move the internal date when we are on the system DST hour. 195 | if (systemDSTChange && dstShift) 196 | date.internal.setUTCMinutes( 197 | date.internal.getUTCMinutes() + systemDSTChange 198 | ); 199 | 200 | //#endregion 201 | 202 | //#region System diff adjustment 203 | 204 | // Now we need to adjust the date, since we just applied internal values. 205 | // We need to calculate the difference between the system and date time zones 206 | // and apply it to the date. 207 | 208 | const offsetDiff = systemOffset - offset; 209 | if (offsetDiff) 210 | Date.prototype.setUTCMinutes.call( 211 | date, 212 | Date.prototype.getUTCMinutes.call(date) + offsetDiff 213 | ); 214 | 215 | //#endregion 216 | 217 | //#region Post-adjustment DST fix 218 | 219 | const postOffset = tzOffset(date.timeZone, date); 220 | const postSystemOffset = -new Date(+date).getTimezoneOffset(); 221 | const postOffsetDiff = postSystemOffset - postOffset; 222 | const offsetChanged = postOffset !== offset; 223 | const postDiff = postOffsetDiff - offsetDiff; 224 | 225 | if (offsetChanged && postDiff) { 226 | Date.prototype.setUTCMinutes.call( 227 | date, 228 | Date.prototype.getUTCMinutes.call(date) + postDiff 229 | ); 230 | 231 | // Now we need to check if got offset change during the post-adjustment. 232 | // If so, we also need both dates to reflect that. 233 | 234 | const newOffset = tzOffset(date.timeZone, date); 235 | const offsetChange = postOffset - newOffset; 236 | 237 | if (offsetChange) { 238 | date.internal.setUTCMinutes(date.internal.getUTCMinutes() + offsetChange); 239 | Date.prototype.setUTCMinutes.call( 240 | date, 241 | Date.prototype.getUTCMinutes.call(date) + offsetChange 242 | ); 243 | } 244 | } 245 | 246 | //#endregion 247 | } 248 | -------------------------------------------------------------------------------- /src/date/tests.ts: -------------------------------------------------------------------------------- 1 | import FakeTimers from "@sinonjs/fake-timers"; 2 | import { afterEach, describe, expect, it } from "vitest"; 3 | import { constructFromSymbol } from "../constants/index.ts"; 4 | import { TZDate } from "./index.js"; 5 | 6 | describe("TZDate", () => { 7 | const defaultDateStr = "1987-02-11T00:00:00.000Z"; 8 | 9 | let timers: FakeTimers.InstalledClock; 10 | let now = new Date(); 11 | 12 | function fakeNow(date = new Date(defaultDateStr)) { 13 | now = date; 14 | timers = FakeTimers.install({ now }); 15 | } 16 | 17 | afterEach(() => timers?.uninstall()); 18 | 19 | describe("static", () => { 20 | describe("constructor", () => { 21 | it("creates a new date", () => { 22 | fakeNow(); 23 | const date = new TZDate(); 24 | expect(+date).toBe(+now); 25 | }); 26 | 27 | it("creates a new date from a timestamp", () => { 28 | expect( 29 | new TZDate(+new Date(defaultDateStr), "Asia/Singapore").toISOString() 30 | ).toBe("1987-02-11T08:00:00.000+08:00"); 31 | expect( 32 | new TZDate( 33 | +new Date(defaultDateStr), 34 | "America/New_York" 35 | ).toISOString() 36 | ).toBe("1987-02-10T19:00:00.000-05:00"); 37 | }); 38 | 39 | it("creates a new date from a string", () => { 40 | const dateStr = "2024-02-11T00:00:00.000Z"; 41 | expect(new TZDate(dateStr, "Asia/Singapore").toISOString()).toBe( 42 | "2024-02-11T08:00:00.000+08:00" 43 | ); 44 | expect(new TZDate("2024-02-11", "Asia/Singapore").toISOString()).toBe( 45 | "2024-02-11T08:00:00.000+08:00" 46 | ); 47 | expect(new TZDate(dateStr, "America/New_York").toISOString()).toBe( 48 | "2024-02-10T19:00:00.000-05:00" 49 | ); 50 | expect(new TZDate("2024-02-11", "America/New_York").toISOString()).toBe( 51 | "2024-02-10T19:00:00.000-05:00" 52 | ); 53 | }); 54 | 55 | it("creates a new date from a date instance", () => { 56 | const nativeDate = new Date(defaultDateStr); 57 | expect(new TZDate(nativeDate, "Asia/Singapore").toISOString()).toBe( 58 | "1987-02-11T08:00:00.000+08:00" 59 | ); 60 | expect(new TZDate(nativeDate, "America/New_York").toISOString()).toBe( 61 | "1987-02-10T19:00:00.000-05:00" 62 | ); 63 | }); 64 | 65 | it("creates a new date from date values", () => { 66 | // Month 67 | expect(new TZDate(2024, 1, "Asia/Singapore").toISOString()).toBe( 68 | "2024-02-01T00:00:00.000+08:00" 69 | ); 70 | expect(new TZDate(2024, 1, "America/New_York").toISOString()).toBe( 71 | "2024-02-01T00:00:00.000-05:00" 72 | ); 73 | // Date 74 | expect(new TZDate(2024, 1, 11, "Asia/Singapore").toISOString()).toBe( 75 | "2024-02-11T00:00:00.000+08:00" 76 | ); 77 | expect(new TZDate(2024, 1, 11, "America/New_York").toISOString()).toBe( 78 | "2024-02-11T00:00:00.000-05:00" 79 | ); 80 | // Hours 81 | expect( 82 | new TZDate(2024, 1, 11, 12, "Asia/Singapore").toISOString() 83 | ).toBe("2024-02-11T12:00:00.000+08:00"); 84 | expect( 85 | new TZDate(2024, 1, 11, 12, "America/New_York").toISOString() 86 | ).toBe("2024-02-11T12:00:00.000-05:00"); 87 | // Minutes 88 | expect( 89 | new TZDate(2024, 1, 11, 12, 30, "Asia/Singapore").toISOString() 90 | ).toBe("2024-02-11T12:30:00.000+08:00"); 91 | expect( 92 | new TZDate(2024, 1, 11, 12, 30, "America/New_York").toISOString() 93 | ).toBe("2024-02-11T12:30:00.000-05:00"); 94 | // Seconds 95 | expect( 96 | new TZDate(2024, 1, 11, 12, 30, 45, "Asia/Singapore").toISOString() 97 | ).toBe("2024-02-11T12:30:45.000+08:00"); 98 | expect( 99 | new TZDate(2024, 1, 11, 12, 30, 45, "America/New_York").toISOString() 100 | ).toBe("2024-02-11T12:30:45.000-05:00"); 101 | // Milliseconds 102 | expect( 103 | new TZDate( 104 | 2024, 105 | 1, 106 | 11, 107 | 12, 108 | 30, 109 | 45, 110 | 987, 111 | "Asia/Singapore" 112 | ).toISOString() 113 | ).toBe("2024-02-11T12:30:45.987+08:00"); 114 | expect( 115 | new TZDate( 116 | 2024, 117 | 1, 118 | 11, 119 | 12, 120 | 30, 121 | 45, 122 | 987, 123 | "America/New_York" 124 | ).toISOString() 125 | ).toBe("2024-02-11T12:30:45.987-05:00"); 126 | }); 127 | 128 | it("returns Invalid Date for invalid date values", () => { 129 | expect(+new TZDate(NaN, "Asia/Singapore")).toBe(NaN); 130 | }); 131 | 132 | describe("DST", () => { 133 | it("America/Los_Angeles", () => { 134 | expect(utcStr(new TZDate(2020, 2, 8, 1, laName))).toBe( 135 | "2020-03-08T09:00:00.000Z" 136 | ); 137 | expect(utcStr(new TZDate(2020, 2, 8, 2, laName))).toBe( 138 | "2020-03-08T10:00:00.000Z" 139 | ); 140 | expect(utcStr(new TZDate(2020, 2, 8, 3, laName))).toBe( 141 | "2020-03-08T10:00:00.000Z" 142 | ); 143 | expect(utcStr(new TZDate(2020, 2, 8, 4, laName))).toBe( 144 | "2020-03-08T11:00:00.000Z" 145 | ); 146 | }); 147 | 148 | it("America/New_York", () => { 149 | expect(utcStr(new TZDate(2020, 2, 8, 1, nyName))).toBe( 150 | "2020-03-08T06:00:00.000Z" 151 | ); 152 | expect(utcStr(new TZDate(2020, 2, 8, 2, nyName))).toBe( 153 | "2020-03-08T07:00:00.000Z" 154 | ); 155 | expect(utcStr(new TZDate(2020, 2, 8, 3, nyName))).toBe( 156 | "2020-03-08T07:00:00.000Z" 157 | ); 158 | expect(utcStr(new TZDate(2020, 2, 8, 4, nyName))).toBe( 159 | "2020-03-08T08:00:00.000Z" 160 | ); 161 | }); 162 | }); 163 | }); 164 | 165 | describe("TZ", () => { 166 | it("constructs now date in the timezone", () => { 167 | fakeNow(); 168 | const date = TZDate.tz("Asia/Singapore"); 169 | expect(date.toISOString()).toBe("1987-02-11T08:00:00.000+08:00"); 170 | }); 171 | 172 | it("constructs a date in the timezone", () => { 173 | // Timestamp 174 | expect( 175 | TZDate.tz("Asia/Singapore", +new Date(defaultDateStr)).toISOString() 176 | ).toBe("1987-02-11T08:00:00.000+08:00"); 177 | expect( 178 | TZDate.tz("America/New_York", +new Date(defaultDateStr)).toISOString() 179 | ).toBe("1987-02-10T19:00:00.000-05:00"); 180 | // Date string 181 | expect(TZDate.tz("Asia/Singapore", defaultDateStr).toISOString()).toBe( 182 | "1987-02-11T08:00:00.000+08:00" 183 | ); 184 | // Date 185 | expect( 186 | TZDate.tz("Asia/Singapore", new Date(defaultDateStr)).toISOString() 187 | ).toBe("1987-02-11T08:00:00.000+08:00"); 188 | expect( 189 | TZDate.tz("America/New_York", defaultDateStr).toISOString() 190 | ).toBe("1987-02-10T19:00:00.000-05:00"); 191 | // Month 192 | expect(TZDate.tz("Asia/Singapore", 2024, 1).toISOString()).toBe( 193 | "2024-02-01T00:00:00.000+08:00" 194 | ); 195 | expect(TZDate.tz("America/New_York", 2024, 1).toISOString()).toBe( 196 | "2024-02-01T00:00:00.000-05:00" 197 | ); 198 | // Date 199 | expect(TZDate.tz("Asia/Singapore", 2024, 1, 11).toISOString()).toBe( 200 | "2024-02-11T00:00:00.000+08:00" 201 | ); 202 | expect(TZDate.tz("America/New_York", 2024, 1, 11).toISOString()).toBe( 203 | "2024-02-11T00:00:00.000-05:00" 204 | ); 205 | // Hours 206 | expect(TZDate.tz("Asia/Singapore", 2024, 1, 11, 12).toISOString()).toBe( 207 | "2024-02-11T12:00:00.000+08:00" 208 | ); 209 | expect( 210 | TZDate.tz("America/New_York", 2024, 1, 11, 12).toISOString() 211 | ).toBe("2024-02-11T12:00:00.000-05:00"); 212 | // Minutes 213 | expect( 214 | TZDate.tz("Asia/Singapore", 2024, 1, 11, 12, 30).toISOString() 215 | ).toBe("2024-02-11T12:30:00.000+08:00"); 216 | expect( 217 | TZDate.tz("America/New_York", 2024, 1, 11, 12, 30).toISOString() 218 | ).toBe("2024-02-11T12:30:00.000-05:00"); 219 | // Seconds 220 | expect( 221 | TZDate.tz("Asia/Singapore", 2024, 1, 11, 12, 30, 45).toISOString() 222 | ).toBe("2024-02-11T12:30:45.000+08:00"); 223 | expect( 224 | TZDate.tz("America/New_York", 2024, 1, 11, 12, 30, 45).toISOString() 225 | ).toBe("2024-02-11T12:30:45.000-05:00"); 226 | // Milliseconds 227 | expect( 228 | TZDate.tz( 229 | "Asia/Singapore", 230 | 2024, 231 | 1, 232 | 11, 233 | 12, 234 | 30, 235 | 45, 236 | 987 237 | ).toISOString() 238 | ).toBe("2024-02-11T12:30:45.987+08:00"); 239 | expect( 240 | TZDate.tz( 241 | "America/New_York", 242 | 2024, 243 | 1, 244 | 11, 245 | 12, 246 | 30, 247 | 45, 248 | 987 249 | ).toISOString() 250 | ).toBe("2024-02-11T12:30:45.987-05:00"); 251 | }); 252 | 253 | it("constructs proper date around DST changes", () => { 254 | expect( 255 | new Date( 256 | +new TZDate(2023, 2, 10, 3, 30, "America/New_York") 257 | ).toISOString() 258 | ).toBe("2023-03-10T08:30:00.000Z"); 259 | expect( 260 | new Date( 261 | +new TZDate(2023, 2, 11, 3, 30, "America/New_York") 262 | ).toISOString() 263 | ).toBe("2023-03-11T08:30:00.000Z"); 264 | expect( 265 | new Date( 266 | +new TZDate(2023, 2, 12, 3, 30, "America/New_York") 267 | ).toISOString() 268 | ).toBe("2023-03-12T07:30:00.000Z"); 269 | expect( 270 | new Date( 271 | +new TZDate(2023, 2, 13, 3, 30, "America/New_York") 272 | ).toISOString() 273 | ).toBe("2023-03-13T07:30:00.000Z"); 274 | }); 275 | }); 276 | 277 | describe("UTC", () => { 278 | it("returns a timestamp in a date in UTC", () => { 279 | expect(new Date(TZDate.UTC(2024, 1)).toISOString()).toBe( 280 | "2024-02-01T00:00:00.000Z" 281 | ); 282 | }); 283 | }); 284 | 285 | describe("parse", () => { 286 | it("parses a date string to a timestamp", () => { 287 | expect( 288 | new Date(TZDate.parse("1987-02-11T00:00:00.000Z")).toISOString() 289 | ).toBe("1987-02-11T00:00:00.000Z"); 290 | expect( 291 | new Date(TZDate.parse("1987-02-11T00:00:00.000Z")).toISOString() 292 | ).toBe("1987-02-11T00:00:00.000Z"); 293 | }); 294 | }); 295 | }); 296 | 297 | describe("time", () => { 298 | describe("getTime", () => { 299 | it("returns the time in the timezone", () => { 300 | const nativeDate = new Date(2020, 0, 1); 301 | expect(new TZDate(+nativeDate, "Asia/Singapore").getTime()).toBe( 302 | +nativeDate 303 | ); 304 | expect(new TZDate(+nativeDate, "America/New_York").getTime()).toBe( 305 | +nativeDate 306 | ); 307 | }); 308 | 309 | it("returns NaN when the date or time zone are invalid", () => { 310 | expect(new TZDate(NaN, "America/New_York").getTime()).toBe(NaN); 311 | expect(new TZDate(Date.now(), "Etc/Invalid").getTime()).toBe(NaN); 312 | }); 313 | }); 314 | 315 | describe("setTime", () => { 316 | it("sets the time in the timezone", () => { 317 | const nativeDate = new Date(2020, 0, 1); 318 | { 319 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 320 | date.setTime(+nativeDate); 321 | expect(+date).toBe(+nativeDate); 322 | } 323 | { 324 | const date = new TZDate(defaultDateStr, "America/New_York"); 325 | date.setTime(+nativeDate); 326 | expect(+date).toBe(+nativeDate); 327 | } 328 | }); 329 | 330 | it("updated time is reflected in ISO timestamp", () => { 331 | const nativeDate = new Date("2020-01-01T06:00:00.000+08:00"); 332 | 333 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 334 | expect(date.toISOString()).toEqual("1987-02-11T08:00:00.000+08:00"); 335 | 336 | date.setTime(+nativeDate); 337 | expect(+date).toBe(+nativeDate); 338 | expect(date.toISOString()).toEqual("2020-01-01T06:00:00.000+08:00"); 339 | }); 340 | }); 341 | 342 | describe("valueOf", () => { 343 | it("returns the primitive value of the date", () => { 344 | const nativeDate = new Date(2020, 0, 1); 345 | expect(new TZDate(+nativeDate, "Asia/Singapore").valueOf()).toBe( 346 | +nativeDate 347 | ); 348 | expect(new TZDate(+nativeDate, "America/New_York").valueOf()).toBe( 349 | +nativeDate 350 | ); 351 | }); 352 | }); 353 | }); 354 | 355 | describe("year", () => { 356 | describe("getFullYear", () => { 357 | it("returns the full year in the timezone", () => { 358 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getFullYear()).toBe( 359 | 2020 360 | ); 361 | expect( 362 | new TZDate(2020, 0, 1, 0, "America/New_York").getFullYear() 363 | ).toBe(2020); 364 | }); 365 | 366 | it("returns NaN when the date or time zone are invalid", () => { 367 | expect(new TZDate(NaN, "America/New_York").getFullYear()).toBe(NaN); 368 | expect(new TZDate(Date.now(), "Etc/Invalid").getFullYear()).toBe(NaN); 369 | }); 370 | }); 371 | 372 | describe("getUTCFullYear", () => { 373 | it("returns the full year in the UTC timezone", () => { 374 | expect( 375 | new TZDate(2020, 0, 1, 0, "Asia/Singapore").getUTCFullYear() 376 | ).toBe(2019); 377 | expect( 378 | new TZDate(2020, 0, 1, 0, "America/New_York").getUTCFullYear() 379 | ).toBe(2020); 380 | }); 381 | 382 | it("returns NaN when the date or time zone are invalid", () => { 383 | expect(new TZDate(NaN, "America/New_York").getUTCFullYear()).toBe(NaN); 384 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCFullYear()).toBe( 385 | NaN 386 | ); 387 | }); 388 | }); 389 | 390 | describe("setFullYear", () => { 391 | it("sets the full year in the timezone", () => { 392 | { 393 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 394 | date.setFullYear(2021); 395 | expect(date.toISOString()).toBe("2021-01-01T00:00:00.000+08:00"); 396 | } 397 | { 398 | const date = new TZDate(2020, 0, 1, "America/New_York"); 399 | date.setFullYear(2021); 400 | expect(date.toISOString()).toBe("2021-01-01T00:00:00.000-05:00"); 401 | } 402 | }); 403 | 404 | it("returns the timestamp after setting", () => { 405 | const date = new TZDate(defaultDateStr, "America/New_York"); 406 | expect(date.setFullYear(2020)).toBe(+date); 407 | }); 408 | 409 | it("allows to set the month and date", () => { 410 | { 411 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 412 | date.setFullYear(2021, 1, 11); 413 | expect(date.toISOString()).toBe("2021-02-11T00:00:00.000+08:00"); 414 | } 415 | { 416 | const date = new TZDate(2020, 0, 1, "America/New_York"); 417 | date.setFullYear(2021, 1, 11); 418 | expect(date.toISOString()).toBe("2021-02-11T00:00:00.000-05:00"); 419 | } 420 | }); 421 | 422 | it("allows to overflow into the future", () => { 423 | { 424 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 425 | date.setFullYear(2021, 15, 45); 426 | expect(date.toISOString()).toBe("2022-05-15T00:00:00.000+08:00"); 427 | } 428 | { 429 | const date = new TZDate(2020, 0, 1, "America/New_York"); 430 | date.setFullYear(2021, 15, 45); 431 | expect(date.toISOString()).toBe("2022-05-15T00:00:00.000-04:00"); 432 | } 433 | }); 434 | 435 | it("allows to overflow into the past", () => { 436 | { 437 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 438 | date.setFullYear(2021, -15, -15); 439 | expect(date.toISOString()).toBe("2019-09-15T00:00:00.000+08:00"); 440 | } 441 | { 442 | const date = new TZDate(2020, 0, 1, "America/New_York"); 443 | date.setFullYear(2021, -15, -150); 444 | expect(date.toISOString()).toBe("2019-05-03T00:00:00.000-04:00"); 445 | } 446 | }); 447 | }); 448 | 449 | describe("setUTCFullYear", () => { 450 | it("sets the full year in the UTC timezone", () => { 451 | { 452 | // 2019-12-31 16:00:00 (UTC) -> ... 453 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 454 | // ... -> 2020-12-31 16:00:00 (UTC) -> 455 | date.setUTCFullYear(2020); 456 | expect(utcStr(date)).toBe("2020-12-31T16:00:00.000Z"); 457 | } 458 | { 459 | // 2020-01-01 05:00:00 (UTC) -> ... 460 | const date = new TZDate(2020, 0, 1, "America/New_York"); 461 | // ... -> 2020-01-01 05:00:00 (UTC) -> 462 | date.setUTCFullYear(2020); 463 | expect(utcStr(date)).toBe("2020-01-01T05:00:00.000Z"); 464 | } 465 | }); 466 | 467 | it("returns the timestamp after setting", () => { 468 | const date = new TZDate(defaultDateStr, "America/New_York"); 469 | expect(date.setUTCFullYear(2020)).toBe(+date); 470 | }); 471 | 472 | it("allows to set the month and date", () => { 473 | { 474 | // 2019-12-31 16:00:00 (UTC) -> ... 475 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 476 | // ... -> 2020-01-01 16:00:00 (UTC) -> ... 477 | date.setUTCFullYear(2020, 0, 1); 478 | expect(date.toISOString()).toBe("2020-01-02T00:00:00.000+08:00"); 479 | } 480 | { 481 | // 2020-01-01 05:00:00 (UTC) -> ... 482 | const date = new TZDate(2020, 0, 1, "America/New_York"); 483 | // ... -> 2020-01-01 05:00:00 (UTC) -> ... 484 | date.setUTCFullYear(2020, 0, 1); 485 | expect(date.toISOString()).toBe("2020-01-01T00:00:00.000-05:00"); 486 | } 487 | }); 488 | 489 | it("allows to overflow into the future", () => { 490 | { 491 | // 2019-12-31 16:00:00 (UTC) -> ... 492 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 493 | // ... -> 2021-04-14 16:00:00 (UTC) -> ... 494 | date.setUTCFullYear(2020, 14, 45); 495 | expect(utcStr(date)).toBe("2021-04-14T16:00:00.000Z"); 496 | } 497 | { 498 | // 2020-01-01 05:00:00 (UTC) -> ... 499 | const date = new TZDate(2020, 0, 1, "America/New_York"); 500 | // ... -> 2021-04-14 05:00:00 (UTC) -> ... 501 | date.setUTCFullYear(2020, 14, 45); 502 | expect(utcStr(date)).toBe("2021-04-14T05:00:00.000Z"); 503 | } 504 | }); 505 | 506 | it("allows to overflow into the past", () => { 507 | { 508 | // 2019-12-31 16:00:00 (UTC) -> ... 509 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 510 | // ... -> 2019-03-01 16:00:00 (UTC) -> ... 511 | date.setUTCFullYear(2020, -8, -60); 512 | expect(date.toISOString()).toBe("2019-03-02T00:00:00.000+08:00"); 513 | } 514 | { 515 | // 2020-01-01 05:00:00 (UTC) -> ... 516 | const date = new TZDate(2020, 0, 1, "America/New_York"); 517 | // ... -> 2019-03-01 05:00:00 (UTC) -> ... 518 | date.setUTCFullYear(2020, -8, -60); 519 | expect(date.toISOString()).toBe("2019-03-01T00:00:00.000-05:00"); 520 | } 521 | }); 522 | }); 523 | }); 524 | 525 | describe("month", () => { 526 | describe("getMonth", () => { 527 | it("returns the month in the timezone", () => { 528 | expect(new TZDate(2020, 0, 1, 0, "America/New_York").getMonth()).toBe( 529 | 0 530 | ); 531 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getMonth()).toBe(0); 532 | }); 533 | 534 | it("returns NaN when the date or time zone are invalid", () => { 535 | expect(new TZDate(NaN, "America/New_York").getMonth()).toBe(NaN); 536 | expect(new TZDate(Date.now(), "Etc/Invalid").getMonth()).toBe(NaN); 537 | }); 538 | }); 539 | 540 | describe("getUTCMonth", () => { 541 | it("returns the month in the UTC timezone", () => { 542 | expect( 543 | new TZDate(2020, 0, 1, 0, "America/New_York").getUTCMonth() 544 | ).toBe(0); 545 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getUTCMonth()).toBe( 546 | 11 547 | ); 548 | }); 549 | 550 | it("returns NaN when the date or time zone are invalid", () => { 551 | expect(new TZDate(NaN, "America/New_York").getUTCMonth()).toBe(NaN); 552 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCMonth()).toBe(NaN); 553 | }); 554 | }); 555 | 556 | describe("setMonth", () => { 557 | it("sets the month in the timezone", () => { 558 | { 559 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 560 | date.setMonth(1); 561 | expect(date.toISOString()).toBe("2020-02-01T00:00:00.000+08:00"); 562 | } 563 | { 564 | const date = new TZDate(2020, 0, 1, "America/New_York"); 565 | date.setMonth(1); 566 | expect(date.toISOString()).toBe("2020-02-01T00:00:00.000-05:00"); 567 | } 568 | }); 569 | 570 | it("returns the timestamp after setting", () => { 571 | const date = new TZDate(defaultDateStr, "America/New_York"); 572 | expect(date.setMonth(1)).toBe(+date); 573 | }); 574 | 575 | it("allows to set the date", () => { 576 | { 577 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 578 | date.setMonth(1, 11); 579 | expect(date.toISOString()).toBe("2020-02-11T00:00:00.000+08:00"); 580 | } 581 | { 582 | const date = new TZDate(2020, 0, 1, "America/New_York"); 583 | date.setMonth(1, 11); 584 | expect(date.toISOString()).toBe("2020-02-11T00:00:00.000-05:00"); 585 | } 586 | }); 587 | 588 | it("allows to overflow into the future", () => { 589 | { 590 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 591 | date.setMonth(15, 45); 592 | expect(date.toISOString()).toBe("2021-05-15T00:00:00.000+08:00"); 593 | } 594 | { 595 | const date = new TZDate(2020, 0, 1, "America/New_York"); 596 | date.setMonth(15, 45); 597 | expect(date.toISOString()).toBe("2021-05-15T00:00:00.000-04:00"); 598 | } 599 | }); 600 | 601 | it("allows to overflow into the past", () => { 602 | { 603 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 604 | date.setMonth(-15, -150); 605 | expect(date.toISOString()).toBe("2018-05-03T00:00:00.000+08:00"); 606 | } 607 | { 608 | const date = new TZDate(2020, 0, 1, "America/New_York"); 609 | date.setMonth(-15, -150); 610 | expect(date.toISOString()).toBe("2018-05-03T00:00:00.000-04:00"); 611 | } 612 | }); 613 | }); 614 | 615 | describe("setUTCMonth", () => { 616 | it("sets the month in the UTC timezone", () => { 617 | { 618 | // 2019-12-31 16:00:00 (UTC) -> ... 619 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 620 | // ... -> 2019-03-03 16:00:00 (UTC) -> ... 621 | date.setUTCMonth(1); 622 | expect(utcStr(date)).toBe("2019-03-03T16:00:00.000Z"); 623 | } 624 | { 625 | // 2020-01-01 05:00:00 (UTC) -> ... 626 | const date = new TZDate(2020, 0, 1, "America/New_York"); 627 | // ... -> 2020-02-01 05:00:00 (UTC) -> ... 628 | date.setUTCMonth(1); 629 | expect(utcStr(date)).toBe("2020-02-01T05:00:00.000Z"); 630 | } 631 | }); 632 | 633 | it("returns the timestamp after setting", () => { 634 | const date = new TZDate(defaultDateStr, "America/New_York"); 635 | expect(date.setUTCMonth(1)).toBe(+date); 636 | }); 637 | 638 | it("allows to set the date", () => { 639 | { 640 | // 2019-12-31 16:00:00 (UTC) -> ... 641 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 642 | // ... -> 2019-02-11 16:00:00 (UTC) -> ... 643 | date.setUTCMonth(1, 11); 644 | expect(date.toISOString()).toBe("2019-02-12T00:00:00.000+08:00"); 645 | } 646 | { 647 | // 2020-01-01 05:00:00 (UTC) -> ... 648 | const date = new TZDate(2020, 0, 1, "America/New_York"); 649 | // ... -> 2020-02-11 05:00:00 (UTC) -> ... 650 | date.setUTCMonth(1, 11); 651 | expect(date.toISOString()).toBe("2020-02-11T00:00:00.000-05:00"); 652 | } 653 | }); 654 | 655 | it("allows to overflow into the future", () => { 656 | { 657 | // 2019-12-31 16:00:00 (UTC) -> ... 658 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 659 | // ... -> 2020-08-14 16:00:00 (UTC) -> ... 660 | date.setUTCMonth(18, 45); 661 | expect(date.toISOString()).toBe("2020-08-15T00:00:00.000+08:00"); 662 | } 663 | { 664 | // 2020-01-01 05:00:00 (UTC) -> ... 665 | const date = new TZDate(2020, 0, 1, "America/New_York"); 666 | // ... -> 2021-08-14 05:00:00 (UTC) -> ... 667 | date.setUTCMonth(18, 45); 668 | expect(utcStr(date)).toBe("2021-08-14T05:00:00.000Z"); 669 | } 670 | }); 671 | 672 | it("allows to overflow into the past", () => { 673 | { 674 | // 2019-12-31 16:00:00 (UTC) -> ... 675 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 676 | // ... -> 2017-05-01 16:00:00 (UTC) -> ... 677 | date.setUTCMonth(-18, -60); 678 | expect(date.toISOString()).toBe("2017-05-02T00:00:00.000+08:00"); 679 | } 680 | { 681 | // 2020-01-01 05:00:00 (UTC) -> ... 682 | const date = new TZDate(2020, 0, 1, "America/New_York"); 683 | // ... -> 2018-05-01 05:00:00 (UTC) -> ... 684 | date.setUTCMonth(-18, -60); 685 | expect(utcStr(date)).toBe("2018-05-01T05:00:00.000Z"); 686 | } 687 | }); 688 | }); 689 | }); 690 | 691 | describe("date", () => { 692 | describe("getDate", () => { 693 | it("returns the date in the timezone", () => { 694 | expect(new TZDate(defaultDateStr, "America/New_York").getDate()).toBe( 695 | 10 696 | ); 697 | expect(new TZDate(defaultDateStr, "Asia/Singapore").getDate()).toBe(11); 698 | }); 699 | 700 | it("returns NaN when the date or time zone are invalid", () => { 701 | expect(new TZDate(NaN, "America/New_York").getDate()).toBe(NaN); 702 | expect(new TZDate(Date.now(), "Etc/Invalid").getDate()).toBe(NaN); 703 | }); 704 | }); 705 | 706 | describe("getUTCDate", () => { 707 | it("returns the date in the UTC timezone", () => { 708 | expect( 709 | new TZDate(defaultDateStr, "America/New_York").getUTCDate() 710 | ).toBe(11); 711 | expect(new TZDate(defaultDateStr, "Asia/Singapore").getUTCDate()).toBe( 712 | 11 713 | ); 714 | }); 715 | 716 | it("returns NaN when the date or time zone are invalid", () => { 717 | expect(new TZDate(NaN, "America/New_York").getUTCDate()).toBe(NaN); 718 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCDate()).toBe(NaN); 719 | }); 720 | }); 721 | 722 | describe("setDate", () => { 723 | it("sets the date in the timezone", () => { 724 | { 725 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 726 | date.setDate(2); 727 | expect(date.toISOString()).toBe("2020-01-02T00:00:00.000+08:00"); 728 | } 729 | { 730 | const date = new TZDate(2020, 0, 1, "America/New_York"); 731 | date.setDate(2); 732 | expect(date.toISOString()).toBe("2020-01-02T00:00:00.000-05:00"); 733 | } 734 | }); 735 | 736 | it("returns the timestamp after setting", () => { 737 | const date = new TZDate(defaultDateStr, "America/New_York"); 738 | expect(date.setDate(2)).toBe(+date); 739 | }); 740 | 741 | it("allows to overflow the month into the future", () => { 742 | { 743 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 744 | date.setDate(92); 745 | expect(date.toISOString()).toBe("2020-04-01T00:00:00.000+08:00"); 746 | } 747 | { 748 | const date = new TZDate(2020, 0, 1, "America/New_York"); 749 | date.setDate(92); 750 | expect(date.toISOString()).toBe("2020-04-01T00:00:00.000-04:00"); 751 | } 752 | }); 753 | 754 | it("allows to overflow the month into the past", () => { 755 | { 756 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 757 | date.setDate(-15); 758 | expect(date.toISOString()).toBe("2019-12-16T00:00:00.000+08:00"); 759 | } 760 | { 761 | const date = new TZDate(2020, 0, 1, "America/New_York"); 762 | date.setDate(-15); 763 | expect(date.toISOString()).toBe("2019-12-16T00:00:00.000-05:00"); 764 | } 765 | }); 766 | }); 767 | 768 | describe("setUTCDate", () => { 769 | it("sets the date in the UTC timezone", () => { 770 | { 771 | // 2019-12-31 16:00:00 (UTC) -> ... 772 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 773 | // ... -> 2019-12-11 16:00:00 (UTC) -> 774 | date.setUTCDate(11); 775 | expect(utcStr(date)).toBe("2019-12-11T16:00:00.000Z"); 776 | } 777 | { 778 | // 2020-01-01 05:00:00 (UTC) -> ... 779 | const date = new TZDate(2020, 0, 1, "America/New_York"); 780 | // ... -> 2020-01-11 05:00:00 (UTC) -> 781 | date.setUTCDate(11); 782 | expect(utcStr(date)).toBe("2020-01-11T05:00:00.000Z"); 783 | } 784 | }); 785 | 786 | it("returns the timestamp after setting", () => { 787 | const date = new TZDate(defaultDateStr, "America/New_York"); 788 | expect(date.setUTCDate(2)).toBe(+date); 789 | }); 790 | 791 | it("allows to overflow into the future", () => { 792 | { 793 | // 2019-12-31 16:00:00 (UTC) -> ... 794 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 795 | // ... -> 2022-07-02 16:00:00 (UTC) -> 796 | date.setUTCDate(945); 797 | expect(date.toISOString()).toBe("2022-07-03T00:00:00.000+08:00"); 798 | } 799 | { 800 | // 2020-01-01 05:00:00 (UTC) -> ... 801 | const date = new TZDate(2020, 0, 1, "America/New_York"); 802 | // ... -> 2022-08-02 05:00:00 (UTC) -> 803 | date.setUTCDate(945); 804 | expect(utcStr(date)).toBe("2022-08-02T05:00:00.000Z"); 805 | } 806 | }); 807 | 808 | it("allows to overflow into the past", () => { 809 | { 810 | // 2019-12-31 16:00:00 (UTC) -> ... 811 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 812 | // ... -> 2019-10-01 16:00:00 (UTC) -> 813 | date.setUTCDate(-60); 814 | expect(utcStr(date)).toBe("2019-10-01T16:00:00.000Z"); 815 | } 816 | { 817 | // 2020-01-01 05:00:00 (UTC) -> ... 818 | const date = new TZDate(2020, 0, 1, "America/New_York"); 819 | // ... -> 2019-11-01 05:00:00 (UTC) -> 820 | date.setUTCDate(-60); 821 | expect(utcStr(date)).toBe("2019-11-01T05:00:00.000Z"); 822 | } 823 | }); 824 | }); 825 | }); 826 | 827 | describe("day", () => { 828 | describe("getDay", () => { 829 | it("returns the day in the timezone", () => { 830 | const dateStr = "2020-01-01T00:00:00.000Z"; 831 | expect(new TZDate(dateStr, "America/New_York").getDay()).toBe(2); 832 | expect(new TZDate(dateStr, "Asia/Singapore").getDay()).toBe(3); 833 | }); 834 | 835 | it("returns NaN when the date or time zone are invalid", () => { 836 | expect(new TZDate(NaN, "America/New_York").getDay()).toBe(NaN); 837 | expect(new TZDate(Date.now(), "Etc/Invalid").getDay()).toBe(NaN); 838 | }); 839 | }); 840 | 841 | describe("getUTCDay", () => { 842 | it("returns the day in the UTC timezone", () => { 843 | expect(new TZDate(2020, 0, 1, 0, "America/New_York").getUTCDay()).toBe( 844 | 3 845 | ); 846 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getUTCDay()).toBe(2); 847 | const dateStr = "2020-01-01T00:00:00.000Z"; 848 | expect(new TZDate(dateStr, "America/New_York").getUTCDay()).toBe(3); 849 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCDay()).toBe(3); 850 | }); 851 | 852 | it("returns NaN when the date or time zone are invalid", () => { 853 | expect(new TZDate(NaN, "America/New_York").getUTCDay()).toBe(NaN); 854 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCDay()).toBe(NaN); 855 | }); 856 | }); 857 | }); 858 | 859 | describe("hours", () => { 860 | describe("getHours", () => { 861 | it("returns the hours in the timezone", () => { 862 | const dateStr = "1987-02-10T09:00:00.000Z"; 863 | expect(new TZDate(dateStr, "Asia/Singapore").getHours()).toBe(17); 864 | expect(new TZDate(dateStr, "America/New_York").getHours()).toBe(4); 865 | }); 866 | 867 | it("returns NaN when the date or time zone are invalid", () => { 868 | expect(new TZDate(NaN, "America/New_York").getHours()).toBe(NaN); 869 | expect(new TZDate(Date.now(), "Etc/Invalid").getHours()).toBe(NaN); 870 | }); 871 | }); 872 | 873 | describe("getUTCHours", () => { 874 | it("returns the hours in the UTC timezone", () => { 875 | const dateStr = "1987-02-10T09:00:00.000Z"; 876 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCHours()).toBe(9); 877 | expect(new TZDate(dateStr, "America/New_York").getUTCHours()).toBe(9); 878 | }); 879 | 880 | it("returns NaN when the date or time zone are invalid", () => { 881 | expect(new TZDate(NaN, "America/New_York").getUTCHours()).toBe(NaN); 882 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCHours()).toBe(NaN); 883 | }); 884 | }); 885 | 886 | describe("setHours", () => { 887 | it("sets the hours in the timezone", () => { 888 | { 889 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 890 | date.setHours(14); 891 | expect(date.toISOString()).toBe("2020-01-01T14:00:00.000+08:00"); 892 | } 893 | { 894 | const date = new TZDate(2020, 0, 1, "America/New_York"); 895 | date.setDate(14); 896 | expect(date.toISOString()).toBe("2020-01-14T00:00:00.000-05:00"); 897 | } 898 | }); 899 | 900 | it("returns the timestamp after setting", () => { 901 | const date = new TZDate(defaultDateStr, "America/New_York"); 902 | expect(date.setHours(14)).toBe(+date); 903 | }); 904 | 905 | it("allows to set hours, minutes, seconds and milliseconds", () => { 906 | { 907 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 908 | date.setHours(14, 30, 45, 987); 909 | expect(date.toISOString()).toBe("2020-01-01T14:30:45.987+08:00"); 910 | } 911 | { 912 | const date = new TZDate(2020, 0, 1, "America/New_York"); 913 | date.setHours(14, 30, 45, 987); 914 | expect(date.toISOString()).toBe("2020-01-01T14:30:45.987-05:00"); 915 | } 916 | }); 917 | 918 | it("allows to overflow the date into the future", () => { 919 | { 920 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 921 | date.setHours(30, 120, 120, 30000); 922 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 923 | // = 1 day + 08:02:30 924 | expect(date.toISOString()).toBe("2020-01-02T08:02:30.000+08:00"); 925 | } 926 | { 927 | const date = new TZDate(2020, 0, 1, "America/New_York"); 928 | date.setHours(30, 120, 120, 30000); 929 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 930 | // = 1 day + 08:02:30 931 | expect(date.toISOString()).toBe("2020-01-02T08:02:30.000-05:00"); 932 | } 933 | }); 934 | 935 | it("allows to overflow the date into the past", () => { 936 | { 937 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 938 | date.setHours(-30, -120, -120, -30000); 939 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 940 | // = 1 day + 08:02:30 941 | expect(date.toISOString()).toBe("2019-12-30T15:57:30.000+08:00"); 942 | } 943 | { 944 | const date = new TZDate(2020, 0, 1, "America/New_York"); 945 | date.setHours(-30, -120, -120, -30000); 946 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 947 | // = 1 day + 08:02:30 948 | expect(date.toISOString()).toBe("2019-12-30T15:57:30.000-05:00"); 949 | } 950 | }); 951 | 952 | it("constructs proper date around DST changes", () => { 953 | const date10 = new TZDate(2023, 2, 10, "America/New_York"); 954 | date10.setHours(3, 30); 955 | expect(new Date(+date10).toISOString()).toBe( 956 | "2023-03-10T08:30:00.000Z" 957 | ); 958 | const date11 = new TZDate(2023, 2, 11, "America/New_York"); 959 | date11.setHours(3, 30); 960 | expect(new Date(+date11).toISOString()).toBe( 961 | "2023-03-11T08:30:00.000Z" 962 | ); 963 | const date12 = new TZDate(2023, 2, 12, "America/New_York"); 964 | date12.setHours(3, 30); 965 | expect(new Date(+date12).toISOString()).toBe( 966 | "2023-03-12T07:30:00.000Z" 967 | ); 968 | const date13 = new TZDate(2023, 2, 13, "America/New_York"); 969 | date13.setHours(3, 30); 970 | expect(new Date(+date13).toISOString()).toBe( 971 | "2023-03-13T07:30:00.000Z" 972 | ); 973 | }); 974 | }); 975 | 976 | describe("setUTCHours", () => { 977 | it("sets the hours in the UTC timezone", () => { 978 | { 979 | // 2019-12-31 16:00:00 (UTC) -> ... 980 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 981 | // ... -> 2019-12-31 12:00:00 (UTC) -> 982 | date.setUTCHours(12); 983 | expect(utcStr(date)).toBe("2019-12-31T12:00:00.000Z"); 984 | } 985 | { 986 | // 2020-01-01 05:00:00 (UTC) -> ... 987 | const date = new TZDate(2020, 0, 1, "America/New_York"); 988 | // ... -> 2020-01-01 12:00:00 (UTC) -> 989 | date.setUTCHours(12); 990 | expect(utcStr(date)).toBe("2020-01-01T12:00:00.000Z"); 991 | } 992 | }); 993 | 994 | it("returns the timestamp after setting", () => { 995 | const date = new TZDate(defaultDateStr, "America/New_York"); 996 | expect(date.setUTCHours(14)).toBe(+date); 997 | }); 998 | 999 | it("allows to set hours, minutes, seconds and milliseconds", () => { 1000 | { 1001 | // 2019-12-31 16:00:00 (UTC) -> ... 1002 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1003 | // ... -> 2019-12-31T12:34:56.789Z (UTC) -> 1004 | date.setUTCHours(12, 34, 56, 789); 1005 | expect(date.toISOString()).toBe("2019-12-31T20:34:56.789+08:00"); 1006 | } 1007 | { 1008 | // 2020-01-01 05:00:00 (UTC) -> ... 1009 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1010 | // ... -> 2020-01-01T12:34:56.789Z (UTC) -> 1011 | date.setUTCHours(12, 34, 56, 789); 1012 | expect(date.toISOString()).toBe("2020-01-01T07:34:56.789-05:00"); 1013 | } 1014 | }); 1015 | 1016 | it("allows to overflow the date into the future", () => { 1017 | { 1018 | // 2019-12-31 16:00:00 (UTC) -> ... 1019 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1020 | // ... -> 2020-01-01 08:02:30 (UTC) -> 1021 | date.setUTCHours(30, 120, 120, 30000); 1022 | expect(date.toISOString()).toBe("2020-01-01T16:02:30.000+08:00"); 1023 | } 1024 | { 1025 | // 2020-01-01 05:00:00 (UTC) -> ... 1026 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1027 | // ... -> 2020-01-02 08:02:30 (UTC) -> 1028 | date.setUTCHours(30, 120, 120, 30000); 1029 | expect(date.toISOString()).toBe("2020-01-02T03:02:30.000-05:00"); 1030 | } 1031 | }); 1032 | 1033 | it("allows to overflow the date into the past", () => { 1034 | { 1035 | // 2019-12-31 16:00:00 (UTC) -> ... 1036 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1037 | // ... -> 2019-12-29 15:57:30 (UTC) -> 1038 | date.setUTCHours(-30, -120, -120, -30000); 1039 | expect(date.toISOString()).toBe("2019-12-29T23:57:30.000+08:00"); 1040 | } 1041 | { 1042 | // 2020-01-01 05:00:00 (UTC) -> ... 1043 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1044 | // ... -> 2019-12-30 15:57:30 (UTC) -> 1045 | date.setUTCHours(-30, -120, -120, -30000); 1046 | expect(date.toISOString()).toBe("2019-12-30T10:57:30.000-05:00"); 1047 | } 1048 | }); 1049 | }); 1050 | }); 1051 | 1052 | describe("minutes", () => { 1053 | describe("getMinutes", () => { 1054 | it("returns the minutes in the timezone", () => { 1055 | const dateStr = "1987-02-10T00:15:00.000Z"; 1056 | expect(new TZDate(dateStr, "America/New_York").getMinutes()).toBe(15); 1057 | expect(new TZDate(dateStr, "Asia/Kolkata").getMinutes()).toBe(45); 1058 | }); 1059 | 1060 | it("returns NaN when the date or time zone are invalid", () => { 1061 | expect(new TZDate(NaN, "America/New_York").getMinutes()).toBe(NaN); 1062 | expect(new TZDate(Date.now(), "Etc/Invalid").getMinutes()).toBe(NaN); 1063 | }); 1064 | }); 1065 | 1066 | describe("getUTCMinutes", () => { 1067 | it("returns the minutes in the UTC timezone", () => { 1068 | const dateStr = "1987-02-10T00:15:00.000Z"; 1069 | expect(new TZDate(dateStr, "America/New_York").getUTCMinutes()).toBe( 1070 | 15 1071 | ); 1072 | expect(new TZDate(dateStr, "Asia/Kolkata").getUTCMinutes()).toBe(15); 1073 | }); 1074 | 1075 | it("returns NaN when the date or time zone are invalid", () => { 1076 | expect(new TZDate(NaN, "America/New_York").getUTCMinutes()).toBe(NaN); 1077 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCMinutes()).toBe(NaN); 1078 | }); 1079 | }); 1080 | 1081 | describe("setMinutes", () => { 1082 | it("sets the minutes in the timezone", () => { 1083 | { 1084 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1085 | date.setMinutes(30); 1086 | expect(date.toISOString()).toBe("2020-01-01T00:30:00.000+08:00"); 1087 | } 1088 | { 1089 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1090 | date.setMinutes(30); 1091 | expect(date.toISOString()).toBe("2020-01-01T00:30:00.000-05:00"); 1092 | } 1093 | }); 1094 | 1095 | it("returns the timestamp after setting", () => { 1096 | const date = new TZDate(defaultDateStr, "America/New_York"); 1097 | expect(date.setMinutes(30)).toBe(+date); 1098 | }); 1099 | 1100 | it("allows to set minutes, seconds and milliseconds", () => { 1101 | { 1102 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1103 | date.setMinutes(30, 45, 987); 1104 | expect(date.toISOString()).toBe("2020-01-01T00:30:45.987+08:00"); 1105 | } 1106 | { 1107 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1108 | date.setMinutes(30, 45, 987); 1109 | expect(date.toISOString()).toBe("2020-01-01T00:30:45.987-05:00"); 1110 | } 1111 | }); 1112 | 1113 | it("allows to overflow the hours into the future", () => { 1114 | { 1115 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1116 | date.setMinutes(120, 120, 30000); 1117 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1118 | // = 02:02:30 1119 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000+08:00"); 1120 | } 1121 | { 1122 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1123 | date.setMinutes(120, 120, 30000); 1124 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1125 | // = 02:02:30 1126 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000-05:00"); 1127 | } 1128 | }); 1129 | 1130 | it("allows to overflow the hours into the past", () => { 1131 | { 1132 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1133 | date.setMinutes(-120, -120, -30000); 1134 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1135 | // = 02:02:30 1136 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000+08:00"); 1137 | } 1138 | { 1139 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1140 | date.setMinutes(-120, -120, -30000); 1141 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1142 | // = 02:02:30 1143 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000-05:00"); 1144 | } 1145 | }); 1146 | }); 1147 | 1148 | describe("setUTCMinutes", () => { 1149 | it("sets the minutes in the UTC timezone", () => { 1150 | { 1151 | // 2019-12-31 16:00:00 (UTC) -> ... 1152 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1153 | // ... -> 2019-12-31 16:34:00 (UTC) -> 1154 | date.setUTCMinutes(34); 1155 | expect(utcStr(date)).toBe("2019-12-31T16:34:00.000Z"); 1156 | } 1157 | { 1158 | // 2020-01-01 05:00:00 (UTC) -> ... 1159 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1160 | // ... -> 2020-01-01 05:34:00 (UTC) -> 1161 | date.setUTCMinutes(34); 1162 | expect(utcStr(date)).toBe("2020-01-01T05:34:00.000Z"); 1163 | } 1164 | }); 1165 | 1166 | it("returns the timestamp after setting", () => { 1167 | const date = new TZDate(defaultDateStr, "America/New_York"); 1168 | expect(date.setUTCMinutes(30)).toBe(+date); 1169 | }); 1170 | 1171 | it("allows to set minutes, seconds and milliseconds", () => { 1172 | { 1173 | // 2019-12-31 16:00:00 (UTC) -> ... 1174 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1175 | // ... -> 2019-12-31 16:34:56 (UTC) -> 1176 | date.setUTCMinutes(34, 56, 789); 1177 | expect(date.toISOString()).toBe("2020-01-01T00:34:56.789+08:00"); 1178 | } 1179 | { 1180 | // 2020-01-01 05:00:00 (UTC) -> ... 1181 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1182 | // ... -> 2020-01-01 05:34:56 (UTC) -> 1183 | date.setUTCMinutes(34, 56, 789); 1184 | expect(date.toISOString()).toBe("2020-01-01T00:34:56.789-05:00"); 1185 | } 1186 | }); 1187 | 1188 | it("allows to overflow the hours into the future", () => { 1189 | { 1190 | // 2019-12-31 16:00:00 (UTC) -> ... 1191 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1192 | // ... -> 2019-12-31 18:02:30 (UTC) -> 1193 | date.setUTCMinutes(120, 120, 30000); 1194 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000+08:00"); 1195 | } 1196 | { 1197 | // 2020-01-01 05:00:00 (UTC) -> ... 1198 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1199 | // ... -> 2020-01-01 07:02:30 (UTC) -> 1200 | date.setUTCMinutes(120, 120, 30000); 1201 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000-05:00"); 1202 | } 1203 | }); 1204 | 1205 | it("allows to overflow the hours into the past", () => { 1206 | { 1207 | // 2019-12-31 16:00:00 (UTC) -> ... 1208 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1209 | // ... -> 2019-12-31 13:57:30 (UTC) -> 1210 | date.setUTCMinutes(-120, -120, -30000); 1211 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000+08:00"); 1212 | } 1213 | { 1214 | // 2020-01-01 05:00:00 (UTC) -> ... 1215 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1216 | // ... -> 2020-01-01 02:57:30 (UTC) -> 1217 | date.setUTCMinutes(-120, -120, -30000); 1218 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000-05:00"); 1219 | } 1220 | }); 1221 | }); 1222 | }); 1223 | 1224 | describe("seconds", () => { 1225 | describe("getSeconds", () => { 1226 | it("returns the seconds in the timezone", () => { 1227 | const dateStr = "1987-02-10T00:00:30.000Z"; 1228 | expect(new TZDate(dateStr, "America/New_York").getSeconds()).toBe(30); 1229 | expect(new TZDate(dateStr, "Asia/Singapore").getSeconds()).toBe(30); 1230 | }); 1231 | 1232 | it("returns NaN when the date or time zone are invalid", () => { 1233 | expect(new TZDate(NaN, "America/New_York").getSeconds()).toBe(NaN); 1234 | expect(new TZDate(Date.now(), "Etc/Invalid").getSeconds()).toBe(NaN); 1235 | }); 1236 | }); 1237 | 1238 | describe("getUTCSeconds", () => { 1239 | it("returns the seconds in the UTC timezone", () => { 1240 | const dateStr = "1987-02-10T00:00:30.000Z"; 1241 | expect(new TZDate(dateStr, "America/New_York").getUTCSeconds()).toBe( 1242 | 30 1243 | ); 1244 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCSeconds()).toBe(30); 1245 | }); 1246 | 1247 | it("returns NaN when the date or time zone are invalid", () => { 1248 | expect(new TZDate(NaN, "America/New_York").getUTCSeconds()).toBe(NaN); 1249 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCSeconds()).toBe(NaN); 1250 | }); 1251 | }); 1252 | 1253 | describe("setSeconds", () => { 1254 | it("sets the seconds in the timezone", () => { 1255 | { 1256 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1257 | date.setSeconds(56); 1258 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.000+08:00"); 1259 | } 1260 | { 1261 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1262 | date.setSeconds(56); 1263 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.000-05:00"); 1264 | } 1265 | }); 1266 | 1267 | it("returns the timestamp after setting", () => { 1268 | const date = new TZDate(defaultDateStr, "America/New_York"); 1269 | expect(date.setSeconds(56)).toBe(+date); 1270 | }); 1271 | 1272 | it("allows to set seconds and milliseconds", () => { 1273 | { 1274 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1275 | date.setSeconds(56, 987); 1276 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.987+08:00"); 1277 | } 1278 | { 1279 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1280 | date.setSeconds(56, 987); 1281 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.987-05:00"); 1282 | } 1283 | }); 1284 | 1285 | it("allows to overflow the minutes into the future", () => { 1286 | { 1287 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1288 | date.setSeconds(120, 30000); 1289 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1290 | // = 02:30 1291 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000+08:00"); 1292 | } 1293 | { 1294 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1295 | date.setSeconds(120, 30000); 1296 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1297 | // = 02:30 1298 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000-05:00"); 1299 | } 1300 | }); 1301 | 1302 | it("allows to overflow the minutes into the past", () => { 1303 | { 1304 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1305 | date.setSeconds(-120, -30000); 1306 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1307 | // = 02:30 1308 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000+08:00"); 1309 | } 1310 | { 1311 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1312 | date.setSeconds(-120, -30000); 1313 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1314 | // = 02:30 1315 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000-05:00"); 1316 | } 1317 | }); 1318 | }); 1319 | 1320 | describe("setUTCSeconds", () => { 1321 | it("sets the seconds in the UTC timezone", () => { 1322 | { 1323 | // 2019-12-31 16:00:00 (UTC) -> ... 1324 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1325 | // ... -> 2019-12-31 16:00:56 (UTC) -> 1326 | date.setUTCSeconds(56); 1327 | expect(utcStr(date)).toBe("2019-12-31T16:00:56.000Z"); 1328 | } 1329 | { 1330 | // 2020-01-01 05:00:00 (UTC) -> ... 1331 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1332 | // ... -> 2020-01-01 05:00:56 (UTC) -> 1333 | date.setUTCSeconds(56); 1334 | expect(utcStr(date)).toBe("2020-01-01T05:00:56.000Z"); 1335 | } 1336 | }); 1337 | 1338 | it("returns the timestamp after setting", () => { 1339 | const date = new TZDate(defaultDateStr, "America/New_York"); 1340 | expect(date.setUTCSeconds(56)).toBe(+date); 1341 | }); 1342 | 1343 | it("allows to set seconds and milliseconds", () => { 1344 | { 1345 | // 2019-12-31 16:00:00 (UTC) -> ... 1346 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1347 | // ... -> 2019-12-31 16:00:56 (UTC) -> 1348 | date.setUTCSeconds(56, 789); 1349 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.789+08:00"); 1350 | } 1351 | { 1352 | // 2020-01-01 05:00:00 (UTC) -> ... 1353 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1354 | // ... -> 2020-01-01 05:00:56 (UTC) -> 1355 | date.setUTCSeconds(56, 789); 1356 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.789-05:00"); 1357 | } 1358 | }); 1359 | 1360 | it("allows to overflow the minutes into the future", () => { 1361 | { 1362 | // 2019-12-31 16:00:00 (UTC) -> ... 1363 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1364 | // ... -> 2019-12-31 16:02:30 (UTC) -> 1365 | date.setUTCSeconds(120, 30000); 1366 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000+08:00"); 1367 | } 1368 | { 1369 | // 2020-01-01 05:00:00 (UTC) -> ... 1370 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1371 | // ... -> 2020-01-01 05:02:30 (UTC) -> 1372 | date.setUTCSeconds(120, 30000); 1373 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000-05:00"); 1374 | } 1375 | }); 1376 | 1377 | it("allows to overflow the minutes into the past", () => { 1378 | { 1379 | // 2019-12-31 16:00:00 (UTC) -> ... 1380 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1381 | // ... -> 2019-12-31 15:57:30 (UTC) -> 1382 | date.setUTCSeconds(-120, -30000); 1383 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000+08:00"); 1384 | } 1385 | { 1386 | // 2020-01-01 05:00:00 (UTC) -> ... 1387 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1388 | // ... -> 2020-01-01 04:57:30 (UTC) -> 1389 | date.setUTCSeconds(-120, -30000); 1390 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000-05:00"); 1391 | } 1392 | }); 1393 | }); 1394 | }); 1395 | 1396 | describe("milliseconds", () => { 1397 | describe("getMilliseconds", () => { 1398 | it("returns the milliseconds in the timezone", () => { 1399 | const dateStr = "1987-02-10T00:00:00.456Z"; 1400 | expect(new TZDate(dateStr, "America/New_York").getMilliseconds()).toBe( 1401 | 456 1402 | ); 1403 | expect(new TZDate(dateStr, "Asia/Singapore").getMilliseconds()).toBe( 1404 | 456 1405 | ); 1406 | }); 1407 | 1408 | it("returns NaN when the date or time zone are invalid", () => { 1409 | expect(new TZDate(NaN, "America/New_York").getMilliseconds()).toBe(NaN); 1410 | expect(new TZDate(Date.now(), "Etc/Invalid").getMilliseconds()).toBe( 1411 | NaN 1412 | ); 1413 | }); 1414 | }); 1415 | 1416 | describe("getUTCMilliseconds", () => { 1417 | it("returns the milliseconds in the UTC timezone", () => { 1418 | const dateStr = "1987-02-10T00:00:00.456Z"; 1419 | expect( 1420 | new TZDate(dateStr, "America/New_York").getUTCMilliseconds() 1421 | ).toBe(456); 1422 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCMilliseconds()).toBe( 1423 | 456 1424 | ); 1425 | }); 1426 | 1427 | it("returns NaN when the date or time zone are invalid", () => { 1428 | expect(new TZDate(NaN, "America/New_York").getUTCMilliseconds()).toBe( 1429 | NaN 1430 | ); 1431 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCMilliseconds()).toBe( 1432 | NaN 1433 | ); 1434 | }); 1435 | }); 1436 | 1437 | describe("setMilliseconds", () => { 1438 | it("sets the milliseconds in the timezone", () => { 1439 | { 1440 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1441 | date.setMilliseconds(789); 1442 | expect(date.toISOString()).toBe("2020-01-01T00:00:00.789+08:00"); 1443 | } 1444 | { 1445 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1446 | date.setMilliseconds(789); 1447 | expect(date.toISOString()).toBe("2020-01-01T00:00:00.789-05:00"); 1448 | } 1449 | }); 1450 | 1451 | it("returns the timestamp after setting", () => { 1452 | const date = new TZDate(defaultDateStr, "America/New_York"); 1453 | expect(date.setMilliseconds(789)).toBe(+date); 1454 | }); 1455 | 1456 | it("allows to overflow the seconds into the future", () => { 1457 | { 1458 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1459 | date.setMilliseconds(1000); 1460 | expect(date.toISOString()).toBe("2020-01-01T00:00:01.000+08:00"); 1461 | } 1462 | { 1463 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1464 | date.setMilliseconds(1000); 1465 | expect(date.toISOString()).toBe("2020-01-01T00:00:01.000-05:00"); 1466 | } 1467 | }); 1468 | 1469 | it("allows to overflow the seconds into the past", () => { 1470 | { 1471 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1472 | date.setMilliseconds(-1000); 1473 | expect(date.toISOString()).toBe("2019-12-31T23:59:59.000+08:00"); 1474 | } 1475 | { 1476 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1477 | date.setMilliseconds(-1000); 1478 | expect(date.toISOString()).toBe("2019-12-31T23:59:59.000-05:00"); 1479 | } 1480 | }); 1481 | }); 1482 | 1483 | describe("setUTCMilliseconds", () => { 1484 | it("sets the milliseconds in the UTC timezone", () => { 1485 | { 1486 | // 2019-12-31 16:00:00 (UTC) -> ... 1487 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1488 | // ... -> 2019-12-31 16:00:00 (UTC) -> 1489 | date.setUTCMilliseconds(789); 1490 | expect(utcStr(date)).toBe("2019-12-31T16:00:00.789Z"); 1491 | } 1492 | { 1493 | // 2020-01-01 05:00:00 (UTC) -> ... 1494 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1495 | // ... -> 2020-01-01 05:00:00 (UTC) -> 1496 | date.setUTCMilliseconds(789); 1497 | expect(utcStr(date)).toBe("2020-01-01T05:00:00.789Z"); 1498 | } 1499 | }); 1500 | 1501 | it("returns the timestamp after setting", () => { 1502 | const date = new TZDate(defaultDateStr, "America/New_York"); 1503 | expect(date.setUTCMilliseconds(789)).toBe(+date); 1504 | }); 1505 | 1506 | it("allows to overflow the seconds into the future", () => { 1507 | { 1508 | // 2019-12-31 16:00:00 (UTC) -> ... 1509 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1510 | // ... -> 2019-12-31 16:00:30 (UTC) -> 1511 | date.setUTCMilliseconds(30000); 1512 | expect(date.toISOString()).toBe("2020-01-01T00:00:30.000+08:00"); 1513 | } 1514 | { 1515 | // 2020-01-01 05:00:00 (UTC) -> ... 1516 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1517 | // ... -> 2019-12-31 15:59:30 (UTC) -> 1518 | date.setUTCMilliseconds(30000); 1519 | expect(date.toISOString()).toBe("2020-01-01T00:00:30.000-05:00"); 1520 | } 1521 | }); 1522 | 1523 | it("allows to overflow the seconds into the past", () => { 1524 | { 1525 | // 2019-12-31 16:00:00 (UTC) -> ... 1526 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1527 | // ... -> 2020-01-01 04:59:30 (UTC) -> 1528 | date.setUTCMilliseconds(-30000); 1529 | expect(date.toISOString()).toBe("2019-12-31T23:59:30.000+08:00"); 1530 | } 1531 | { 1532 | // 2020-01-01 05:00:00 (UTC) -> ... 1533 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1534 | // ... -> 2020-01-01 04:59:30 (UTC) -> 1535 | date.setUTCMilliseconds(-30000); 1536 | expect(date.toISOString()).toBe("2019-12-31T23:59:30.000-05:00"); 1537 | } 1538 | }); 1539 | }); 1540 | }); 1541 | 1542 | describe("time zone", () => { 1543 | describe("withTimeZone", () => { 1544 | it("returns a new date with the given timezone", () => { 1545 | const date = new TZDate(defaultDateStr, "America/New_York"); 1546 | const newDate = date.withTimeZone("Asia/Tokyo"); 1547 | 1548 | expect(date.toISOString()).toBe("1987-02-10T19:00:00.000-05:00"); 1549 | expect(newDate.toISOString()).toBe("1987-02-11T09:00:00.000+09:00"); 1550 | }); 1551 | }); 1552 | 1553 | describe("getTimezoneOffset", () => { 1554 | it("returns the timezone offset", () => { 1555 | expect( 1556 | new TZDate(defaultDateStr, "America/New_York").getTimezoneOffset() 1557 | ).toBe(300); 1558 | expect( 1559 | new TZDate(defaultDateStr, "Asia/Singapore").getTimezoneOffset() 1560 | ).toBe(-480); 1561 | }); 1562 | 1563 | it("returns NaN when the date is invalid", () => { 1564 | expect(new TZDate(NaN, "America/New_York").getTimezoneOffset()).toBe( 1565 | NaN 1566 | ); 1567 | }); 1568 | }); 1569 | }); 1570 | 1571 | describe("representation", () => { 1572 | describe("[Symbol.toPrimitive]", () => { 1573 | it("returns string representation of the date when the hint is 'string'", () => { 1574 | expect( 1575 | new TZDate(2020, 0, 1, "Asia/Singapore")[Symbol.toPrimitive]("string") 1576 | ).toBe("Wed Jan 01 2020 00:00:00 GMT+0800 (Singapore Standard Time)"); 1577 | expect( 1578 | new TZDate(2020, 0, 1, "America/New_York")[Symbol.toPrimitive]( 1579 | "string" 1580 | ) 1581 | ).toBe("Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)"); 1582 | }); 1583 | 1584 | it("returns string representation of the date when the hint is 'default'", () => { 1585 | expect( 1586 | new TZDate(2020, 0, 1, "Asia/Singapore")[Symbol.toPrimitive]( 1587 | "default" 1588 | ) 1589 | ).toBe("Wed Jan 01 2020 00:00:00 GMT+0800 (Singapore Standard Time)"); 1590 | expect( 1591 | new TZDate(2020, 0, 1, "America/New_York")[Symbol.toPrimitive]( 1592 | "default" 1593 | ) 1594 | ).toBe("Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)"); 1595 | }); 1596 | 1597 | it("returns number representation of the date when the hint is 'number'", () => { 1598 | const nativeDate = new Date("2020-01-01T00:00:00.000+08:00"); 1599 | expect( 1600 | new TZDate(+nativeDate, "Asia/Singapore")[Symbol.toPrimitive]( 1601 | "number" 1602 | ) 1603 | ).toBe(+nativeDate); 1604 | expect( 1605 | new TZDate(+nativeDate, "America/New_York")[Symbol.toPrimitive]( 1606 | "number" 1607 | ) 1608 | ).toBe(+nativeDate); 1609 | }); 1610 | }); 1611 | 1612 | describe("toISOString", () => { 1613 | it("returns ISO 8601 formatted date in the timezone", () => { 1614 | expect(new TZDate(2020, 0, 1, "America/New_York").toISOString()).toBe( 1615 | "2020-01-01T00:00:00.000-05:00" 1616 | ); 1617 | expect(new TZDate(2020, 0, 1, "Asia/Singapore").toISOString()).toBe( 1618 | "2020-01-01T00:00:00.000+08:00" 1619 | ); 1620 | expect(new TZDate(2020, 0, 1, "Asia/Kolkata").toISOString()).toBe( 1621 | "2020-01-01T00:00:00.000+05:30" 1622 | ); 1623 | expect(new TZDate(2015, 7, 1, "Asia/Pyongyang").toISOString()).toBe( 1624 | "2015-08-01T00:00:00.000+09:00" 1625 | ); 1626 | expect(new TZDate(2015, 8, 1, "Asia/Pyongyang").toISOString()).toBe( 1627 | "2015-09-01T00:00:00.000+08:30" 1628 | ); 1629 | }); 1630 | }); 1631 | 1632 | describe("toJSON", () => { 1633 | it("works the same as toISOString", () => { 1634 | expect(new TZDate(2020, 0, 1, "America/New_York").toJSON()).toBe( 1635 | "2020-01-01T00:00:00.000-05:00" 1636 | ); 1637 | expect(new TZDate(2015, 7, 1, "Asia/Pyongyang").toJSON()).toBe( 1638 | "2015-08-01T00:00:00.000+09:00" 1639 | ); 1640 | expect(new TZDate(2015, 8, 1, "Asia/Pyongyang").toJSON()).toBe( 1641 | "2015-09-01T00:00:00.000+08:30" 1642 | ); 1643 | }); 1644 | }); 1645 | 1646 | describe("toString", () => { 1647 | it("returns string representation of the date in the timezone", () => { 1648 | expect(new TZDate(2020, 0, 1, "America/New_York").toString()).toBe( 1649 | "Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)" 1650 | ); 1651 | expect( 1652 | new TZDate("2020-01-01T00:00:00.000Z", "America/New_York").toString() 1653 | ).toBe("Tue Dec 31 2019 19:00:00 GMT-0500 (Eastern Standard Time)"); 1654 | expect(new TZDate(2020, 5, 1, "America/New_York").toString()).toBe( 1655 | "Mon Jun 01 2020 00:00:00 GMT-0400 (Eastern Daylight Time)" 1656 | ); 1657 | }); 1658 | }); 1659 | 1660 | describe("toDateString", () => { 1661 | it("returns formatted date portion of the in the timezone", () => { 1662 | expect(new TZDate(2020, 0, 1, "America/New_York").toDateString()).toBe( 1663 | "Wed Jan 01 2020" 1664 | ); 1665 | expect( 1666 | new TZDate("2020-01-01T00:00:00Z", "America/New_York").toDateString() 1667 | ).toBe("Tue Dec 31 2019"); 1668 | }); 1669 | }); 1670 | 1671 | describe("toTimeString", () => { 1672 | it("returns formatted time portion of the in the timezone", () => { 1673 | expect(new TZDate(2020, 0, 1, "America/New_York").toTimeString()).toBe( 1674 | "00:00:00 GMT-0500 (Eastern Standard Time)" 1675 | ); 1676 | expect( 1677 | new TZDate( 1678 | "2020-01-01T00:00:00.000Z", 1679 | "America/New_York" 1680 | ).toTimeString() 1681 | ).toBe("19:00:00 GMT-0500 (Eastern Standard Time)"); 1682 | expect(new TZDate(2020, 5, 1, "America/New_York").toTimeString()).toBe( 1683 | "00:00:00 GMT-0400 (Eastern Daylight Time)" 1684 | ); 1685 | }); 1686 | }); 1687 | 1688 | describe("toUTCString", () => { 1689 | it("returns string representation of the date in UTC", () => { 1690 | expect( 1691 | new TZDate( 1692 | "2020-02-11T08:00:00.000Z", 1693 | "America/New_York" 1694 | ).toUTCString() 1695 | ).toBe("Tue, 11 Feb 2020 08:00:00 GMT"); 1696 | expect( 1697 | new TZDate("2020-02-11T08:00:00.000Z", "Asia/Singapore").toUTCString() 1698 | ).toBe("Tue, 11 Feb 2020 08:00:00 GMT"); 1699 | }); 1700 | }); 1701 | 1702 | describe("toLocaleString", () => { 1703 | it("returns localized date and time in the timezone", () => { 1704 | expect( 1705 | new TZDate(2020, 0, 1, "America/New_York").toLocaleString() 1706 | ).toBe("1/1/2020, 12:00:00 AM"); 1707 | expect( 1708 | new TZDate( 1709 | "2020-01-01T00:00:00.000Z", 1710 | "America/New_York" 1711 | ).toLocaleString() 1712 | ).toBe("12/31/2019, 7:00:00 PM"); 1713 | expect( 1714 | new TZDate(2020, 5, 1, "America/New_York").toLocaleString("es-ES", { 1715 | dateStyle: "full", 1716 | timeStyle: "full", 1717 | }) 1718 | ).toBe("lunes, 1 de junio de 2020, 0:00:00 (hora de verano oriental)"); 1719 | expect( 1720 | new TZDate( 1721 | "2020-01-01T02:00:00.000Z", 1722 | "America/New_York" 1723 | ).toLocaleString("es-ES", { 1724 | dateStyle: "full", 1725 | timeStyle: "full", 1726 | }) 1727 | ).toBe( 1728 | "martes, 31 de diciembre de 2019, 21:00:00 (hora estándar oriental)" 1729 | ); 1730 | expect( 1731 | new TZDate( 1732 | "2020-01-01T02:00:00.000Z", 1733 | "America/New_York" 1734 | ).toLocaleString("es-ES", { 1735 | dateStyle: "full", 1736 | timeStyle: "full", 1737 | timeZone: "Asia/Singapore", 1738 | }) 1739 | ).toBe("miércoles, 1 de enero de 2020, 10:00:00 (hora de Singapur)"); 1740 | }); 1741 | }); 1742 | 1743 | describe("toLocaleDateString", () => { 1744 | it("returns localized date portion of the in the timezone", () => { 1745 | expect( 1746 | new TZDate(2020, 0, 1, "America/New_York").toLocaleDateString() 1747 | ).toBe("1/1/2020"); 1748 | expect( 1749 | new TZDate( 1750 | "2020-01-01T00:00:00.000Z", 1751 | "America/New_York" 1752 | ).toLocaleDateString() 1753 | ).toBe("12/31/2019"); 1754 | expect( 1755 | new TZDate(2020, 5, 1, "America/New_York").toLocaleDateString( 1756 | "es-ES", 1757 | { dateStyle: "full" } 1758 | ) 1759 | ).toBe("lunes, 1 de junio de 2020"); 1760 | expect( 1761 | new TZDate( 1762 | "2020-01-01T02:00:00.000Z", 1763 | "America/New_York" 1764 | ).toLocaleDateString("es-ES", { 1765 | dateStyle: "full", 1766 | }) 1767 | ).toBe("martes, 31 de diciembre de 2019"); 1768 | expect( 1769 | new TZDate(2020, 0, 1, 10, "America/New_York").toLocaleDateString( 1770 | "es-ES", 1771 | { dateStyle: "full", timeZone: "Asia/Singapore" } 1772 | ) 1773 | ).toBe("miércoles, 1 de enero de 2020"); 1774 | }); 1775 | }); 1776 | 1777 | describe("toLocaleTimeString", () => { 1778 | it("returns localized time portion of the in the timezone", () => { 1779 | expect( 1780 | new TZDate(2020, 0, 1, "America/New_York").toLocaleTimeString() 1781 | ).toBe("12:00:00 AM"); 1782 | expect( 1783 | new TZDate( 1784 | "2020-02-11T00:00:00.000Z", 1785 | "America/New_York" 1786 | ).toLocaleTimeString() 1787 | ).toBe("7:00:00 PM"); 1788 | expect( 1789 | new TZDate(2020, 5, 1, "America/New_York").toLocaleTimeString( 1790 | "es-ES", 1791 | { timeStyle: "full" } 1792 | ) 1793 | ).toBe("0:00:00 (hora de verano oriental)"); 1794 | expect( 1795 | new TZDate( 1796 | "2020-01-01T00:00:00.000Z", 1797 | "America/New_York" 1798 | ).toLocaleTimeString("es-ES", { timeStyle: "full" }) 1799 | ).toBe("19:00:00 (hora estándar oriental)"); 1800 | expect( 1801 | new TZDate( 1802 | "2020-01-01T00:00:00.000Z", 1803 | "America/New_York" 1804 | ).toLocaleTimeString("es-ES", { 1805 | timeStyle: "full", 1806 | timeZone: "Asia/Singapore", 1807 | }) 1808 | ).toBe("8:00:00 (hora de Singapur)"); 1809 | }); 1810 | }); 1811 | }); 1812 | 1813 | describe('[Symbol.for("constructDateFrom")]', () => { 1814 | it("constructs a new date from value and the instance time zone", () => { 1815 | const dateStr = "2020-01-01T00:00:00.000Z"; 1816 | const nativeDate = new Date(dateStr); 1817 | { 1818 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 1819 | const result = date[constructFromSymbol](nativeDate); 1820 | expect(result.toISOString()).toBe("2020-01-01T08:00:00.000+08:00"); 1821 | } 1822 | { 1823 | const date = new TZDate(defaultDateStr, "America/New_York"); 1824 | const result = date[constructFromSymbol](nativeDate); 1825 | expect(result.toISOString()).toBe("2019-12-31T19:00:00.000-05:00"); 1826 | } 1827 | { 1828 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 1829 | const result = date[constructFromSymbol](+nativeDate); 1830 | expect(result.toISOString()).toBe("2020-01-01T08:00:00.000+08:00"); 1831 | } 1832 | { 1833 | const date = new TZDate(defaultDateStr, "America/New_York"); 1834 | const result = date[constructFromSymbol](+nativeDate); 1835 | expect(result.toISOString()).toBe("2019-12-31T19:00:00.000-05:00"); 1836 | } 1837 | { 1838 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 1839 | const result = date[constructFromSymbol](dateStr); 1840 | expect(result.toISOString()).toBe("2020-01-01T08:00:00.000+08:00"); 1841 | } 1842 | { 1843 | const date = new TZDate(defaultDateStr, "America/New_York"); 1844 | const result = date[constructFromSymbol](dateStr); 1845 | expect(result.toISOString()).toBe("2019-12-31T19:00:00.000-05:00"); 1846 | } 1847 | }); 1848 | }); 1849 | 1850 | describe("DST", () => { 1851 | describe("setting the DST time", () => { 1852 | describe("DST start", () => { 1853 | describe("default methods", () => { 1854 | it("America/Los_Angeles", () => { 1855 | withDSTStart(laName, (date) => { 1856 | expect(utcStr(date)).toBe("2020-03-08T08:00:00.000Z"); 1857 | }); 1858 | 1859 | // Set on the DST hour 1860 | withDSTStart(laName, (date) => { 1861 | date.setHours(2); 1862 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1863 | }); 1864 | 1865 | // Set on the hour DST moves to 1866 | withDSTStart(laName, (date) => { 1867 | date.setHours(3); 1868 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1869 | }); 1870 | 1871 | // Set after the DST hour 1872 | withDSTStart(laName, (date) => { 1873 | date.setHours(5); 1874 | expect(utcStr(date)).toBe("2020-03-08T12:00:00.000Z"); 1875 | }); 1876 | }); 1877 | 1878 | it("America/New_York", () => { 1879 | withDSTStart(nyName, (date) => { 1880 | expect(utcStr(date)).toBe("2020-03-08T05:00:00.000Z"); 1881 | }); 1882 | 1883 | // Set on the DST hour 1884 | withDSTStart(nyName, (date) => { 1885 | date.setHours(2); 1886 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 1887 | }); 1888 | 1889 | // Set on the hour DST moves to 1890 | withDSTStart(nyName, (date) => { 1891 | date.setHours(3); 1892 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 1893 | }); 1894 | 1895 | // Set after the DST hour 1896 | withDSTStart(nyName, (date) => { 1897 | date.setHours(5); 1898 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 1899 | }); 1900 | }); 1901 | }); 1902 | 1903 | describe("UTC methods", () => { 1904 | it("America/Los_Angeles", () => { 1905 | // Set on the DST hour 1906 | withDSTStart(laName, (date) => { 1907 | date.setUTCHours(10); 1908 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1909 | }); 1910 | 1911 | // Set after the DST hour 1912 | withDSTStart(laName, (date) => { 1913 | date.setUTCHours(12); 1914 | expect(utcStr(date)).toBe("2020-03-08T12:00:00.000Z"); 1915 | }); 1916 | }); 1917 | 1918 | it("America/New_York", () => { 1919 | // Set on the DST hour 1920 | withDSTStart(nyName, (date) => { 1921 | date.setUTCHours(7); 1922 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 1923 | }); 1924 | 1925 | // Set after the DST hour 1926 | withDSTStart(nyName, (date) => { 1927 | date.setUTCHours(9); 1928 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 1929 | }); 1930 | }); 1931 | }); 1932 | }); 1933 | }); 1934 | 1935 | describe("updating to the DST time", () => { 1936 | describe("DST start", () => { 1937 | describe("default methods", () => { 1938 | it("America/Los_Angeles", () => { 1939 | withDSTStart(laName, (date) => { 1940 | date.setHours(1); 1941 | date.setHours(1); 1942 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 1943 | }); 1944 | 1945 | // Update to the same DST hour 1946 | withDSTStart(laName, (date) => { 1947 | date.setHours(2); 1948 | date.setHours(2); 1949 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1950 | }); 1951 | 1952 | // Update to the hour DST moves to 1953 | withDSTStart(laName, (date) => { 1954 | date.setHours(2); 1955 | date.setHours(3); 1956 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1957 | }); 1958 | 1959 | // Update to same DST hour 1960 | withDSTStart(laName, (date) => { 1961 | date.setHours(2); 1962 | date.setHours(date.getHours()); 1963 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1964 | }); 1965 | 1966 | // Update to same hour DST moves to 1967 | withDSTStart(laName, (date) => { 1968 | date.setHours(3); 1969 | date.setHours(date.getHours()); 1970 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1971 | }); 1972 | 1973 | // Update from after the DST hour 1974 | withDSTStart(laName, (date) => { 1975 | date.setHours(5); 1976 | date.setHours(2); 1977 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 1978 | }); 1979 | 1980 | // Update to another year with the same DST hour 1981 | withDSTStart(laName, (date) => { 1982 | date.setHours(2); 1983 | date.setFullYear(2015); 1984 | expect(utcStr(date)).toBe("2015-03-08T10:00:00.000Z"); 1985 | }); 1986 | }); 1987 | 1988 | it("America/New_York", () => { 1989 | withDSTStart(nyName, (date) => { 1990 | date.setHours(1); 1991 | date.setHours(1); 1992 | expect(utcStr(date)).toBe("2020-03-08T06:00:00.000Z"); 1993 | }); 1994 | 1995 | // Update to the same DST hour 1996 | withDSTStart(nyName, (date) => { 1997 | date.setHours(2); 1998 | date.setHours(2); 1999 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2000 | }); 2001 | 2002 | // Update to the hour DST moves to 2003 | withDSTStart(nyName, (date) => { 2004 | date.setHours(2); 2005 | date.setHours(3); 2006 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2007 | }); 2008 | 2009 | // Update to same DST hour 2010 | withDSTStart(nyName, (date) => { 2011 | date.setHours(2); 2012 | date.setHours(date.getHours()); 2013 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2014 | }); 2015 | 2016 | // Update to same hour DST moves to 2017 | withDSTStart(nyName, (date) => { 2018 | date.setHours(3); 2019 | date.setHours(date.getHours()); 2020 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2021 | }); 2022 | 2023 | // Update from after the DST hour 2024 | withDSTStart(nyName, (date) => { 2025 | date.setHours(5); 2026 | date.setHours(2); 2027 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2028 | }); 2029 | 2030 | // Update to another year with the same DST hour 2031 | withDSTStart(nyName, (date) => { 2032 | date.setHours(2); 2033 | date.setFullYear(2015); 2034 | expect(utcStr(date)).toBe("2015-03-08T07:00:00.000Z"); 2035 | }); 2036 | }); 2037 | }); 2038 | 2039 | describe("UTC methods", () => { 2040 | it("America/Los_Angeles", () => { 2041 | withDSTStart(laName, (date) => { 2042 | date.setUTCHours(9); 2043 | date.setUTCHours(9); 2044 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 2045 | }); 2046 | 2047 | // Update to the same DST hour 2048 | withDSTStart(laName, (date) => { 2049 | date.setUTCHours(10); 2050 | date.setUTCHours(10); 2051 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2052 | }); 2053 | 2054 | // Update from after the DST hour 2055 | withDSTStart(laName, (date) => { 2056 | date.setUTCHours(12); 2057 | date.setUTCHours(10); 2058 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2059 | }); 2060 | 2061 | // Update to another year with the same DST hour 2062 | withDSTStart(laName, (date) => { 2063 | date.setUTCHours(10); 2064 | date.setUTCFullYear(2015); 2065 | expect(utcStr(date)).toBe("2015-03-08T10:00:00.000Z"); 2066 | }); 2067 | }); 2068 | 2069 | it("America/New_York", () => { 2070 | withDSTStart(nyName, (date) => { 2071 | date.setUTCHours(6); 2072 | date.setUTCHours(6); 2073 | expect(utcStr(date)).toBe("2020-03-08T06:00:00.000Z"); 2074 | }); 2075 | 2076 | // Update to the same DST hour 2077 | withDSTStart(nyName, (date) => { 2078 | date.setUTCHours(7); 2079 | date.setUTCHours(7); 2080 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2081 | }); 2082 | 2083 | // Update from after the DST hour 2084 | withDSTStart(nyName, (date) => { 2085 | date.setUTCHours(10); 2086 | date.setUTCHours(7); 2087 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2088 | }); 2089 | 2090 | // Update to another year with the same DST hour 2091 | withDSTStart(nyName, (date) => { 2092 | date.setUTCHours(7); 2093 | date.setUTCFullYear(2015); 2094 | expect(utcStr(date)).toBe("2015-03-08T07:00:00.000Z"); 2095 | }); 2096 | }); 2097 | }); 2098 | }); 2099 | }); 2100 | }); 2101 | }); 2102 | 2103 | function withDSTStart(tz: string, fn: (date: TZDate) => void) { 2104 | fn(new TZDate(2020, 2, 8, tz)); 2105 | } 2106 | 2107 | function withDSTEnd(tz: string, fn: (date: TZDate) => void) { 2108 | fn(new TZDate(2020, 10, 1, tz)); 2109 | } 2110 | 2111 | function utcStr(date: TZDate) { 2112 | return new Date(+date).toISOString(); 2113 | } 2114 | 2115 | const laName = "America/Los_Angeles"; 2116 | const nyName = "America/New_York"; 2117 | const sgName = "Asia/Singapore"; 2118 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants/index.ts"; 2 | export * from "./date/index.js"; 3 | export * from "./date/mini.js"; 4 | export * from "./tz/index.ts"; 5 | export * from "./tzOffset/index.ts"; 6 | export * from "./tzScan/index.ts"; 7 | -------------------------------------------------------------------------------- /src/tests.ts: -------------------------------------------------------------------------------- 1 | import { eachDayOfInterval, eachHourOfInterval, parse } from "date-fns"; 2 | import { describe, expect, it } from "vitest"; 3 | import { TZDate } from "./date/index.js"; 4 | import { tz } from "./tz/index.ts"; 5 | 6 | describe("date-fns integration", () => { 7 | describe("DST transitions", () => { 8 | it("skips the DST transitions", () => { 9 | // This test hours crafted to test the DST transitions. 10 | // 11 | // When ran with TZ=America/New_York, the NY dates land on the DST hour 12 | // before offset adjustments while the Singapore dates land on the DST 13 | // hour after the adjustments. 14 | // 15 | // When ran with TZ=America/Los_Angeles where the DST is on the same time, 16 | // but the offset is different, the dates also land on DST hours. 17 | 18 | const interval = { 19 | start: "2020-03-08T06:00:00.000Z", 20 | end: "2020-03-08T08:00:00.000Z", 21 | }; 22 | 23 | const hours = [ 24 | "2020-03-08T06:00:00.000Z", 25 | "2020-03-08T07:00:00.000Z", 26 | "2020-03-08T08:00:00.000Z", 27 | ]; 28 | 29 | const ny = eachHourOfInterval(interval, { 30 | in: tz("America/New_York"), 31 | }).map((date) => new Date(+date).toISOString()); 32 | expect(ny).toEqual(hours); 33 | 34 | const sg = eachHourOfInterval(interval, { 35 | in: tz("Asia/Singapore"), 36 | }).map((date) => new Date(+date).toISOString()); 37 | expect(sg).toEqual(hours); 38 | }); 39 | 40 | it("doesn't add hour shift on DST transitions", () => { 41 | const ny = eachDayOfInterval({ 42 | start: new TZDate(2020, 2, 5, "America/New_York"), 43 | end: new TZDate(2020, 2, 12, "America/New_York"), 44 | }).map((date) => date.toISOString()); 45 | expect(ny).toEqual([ 46 | "2020-03-05T00:00:00.000-05:00", 47 | "2020-03-06T00:00:00.000-05:00", 48 | "2020-03-07T00:00:00.000-05:00", 49 | "2020-03-08T00:00:00.000-05:00", 50 | "2020-03-09T00:00:00.000-04:00", 51 | "2020-03-10T00:00:00.000-04:00", 52 | "2020-03-11T00:00:00.000-04:00", 53 | "2020-03-12T00:00:00.000-04:00", 54 | ]); 55 | 56 | const sg = eachDayOfInterval({ 57 | start: new TZDate(2020, 2, 5, "Asia/Singapore"), 58 | end: new TZDate(2020, 2, 12, "Asia/Singapore"), 59 | }).map((date) => date.toISOString()); 60 | expect(sg).toEqual([ 61 | "2020-03-05T00:00:00.000+08:00", 62 | "2020-03-06T00:00:00.000+08:00", 63 | "2020-03-07T00:00:00.000+08:00", 64 | "2020-03-08T00:00:00.000+08:00", 65 | "2020-03-09T00:00:00.000+08:00", 66 | "2020-03-10T00:00:00.000+08:00", 67 | "2020-03-11T00:00:00.000+08:00", 68 | "2020-03-12T00:00:00.000+08:00", 69 | ]); 70 | }); 71 | 72 | it("properly parses around DST transitions", () => { 73 | const format = "yyyy-MM-dd HH:mm"; 74 | const ny = tz("America/New_York"); 75 | expect( 76 | parse("2023-03-11 01:30", format, new Date(), { in: ny }).toISOString() 77 | ).toBe("2023-03-11T01:30:00.000-05:00"); 78 | expect( 79 | parse("2023-03-12 01:30", format, new Date(), { in: ny }).toISOString() 80 | ).toBe("2023-03-12T01:30:00.000-05:00"); 81 | expect( 82 | parse("2023-03-12 02:00", format, new Date(), { in: ny }).toISOString() 83 | ).toBe("2023-03-12T03:00:00.000-04:00"); 84 | expect( 85 | parse("2023-03-12 03:00", format, new Date(), { in: ny }).toISOString() 86 | ).toBe("2023-03-12T03:00:00.000-04:00"); 87 | expect( 88 | parse("2023-03-12 03:30", format, new Date(), { in: ny }).toISOString() 89 | ).toBe("2023-03-12T03:30:00.000-04:00"); 90 | expect( 91 | parse("2023-03-13 03:30", format, new Date(), { in: ny }).toISOString() 92 | ).toBe("2023-03-13T03:30:00.000-04:00"); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/tz/index.ts: -------------------------------------------------------------------------------- 1 | import { TZDate } from "../date/index.js"; 2 | 3 | /** 4 | * The function creates accepts a time zone and returns a function that creates 5 | * a new `TZDate` instance in the time zone from the provided value. Use it to 6 | * provide the context for the date-fns functions, via the `in` option. 7 | * 8 | * @param timeZone - Time zone name (IANA or UTC offset) 9 | * 10 | * @returns Function that creates a new `TZDate` instance in the time zone 11 | */ 12 | export const tz = (timeZone: string) => (value: Date | number | string) => 13 | TZDate.tz(timeZone, +new Date(value)); 14 | -------------------------------------------------------------------------------- /src/tz/tests.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { tz } from "./index.ts"; 3 | 4 | describe("tz", () => { 5 | const dateStr = "2020-01-01T00:00:00.000Z"; 6 | it("creates a function that converts a date to a specific time zone date", () => { 7 | expect(tz("Asia/Singapore")(dateStr).toISOString()).toBe( 8 | "2020-01-01T08:00:00.000+08:00" 9 | ); 10 | expect(tz("America/New_York")(dateStr).toISOString()).toBe( 11 | "2019-12-31T19:00:00.000-05:00" 12 | ); 13 | expect(tz("Asia/Singapore")(+new Date(dateStr)).toISOString()).toBe( 14 | "2020-01-01T08:00:00.000+08:00" 15 | ); 16 | expect(tz("America/New_York")(+new Date(dateStr)).toISOString()).toBe( 17 | "2019-12-31T19:00:00.000-05:00" 18 | ); 19 | expect(tz("Asia/Singapore")(new Date(dateStr)).toISOString()).toBe( 20 | "2020-01-01T08:00:00.000+08:00" 21 | ); 22 | expect(tz("America/New_York")(new Date(dateStr)).toISOString()).toBe( 23 | "2019-12-31T19:00:00.000-05:00" 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/tzOffset/index.ts: -------------------------------------------------------------------------------- 1 | const offsetFormatCache: Record = {}; 2 | 3 | const offsetCache: Record = {}; 4 | 5 | /** 6 | * The function extracts UTC offset in minutes from the given date in specified 7 | * time zone. 8 | * 9 | * Unlike `Date.prototype.getTimezoneOffset`, this function returns the value 10 | * mirrored to the sign of the offset in the time zone. For Asia/Singapore 11 | * (UTC+8), `tzOffset` returns 480, while `getTimezoneOffset` returns -480. 12 | * 13 | * @param timeZone - Time zone name (IANA or UTC offset) 14 | * @param date - Date to check the offset for 15 | * 16 | * @returns UTC offset in minutes 17 | */ 18 | export function tzOffset(timeZone: string | undefined, date: Date): number { 19 | try { 20 | const format = (offsetFormatCache[timeZone!] ||= new Intl.DateTimeFormat( 21 | "en-GB", 22 | { timeZone, hour: "numeric", timeZoneName: "longOffset" } 23 | ).format); 24 | 25 | const offsetStr = format(date).split('GMT')[1] || ''; 26 | if (offsetStr in offsetCache) return offsetCache[offsetStr]!; 27 | 28 | return calcOffset(offsetStr, offsetStr.split(":")); 29 | } catch { 30 | // Fallback to manual parsing if the runtime doesn't support ±HH:MM/±HHMM/±HH 31 | // See: https://github.com/nodejs/node/issues/53419 32 | if (timeZone! in offsetCache) return offsetCache[timeZone!]!; 33 | const captures = timeZone?.match(offsetRe); 34 | if (captures) return calcOffset(timeZone!, captures.slice(1)); 35 | 36 | return NaN; 37 | } 38 | } 39 | 40 | const offsetRe = /([+-]\d\d):?(\d\d)?/; 41 | 42 | function calcOffset(cacheStr: string, values: string[]): number { 43 | const hours = +values[0]!; 44 | const minutes = +(values[1] || 0); 45 | return (offsetCache[cacheStr] = 46 | hours > 0 ? hours * 60 + minutes : hours * 60 - minutes); 47 | } 48 | -------------------------------------------------------------------------------- /src/tzOffset/tests.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { tzOffset } from "./index.ts"; 3 | 4 | describe("tzOffset", () => { 5 | it("returns the timezone offset for the given date", () => { 6 | const date = new Date("2020-01-15T00:00:00Z"); 7 | expect(tzOffset("America/New_York", date)).toBe(-5 * 60); 8 | expect(tzOffset("Asia/Pyongyang", date)).toBe(9 * 60); 9 | expect(tzOffset("Asia/Kathmandu", date)).toBe(345); 10 | }); 11 | 12 | it("works at the end of the day", () => { 13 | const date = new Date("2020-01-15T23:59:59Z"); 14 | expect(tzOffset("America/New_York", date)).toBe(-5 * 60); 15 | expect(tzOffset("Asia/Pyongyang", date)).toBe(9 * 60); 16 | expect(tzOffset("Asia/Kathmandu", date)).toBe(345); 17 | }); 18 | 19 | it("works at the end of a month", () => { 20 | const date = new Date("2020-01-31T23:59:59Z"); 21 | expect(tzOffset("America/New_York", date)).toBe(-5 * 60); 22 | expect(tzOffset("Asia/Pyongyang", date)).toBe(9 * 60); 23 | expect(tzOffset("Asia/Kathmandu", date)).toBe(345); 24 | }); 25 | 26 | it("works at midnight", () => { 27 | expect(tzOffset("America/New_York", new Date("2020-01-15T05:00:00Z"))).toBe( 28 | -5 * 60 29 | ); 30 | }); 31 | 32 | it("returns the local timezone offset when the timezone is undefined", () => { 33 | const date = new Date("2020-01-15T05:00:00Z"); 34 | expect(tzOffset(undefined, date)).toBe(-date.getTimezoneOffset()); 35 | }); 36 | 37 | it("returns 0 if the offset is 0", () => { 38 | const date = new Date("2020-01-15T00:00:00Z"); 39 | expect(tzOffset("Europe/London", date)).toBe(0); 40 | }); 41 | 42 | it("returns NaN if the offset the date or time zone are invalid", () => { 43 | expect(tzOffset("Etc/Invalid", new Date("2020-01-15T00:00:00Z"))).toBe(NaN); 44 | expect(tzOffset("America/New_York", new Date(NaN))).toBe(NaN); 45 | }); 46 | 47 | describe("time zone name formats", () => { 48 | const date = new Date("2020-01-15T00:00:00Z"); 49 | 50 | it("works with IANA time zone names", () => { 51 | expect(tzOffset("America/New_York", date)).toBe(-300); 52 | expect(tzOffset("Asia/Pyongyang", date)).toBe(540); 53 | }); 54 | 55 | it("works with ±HH:MM", () => { 56 | expect(tzOffset("-05:00", date)).toBe(-300); 57 | expect(tzOffset("-02:30", date)).toBe(-150); 58 | expect(tzOffset("+05:00", date)).toBe(300); 59 | expect(tzOffset("+02:30", date)).toBe(150); 60 | }); 61 | 62 | it("works with ±HHMM", () => { 63 | expect(tzOffset("-0500", date)).toBe(-300); 64 | expect(tzOffset("-0230", date)).toBe(-150); 65 | expect(tzOffset("+0500", date)).toBe(300); 66 | expect(tzOffset("+0230", date)).toBe(150); 67 | }); 68 | 69 | it("works with ±HH", () => { 70 | expect(tzOffset("-05", date)).toBe(-300); 71 | expect(tzOffset("+05", date)).toBe(300); 72 | }); 73 | }); 74 | 75 | describe("fractional time zones", () => { 76 | it("works negative fractional time zones", () => { 77 | const dst = new Date("2023-03-15T18:00:00.000Z"); 78 | const date = new Date("2023-03-03T18:00:00.000Z"); 79 | expect(tzOffset("America/St_Johns", dst)).toBe(-150); 80 | expect(tzOffset("America/St_Johns", date)).toBe(-210); 81 | }); 82 | 83 | it("works positive fractional time zones", () => { 84 | const dst = new Date("2024-04-06T16:00:00.000Z"); 85 | const date = new Date("2024-04-06T16:30:00.000Z"); 86 | expect(tzOffset("Australia/Adelaide", dst)).toBe(630); 87 | expect(tzOffset("Australia/Adelaide", date)).toBe(570); 88 | }); 89 | }); 90 | 91 | describe('Intl.DateTimeFormat format', () => { 92 | let mockFormat = vi.fn(); 93 | beforeEach(() => { 94 | mockFormat = vi.fn(() => '5 GMT+08:00'); 95 | const dtf = new Intl.DateTimeFormat(); 96 | vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => { 97 | return { ...dtf, format: mockFormat }; 98 | }); 99 | }); 100 | 101 | afterEach(() => { 102 | vi.mocked(Intl.DateTimeFormat).mockRestore(); 103 | }); 104 | 105 | it("reads offset from expected format", () => { 106 | mockFormat.mockReturnValue('5 GMT+08:00'); 107 | const date = new Date("2020-01-15T00:00:00Z"); 108 | expect(tzOffset("Asia/Manila", date)).toBe(480); 109 | }); 110 | 111 | it("reads offset from polyfill", () => { 112 | mockFormat.mockReturnValue('5:53 PM GMT-9:30'); 113 | const date = new Date("2020-01-15T00:00:00Z"); 114 | expect(tzOffset("Pacific/Marquesas", date)).toBe(-570); 115 | }); 116 | 117 | it("reads offset from polyfill (without offset)", () => { 118 | mockFormat.mockReturnValue('5:53 PM GMT'); 119 | const date = new Date("2020-01-15T00:00:00Z"); 120 | expect(tzOffset("UTC", date)).toBe(0); 121 | }); 122 | }) 123 | }); 124 | -------------------------------------------------------------------------------- /src/tzScan/index.ts: -------------------------------------------------------------------------------- 1 | import { tzOffset } from "../tzOffset/index.ts"; 2 | 3 | /** 4 | * Time interval. 5 | */ 6 | export interface TZChangeInterval { 7 | /** Start date. */ 8 | start: Date; 9 | /** End date. */ 10 | end: Date; 11 | } 12 | 13 | /** 14 | * Time zone change record. 15 | */ 16 | export interface TZChange { 17 | /** Date time the change occurs */ 18 | date: Date; 19 | /** Offset change in minutes */ 20 | change: number; 21 | /** New UTC offset in minutes */ 22 | offset: number; 23 | } 24 | 25 | /** 26 | * The function scans the time zone for changes in the given interval. 27 | * 28 | * @param timeZone - Time zone name (IANA or UTC offset) 29 | * @param interval - Time interval to scan for changes 30 | * 31 | * @returns Array of time zone changes 32 | */ 33 | export function tzScan( 34 | timeZone: string, 35 | interval: TZChangeInterval 36 | ): TZChange[] { 37 | const changes: TZChange[] = []; 38 | 39 | const monthDate = new Date(interval.start); 40 | monthDate.setUTCSeconds(0, 0); 41 | 42 | const endDate = new Date(interval.end); 43 | endDate.setUTCSeconds(0, 0); 44 | 45 | const endMonthTime = +endDate; 46 | let lastOffset = tzOffset(timeZone, monthDate); 47 | while (+monthDate < endMonthTime) { 48 | // Month forward 49 | monthDate.setUTCMonth(monthDate.getUTCMonth() + 1); 50 | 51 | // Find the month where the offset changes 52 | const offset = tzOffset(timeZone, monthDate); 53 | if (offset != lastOffset) { 54 | // Rewind a month back to find the day where the offset changes 55 | const dayDate = new Date(monthDate); 56 | dayDate.setUTCMonth(dayDate.getUTCMonth() - 1); 57 | 58 | const endDayTime = +monthDate; 59 | lastOffset = tzOffset(timeZone, dayDate); 60 | while (+dayDate < endDayTime) { 61 | // Day forward 62 | dayDate.setUTCDate(dayDate.getUTCDate() + 1); 63 | 64 | // Find the day where the offset changes 65 | const offset = tzOffset(timeZone, dayDate); 66 | if (offset != lastOffset) { 67 | // Rewind a day back to find the time where the offset changes 68 | const hourDate = new Date(dayDate); 69 | hourDate.setUTCDate(hourDate.getUTCDate() - 1); 70 | 71 | const endHourTime = +dayDate; 72 | lastOffset = tzOffset(timeZone, hourDate); 73 | while (+hourDate < endHourTime) { 74 | // Hour forward 75 | hourDate.setUTCHours(hourDate.getUTCHours() + 1); 76 | 77 | // Find the hour where the offset changes 78 | const hourOffset = tzOffset(timeZone, hourDate); 79 | if (hourOffset !== lastOffset) { 80 | changes.push({ 81 | date: new Date(hourDate), 82 | change: hourOffset - lastOffset, 83 | offset: hourOffset, 84 | }); 85 | } 86 | 87 | lastOffset = hourOffset; 88 | } 89 | } 90 | 91 | lastOffset = offset; 92 | } 93 | } 94 | 95 | lastOffset = offset; 96 | } 97 | 98 | return changes; 99 | } 100 | -------------------------------------------------------------------------------- /src/tzScan/tests.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { tzScan } from "./index.ts"; 3 | 4 | describe("tzScan", () => { 5 | it("searches for DST changes in the given period", () => { 6 | const changes = tzScan("America/New_York", { 7 | start: new Date("2020-01-01T00:00:00Z"), 8 | end: new Date("2021-12-31T00:00:00Z"), 9 | }); 10 | 11 | expect(changes).toEqual([ 12 | { 13 | date: new Date("2020-03-08T07:00:00.000Z"), 14 | change: 1 * 60, 15 | offset: -4 * 60, 16 | }, 17 | { 18 | date: new Date("2020-11-01T06:00:00.000Z"), 19 | change: -1 * 60, 20 | offset: -5 * 60, 21 | }, 22 | { 23 | date: new Date("2021-03-14T07:00:00.000Z"), 24 | change: 1 * 60, 25 | offset: -4 * 60, 26 | }, 27 | { 28 | date: new Date("2021-11-07T06:00:00.000Z"), 29 | change: -1 * 60, 30 | offset: -5 * 60, 31 | }, 32 | ]); 33 | }); 34 | 35 | it("searches for permanent DST changes in the given period", () => { 36 | const changes = tzScan("Turkey", { 37 | start: new Date("2015-01-01T00:00:00Z"), 38 | end: new Date("2018-12-31T00:00:00Z"), 39 | }); 40 | 41 | expect(changes).toEqual([ 42 | { 43 | date: new Date("2015-03-29T01:00:00.000Z"), 44 | change: 1 * 60, 45 | offset: 3 * 60, 46 | }, 47 | { 48 | date: new Date("2015-11-08T01:00:00.000Z"), 49 | change: -1 * 60, 50 | offset: 2 * 60, 51 | }, 52 | { 53 | date: new Date("2016-03-27T01:00:00.000Z"), 54 | change: 1 * 60, 55 | offset: 3 * 60, 56 | }, 57 | ]); 58 | }); 59 | 60 | it("searches for timezone changes", () => { 61 | const changes = tzScan("Asia/Pyongyang", { 62 | start: new Date("2010-01-01T00:00:00Z"), 63 | end: new Date("2019-12-31T00:00:00Z"), 64 | }); 65 | 66 | expect(changes).toEqual([ 67 | { 68 | date: new Date("2015-08-14T15:00:00.000Z"), 69 | change: -0.5 * 60, 70 | offset: 8.5 * 60, 71 | }, 72 | { 73 | date: new Date("2018-05-04T15:00:00.000Z"), 74 | change: 0.5 * 60, 75 | offset: 9 * 60, 76 | }, 77 | ]); 78 | }); 79 | 80 | it("searches for huge timezone changes", () => { 81 | const changes = tzScan("Pacific/Apia", { 82 | start: new Date("2010-01-01T00:00:00Z"), 83 | end: new Date("2012-12-31T00:00:00Z"), 84 | }); 85 | 86 | expect(changes).toEqual([ 87 | { 88 | date: new Date("2010-09-26T11:00:00.000Z"), 89 | change: 1 * 60, 90 | offset: -10 * 60, 91 | }, 92 | { 93 | date: new Date("2011-04-02T14:00:00.000Z"), 94 | change: -1 * 60, 95 | offset: -11 * 60, 96 | }, 97 | { 98 | date: new Date("2011-09-24T14:00:00.000Z"), 99 | change: 1 * 60, 100 | offset: -10 * 60, 101 | }, 102 | { 103 | date: new Date("2011-12-30T10:00:00.000Z"), 104 | change: 24 * 60, 105 | offset: 14 * 60, 106 | }, 107 | { 108 | date: new Date("2012-03-31T14:00:00.000Z"), 109 | change: -1 * 60, 110 | offset: 13 * 60, 111 | }, 112 | { 113 | date: new Date("2012-09-29T14:00:00.000Z"), 114 | change: 1 * 60, 115 | offset: 14 * 60, 116 | }, 117 | ]); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "target": "ESNext", 6 | "module": "NodeNext", 7 | "allowImportingTsExtensions": true, 8 | "emitDeclarationOnly": true, 9 | "declaration": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "exactOptionalPropertyTypes": true, 13 | "noUncheckedIndexedAccess": true, 14 | "skipLibCheck": true, 15 | "allowJs": true 16 | }, 17 | "exclude": [] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "sourceMap": true, 6 | "outDir": "lib", 7 | "skipLibCheck": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true 10 | }, 11 | "include": ["src/**/*.ts"], 12 | "exclude": ["**/tysts.ts", "**/tests.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ["src/**/tests.ts"], 6 | isolate: false, 7 | sequence: { 8 | // It will speed up the tests but won't work with Sinon 9 | // concurrent: true, 10 | }, 11 | }, 12 | }); 13 | --------------------------------------------------------------------------------