├── .nvmrc ├── test ├── engines │ ├── .gitignore │ ├── src │ │ ├── javascriptcore.ts │ │ ├── hermes.ts │ │ └── test.ts │ ├── babel.config.json │ ├── metro.config.js │ └── package.json └── node │ ├── esm.js │ └── commonjs.cjs ├── .devcontainer ├── Dockerfile ├── scripts │ ├── post-create.sh │ ├── on-update.sh │ └── on-create.sh └── devcontainer.json ├── pnpm-workspace.yaml ├── .gitignore ├── .vscode └── settings.json ├── src ├── constants │ └── index.ts ├── index.ts ├── tz │ ├── index.ts │ └── tests.ts ├── date │ ├── mini.d.ts │ ├── index.js │ ├── index.d.ts │ ├── mini.js │ └── tests.ts ├── tzName │ ├── tests.ts │ └── index.ts ├── tzOffset │ ├── index.ts │ └── tests.ts ├── tzScan │ ├── index.ts │ └── tests.ts └── tests.ts ├── mise.toml ├── vitest.config.ts ├── tsconfig.lib.json ├── benchmark.ts ├── tsconfig.json ├── scripts └── test │ ├── node.sh │ └── engines.sh ├── LICENSE.md ├── babel.config.json ├── Makefile ├── copy.mjs ├── size.mjs ├── CHANGELOG.md ├── playground.mjs ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.18.0 2 | -------------------------------------------------------------------------------- /test/engines/.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ -------------------------------------------------------------------------------- /test/node/esm.js: -------------------------------------------------------------------------------- 1 | import "../../lib/index.js"; 2 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kossnocorp/dev-node:latest -------------------------------------------------------------------------------- /test/engines/src/javascriptcore.ts: -------------------------------------------------------------------------------- 1 | import "./test.ts"; 2 | -------------------------------------------------------------------------------- /test/node/commonjs.cjs: -------------------------------------------------------------------------------- 1 | require("../../lib/index.cjs"); 2 | -------------------------------------------------------------------------------- /test/engines/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["module:@react-native/babel-preset"] 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "." 3 | - "test/engines" 4 | 5 | onlyBuiltDependencies: 6 | - "@swc/core" 7 | - esbuild 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | .pnpm-store/ 4 | # TypeScript 5 | .ts/ 6 | # Dist 7 | /lib/ 8 | # Temporary 9 | /tmp/ 10 | # Secrets 11 | .env 12 | .env.* 13 | !.env.example -------------------------------------------------------------------------------- /.devcontainer/scripts/post-create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is run after the container is created. 4 | 5 | set -e 6 | 7 | # Set up fish shell 8 | sudo chsh -s /usr/bin/fish $(whoami) -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "debug.javascript.defaultRuntimeExecutable": { 4 | "pwa-node": "/home/vscode/.local/share/mise/shims/node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.devcontainer/scripts/on-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is when the container is updated. 4 | 5 | set -e 6 | 7 | eval "$(mise activate bash --shims)" 8 | 9 | # Update mise 10 | mise self-update -y 11 | 12 | # Install stack 13 | mise install -------------------------------------------------------------------------------- /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 | export * from "./tzName/index.ts"; 8 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | ## Stack 3 | node = { version = "lts", postinstall = "corepack enable && node -v > .nvmrc" } 4 | 5 | ## Tools 6 | "npm:jsvu" = "latest" 7 | 8 | [settings] 9 | # Enable extra features 10 | experimental = true 11 | # Ignore idiomatic version files 12 | idiomatic_version_file_enable_tools = [] 13 | -------------------------------------------------------------------------------- /.devcontainer/scripts/on-create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is run when the container is created. 4 | 5 | set -e 6 | 7 | # Trust mise setup first 8 | mise trust 9 | 10 | eval "$(mise activate bash --shims)" 11 | 12 | # Install just completions 13 | just --completions fish > ~/.config/fish/completions/just.fish -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "composite": false, 11 | "incremental": false 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["**/tysts.ts", "**/tests.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /benchmark.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from "tinybench"; 2 | import { tzOffset, tzScan } from "./src/index.ts"; 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 | -------------------------------------------------------------------------------- /test/engines/src/hermes.ts: -------------------------------------------------------------------------------- 1 | // Include Intl polyfills 2 | import "@formatjs/intl-getcanonicallocales/polyfill"; 3 | import "@formatjs/intl-locale/polyfill"; 4 | import "@formatjs/intl-pluralrules/polyfill"; 5 | import "@formatjs/intl-numberformat/polyfill"; 6 | import "@formatjs/intl-numberformat/locale-data/en"; 7 | import "@formatjs/intl-datetimeformat/polyfill"; 8 | import "@formatjs/intl-datetimeformat/locale-data/en"; 9 | import "@formatjs/intl-datetimeformat/add-golden-tz"; // or: "@formatjs/intl-datetimeformat/add-all-tz" 10 | // Tests 11 | import "./test.ts"; 12 | -------------------------------------------------------------------------------- /test/engines/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const projectRoot = __dirname; 4 | const monorepoRoot = path.resolve(projectRoot, "../.."); 5 | 6 | module.exports = { 7 | resolver: { 8 | nodeModulesPaths: [ 9 | path.resolve(projectRoot, "node_modules"), 10 | path.resolve(monorepoRoot, "node_modules"), 11 | ], 12 | }, 13 | watchFolders: [monorepoRoot], 14 | transformer: { 15 | getTransformOptions: async () => ({ 16 | transform: { 17 | experimentalImportSupport: true, 18 | }, 19 | }), 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /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/date/mini.d.ts: -------------------------------------------------------------------------------- 1 | import type { TZDate } from "./index.js"; 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 | -------------------------------------------------------------------------------- /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 | "declarationMap": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "exactOptionalPropertyTypes": true, 14 | "noUncheckedIndexedAccess": true, 15 | "skipLibCheck": true, 16 | "allowJs": true, 17 | // Project References setup 18 | "composite": true, 19 | "incremental": true, 20 | "tsBuildInfoFile": ".ts/tsconfig.tsbuildinfo" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/engines/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@date-fns/tz-test-engines", 3 | "devDependencies": { 4 | "@babel/core": "^7.28.0", 5 | "@babel/preset-typescript": "^7.27.1", 6 | "@formatjs/intl": "^3.1.6", 7 | "metro": "^0.83.1", 8 | "metro-core": "^0.83.1", 9 | "rolldown": "1.0.0-beta.30" 10 | }, 11 | "dependencies": { 12 | "@babel/runtime": "^7.28.2", 13 | "@formatjs/intl-datetimeformat": "^6.18.0", 14 | "@formatjs/intl-getcanonicallocales": "^2.5.5", 15 | "@formatjs/intl-locale": "^4.2.11", 16 | "@formatjs/intl-numberformat": "^8.15.4", 17 | "@formatjs/intl-pluralrules": "^5.4.4", 18 | "@react-native/babel-preset": "^0.80.2", 19 | "metro-runtime": "^0.83.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/test/node.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # This script runs tests against different Node.js versions. 4 | 5 | set -e 6 | 7 | printf "🤖 Running Node.js versions tests\n" 8 | 9 | # First, make sure the library is built 10 | printf "👷 Building the package\n" 11 | make build > /dev/null 12 | 13 | versions=( 14 | "18" 15 | "20" 16 | "22" 17 | "23" 18 | "24" 19 | ) 20 | 21 | for version in "${versions[@]}"; do 22 | printf "\n🚧 Running tests in Node.js v$version\n\n" 23 | cmd="node@${version}" 24 | 25 | mise x $cmd -- node test/node/commonjs.cjs 26 | printf "✅ Package CommonJS is ok!\n" 27 | 28 | mise x $cmd -- node test/node/esm.js 29 | printf "✅ Package ESM is ok!\n" 30 | 31 | printf "👷 Running unit tests\n" 32 | TZ=Asia/Singapore mise x $cmd -- pnpm vitest run 33 | 34 | printf "\n✅ Node.js v$version is ok!\n" 35 | done 36 | 37 | printf "\n✅ All Node.js tests passed\n" -------------------------------------------------------------------------------- /test/engines/src/test.ts: -------------------------------------------------------------------------------- 1 | import { TZDate, tzOffset } from "../../../src/index.ts"; 2 | 3 | { 4 | const date = new Date(2022, 2, 13); 5 | assertEqual(tzOffset("Asia/Singapore", date), 480); 6 | assertEqual(tzOffset("Asia/Katmandu", date), 345); 7 | } 8 | 9 | { 10 | const date = new TZDate(2022, 2, 13, "Asia/Singapore"); 11 | assertEqual(date.toISOString(), "2022-03-13T00:00:00.000+08:00"); 12 | assertEqual( 13 | date.toTimeString(), 14 | "00:00:00 GMT+0800 (Singapore Standard Time)" 15 | ); 16 | } 17 | 18 | function assertEqual(received: Type, expected: Type) { 19 | if (received === expected) return; 20 | throw new Error(`Expected "${expected}", but got "${received}"`); 21 | } 22 | 23 | // Edge case, see: https://github.com/date-fns/tz/issues/32#issuecomment-2832031823 24 | { 25 | const date = new Date(2022, 2, 13); 26 | assertEqual(tzOffset("Asia/Kolkata", date), 330); 27 | assertEqual(tzOffset("Asia/Calcutta", date), 330); 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | expect( 26 | tz("America/New_York")( 27 | new Date("1880-01-01T00:00:00.000Z"), 28 | ).toISOString(), 29 | ).toBe("1879-12-31T19:03:58.000-04:56"); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @env TZ=Asia/Singapore pnpm vitest run 3 | .PHONY: test 4 | 5 | test-watch: 6 | @env TZ=Asia/Singapore pnpm vitest 7 | 8 | test-node: 9 | ./scripts/test/node.sh 10 | 11 | types: 12 | @pnpm tsc --noEmit 13 | 14 | types-watch: 15 | @pnpm tsc --noEmit --watch 16 | 17 | test-types: build 18 | @cd lib && pnpm pack --out ../tmp/lib.tgz 19 | @pnpm attw tmp/lib.tgz 20 | 21 | build: prepare-build 22 | @pnpm tsc -p tsconfig.lib.json 23 | @env BABEL_ENV=esm pnpm babel src --config-file ./babel.config.json --source-root src --out-dir lib --extensions .js,.ts --out-file-extension .js --quiet 24 | @env BABEL_ENV=cjs pnpm babel src --config-file ./babel.config.json --source-root src --out-dir lib --extensions .js,.ts --out-file-extension .cjs --quiet 25 | @node copy.mjs 26 | @make build-cts 27 | 28 | build-cts: 29 | @find lib -name '*.d.ts' | while read file; do \ 30 | new_file=$${file%.d.ts}.d.cts; \ 31 | sed 's/\.js"/\.cjs"/g; s/\.ts"/\.cts"/g' $$file > $$new_file; \ 32 | done 33 | 34 | prepare-build: 35 | @rm -rf lib 36 | @mkdir -p lib 37 | 38 | publish: build 39 | cd lib && pnpm publish --access public 40 | 41 | publish-next: build 42 | cd lib && pnpm publish --access public --tag next 43 | 44 | link: 45 | @cd lib && pnpm link 46 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@date-fns/tz", 3 | 4 | "build": { "dockerfile": "Dockerfile", "context": ".." }, 5 | 6 | // Put repo to /wrkspc 7 | "workspaceMount": "source=${localWorkspaceFolder},target=/wrkspc/${localWorkspaceFolderBasename},type=bind", 8 | "workspaceFolder": "/wrkspc/${localWorkspaceFolderBasename}", 9 | 10 | "onCreateCommand": ".devcontainer/scripts/on-create.sh", 11 | "postCreateCommand": ".devcontainer/scripts/post-create.sh", 12 | "updateContentCommand": ".devcontainer/scripts/on-update.sh", 13 | 14 | "customizations": { 15 | "vscode": { 16 | "extensions": [ 17 | "tamasfe.even-better-toml", 18 | "nefrob.vscode-just-syntax", 19 | "docker.docker", 20 | "esbenp.prettier-vscode", 21 | "hverlin.mise-vscode", 22 | "tekumara.typos-vscode", 23 | "davidlday.languagetool-linter", 24 | "vitest.explorer" 25 | ], 26 | "settings": { 27 | "terminal.integrated.defaultProfile.linux": "fish", 28 | "terminal.integrated.profiles.linux": { 29 | "fish": { "path": "/usr/bin/fish" } 30 | }, 31 | "mise.binPath": "/home/vscode/.local/bin/mise", 32 | "debug.javascript.defaultRuntimeExecutable": { 33 | "pwa-node": "/home/vscode/.local/share/mise/shims/node" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tzName/tests.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { tzName } from "./index.ts"; 3 | 4 | describe("tzName", () => { 5 | const dateStr = "2020-01-01T00:00:00.000Z"; 6 | 7 | it("returns time zone name", () => { 8 | expect(tzName("Asia/Singapore", new Date(dateStr))).toBe( 9 | "Singapore Standard Time", 10 | ); 11 | expect(tzName("America/New_York", new Date(dateStr))).toBe( 12 | "Eastern Standard Time", 13 | ); 14 | }); 15 | 16 | describe("with format", () => { 17 | it("short", () => { 18 | expect(tzName("Asia/Singapore", new Date(dateStr), "short")).toBe( 19 | "GMT+8", 20 | ); 21 | expect(tzName("America/New_York", new Date(dateStr), "short")).toBe( 22 | "EST", 23 | ); 24 | }); 25 | 26 | it("long", () => { 27 | expect(tzName("Asia/Singapore", new Date(dateStr), "long")).toBe( 28 | "Singapore Standard Time", 29 | ); 30 | expect(tzName("America/New_York", new Date(dateStr), "long")).toBe( 31 | "Eastern Standard Time", 32 | ); 33 | }); 34 | 35 | it("shortGeneric", () => { 36 | expect(tzName("Asia/Singapore", new Date(dateStr), "shortGeneric")).toBe( 37 | "Singapore Time", 38 | ); 39 | expect( 40 | tzName("America/New_York", new Date(dateStr), "shortGeneric"), 41 | ).toBe("ET"); 42 | }); 43 | 44 | it("longGeneric", () => { 45 | expect(tzName("Asia/Singapore", new Date(dateStr), "longGeneric")).toBe( 46 | "Singapore Standard Time", 47 | ); 48 | expect(tzName("America/New_York", new Date(dateStr), "longGeneric")).toBe( 49 | "Eastern Time", 50 | ); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /scripts/test/engines.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # This script runs tests against different JavaScript engines. 4 | 5 | set -e 6 | 7 | printf "🤖 Running JavaScript engines tests\n" 8 | 9 | cd test/engines 10 | 11 | engines=( 12 | "hermes" 13 | "javascriptcore" 14 | ) 15 | 16 | # Install engines 17 | printf "👷 Installing the engines\n" 18 | engines_list=$(IFS=,; echo "${engines[*]}") 19 | jsvu --os=linux64 --engines=$engines_list > /dev/null 20 | 21 | # Add jsvu to PATH 22 | PATH="$PATH:~/.jsvu/bin" 23 | 24 | failed=0 25 | 26 | for engine in "${engines[@]}"; do 27 | code="src/$engine.ts" 28 | bundle="dist/$engine.js" 29 | 30 | if [[ "$engine" == "hermes" ]]; then 31 | name="Hermes" 32 | bundle_cmd="pnpm exec metro build $code --out $bundle --minify false" 33 | engine_cmd="hermes -w" 34 | 35 | elif [[ "$engine" == "javascriptcore" ]]; then 36 | name="JavaScriptCore" 37 | bundle_cmd="pnpm exec rolldown --no-treeshake --file=$bundle $code" 38 | engine_cmd="javascriptcore" 39 | 40 | else 41 | echo "🛑 Unknown engine $engine" 42 | exit 1 43 | fi 44 | 45 | printf "\n🚧 Running tests with $name engine\n\n" 46 | 47 | printf "👷 Building engine bundle $bundle\n" 48 | eval $bundle_cmd 1> /dev/null || { 49 | printf "\n🛑 $name engine bundle build failed\n" 50 | failed=1 51 | continue 52 | } 53 | 54 | printf "👷 Running tests\n" 55 | $engine_cmd $bundle || { 56 | printf "\n🛑 $name engine tests failed\n" 57 | failed=1 58 | continue 59 | } 60 | 61 | printf "✅ $name is ok!\n" 62 | done 63 | 64 | if [[ $failed -ne 0 ]]; then 65 | printf "\n🛑 Some JavaScript engines tests failed\n" 66 | exit 1 67 | fi 68 | 69 | printf "\n✅ All JavaScript engines tests passed\n" -------------------------------------------------------------------------------- /src/tzName/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Time zone name format. 3 | */ 4 | export type TZNameFormat = "short" | "long" | "shortGeneric" | "longGeneric"; 5 | 6 | /** 7 | * The function returns the time zone name for the given date in the specified 8 | * time zone. 9 | * 10 | * It uses the `Intl.DateTimeFormat` API and by default outputs the time zone 11 | * name in a long format, e.g. "Pacific Standard Time" or 12 | * "Singapore Standard Time". 13 | * 14 | * It is possible to specify the format as the third argument using one of the following options 15 | * 16 | * - "short": e.g. "EDT" or "GMT+8". 17 | * - "long": e.g. "Eastern Daylight Time". 18 | * - "shortGeneric": e.g. "ET" or "Singapore Time". 19 | * - "longGeneric": e.g. "Eastern Time" or "Singapore Standard Time". 20 | * 21 | * These options correspond to TR35 tokens `z..zzz`, `zzzz`, `v`, and `vvvv` respectively: https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-zone 22 | * 23 | * @param timeZone - Time zone name (IANA or UTC offset) 24 | * @param date - Date object to get the time zone name for 25 | * @param format - Optional format of the time zone name. Defaults to "long". Can be "short", "long", "shortGeneric", or "longGeneric". 26 | * 27 | * @returns Time zone name (e.g. "Singapore Standard Time") 28 | */ 29 | export function tzName( 30 | timeZone: string, 31 | date: Date, 32 | format: TZNameFormat = "long", 33 | ): string { 34 | return new Intl.DateTimeFormat("en-US", { 35 | // Enforces engine to render the time. Without the option JavaScriptCore omits it. 36 | hour: "numeric", 37 | timeZone: timeZone, 38 | timeZoneName: format, 39 | }) 40 | .format(date) 41 | .split(/\s/g) // Format.JS uses non-breaking spaces 42 | .slice(2) // Skip the hour and AM/PM parts 43 | .join(" "); 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-US", 22 | { timeZone, 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] || 0); 44 | const minutes = +(values[1] || 0); 45 | // Convert seconds to minutes by dividing by 60 to keep the function return in minutes. 46 | const seconds = +(values[2] || 0) / 60; 47 | return (offsetCache[cacheStr] = 48 | hours * 60 + minutes > 0 49 | ? hours * 60 + minutes + seconds 50 | : hours * 60 - minutes - seconds); 51 | } 52 | -------------------------------------------------------------------------------- /src/date/index.js: -------------------------------------------------------------------------------- 1 | import { tzName } from "../tzName/index.ts"; 2 | import { TZDateMini } from "./mini.js"; 3 | 4 | export class TZDate extends TZDateMini { 5 | //#region static 6 | 7 | static tz(tz, ...args) { 8 | return args.length ? new TZDate(...args, tz) : new TZDate(Date.now(), tz); 9 | } 10 | 11 | //#endregion 12 | 13 | //#region representation 14 | 15 | toISOString() { 16 | const [sign, hours, minutes] = this.tzComponents(); 17 | const tz = `${sign}${hours}:${minutes}`; 18 | return this.internal.toISOString().slice(0, -1) + tz; 19 | } 20 | 21 | toString() { 22 | // "Tue Aug 13 2024 07:50:19 GMT+0800 (Singapore Standard Time)"; 23 | return `${this.toDateString()} ${this.toTimeString()}`; 24 | } 25 | 26 | toDateString() { 27 | // toUTCString returns RFC 7231 ("Mon, 12 Aug 2024 23:36:08 GMT") 28 | const [day, date, month, year] = this.internal.toUTCString().split(" "); 29 | // "Tue Aug 13 2024" 30 | return `${day?.slice(0, -1) /* Remove "," */} ${month} ${date} ${year}`; 31 | } 32 | 33 | toTimeString() { 34 | // toUTCString returns RFC 7231 ("Mon, 12 Aug 2024 23:36:08 GMT") 35 | const time = this.internal.toUTCString().split(" ")[4]; 36 | const [sign, hours, minutes] = this.tzComponents(); 37 | // "07:42:23 GMT+0800 (Singapore Standard Time)" 38 | return `${time} GMT${sign}${hours}${minutes} (${tzName( 39 | this.timeZone, 40 | this, 41 | )})`; 42 | } 43 | 44 | toLocaleString(locales, options) { 45 | return Date.prototype.toLocaleString.call(this, locales, { 46 | ...options, 47 | timeZone: options?.timeZone || this.timeZone, 48 | }); 49 | } 50 | 51 | toLocaleDateString(locales, options) { 52 | return Date.prototype.toLocaleDateString.call(this, locales, { 53 | ...options, 54 | timeZone: options?.timeZone || this.timeZone, 55 | }); 56 | } 57 | 58 | toLocaleTimeString(locales, options) { 59 | return Date.prototype.toLocaleTimeString.call(this, locales, { 60 | ...options, 61 | timeZone: options?.timeZone || this.timeZone, 62 | }); 63 | } 64 | 65 | //#endregion 66 | 67 | //#region private 68 | 69 | tzComponents() { 70 | const offset = this.getTimezoneOffset(); 71 | const sign = offset > 0 ? "-" : "+"; 72 | const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0"); 73 | const minutes = String(Math.abs(offset) % 60).padStart(2, "0"); 74 | return [sign, hours, minutes]; 75 | } 76 | 77 | //#endregion 78 | 79 | withTimeZone(timeZone) { 80 | return new TZDate(+this, timeZone); 81 | } 82 | 83 | //#region date-fns integration 84 | 85 | [Symbol.for("constructDateFrom")](date) { 86 | return new TZDate(+new Date(date), this.timeZone); 87 | } 88 | 89 | //#endregion 90 | } 91 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.4.1 - 2025-08-12 12 | 13 | ### Fixed 14 | 15 | - Fixed incorrect `package.json` published with `@date-fns/tz@1.4.0`. 16 | 17 | ## v1.4.0 - 2025-08-12 18 | 19 | ### Added 20 | 21 | - [Added support for time zones with seconds offset](https://github.com/date-fns/tz/pull/47). It allows to handle dates before the implementation of the GMT system in 1883. Huge kudos to [@GianlucaWassermeyer](https://github.com/GianlucaWassermeyer)! 22 | 23 | ## v1.3.1 - 2025-08-01 24 | 25 | ### Fixed 26 | 27 | - Fixed TypeScript definitions missing in `@date-fns@1.3.0`. 28 | 29 | ## v1.3.0 - 2025-08-01 30 | 31 | ### Fixed 32 | 33 | - Fixed Format.JS support when running in Hermes engine (React Native). Ensured compatibility with JavaScriptCore engine (Safari). 34 | 35 | - Fixed TypeScript `node16` module resolution [#59](https://github.com/date-fns/tz/pull/59). Thanks to [@samchungy](https://github.com/samchungy). 36 | 37 | ### Added 38 | 39 | - Added `tzName` function that formats time zone name in given date time and format. It supports `"short"`, `"long"`, `"shortGeneric"`, and `"longGeneric"` formats, corresponding to TR35 tokens `z..zzz`, `zzzz`, `v`, and `vvvv` respectively. See [README](./README.md) for more details. 40 | 41 | ## v1.2.0 - 2024-10-31 42 | 43 | ### Fixed 44 | 45 | - 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). 46 | 47 | ## v1.1.2 - 2024-09-24 48 | 49 | ### Fixed 50 | 51 | - Improved compatibility with FormatJS Intl polifyll [#8](https://github.com/date-fns/tz/issues/8). Thanks to [@kevin-abiera](https://github.com/kevin-abiera). 52 | 53 | ## v1.1.1 - 2024-09-23 54 | 55 | ### Fixed 56 | 57 | - 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. 58 | 59 | ## v1.1.0 - 2024-09-22 60 | 61 | 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). 62 | 63 | ### Fixed 64 | 65 | - [Fixed negative fractional time zones like `America/St_Johns`](https://github.com/date-fns/tz/pull/7) [@allohamora](https://github.com/allohamora). 66 | 67 | - Fixed the DST bug when creating a date in the DST transition hour. 68 | 69 | ### Added 70 | 71 | - 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) 72 | 73 | ## v1.0.2 - 2024-09-14 74 | 75 | This release fixes a couple of critical bugs in the previous release. 76 | 77 | ### Fixed 78 | 79 | - Fixed UTC setters functions generation. 80 | 81 | - Create `Invalid Date` instead of throwing an error on invalid arguments. 82 | 83 | - Make all the number getters return `NaN` when the date or time zone is invalid. 84 | 85 | - Make `tzOffset` return `NaN` when the date or the time zone is invalid. 86 | 87 | ## v1.0.1 - 2024-09-13 88 | 89 | Initial version 90 | 91 | ``` 92 | 93 | ``` 94 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@date-fns/tz", 3 | "version": "1.4.1", 4 | "description": "date-fns timezone utils", 5 | "type": "module", 6 | "main": "./src/index.ts", 7 | "module": "./src/index.ts", 8 | "exports": { 9 | ".": "./src/index.ts", 10 | "./tzOffset": "./src/tzOffset/index.ts", 11 | "./tzScan": "./src/tzScan/index.ts", 12 | "./tzName": "./src/tzName/index.ts", 13 | "./date": "./src/date/index.js", 14 | "./date/mini": "./src/date/mini.js", 15 | "./tz": "./src/tz/index.ts", 16 | "./constants": "./src/constants/index.ts" 17 | }, 18 | "publishConfig": { 19 | "main": "index.cjs", 20 | "module": "index.js", 21 | "exports": { 22 | "./package.json": "./package.json", 23 | ".": { 24 | "require": { 25 | "types": "./index.d.cts", 26 | "default": "./index.cjs" 27 | }, 28 | "import": { 29 | "types": "./index.d.ts", 30 | "default": "./index.js" 31 | } 32 | }, 33 | "./tzOffset": { 34 | "require": { 35 | "types": "./tzOffset/index.d.cts", 36 | "default": "./tzOffset/index.cjs" 37 | }, 38 | "import": { 39 | "types": "./tzOffset/index.d.ts", 40 | "default": "./tzOffset/index.js" 41 | } 42 | }, 43 | "./tzScan": { 44 | "require": { 45 | "types": "./tzScan/index.d.cts", 46 | "default": "./tzScan/index.cjs" 47 | }, 48 | "import": { 49 | "types": "./tzScan/index.d.ts", 50 | "default": "./tzScan/index.js" 51 | } 52 | }, 53 | "./tzName": { 54 | "require": { 55 | "types": "./tzName/index.d.cts", 56 | "default": "./tzName/index.cjs" 57 | }, 58 | "import": { 59 | "types": "./tzName/index.d.ts", 60 | "default": "./tzName/index.js" 61 | } 62 | }, 63 | "./date": { 64 | "require": { 65 | "types": "./date/index.d.cts", 66 | "default": "./date/index.cjs" 67 | }, 68 | "import": { 69 | "types": "./date/index.d.ts", 70 | "default": "./date/index.js" 71 | } 72 | }, 73 | "./date/mini": { 74 | "require": { 75 | "types": "./date/mini.d.cts", 76 | "default": "./date/mini.cjs" 77 | }, 78 | "import": { 79 | "types": "./date/mini.d.ts", 80 | "default": "./date/mini.js" 81 | } 82 | }, 83 | "./tz": { 84 | "require": { 85 | "types": "./tz/index.d.cts", 86 | "default": "./tz/index.cjs" 87 | }, 88 | "import": { 89 | "types": "./tz/index.d.ts", 90 | "default": "./tz/index.js" 91 | } 92 | }, 93 | "./constants": { 94 | "require": { 95 | "types": "./constants/index.d.cts", 96 | "default": "./constants/index.cjs" 97 | }, 98 | "import": { 99 | "types": "./constants/index.d.ts", 100 | "default": "./constants/index.js" 101 | } 102 | } 103 | } 104 | }, 105 | "scripts": { 106 | "test": "vitest run" 107 | }, 108 | "repository": { 109 | "type": "git", 110 | "url": "git+https://github.com/date-fns/tz.git" 111 | }, 112 | "keywords": [ 113 | "date-fns", 114 | "tz", 115 | "timezones", 116 | "date", 117 | "time", 118 | "datetime" 119 | ], 120 | "author": "Sasha Koss ", 121 | "license": "MIT", 122 | "bugs": { 123 | "url": "https://github.com/date-fns/tz/issues" 124 | }, 125 | "homepage": "https://github.com/date-fns/tz#readme", 126 | "devDependencies": { 127 | "@arethetypeswrong/cli": "^0.18.2", 128 | "@babel/cli": "^7.28.0", 129 | "@babel/core": "^7.28.0", 130 | "@babel/plugin-transform-modules-commonjs": "^7.27.1", 131 | "@babel/preset-env": "^7.28.0", 132 | "@babel/preset-typescript": "^7.27.1", 133 | "@parcel/watcher": "^2.4.1", 134 | "@sinonjs/fake-timers": "^11.2.2", 135 | "@swc/core": "^1.4.13", 136 | "@types/sinonjs__fake-timers": "^8.1.5", 137 | "babel-plugin-replace-import-extension": "^1.1.5", 138 | "bytes-iec": "^3.1.1", 139 | "date-fns": "4.0.0-alpha.1", 140 | "glob": "^10.3.12", 141 | "minimatch": "^10.0.1", 142 | "picocolors": "^1.0.0", 143 | "prettier": "^3.6.2", 144 | "tinybench": "^2.7.0", 145 | "typescript": "5.9.2", 146 | "vitest": "^3.2.4" 147 | }, 148 | "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" 149 | } 150 | -------------------------------------------------------------------------------- /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 | expect(tzOffset("America/New_York", new Date("1880-01-15T00:00:00Z"))).toBe( 11 | -296.03333333333336, 12 | ); 13 | }); 14 | 15 | it("works at the end of the day", () => { 16 | const date = new Date("2020-01-15T23:59:59Z"); 17 | expect(tzOffset("America/New_York", date)).toBe(-5 * 60); 18 | expect(tzOffset("Asia/Pyongyang", date)).toBe(9 * 60); 19 | expect(tzOffset("Asia/Kathmandu", date)).toBe(345); 20 | expect(tzOffset("America/New_York", new Date("1880-01-15T23:59:59Z"))).toBe( 21 | -296.03333333333336, 22 | ); 23 | }); 24 | 25 | it("works at the end of a month", () => { 26 | const date = new Date("2020-01-31T23:59:59Z"); 27 | expect(tzOffset("America/New_York", date)).toBe(-5 * 60); 28 | expect(tzOffset("Asia/Pyongyang", date)).toBe(9 * 60); 29 | expect(tzOffset("Asia/Kathmandu", date)).toBe(345); 30 | expect(tzOffset("America/New_York", new Date("1880-01-31T23:59:59Z"))).toBe( 31 | -296.03333333333336, 32 | ); 33 | }); 34 | 35 | it("works at midnight", () => { 36 | expect(tzOffset("America/New_York", new Date("2020-01-15T05:00:00Z"))).toBe( 37 | -5 * 60, 38 | ); 39 | expect(tzOffset("America/New_York", new Date("1880-01-15T05:00:00Z"))).toBe( 40 | -296.03333333333336, 41 | ); 42 | }); 43 | 44 | it("returns the local timezone offset when the timezone is undefined", () => { 45 | const date = new Date("2020-01-15T05:00:00Z"); 46 | expect(tzOffset(undefined, date)).toBe(-date.getTimezoneOffset() || 0); 47 | }); 48 | 49 | it("returns 0 if the offset is 0", () => { 50 | const date = new Date("2020-01-15T00:00:00Z"); 51 | expect(tzOffset("Europe/London", date)).toBe(0); 52 | }); 53 | 54 | it("returns NaN if the offset the date or time zone are invalid", () => { 55 | expect(tzOffset("Etc/Invalid", new Date("2020-01-15T00:00:00Z"))).toBe(NaN); 56 | expect(tzOffset("America/New_York", new Date(NaN))).toBe(NaN); 57 | }); 58 | 59 | describe("time zone name formats", () => { 60 | const date = new Date("2020-01-15T00:00:00Z"); 61 | 62 | it("works with IANA time zone names", () => { 63 | expect(tzOffset("America/New_York", date)).toBe(-300); 64 | expect(tzOffset("Asia/Pyongyang", date)).toBe(540); 65 | }); 66 | 67 | it("works with ±HH:MM", () => { 68 | expect(tzOffset("-05:00", date)).toBe(-300); 69 | expect(tzOffset("-02:30", date)).toBe(-150); 70 | expect(tzOffset("+05:00", date)).toBe(300); 71 | expect(tzOffset("+02:30", date)).toBe(150); 72 | expect(tzOffset("+05:00", new Date("1880-01-15T00:00:00Z"))).toBe(300); 73 | }); 74 | 75 | it("works with ±HHMM", () => { 76 | expect(tzOffset("-0500", date)).toBe(-300); 77 | expect(tzOffset("-0230", date)).toBe(-150); 78 | expect(tzOffset("+0500", date)).toBe(300); 79 | expect(tzOffset("+0230", date)).toBe(150); 80 | expect(tzOffset("+0230", new Date("1880-01-15T00:00:00Z"))).toBe(150); 81 | }); 82 | 83 | it("works with ±HH", () => { 84 | expect(tzOffset("-05", date)).toBe(-300); 85 | expect(tzOffset("+05", date)).toBe(300); 86 | expect(tzOffset("+05", new Date("1880-01-15T00:00:00Z"))).toBe(300); 87 | }); 88 | }); 89 | 90 | describe("fractional time zones", () => { 91 | it("works negative fractional time zones", () => { 92 | const dst = new Date("2023-03-15T18:00:00.000Z"); 93 | const date = new Date("2023-03-03T18:00:00.000Z"); 94 | expect(tzOffset("America/St_Johns", dst)).toBe(-150); 95 | expect(tzOffset("America/St_Johns", date)).toBe(-210); 96 | }); 97 | 98 | it("works positive fractional time zones", () => { 99 | const dst = new Date("2024-04-06T16:00:00.000Z"); 100 | const date = new Date("2024-04-06T16:30:00.000Z"); 101 | expect(tzOffset("Australia/Adelaide", dst)).toBe(630); 102 | expect(tzOffset("Australia/Adelaide", date)).toBe(570); 103 | }); 104 | }); 105 | 106 | describe("Intl.DateTimeFormat format", () => { 107 | let mockFormat = vi.fn(); 108 | beforeEach(() => { 109 | mockFormat = vi.fn(() => "5 GMT+08:00"); 110 | const dtf = new Intl.DateTimeFormat(); 111 | vi.spyOn(Intl, "DateTimeFormat").mockImplementation(() => { 112 | return { ...dtf, format: mockFormat }; 113 | }); 114 | }); 115 | 116 | afterEach(() => { 117 | vi.mocked(Intl.DateTimeFormat).mockRestore(); 118 | }); 119 | 120 | it("reads offset from expected format", () => { 121 | mockFormat.mockReturnValue("5 GMT+08:00"); 122 | const date = new Date("2020-01-15T00:00:00Z"); 123 | expect(tzOffset("Asia/Manila", date)).toBe(480); 124 | }); 125 | 126 | it("reads offset from polyfill", () => { 127 | mockFormat.mockReturnValue("5:53 PM GMT-9:30"); 128 | const date = new Date("2020-01-15T00:00:00Z"); 129 | expect(tzOffset("Pacific/Marquesas", date)).toBe(-570); 130 | }); 131 | 132 | it("reads offset from polyfill (without offset)", () => { 133 | mockFormat.mockReturnValue("5:53 PM GMT"); 134 | const date = new Date("2020-01-15T00:00:00Z"); 135 | expect(tzOffset("UTC", date)).toBe(0); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /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/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 | const offset = -tzOffset(this.timeZone, this); 54 | // Remove the seconds offset 55 | // use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones. 56 | return offset > 0 ? Math.floor(offset) : Math.ceil(offset); 57 | } 58 | 59 | //#endregion 60 | 61 | //#region time 62 | 63 | setTime(time) { 64 | Date.prototype.setTime.apply(this, arguments); 65 | syncToInternal(this); 66 | return +this; 67 | } 68 | 69 | //#endregion 70 | 71 | //#region date-fns integration 72 | 73 | [Symbol.for("constructDateFrom")](date) { 74 | return new TZDateMini(+new Date(date), this.timeZone); 75 | } 76 | 77 | //#endregion 78 | } 79 | 80 | // Assign getters and setters 81 | const re = /^(get|set)(?!UTC)/; 82 | Object.getOwnPropertyNames(Date.prototype).forEach((method) => { 83 | if (!re.test(method)) return; 84 | 85 | const utcMethod = method.replace(re, "$1UTC"); 86 | // Filter out methods without UTC counterparts 87 | if (!TZDateMini.prototype[utcMethod]) return; 88 | 89 | if (method.startsWith("get")) { 90 | // Delegate to internal date's UTC method 91 | TZDateMini.prototype[method] = function () { 92 | return this.internal[utcMethod](); 93 | }; 94 | } else { 95 | // Assign regular setter 96 | TZDateMini.prototype[method] = function () { 97 | Date.prototype[utcMethod].apply(this.internal, arguments); 98 | syncFromInternal(this); 99 | return +this; 100 | }; 101 | 102 | // Assign UTC setter 103 | TZDateMini.prototype[utcMethod] = function () { 104 | Date.prototype[utcMethod].apply(this, arguments); 105 | syncToInternal(this); 106 | return +this; 107 | }; 108 | } 109 | }); 110 | 111 | /** 112 | * Function syncs time to internal date, applying the time zone offset. 113 | * 114 | * @param {Date} date - Date to sync 115 | */ 116 | function syncToInternal(date) { 117 | date.internal.setTime(+date); 118 | date.internal.setUTCSeconds( 119 | date.internal.getUTCSeconds() - 120 | Math.round(-tzOffset(date.timeZone, date) * 60), 121 | ); 122 | } 123 | 124 | /** 125 | * Function syncs the internal date UTC values to the date. It allows to get 126 | * accurate timestamp value. 127 | * 128 | * @param {Date} date - The date to sync 129 | */ 130 | function syncFromInternal(date) { 131 | // First we transpose the internal values 132 | Date.prototype.setFullYear.call( 133 | date, 134 | date.internal.getUTCFullYear(), 135 | date.internal.getUTCMonth(), 136 | date.internal.getUTCDate(), 137 | ); 138 | Date.prototype.setHours.call( 139 | date, 140 | date.internal.getUTCHours(), 141 | date.internal.getUTCMinutes(), 142 | date.internal.getUTCSeconds(), 143 | date.internal.getUTCMilliseconds(), 144 | ); 145 | 146 | // Now we have to adjust the date to the system time zone 147 | adjustToSystemTZ(date); 148 | } 149 | 150 | /** 151 | * Function adjusts the date to the system time zone. It uses the time zone 152 | * differences to calculate the offset and adjust the date. 153 | * 154 | * @param {Date} date - Date to adjust 155 | */ 156 | function adjustToSystemTZ(date) { 157 | // Save the time zone offset before all the adjustments 158 | const baseOffset = tzOffset(date.timeZone, date); 159 | // Remove the seconds offset 160 | // use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones. 161 | const offset = 162 | baseOffset > 0 ? Math.floor(baseOffset) : Math.ceil(baseOffset); 163 | //#region System DST adjustment 164 | 165 | // The biggest problem with using the system time zone is that when we create 166 | // a date from internal values stored in UTC, the system time zone might end 167 | // up on the DST hour: 168 | // 169 | // $ TZ=America/New_York node 170 | // > new Date(2020, 2, 8, 1).toString() 171 | // 'Sun Mar 08 2020 01:00:00 GMT-0500 (Eastern Standard Time)' 172 | // > new Date(2020, 2, 8, 2).toString() 173 | // 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)' 174 | // > new Date(2020, 2, 8, 3).toString() 175 | // 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)' 176 | // > new Date(2020, 2, 8, 4).toString() 177 | // 'Sun Mar 08 2020 04:00:00 GMT-0400 (Eastern Daylight Time)' 178 | // 179 | // Here we get the same hour for both 2 and 3, because the system time zone 180 | // has DST beginning at 8 March 2020, 2 a.m. and jumps to 3 a.m. So we have 181 | // to adjust the internal date to reflect that. 182 | // 183 | // However we want to adjust only if that's the DST hour the change happenes, 184 | // not the hour where DST moves to. 185 | 186 | // We calculate the previous hour to see if the time zone offset has changed 187 | // and we have landed on the DST hour. 188 | const prevHour = new Date(+date); 189 | // We use UTC methods here as we don't want to land on the same hour again 190 | // in case of DST. 191 | prevHour.setUTCHours(prevHour.getUTCHours() - 1); 192 | 193 | // Calculate if we are on the system DST hour. 194 | const systemOffset = -new Date(+date).getTimezoneOffset(); 195 | const prevHourSystemOffset = -new Date(+prevHour).getTimezoneOffset(); 196 | const systemDSTChange = systemOffset - prevHourSystemOffset; 197 | // Detect the DST shift. System DST change will occur both on 198 | const dstShift = 199 | Date.prototype.getHours.apply(date) !== date.internal.getUTCHours(); 200 | 201 | // Move the internal date when we are on the system DST hour. 202 | if (systemDSTChange && dstShift) 203 | date.internal.setUTCMinutes( 204 | date.internal.getUTCMinutes() + systemDSTChange, 205 | ); 206 | 207 | //#endregion 208 | 209 | //#region System diff adjustment 210 | 211 | // Now we need to adjust the date, since we just applied internal values. 212 | // We need to calculate the difference between the system and date time zones 213 | // and apply it to the date. 214 | 215 | const offsetDiff = systemOffset - offset; 216 | if (offsetDiff) 217 | Date.prototype.setUTCMinutes.call( 218 | date, 219 | Date.prototype.getUTCMinutes.call(date) + offsetDiff, 220 | ); 221 | 222 | //#endregion 223 | 224 | //#region Seconds System diff adjustment 225 | 226 | const systemDate = new Date(+date); 227 | // Set the UTC seconds to 0 to isolate the timezone offset in seconds. 228 | systemDate.setUTCSeconds(0); 229 | // For negative systemOffset, invert the seconds. 230 | const systemSecondsOffset = 231 | systemOffset > 0 232 | ? systemDate.getSeconds() 233 | : (systemDate.getSeconds() - 60) % 60; 234 | 235 | // Calculate the seconds offset based on the timezone offset. 236 | const secondsOffset = Math.round(-(tzOffset(date.timeZone, date) * 60)) % 60; 237 | 238 | if (secondsOffset || systemSecondsOffset) { 239 | date.internal.setUTCSeconds(date.internal.getUTCSeconds() + secondsOffset); 240 | Date.prototype.setUTCSeconds.call( 241 | date, 242 | Date.prototype.getUTCSeconds.call(date) + 243 | secondsOffset + 244 | systemSecondsOffset, 245 | ); 246 | } 247 | 248 | //#endregion 249 | 250 | //#region Post-adjustment DST fix 251 | 252 | const postBaseOffset = tzOffset(date.timeZone, date); 253 | // Remove the seconds offset 254 | // use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones. 255 | const postOffset = 256 | postBaseOffset > 0 ? Math.floor(postBaseOffset) : Math.ceil(postBaseOffset); 257 | const postSystemOffset = -new Date(+date).getTimezoneOffset(); 258 | const postOffsetDiff = postSystemOffset - postOffset; 259 | const offsetChanged = postOffset !== offset; 260 | const postDiff = postOffsetDiff - offsetDiff; 261 | 262 | if (offsetChanged && postDiff) { 263 | Date.prototype.setUTCMinutes.call( 264 | date, 265 | Date.prototype.getUTCMinutes.call(date) + postDiff, 266 | ); 267 | 268 | // Now we need to check if got offset change during the post-adjustment. 269 | // If so, we also need both dates to reflect that. 270 | 271 | const newBaseOffset = tzOffset(date.timeZone, date); 272 | // Remove the seconds offset 273 | // use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones. 274 | const newOffset = 275 | newBaseOffset > 0 ? Math.floor(newBaseOffset) : Math.ceil(newBaseOffset); 276 | const offsetChange = postOffset - newOffset; 277 | 278 | if (offsetChange) { 279 | date.internal.setUTCMinutes(date.internal.getUTCMinutes() + offsetChange); 280 | Date.prototype.setUTCMinutes.call( 281 | date, 282 | Date.prototype.getUTCMinutes.call(date) + offsetChange, 283 | ); 284 | } 285 | } 286 | 287 | //#endregion 288 | } 289 | -------------------------------------------------------------------------------- /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 also be 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 | ### React Native / Hermes JS Engine 72 | 73 | Starting with [v1.3.0](https://github.com/date-fns/tz/releases/tag/v1.3.0), `@date-fns/tz` supports [Format.JS polyfills](https://formatjs.github.io/docs/polyfills/intl-datetimeformat/) that are required for [Hermes JS Engine](https://github.com/facebook/hermes/blob/main/README.md) powering React Native runtime to work correctly. 74 | 75 | To use it, you need to import the following polyfills in your entry point: 76 | 77 | ```ts 78 | import "@formatjs/intl-getcanonicallocales/polyfill"; 79 | import "@formatjs/intl-locale/polyfill"; 80 | import "@formatjs/intl-pluralrules/polyfill"; 81 | import "@formatjs/intl-numberformat/polyfill"; 82 | import "@formatjs/intl-numberformat/locale-data/en"; 83 | import "@formatjs/intl-datetimeformat/polyfill"; 84 | import "@formatjs/intl-datetimeformat/locale-data/en"; 85 | import "@formatjs/intl-datetimeformat/add-golden-tz"; // or: "@formatjs/intl-datetimeformat/add-all-tz" 86 | ``` 87 | 88 | [The JavaScriptCore engine](https://github.com/apple-opensource/JavaScriptCore) is also supported and tested but does not require any polyfills. 89 | 90 | ## API 91 | 92 | - [`TZDate`](#tzdate) 93 | - [`tz`](#tz) 94 | - [`tzOffset`](#tzoffset) 95 | - [`tzScan`](#tzscan) 96 | 97 | ### `TZDate` 98 | 99 | All the `TZDate` docs are also true for `TZDateMini`. 100 | 101 | #### Constructor 102 | 103 | When creating `TZDate`, you can pass the time zone as the last argument: 104 | 105 | ```ts 106 | new TZDate(2022, 2, "Asia/Singapore"); 107 | 108 | new TZDate(timestamp, "Asia/Singapore"); 109 | 110 | new TZDate("2024-09-12T00:00:00Z", "Asia/Singapore"); 111 | ``` 112 | 113 | The constructor mirrors the original `Date` parameters except for the last time zone parameter. 114 | 115 | #### `TZDate.tz` 116 | 117 | The static `tz` function allows to construct `TZDate` instance with just a time zone: 118 | 119 | ```ts 120 | // Create now in Singapore time zone: 121 | TZDate.tz("Asia/Singapore"); 122 | 123 | // ❌ This will not work, as TZDate expects a date string: 124 | new TZDate("Asia/Singapore"); 125 | //=> Invalid Date 126 | ``` 127 | 128 | Just like the constructor, the function accepts all parameters variants: 129 | 130 | ```ts 131 | TZDate.tz("Asia/Singapore", 2022, 2); 132 | 133 | TZDate.tz("Asia/Singapore", timestamp); 134 | 135 | TZDate.tz("Asia/Singapore", "2024-09-12T00:00:00Z"); 136 | ``` 137 | 138 | #### `timeZone` 139 | 140 | The readonly `timeZone` property returns the time zone name assigned to the instance: 141 | 142 | ```ts 143 | new TZDate(2022, 2, 13, "Asia/Singapore").timeZone; 144 | // "Asia/Singapore" 145 | ``` 146 | 147 | The property might be `undefined` when created without a time zone: 148 | 149 | ```ts 150 | new TZDate().timeZone; 151 | // undefined 152 | ``` 153 | 154 | #### `withTimeZone` 155 | 156 | The `withTimeZone` method allows to create a new `TZDate` instance with a different time zone: 157 | 158 | ```ts 159 | const sg = new TZDate(2022, 2, 13, "Asia/Singapore"); 160 | const ny = sg.withTimeZone("America/New_York"); 161 | 162 | sg.toString(); 163 | //=> 'Sun Mar 13 2022 00:00:00 GMT+0800 (Singapore Standard Time)' 164 | 165 | ny.toString(); 166 | //=> 'Sat Mar 12 2022 11:00:00 GMT-0500 (Eastern Standard Time)' 167 | ``` 168 | 169 | #### `[Symbol.for("constructDateFrom")]` 170 | 171 | The `TZDate` instance also exposes a method to construct a `Date` instance in the same time zone: 172 | 173 | ```ts 174 | const sg = TZDate.tz("Asia/Singapore"); 175 | 176 | // Given that the system time zone is America/Los_Angeles 177 | 178 | const date = sg[Symbol.for("constructDateFrom")](new Date(2024, 0, 1)); 179 | 180 | date.toString(); 181 | //=> 'Mon Jan 01 2024 16:00:00 GMT+0800 (Singapore Standard Time)' 182 | ``` 183 | 184 | 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: 185 | 186 | ```ts 187 | import { constructFromSymbol } from "@date-fns/tz"; 188 | ``` 189 | 190 | ### `tz` 191 | 192 | The `tz` function allows to specify the context for the [date-fns] functions (**starting from date-fns@4**): 193 | 194 | ```ts 195 | import { isSameDay } from "date-fns"; 196 | import { tz } from "@date-fns/tz"; 197 | 198 | isSameDay("2024-09-09T23:00:00-04:00", "2024-09-10T10:00:00+08:00", { 199 | in: tz("Europe/Prague"), 200 | }); 201 | //=> true 202 | ``` 203 | 204 | ### `tzOffset` 205 | 206 | The `tzOffset` function allows to get the time zone UTC offset in minutes from the given time zone and a date: 207 | 208 | ```ts 209 | import { tzOffset } from "@date-fns/tz"; 210 | 211 | const date = new Date("2020-01-15T00:00:00Z"); 212 | 213 | tzOffset("Asia/Singapore", date); 214 | //=> 480 215 | 216 | tzOffset("America/New_York", date); 217 | //=> -300 218 | 219 | // Summer time: 220 | tzOffset("America/New_York", "2020-01-15T00:00:00Z"); 221 | //=> -240 222 | ``` 223 | 224 | 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. 225 | 226 | ### `tzScan` 227 | 228 | 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: 229 | 230 | ```ts 231 | import { tzScan } from "@date-fns/tz"; 232 | 233 | tzScan("America/New_York", { 234 | start: new Date("2020-01-01T00:00:00Z"), 235 | end: new Date("2024-01-01T00:00:00Z"), 236 | }); 237 | //=> [ 238 | //=> { date: 2020-03-08T07:00:00.000Z, change: 60, offset: -240 }, 239 | //=> { date: 2020-11-01T06:00:00.000Z, change: -60, offset: -300 }, 240 | //=> { date: 2021-03-14T07:00:00.000Z, change: 60, offset: -240 }, 241 | //=> { date: 2021-11-07T06:00:00.000Z, change: -60, offset: -300 }, 242 | //=> { date: 2022-03-13T07:00:00.000Z, change: 60, offset: -240 }, 243 | //=> { date: 2022-11-06T06:00:00.000Z, change: -60, offset: -300 }, 244 | //=> { date: 2023-03-12T07:00:00.000Z, change: 60, offset: -240 }, 245 | //=> { date: 2023-11-05T06:00:00.000Z, change: -60, offset: -300 } 246 | //=> ] 247 | ``` 248 | 249 | ### `tzName` 250 | 251 | The function returns time zone name in human-readable format, e.g. `"Singapore Standard Time"` in the give date and time. 252 | 253 | ```ts 254 | import { tzName } from "@date-fns/tz"; 255 | 256 | tzName("Asia/Singapore", new Date("2020-01-01T00:00:00Z")); 257 | //=> "Singapore Standard Time" 258 | ``` 259 | 260 | It is possible to specify the format as the third argument using one of the following options: 261 | 262 | - `"short"`e.g., `"EDT"` or "GMT+8"`. 263 | - `"long"`: e.g., `"Eastern Daylight Time"` or `"Singapore Standard Time"`. 264 | - `"shortGeneric"`: e.g., `"ET"` or `"Singapore Time"`. 265 | - `"longGeneric"`: e.g., `"Eastern Time"` or `"Singapore Standard Time"`. 266 | 267 | These options correspond to [TR35 tokens](https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-zone) `z..zzz`, `zzzz`, `v`, and `vvvv` respectively. 268 | 269 | ```ts 270 | import { tzName } from "@date-fns/tz"; 271 | 272 | const date = new Date("2020-01-01T00:00:00.000Z"); 273 | 274 | tzName("America/New_York", date, "short"); 275 | //=> "EST" 276 | 277 | tzName("America/New_York", date, "shortGeneric"); 278 | //=> "ET" 279 | ``` 280 | 281 | ## Changelog 282 | 283 | See [the changelog](./CHANGELOG.md). 284 | 285 | ## License 286 | 287 | [MIT © Sasha Koss](https://kossnocorp.mit-license.org/) 288 | -------------------------------------------------------------------------------- /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 | const beforeGMTDateStr = "1880-02-11T00:00:00.000Z"; 9 | 10 | let timers: FakeTimers.InstalledClock; 11 | let now = new Date(); 12 | 13 | function fakeNow(date = new Date(defaultDateStr)) { 14 | now = date; 15 | timers = FakeTimers.install({ now }); 16 | } 17 | 18 | afterEach(() => timers?.uninstall()); 19 | 20 | describe("static", () => { 21 | describe("constructor", () => { 22 | it("creates a new date", () => { 23 | fakeNow(); 24 | const date = new TZDate(); 25 | expect(+date).toBe(+now); 26 | }); 27 | 28 | it("creates a new date from a timestamp", () => { 29 | expect( 30 | new TZDate(+new Date(defaultDateStr), "Asia/Singapore").toISOString(), 31 | ).toBe("1987-02-11T08:00:00.000+08:00"); 32 | expect( 33 | new TZDate( 34 | +new Date(defaultDateStr), 35 | "America/New_York", 36 | ).toISOString(), 37 | ).toBe("1987-02-10T19:00:00.000-05:00"); 38 | }); 39 | 40 | it("creates a new date from a string", () => { 41 | const dateStr = "2024-02-11T00:00:00.000Z"; 42 | expect(new TZDate(dateStr, "Asia/Singapore").toISOString()).toBe( 43 | "2024-02-11T08:00:00.000+08:00", 44 | ); 45 | expect(new TZDate("2024-02-11", "Asia/Singapore").toISOString()).toBe( 46 | "2024-02-11T08:00:00.000+08:00", 47 | ); 48 | expect(new TZDate(dateStr, "America/New_York").toISOString()).toBe( 49 | "2024-02-10T19:00:00.000-05:00", 50 | ); 51 | expect(new TZDate("2024-02-11", "America/New_York").toISOString()).toBe( 52 | "2024-02-10T19:00:00.000-05:00", 53 | ); 54 | expect(new TZDate(beforeGMTDateStr, "America/New_York").toISOString()).toBe( 55 | "1880-02-10T19:03:58.000-04:56" 56 | ); 57 | expect(new TZDate("1880-02-11", "America/New_York").toISOString()).toBe( 58 | "1880-02-10T19:03:58.000-04:56" 59 | ); 60 | expect(new TZDate(beforeGMTDateStr, "Asia/Singapore").toISOString()).toBe( 61 | "1880-02-11T06:55:25.000+06:55" 62 | ); 63 | expect(new TZDate("1880-02-11", "Asia/Singapore").toISOString()).toBe( 64 | "1880-02-11T06:55:25.000+06:55" 65 | ); 66 | }); 67 | 68 | it("creates a new date from a date instance", () => { 69 | const nativeDate = new Date(defaultDateStr); 70 | const beforeGMTNativeDate = new Date(beforeGMTDateStr); 71 | expect(new TZDate(nativeDate, "Asia/Singapore").toISOString()).toBe( 72 | "1987-02-11T08:00:00.000+08:00", 73 | ); 74 | expect(new TZDate(nativeDate, "America/New_York").toISOString()).toBe( 75 | "1987-02-10T19:00:00.000-05:00", 76 | ); 77 | expect(new TZDate(beforeGMTNativeDate, "Asia/Singapore").toISOString()).toBe( 78 | "1880-02-11T06:55:25.000+06:55" 79 | ); 80 | expect(new TZDate(beforeGMTNativeDate, "America/New_York").toISOString()).toBe( 81 | "1880-02-10T19:03:58.000-04:56" 82 | ); 83 | }); 84 | 85 | it("creates a new date from date values", () => { 86 | // Month 87 | expect(new TZDate(2024, 1, "Asia/Singapore").toISOString()).toBe( 88 | "2024-02-01T00:00:00.000+08:00", 89 | ); 90 | expect(new TZDate(2024, 1, "America/New_York").toISOString()).toBe( 91 | "2024-02-01T00:00:00.000-05:00", 92 | ); 93 | expect(new TZDate(1880, 1, "Asia/Singapore").toISOString()).toBe( 94 | "1880-02-01T00:00:00.000+06:55" 95 | ); 96 | expect(new TZDate(1880, 1, "America/New_York").toISOString()).toBe( 97 | "1880-02-01T00:00:00.000-04:56" 98 | ); 99 | // Date 100 | expect(new TZDate(2024, 1, 11, "Asia/Singapore").toISOString()).toBe( 101 | "2024-02-11T00:00:00.000+08:00", 102 | ); 103 | expect(new TZDate(2024, 1, 11, "America/New_York").toISOString()).toBe( 104 | "2024-02-11T00:00:00.000-05:00", 105 | ); 106 | expect(new TZDate(1880, 1, 11, "Asia/Singapore").toISOString()).toBe( 107 | "1880-02-11T00:00:00.000+06:55" 108 | ); 109 | expect(new TZDate(1880, 1, 11, "America/New_York").toISOString()).toBe( 110 | "1880-02-11T00:00:00.000-04:56" 111 | ); 112 | // Hours 113 | expect( 114 | new TZDate(2024, 1, 11, 12, "Asia/Singapore").toISOString(), 115 | ).toBe("2024-02-11T12:00:00.000+08:00"); 116 | expect( 117 | new TZDate(2024, 1, 11, 12, "America/New_York").toISOString(), 118 | ).toBe("2024-02-11T12:00:00.000-05:00"); 119 | expect( 120 | new TZDate(1880, 1, 11, 12, "Asia/Singapore").toISOString() 121 | ).toBe("1880-02-11T12:00:00.000+06:55"); 122 | expect( 123 | new TZDate(1880, 1, 11, 12, "America/New_York").toISOString() 124 | ).toBe("1880-02-11T12:00:00.000-04:56"); 125 | // Minutes 126 | expect( 127 | new TZDate(2024, 1, 11, 12, 30, "Asia/Singapore").toISOString(), 128 | ).toBe("2024-02-11T12:30:00.000+08:00"); 129 | expect( 130 | new TZDate(2024, 1, 11, 12, 30, "America/New_York").toISOString(), 131 | ).toBe("2024-02-11T12:30:00.000-05:00"); 132 | expect( 133 | new TZDate(1880, 1, 11, 12, 30, "Asia/Singapore").toISOString() 134 | ).toBe("1880-02-11T12:30:00.000+06:55"); 135 | expect( 136 | new TZDate(1880, 1, 11, 12, 30, "America/New_York").toISOString() 137 | ).toBe("1880-02-11T12:30:00.000-04:56"); 138 | // Seconds 139 | expect( 140 | new TZDate(2024, 1, 11, 12, 30, 45, "Asia/Singapore").toISOString(), 141 | ).toBe("2024-02-11T12:30:45.000+08:00"); 142 | expect( 143 | new TZDate(2024, 1, 11, 12, 30, 45, "America/New_York").toISOString(), 144 | ).toBe("2024-02-11T12:30:45.000-05:00"); 145 | expect( 146 | new TZDate(1880, 1, 11, 12, 30, 45, "Asia/Singapore").toISOString() 147 | ).toBe("1880-02-11T12:30:45.000+06:55"); 148 | expect( 149 | new TZDate(1880, 1, 11, 12, 30, 45, "America/New_York").toISOString() 150 | ).toBe("1880-02-11T12:30:45.000-04:56"); 151 | // Milliseconds 152 | expect( 153 | new TZDate( 154 | 2024, 155 | 1, 156 | 11, 157 | 12, 158 | 30, 159 | 45, 160 | 987, 161 | "Asia/Singapore", 162 | ).toISOString(), 163 | ).toBe("2024-02-11T12:30:45.987+08:00"); 164 | expect( 165 | new TZDate( 166 | 2024, 167 | 1, 168 | 11, 169 | 12, 170 | 30, 171 | 45, 172 | 987, 173 | "America/New_York", 174 | ).toISOString(), 175 | ).toBe("2024-02-11T12:30:45.987-05:00"); 176 | expect( 177 | new TZDate( 178 | 1880, 179 | 1, 180 | 11, 181 | 12, 182 | 30, 183 | 45, 184 | 987, 185 | "Asia/Singapore" 186 | ).toISOString() 187 | ).toBe("1880-02-11T12:30:45.987+06:55"); 188 | expect( 189 | new TZDate( 190 | 1880, 191 | 1, 192 | 11, 193 | 12, 194 | 30, 195 | 45, 196 | 987, 197 | "America/New_York" 198 | ).toISOString() 199 | ).toBe("1880-02-11T12:30:45.987-04:56"); 200 | }); 201 | 202 | it("returns Invalid Date for invalid date values", () => { 203 | expect(+new TZDate(NaN, "Asia/Singapore")).toBe(NaN); 204 | }); 205 | 206 | describe("DST", () => { 207 | it("America/Los_Angeles", () => { 208 | expect(utcStr(new TZDate(2020, 2, 8, 1, laName))).toBe( 209 | "2020-03-08T09:00:00.000Z", 210 | ); 211 | expect(utcStr(new TZDate(2020, 2, 8, 2, laName))).toBe( 212 | "2020-03-08T10:00:00.000Z", 213 | ); 214 | expect(utcStr(new TZDate(2020, 2, 8, 3, laName))).toBe( 215 | "2020-03-08T10:00:00.000Z", 216 | ); 217 | expect(utcStr(new TZDate(2020, 2, 8, 4, laName))).toBe( 218 | "2020-03-08T11:00:00.000Z", 219 | ); 220 | }); 221 | 222 | it("America/New_York", () => { 223 | expect(utcStr(new TZDate(2020, 2, 8, 1, nyName))).toBe( 224 | "2020-03-08T06:00:00.000Z", 225 | ); 226 | expect(utcStr(new TZDate(2020, 2, 8, 2, nyName))).toBe( 227 | "2020-03-08T07:00:00.000Z", 228 | ); 229 | expect(utcStr(new TZDate(2020, 2, 8, 3, nyName))).toBe( 230 | "2020-03-08T07:00:00.000Z", 231 | ); 232 | expect(utcStr(new TZDate(2020, 2, 8, 4, nyName))).toBe( 233 | "2020-03-08T08:00:00.000Z", 234 | ); 235 | }); 236 | }); 237 | }); 238 | 239 | describe("TZ", () => { 240 | it("constructs now date in the timezone", () => { 241 | fakeNow(); 242 | const date = TZDate.tz("Asia/Singapore"); 243 | expect(date.toISOString()).toBe("1987-02-11T08:00:00.000+08:00"); 244 | }); 245 | 246 | it("constructs a date in the timezone", () => { 247 | // Timestamp 248 | expect( 249 | TZDate.tz("Asia/Singapore", +new Date(defaultDateStr)).toISOString(), 250 | ).toBe("1987-02-11T08:00:00.000+08:00"); 251 | expect( 252 | TZDate.tz( 253 | "America/New_York", 254 | +new Date(defaultDateStr), 255 | ).toISOString(), 256 | ).toBe("1987-02-10T19:00:00.000-05:00"); 257 | // Date string 258 | expect(TZDate.tz("Asia/Singapore", defaultDateStr).toISOString()).toBe( 259 | "1987-02-11T08:00:00.000+08:00", 260 | ); 261 | // Date 262 | expect( 263 | TZDate.tz("Asia/Singapore", new Date(defaultDateStr)).toISOString(), 264 | ).toBe("1987-02-11T08:00:00.000+08:00"); 265 | expect( 266 | TZDate.tz("America/New_York", defaultDateStr).toISOString(), 267 | ).toBe("1987-02-10T19:00:00.000-05:00"); 268 | // Month 269 | expect(TZDate.tz("Asia/Singapore", 2024, 1).toISOString()).toBe( 270 | "2024-02-01T00:00:00.000+08:00", 271 | ); 272 | expect(TZDate.tz("America/New_York", 2024, 1).toISOString()).toBe( 273 | "2024-02-01T00:00:00.000-05:00", 274 | ); 275 | // Date 276 | expect(TZDate.tz("Asia/Singapore", 2024, 1, 11).toISOString()).toBe( 277 | "2024-02-11T00:00:00.000+08:00", 278 | ); 279 | expect(TZDate.tz("America/New_York", 2024, 1, 11).toISOString()).toBe( 280 | "2024-02-11T00:00:00.000-05:00", 281 | ); 282 | // Hours 283 | expect(TZDate.tz("Asia/Singapore", 2024, 1, 11, 12).toISOString()).toBe( 284 | "2024-02-11T12:00:00.000+08:00", 285 | ); 286 | expect( 287 | TZDate.tz("America/New_York", 2024, 1, 11, 12).toISOString(), 288 | ).toBe("2024-02-11T12:00:00.000-05:00"); 289 | // Minutes 290 | expect( 291 | TZDate.tz("Asia/Singapore", 2024, 1, 11, 12, 30).toISOString(), 292 | ).toBe("2024-02-11T12:30:00.000+08:00"); 293 | expect( 294 | TZDate.tz("America/New_York", 2024, 1, 11, 12, 30).toISOString(), 295 | ).toBe("2024-02-11T12:30:00.000-05:00"); 296 | // Seconds 297 | expect( 298 | TZDate.tz("Asia/Singapore", 2024, 1, 11, 12, 30, 45).toISOString(), 299 | ).toBe("2024-02-11T12:30:45.000+08:00"); 300 | expect( 301 | TZDate.tz("America/New_York", 2024, 1, 11, 12, 30, 45).toISOString(), 302 | ).toBe("2024-02-11T12:30:45.000-05:00"); 303 | // Milliseconds 304 | expect( 305 | TZDate.tz( 306 | "Asia/Singapore", 307 | 2024, 308 | 1, 309 | 11, 310 | 12, 311 | 30, 312 | 45, 313 | 987, 314 | ).toISOString(), 315 | ).toBe("2024-02-11T12:30:45.987+08:00"); 316 | expect( 317 | TZDate.tz( 318 | "America/New_York", 319 | 2024, 320 | 1, 321 | 11, 322 | 12, 323 | 30, 324 | 45, 325 | 987, 326 | ).toISOString(), 327 | ).toBe("2024-02-11T12:30:45.987-05:00"); 328 | }); 329 | 330 | it("constructs proper date around DST changes", () => { 331 | expect( 332 | new Date( 333 | +new TZDate(2023, 2, 10, 3, 30, "America/New_York"), 334 | ).toISOString(), 335 | ).toBe("2023-03-10T08:30:00.000Z"); 336 | expect( 337 | new Date( 338 | +new TZDate(2023, 2, 11, 3, 30, "America/New_York"), 339 | ).toISOString(), 340 | ).toBe("2023-03-11T08:30:00.000Z"); 341 | expect( 342 | new Date( 343 | +new TZDate(2023, 2, 12, 3, 30, "America/New_York"), 344 | ).toISOString(), 345 | ).toBe("2023-03-12T07:30:00.000Z"); 346 | expect( 347 | new Date( 348 | +new TZDate(2023, 2, 13, 3, 30, "America/New_York"), 349 | ).toISOString(), 350 | ).toBe("2023-03-13T07:30:00.000Z"); 351 | }); 352 | }); 353 | 354 | describe("UTC", () => { 355 | it("returns a timestamp in a date in UTC", () => { 356 | expect(new Date(TZDate.UTC(2024, 1)).toISOString()).toBe( 357 | "2024-02-01T00:00:00.000Z", 358 | ); 359 | }); 360 | }); 361 | 362 | describe("parse", () => { 363 | it("parses a date string to a timestamp", () => { 364 | expect( 365 | new Date(TZDate.parse("1987-02-11T00:00:00.000Z")).toISOString(), 366 | ).toBe("1987-02-11T00:00:00.000Z"); 367 | expect( 368 | new Date(TZDate.parse("1987-02-11T00:00:00.000Z")).toISOString(), 369 | ).toBe("1987-02-11T00:00:00.000Z"); 370 | }); 371 | }); 372 | }); 373 | 374 | describe("time", () => { 375 | describe("getTime", () => { 376 | it("returns the time in the timezone", () => { 377 | const nativeDate = new Date(2020, 0, 1); 378 | expect(new TZDate(+nativeDate, "Asia/Singapore").getTime()).toBe( 379 | +nativeDate, 380 | ); 381 | expect(new TZDate(+nativeDate, "America/New_York").getTime()).toBe( 382 | +nativeDate, 383 | ); 384 | }); 385 | 386 | it("returns NaN when the date or time zone are invalid", () => { 387 | expect(new TZDate(NaN, "America/New_York").getTime()).toBe(NaN); 388 | expect(new TZDate(Date.now(), "Etc/Invalid").getTime()).toBe(NaN); 389 | }); 390 | }); 391 | 392 | describe("setTime", () => { 393 | it("sets the time in the timezone", () => { 394 | const nativeDate = new Date(2020, 0, 1); 395 | { 396 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 397 | date.setTime(+nativeDate); 398 | expect(+date).toBe(+nativeDate); 399 | } 400 | { 401 | const date = new TZDate(defaultDateStr, "America/New_York"); 402 | date.setTime(+nativeDate); 403 | expect(+date).toBe(+nativeDate); 404 | } 405 | }); 406 | 407 | it("updated time is reflected in ISO timestamp", () => { 408 | const nativeDate = new Date("2020-01-01T06:00:00.000+08:00"); 409 | 410 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 411 | expect(date.toISOString()).toEqual("1987-02-11T08:00:00.000+08:00"); 412 | 413 | date.setTime(+nativeDate); 414 | expect(+date).toBe(+nativeDate); 415 | expect(date.toISOString()).toEqual("2020-01-01T06:00:00.000+08:00"); 416 | }); 417 | }); 418 | 419 | describe("valueOf", () => { 420 | it("returns the primitive value of the date", () => { 421 | const nativeDate = new Date(2020, 0, 1); 422 | expect(new TZDate(+nativeDate, "Asia/Singapore").valueOf()).toBe( 423 | +nativeDate, 424 | ); 425 | expect(new TZDate(+nativeDate, "America/New_York").valueOf()).toBe( 426 | +nativeDate, 427 | ); 428 | }); 429 | }); 430 | }); 431 | 432 | describe("year", () => { 433 | describe("getFullYear", () => { 434 | it("returns the full year in the timezone", () => { 435 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getFullYear()).toBe( 436 | 2020, 437 | ); 438 | expect( 439 | new TZDate(2020, 0, 1, 0, "America/New_York").getFullYear(), 440 | ).toBe(2020); 441 | }); 442 | 443 | it("returns NaN when the date or time zone are invalid", () => { 444 | expect(new TZDate(NaN, "America/New_York").getFullYear()).toBe(NaN); 445 | expect(new TZDate(Date.now(), "Etc/Invalid").getFullYear()).toBe(NaN); 446 | }); 447 | }); 448 | 449 | describe("getUTCFullYear", () => { 450 | it("returns the full year in the UTC timezone", () => { 451 | expect( 452 | new TZDate(2020, 0, 1, 0, "Asia/Singapore").getUTCFullYear(), 453 | ).toBe(2019); 454 | expect( 455 | new TZDate(2020, 0, 1, 0, "America/New_York").getUTCFullYear(), 456 | ).toBe(2020); 457 | }); 458 | 459 | it("returns NaN when the date or time zone are invalid", () => { 460 | expect(new TZDate(NaN, "America/New_York").getUTCFullYear()).toBe(NaN); 461 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCFullYear()).toBe( 462 | NaN, 463 | ); 464 | }); 465 | }); 466 | 467 | describe("setFullYear", () => { 468 | it("sets the full year in the timezone", () => { 469 | { 470 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 471 | date.setFullYear(2021); 472 | expect(date.toISOString()).toBe("2021-01-01T00:00:00.000+08:00"); 473 | } 474 | { 475 | const date = new TZDate(2020, 0, 1, "America/New_York"); 476 | date.setFullYear(2021); 477 | expect(date.toISOString()).toBe("2021-01-01T00:00:00.000-05:00"); 478 | } 479 | }); 480 | 481 | it("returns the timestamp after setting", () => { 482 | const date = new TZDate(defaultDateStr, "America/New_York"); 483 | expect(date.setFullYear(2020)).toBe(+date); 484 | }); 485 | 486 | it("allows to set the month and date", () => { 487 | { 488 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 489 | date.setFullYear(2021, 1, 11); 490 | expect(date.toISOString()).toBe("2021-02-11T00:00:00.000+08:00"); 491 | } 492 | { 493 | const date = new TZDate(2020, 0, 1, "America/New_York"); 494 | date.setFullYear(2021, 1, 11); 495 | expect(date.toISOString()).toBe("2021-02-11T00:00:00.000-05:00"); 496 | } 497 | }); 498 | 499 | it("allows to overflow into the future", () => { 500 | { 501 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 502 | date.setFullYear(2021, 15, 45); 503 | expect(date.toISOString()).toBe("2022-05-15T00:00:00.000+08:00"); 504 | } 505 | { 506 | const date = new TZDate(2020, 0, 1, "America/New_York"); 507 | date.setFullYear(2021, 15, 45); 508 | expect(date.toISOString()).toBe("2022-05-15T00:00:00.000-04:00"); 509 | } 510 | }); 511 | 512 | it("allows to overflow into the past", () => { 513 | { 514 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 515 | date.setFullYear(2021, -15, -15); 516 | expect(date.toISOString()).toBe("2019-09-15T00:00:00.000+08:00"); 517 | } 518 | { 519 | const date = new TZDate(2020, 0, 1, "America/New_York"); 520 | date.setFullYear(2021, -15, -150); 521 | expect(date.toISOString()).toBe("2019-05-03T00:00:00.000-04:00"); 522 | } 523 | }); 524 | }); 525 | 526 | describe("setUTCFullYear", () => { 527 | it("sets the full year in the UTC timezone", () => { 528 | { 529 | // 2019-12-31 16:00:00 (UTC) -> ... 530 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 531 | // ... -> 2020-12-31 16:00:00 (UTC) -> 532 | date.setUTCFullYear(2020); 533 | expect(utcStr(date)).toBe("2020-12-31T16:00:00.000Z"); 534 | } 535 | { 536 | // 2020-01-01 05:00:00 (UTC) -> ... 537 | const date = new TZDate(2020, 0, 1, "America/New_York"); 538 | // ... -> 2020-01-01 05:00:00 (UTC) -> 539 | date.setUTCFullYear(2020); 540 | expect(utcStr(date)).toBe("2020-01-01T05:00:00.000Z"); 541 | } 542 | }); 543 | 544 | it("returns the timestamp after setting", () => { 545 | const date = new TZDate(defaultDateStr, "America/New_York"); 546 | expect(date.setUTCFullYear(2020)).toBe(+date); 547 | }); 548 | 549 | it("allows to set the month and date", () => { 550 | { 551 | // 2019-12-31 16:00:00 (UTC) -> ... 552 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 553 | // ... -> 2020-01-01 16:00:00 (UTC) -> ... 554 | date.setUTCFullYear(2020, 0, 1); 555 | expect(date.toISOString()).toBe("2020-01-02T00:00:00.000+08:00"); 556 | } 557 | { 558 | // 2020-01-01 05:00:00 (UTC) -> ... 559 | const date = new TZDate(2020, 0, 1, "America/New_York"); 560 | // ... -> 2020-01-01 05:00:00 (UTC) -> ... 561 | date.setUTCFullYear(2020, 0, 1); 562 | expect(date.toISOString()).toBe("2020-01-01T00:00:00.000-05:00"); 563 | } 564 | }); 565 | 566 | it("allows to overflow into the future", () => { 567 | { 568 | // 2019-12-31 16:00:00 (UTC) -> ... 569 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 570 | // ... -> 2021-04-14 16:00:00 (UTC) -> ... 571 | date.setUTCFullYear(2020, 14, 45); 572 | expect(utcStr(date)).toBe("2021-04-14T16:00:00.000Z"); 573 | } 574 | { 575 | // 2020-01-01 05:00:00 (UTC) -> ... 576 | const date = new TZDate(2020, 0, 1, "America/New_York"); 577 | // ... -> 2021-04-14 05:00:00 (UTC) -> ... 578 | date.setUTCFullYear(2020, 14, 45); 579 | expect(utcStr(date)).toBe("2021-04-14T05:00:00.000Z"); 580 | } 581 | }); 582 | 583 | it("allows to overflow into the past", () => { 584 | { 585 | // 2019-12-31 16:00:00 (UTC) -> ... 586 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 587 | // ... -> 2019-03-01 16:00:00 (UTC) -> ... 588 | date.setUTCFullYear(2020, -8, -60); 589 | expect(date.toISOString()).toBe("2019-03-02T00:00:00.000+08:00"); 590 | } 591 | { 592 | // 2020-01-01 05:00:00 (UTC) -> ... 593 | const date = new TZDate(2020, 0, 1, "America/New_York"); 594 | // ... -> 2019-03-01 05:00:00 (UTC) -> ... 595 | date.setUTCFullYear(2020, -8, -60); 596 | expect(date.toISOString()).toBe("2019-03-01T00:00:00.000-05:00"); 597 | } 598 | }); 599 | }); 600 | }); 601 | 602 | describe("month", () => { 603 | describe("getMonth", () => { 604 | it("returns the month in the timezone", () => { 605 | expect(new TZDate(2020, 0, 1, 0, "America/New_York").getMonth()).toBe( 606 | 0, 607 | ); 608 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getMonth()).toBe(0); 609 | }); 610 | 611 | it("returns NaN when the date or time zone are invalid", () => { 612 | expect(new TZDate(NaN, "America/New_York").getMonth()).toBe(NaN); 613 | expect(new TZDate(Date.now(), "Etc/Invalid").getMonth()).toBe(NaN); 614 | }); 615 | }); 616 | 617 | describe("getUTCMonth", () => { 618 | it("returns the month in the UTC timezone", () => { 619 | expect( 620 | new TZDate(2020, 0, 1, 0, "America/New_York").getUTCMonth(), 621 | ).toBe(0); 622 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getUTCMonth()).toBe( 623 | 11, 624 | ); 625 | }); 626 | 627 | it("returns NaN when the date or time zone are invalid", () => { 628 | expect(new TZDate(NaN, "America/New_York").getUTCMonth()).toBe(NaN); 629 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCMonth()).toBe(NaN); 630 | }); 631 | }); 632 | 633 | describe("setMonth", () => { 634 | it("sets the month in the timezone", () => { 635 | { 636 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 637 | date.setMonth(1); 638 | expect(date.toISOString()).toBe("2020-02-01T00:00:00.000+08:00"); 639 | } 640 | { 641 | const date = new TZDate(2020, 0, 1, "America/New_York"); 642 | date.setMonth(1); 643 | expect(date.toISOString()).toBe("2020-02-01T00:00:00.000-05:00"); 644 | } 645 | }); 646 | 647 | it("returns the timestamp after setting", () => { 648 | const date = new TZDate(defaultDateStr, "America/New_York"); 649 | expect(date.setMonth(1)).toBe(+date); 650 | }); 651 | 652 | it("allows to set the date", () => { 653 | { 654 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 655 | date.setMonth(1, 11); 656 | expect(date.toISOString()).toBe("2020-02-11T00:00:00.000+08:00"); 657 | } 658 | { 659 | const date = new TZDate(2020, 0, 1, "America/New_York"); 660 | date.setMonth(1, 11); 661 | expect(date.toISOString()).toBe("2020-02-11T00:00:00.000-05:00"); 662 | } 663 | }); 664 | 665 | it("allows to overflow into the future", () => { 666 | { 667 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 668 | date.setMonth(15, 45); 669 | expect(date.toISOString()).toBe("2021-05-15T00:00:00.000+08:00"); 670 | } 671 | { 672 | const date = new TZDate(2020, 0, 1, "America/New_York"); 673 | date.setMonth(15, 45); 674 | expect(date.toISOString()).toBe("2021-05-15T00:00:00.000-04:00"); 675 | } 676 | }); 677 | 678 | it("allows to overflow into the past", () => { 679 | { 680 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 681 | date.setMonth(-15, -150); 682 | expect(date.toISOString()).toBe("2018-05-03T00:00:00.000+08:00"); 683 | } 684 | { 685 | const date = new TZDate(2020, 0, 1, "America/New_York"); 686 | date.setMonth(-15, -150); 687 | expect(date.toISOString()).toBe("2018-05-03T00:00:00.000-04:00"); 688 | } 689 | }); 690 | }); 691 | 692 | describe("setUTCMonth", () => { 693 | it("sets the month in the UTC timezone", () => { 694 | { 695 | // 2019-12-31 16:00:00 (UTC) -> ... 696 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 697 | // ... -> 2019-03-03 16:00:00 (UTC) -> ... 698 | date.setUTCMonth(1); 699 | expect(utcStr(date)).toBe("2019-03-03T16:00:00.000Z"); 700 | } 701 | { 702 | // 2020-01-01 05:00:00 (UTC) -> ... 703 | const date = new TZDate(2020, 0, 1, "America/New_York"); 704 | // ... -> 2020-02-01 05:00:00 (UTC) -> ... 705 | date.setUTCMonth(1); 706 | expect(utcStr(date)).toBe("2020-02-01T05:00:00.000Z"); 707 | } 708 | }); 709 | 710 | it("returns the timestamp after setting", () => { 711 | const date = new TZDate(defaultDateStr, "America/New_York"); 712 | expect(date.setUTCMonth(1)).toBe(+date); 713 | }); 714 | 715 | it("allows to set the date", () => { 716 | { 717 | // 2019-12-31 16:00:00 (UTC) -> ... 718 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 719 | // ... -> 2019-02-11 16:00:00 (UTC) -> ... 720 | date.setUTCMonth(1, 11); 721 | expect(date.toISOString()).toBe("2019-02-12T00:00:00.000+08:00"); 722 | } 723 | { 724 | // 2020-01-01 05:00:00 (UTC) -> ... 725 | const date = new TZDate(2020, 0, 1, "America/New_York"); 726 | // ... -> 2020-02-11 05:00:00 (UTC) -> ... 727 | date.setUTCMonth(1, 11); 728 | expect(date.toISOString()).toBe("2020-02-11T00:00:00.000-05:00"); 729 | } 730 | }); 731 | 732 | it("allows to overflow into the future", () => { 733 | { 734 | // 2019-12-31 16:00:00 (UTC) -> ... 735 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 736 | // ... -> 2020-08-14 16:00:00 (UTC) -> ... 737 | date.setUTCMonth(18, 45); 738 | expect(date.toISOString()).toBe("2020-08-15T00:00:00.000+08:00"); 739 | } 740 | { 741 | // 2020-01-01 05:00:00 (UTC) -> ... 742 | const date = new TZDate(2020, 0, 1, "America/New_York"); 743 | // ... -> 2021-08-14 05:00:00 (UTC) -> ... 744 | date.setUTCMonth(18, 45); 745 | expect(utcStr(date)).toBe("2021-08-14T05:00:00.000Z"); 746 | } 747 | }); 748 | 749 | it("allows to overflow into the past", () => { 750 | { 751 | // 2019-12-31 16:00:00 (UTC) -> ... 752 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 753 | // ... -> 2017-05-01 16:00:00 (UTC) -> ... 754 | date.setUTCMonth(-18, -60); 755 | expect(date.toISOString()).toBe("2017-05-02T00:00:00.000+08:00"); 756 | } 757 | { 758 | // 2020-01-01 05:00:00 (UTC) -> ... 759 | const date = new TZDate(2020, 0, 1, "America/New_York"); 760 | // ... -> 2018-05-01 05:00:00 (UTC) -> ... 761 | date.setUTCMonth(-18, -60); 762 | expect(utcStr(date)).toBe("2018-05-01T05:00:00.000Z"); 763 | } 764 | }); 765 | }); 766 | }); 767 | 768 | describe("date", () => { 769 | describe("getDate", () => { 770 | it("returns the date in the timezone", () => { 771 | expect(new TZDate(defaultDateStr, "America/New_York").getDate()).toBe( 772 | 10, 773 | ); 774 | expect(new TZDate(defaultDateStr, "Asia/Singapore").getDate()).toBe(11); 775 | }); 776 | 777 | it("returns NaN when the date or time zone are invalid", () => { 778 | expect(new TZDate(NaN, "America/New_York").getDate()).toBe(NaN); 779 | expect(new TZDate(Date.now(), "Etc/Invalid").getDate()).toBe(NaN); 780 | }); 781 | }); 782 | 783 | describe("getUTCDate", () => { 784 | it("returns the date in the UTC timezone", () => { 785 | expect( 786 | new TZDate(defaultDateStr, "America/New_York").getUTCDate(), 787 | ).toBe(11); 788 | expect(new TZDate(defaultDateStr, "Asia/Singapore").getUTCDate()).toBe( 789 | 11, 790 | ); 791 | }); 792 | 793 | it("returns NaN when the date or time zone are invalid", () => { 794 | expect(new TZDate(NaN, "America/New_York").getUTCDate()).toBe(NaN); 795 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCDate()).toBe(NaN); 796 | }); 797 | }); 798 | 799 | describe("setDate", () => { 800 | it("sets the date in the timezone", () => { 801 | { 802 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 803 | date.setDate(2); 804 | expect(date.toISOString()).toBe("2020-01-02T00:00:00.000+08:00"); 805 | } 806 | { 807 | const date = new TZDate(2020, 0, 1, "America/New_York"); 808 | date.setDate(2); 809 | expect(date.toISOString()).toBe("2020-01-02T00:00:00.000-05:00"); 810 | } 811 | }); 812 | 813 | it("returns the timestamp after setting", () => { 814 | const date = new TZDate(defaultDateStr, "America/New_York"); 815 | expect(date.setDate(2)).toBe(+date); 816 | }); 817 | 818 | it("allows to overflow the month into the future", () => { 819 | { 820 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 821 | date.setDate(92); 822 | expect(date.toISOString()).toBe("2020-04-01T00:00:00.000+08:00"); 823 | } 824 | { 825 | const date = new TZDate(2020, 0, 1, "America/New_York"); 826 | date.setDate(92); 827 | expect(date.toISOString()).toBe("2020-04-01T00:00:00.000-04:00"); 828 | } 829 | }); 830 | 831 | it("allows to overflow the month into the past", () => { 832 | { 833 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 834 | date.setDate(-15); 835 | expect(date.toISOString()).toBe("2019-12-16T00:00:00.000+08:00"); 836 | } 837 | { 838 | const date = new TZDate(2020, 0, 1, "America/New_York"); 839 | date.setDate(-15); 840 | expect(date.toISOString()).toBe("2019-12-16T00:00:00.000-05:00"); 841 | } 842 | }); 843 | }); 844 | 845 | describe("setUTCDate", () => { 846 | it("sets the date in the UTC timezone", () => { 847 | { 848 | // 2019-12-31 16:00:00 (UTC) -> ... 849 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 850 | // ... -> 2019-12-11 16:00:00 (UTC) -> 851 | date.setUTCDate(11); 852 | expect(utcStr(date)).toBe("2019-12-11T16:00:00.000Z"); 853 | } 854 | { 855 | // 2020-01-01 05:00:00 (UTC) -> ... 856 | const date = new TZDate(2020, 0, 1, "America/New_York"); 857 | // ... -> 2020-01-11 05:00:00 (UTC) -> 858 | date.setUTCDate(11); 859 | expect(utcStr(date)).toBe("2020-01-11T05:00:00.000Z"); 860 | } 861 | }); 862 | 863 | it("returns the timestamp after setting", () => { 864 | const date = new TZDate(defaultDateStr, "America/New_York"); 865 | expect(date.setUTCDate(2)).toBe(+date); 866 | }); 867 | 868 | it("allows to overflow into the future", () => { 869 | { 870 | // 2019-12-31 16:00:00 (UTC) -> ... 871 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 872 | // ... -> 2022-07-02 16:00:00 (UTC) -> 873 | date.setUTCDate(945); 874 | expect(date.toISOString()).toBe("2022-07-03T00:00:00.000+08:00"); 875 | } 876 | { 877 | // 2020-01-01 05:00:00 (UTC) -> ... 878 | const date = new TZDate(2020, 0, 1, "America/New_York"); 879 | // ... -> 2022-08-02 05:00:00 (UTC) -> 880 | date.setUTCDate(945); 881 | expect(utcStr(date)).toBe("2022-08-02T05:00:00.000Z"); 882 | } 883 | }); 884 | 885 | it("allows to overflow into the past", () => { 886 | { 887 | // 2019-12-31 16:00:00 (UTC) -> ... 888 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 889 | // ... -> 2019-10-01 16:00:00 (UTC) -> 890 | date.setUTCDate(-60); 891 | expect(utcStr(date)).toBe("2019-10-01T16:00:00.000Z"); 892 | } 893 | { 894 | // 2020-01-01 05:00:00 (UTC) -> ... 895 | const date = new TZDate(2020, 0, 1, "America/New_York"); 896 | // ... -> 2019-11-01 05:00:00 (UTC) -> 897 | date.setUTCDate(-60); 898 | expect(utcStr(date)).toBe("2019-11-01T05:00:00.000Z"); 899 | } 900 | }); 901 | }); 902 | }); 903 | 904 | describe("day", () => { 905 | describe("getDay", () => { 906 | it("returns the day in the timezone", () => { 907 | const dateStr = "2020-01-01T00:00:00.000Z"; 908 | expect(new TZDate(dateStr, "America/New_York").getDay()).toBe(2); 909 | expect(new TZDate(dateStr, "Asia/Singapore").getDay()).toBe(3); 910 | }); 911 | 912 | it("returns NaN when the date or time zone are invalid", () => { 913 | expect(new TZDate(NaN, "America/New_York").getDay()).toBe(NaN); 914 | expect(new TZDate(Date.now(), "Etc/Invalid").getDay()).toBe(NaN); 915 | }); 916 | }); 917 | 918 | describe("getUTCDay", () => { 919 | it("returns the day in the UTC timezone", () => { 920 | expect(new TZDate(2020, 0, 1, 0, "America/New_York").getUTCDay()).toBe( 921 | 3, 922 | ); 923 | expect(new TZDate(2020, 0, 1, 0, "Asia/Singapore").getUTCDay()).toBe(2); 924 | const dateStr = "2020-01-01T00:00:00.000Z"; 925 | expect(new TZDate(dateStr, "America/New_York").getUTCDay()).toBe(3); 926 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCDay()).toBe(3); 927 | }); 928 | 929 | it("returns NaN when the date or time zone are invalid", () => { 930 | expect(new TZDate(NaN, "America/New_York").getUTCDay()).toBe(NaN); 931 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCDay()).toBe(NaN); 932 | }); 933 | }); 934 | }); 935 | 936 | describe("hours", () => { 937 | describe("getHours", () => { 938 | it("returns the hours in the timezone", () => { 939 | const dateStr = "1987-02-10T09:00:00.000Z"; 940 | expect(new TZDate(dateStr, "Asia/Singapore").getHours()).toBe(17); 941 | expect(new TZDate(dateStr, "America/New_York").getHours()).toBe(4); 942 | }); 943 | 944 | it("returns NaN when the date or time zone are invalid", () => { 945 | expect(new TZDate(NaN, "America/New_York").getHours()).toBe(NaN); 946 | expect(new TZDate(Date.now(), "Etc/Invalid").getHours()).toBe(NaN); 947 | }); 948 | }); 949 | 950 | describe("getUTCHours", () => { 951 | it("returns the hours in the UTC timezone", () => { 952 | const dateStr = "1987-02-10T09:00:00.000Z"; 953 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCHours()).toBe(9); 954 | expect(new TZDate(dateStr, "America/New_York").getUTCHours()).toBe(9); 955 | }); 956 | 957 | it("returns NaN when the date or time zone are invalid", () => { 958 | expect(new TZDate(NaN, "America/New_York").getUTCHours()).toBe(NaN); 959 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCHours()).toBe(NaN); 960 | }); 961 | }); 962 | 963 | describe("setHours", () => { 964 | it("sets the hours in the timezone", () => { 965 | { 966 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 967 | date.setHours(14); 968 | expect(date.toISOString()).toBe("2020-01-01T14:00:00.000+08:00"); 969 | } 970 | { 971 | const date = new TZDate(2020, 0, 1, "America/New_York"); 972 | date.setDate(14); 973 | expect(date.toISOString()).toBe("2020-01-14T00:00:00.000-05:00"); 974 | } 975 | }); 976 | 977 | it("returns the timestamp after setting", () => { 978 | const date = new TZDate(defaultDateStr, "America/New_York"); 979 | expect(date.setHours(14)).toBe(+date); 980 | }); 981 | 982 | it("allows to set hours, minutes, seconds and milliseconds", () => { 983 | { 984 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 985 | date.setHours(14, 30, 45, 987); 986 | expect(date.toISOString()).toBe("2020-01-01T14:30:45.987+08:00"); 987 | } 988 | { 989 | const date = new TZDate(2020, 0, 1, "America/New_York"); 990 | date.setHours(14, 30, 45, 987); 991 | expect(date.toISOString()).toBe("2020-01-01T14:30:45.987-05:00"); 992 | } 993 | }); 994 | 995 | it("allows to overflow the date into the future", () => { 996 | { 997 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 998 | date.setHours(30, 120, 120, 30000); 999 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1000 | // = 1 day + 08:02:30 1001 | expect(date.toISOString()).toBe("2020-01-02T08:02:30.000+08:00"); 1002 | } 1003 | { 1004 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1005 | date.setHours(30, 120, 120, 30000); 1006 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1007 | // = 1 day + 08:02:30 1008 | expect(date.toISOString()).toBe("2020-01-02T08:02:30.000-05:00"); 1009 | } 1010 | }); 1011 | 1012 | it("allows to overflow the date into the past", () => { 1013 | { 1014 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1015 | date.setHours(-30, -120, -120, -30000); 1016 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1017 | // = 1 day + 08:02:30 1018 | expect(date.toISOString()).toBe("2019-12-30T15:57:30.000+08:00"); 1019 | } 1020 | { 1021 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1022 | date.setHours(-30, -120, -120, -30000); 1023 | // 30 hours + 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1024 | // = 1 day + 08:02:30 1025 | expect(date.toISOString()).toBe("2019-12-30T15:57:30.000-05:00"); 1026 | } 1027 | }); 1028 | 1029 | it("constructs proper date around DST changes", () => { 1030 | const date10 = new TZDate(2023, 2, 10, "America/New_York"); 1031 | date10.setHours(3, 30); 1032 | expect(new Date(+date10).toISOString()).toBe( 1033 | "2023-03-10T08:30:00.000Z", 1034 | ); 1035 | const date11 = new TZDate(2023, 2, 11, "America/New_York"); 1036 | date11.setHours(3, 30); 1037 | expect(new Date(+date11).toISOString()).toBe( 1038 | "2023-03-11T08:30:00.000Z", 1039 | ); 1040 | const date12 = new TZDate(2023, 2, 12, "America/New_York"); 1041 | date12.setHours(3, 30); 1042 | expect(new Date(+date12).toISOString()).toBe( 1043 | "2023-03-12T07:30:00.000Z", 1044 | ); 1045 | const date13 = new TZDate(2023, 2, 13, "America/New_York"); 1046 | date13.setHours(3, 30); 1047 | expect(new Date(+date13).toISOString()).toBe( 1048 | "2023-03-13T07:30:00.000Z", 1049 | ); 1050 | }); 1051 | }); 1052 | 1053 | describe("setUTCHours", () => { 1054 | it("sets the hours in the UTC timezone", () => { 1055 | { 1056 | // 2019-12-31 16:00:00 (UTC) -> ... 1057 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1058 | // ... -> 2019-12-31 12:00:00 (UTC) -> 1059 | date.setUTCHours(12); 1060 | expect(utcStr(date)).toBe("2019-12-31T12:00:00.000Z"); 1061 | } 1062 | { 1063 | // 2020-01-01 05:00:00 (UTC) -> ... 1064 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1065 | // ... -> 2020-01-01 12:00:00 (UTC) -> 1066 | date.setUTCHours(12); 1067 | expect(utcStr(date)).toBe("2020-01-01T12:00:00.000Z"); 1068 | } 1069 | }); 1070 | 1071 | it("returns the timestamp after setting", () => { 1072 | const date = new TZDate(defaultDateStr, "America/New_York"); 1073 | expect(date.setUTCHours(14)).toBe(+date); 1074 | }); 1075 | 1076 | it("allows to set hours, minutes, seconds and milliseconds", () => { 1077 | { 1078 | // 2019-12-31 16:00:00 (UTC) -> ... 1079 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1080 | // ... -> 2019-12-31T12:34:56.789Z (UTC) -> 1081 | date.setUTCHours(12, 34, 56, 789); 1082 | expect(date.toISOString()).toBe("2019-12-31T20:34:56.789+08:00"); 1083 | } 1084 | { 1085 | // 2020-01-01 05:00:00 (UTC) -> ... 1086 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1087 | // ... -> 2020-01-01T12:34:56.789Z (UTC) -> 1088 | date.setUTCHours(12, 34, 56, 789); 1089 | expect(date.toISOString()).toBe("2020-01-01T07:34:56.789-05:00"); 1090 | } 1091 | }); 1092 | 1093 | it("allows to overflow the date into the future", () => { 1094 | { 1095 | // 2019-12-31 16:00:00 (UTC) -> ... 1096 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1097 | // ... -> 2020-01-01 08:02:30 (UTC) -> 1098 | date.setUTCHours(30, 120, 120, 30000); 1099 | expect(date.toISOString()).toBe("2020-01-01T16:02:30.000+08:00"); 1100 | } 1101 | { 1102 | // 2020-01-01 05:00:00 (UTC) -> ... 1103 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1104 | // ... -> 2020-01-02 08:02:30 (UTC) -> 1105 | date.setUTCHours(30, 120, 120, 30000); 1106 | expect(date.toISOString()).toBe("2020-01-02T03:02:30.000-05:00"); 1107 | } 1108 | }); 1109 | 1110 | it("allows to overflow the date into the past", () => { 1111 | { 1112 | // 2019-12-31 16:00:00 (UTC) -> ... 1113 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1114 | // ... -> 2019-12-29 15:57:30 (UTC) -> 1115 | date.setUTCHours(-30, -120, -120, -30000); 1116 | expect(date.toISOString()).toBe("2019-12-29T23:57:30.000+08:00"); 1117 | } 1118 | { 1119 | // 2020-01-01 05:00:00 (UTC) -> ... 1120 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1121 | // ... -> 2019-12-30 15:57:30 (UTC) -> 1122 | date.setUTCHours(-30, -120, -120, -30000); 1123 | expect(date.toISOString()).toBe("2019-12-30T10:57:30.000-05:00"); 1124 | } 1125 | }); 1126 | }); 1127 | }); 1128 | 1129 | describe("minutes", () => { 1130 | describe("getMinutes", () => { 1131 | it("returns the minutes in the timezone", () => { 1132 | const dateStr = "1987-02-10T00:15:00.000Z"; 1133 | expect(new TZDate(dateStr, "America/New_York").getMinutes()).toBe(15); 1134 | expect(new TZDate(dateStr, "Asia/Kolkata").getMinutes()).toBe(45); 1135 | }); 1136 | 1137 | it("returns NaN when the date or time zone are invalid", () => { 1138 | expect(new TZDate(NaN, "America/New_York").getMinutes()).toBe(NaN); 1139 | expect(new TZDate(Date.now(), "Etc/Invalid").getMinutes()).toBe(NaN); 1140 | }); 1141 | }); 1142 | 1143 | describe("getUTCMinutes", () => { 1144 | it("returns the minutes in the UTC timezone", () => { 1145 | const dateStr = "1987-02-10T00:15:00.000Z"; 1146 | expect(new TZDate(dateStr, "America/New_York").getUTCMinutes()).toBe( 1147 | 15, 1148 | ); 1149 | expect(new TZDate(dateStr, "Asia/Kolkata").getUTCMinutes()).toBe(15); 1150 | }); 1151 | 1152 | it("returns NaN when the date or time zone are invalid", () => { 1153 | expect(new TZDate(NaN, "America/New_York").getUTCMinutes()).toBe(NaN); 1154 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCMinutes()).toBe(NaN); 1155 | }); 1156 | }); 1157 | 1158 | describe("setMinutes", () => { 1159 | it("sets the minutes in the timezone", () => { 1160 | { 1161 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1162 | date.setMinutes(30); 1163 | expect(date.toISOString()).toBe("2020-01-01T00:30:00.000+08:00"); 1164 | } 1165 | { 1166 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1167 | date.setMinutes(30); 1168 | expect(date.toISOString()).toBe("2020-01-01T00:30:00.000-05:00"); 1169 | } 1170 | }); 1171 | 1172 | it("returns the timestamp after setting", () => { 1173 | const date = new TZDate(defaultDateStr, "America/New_York"); 1174 | expect(date.setMinutes(30)).toBe(+date); 1175 | }); 1176 | 1177 | it("allows to set minutes, seconds and milliseconds", () => { 1178 | { 1179 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1180 | date.setMinutes(30, 45, 987); 1181 | expect(date.toISOString()).toBe("2020-01-01T00:30:45.987+08:00"); 1182 | } 1183 | { 1184 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1185 | date.setMinutes(30, 45, 987); 1186 | expect(date.toISOString()).toBe("2020-01-01T00:30:45.987-05:00"); 1187 | } 1188 | }); 1189 | 1190 | it("allows to overflow the hours into the future", () => { 1191 | { 1192 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1193 | date.setMinutes(120, 120, 30000); 1194 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1195 | // = 02:02:30 1196 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000+08:00"); 1197 | } 1198 | { 1199 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1200 | date.setMinutes(120, 120, 30000); 1201 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1202 | // = 02:02:30 1203 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000-05:00"); 1204 | } 1205 | }); 1206 | 1207 | it("allows to overflow the hours into the past", () => { 1208 | { 1209 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1210 | date.setMinutes(-120, -120, -30000); 1211 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1212 | // = 02:02:30 1213 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000+08:00"); 1214 | } 1215 | { 1216 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1217 | date.setMinutes(-120, -120, -30000); 1218 | // 120 minutes (2 hours) + 120 seconds (2 minutes) + 30000 ms (30 seconds) 1219 | // = 02:02:30 1220 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000-05:00"); 1221 | } 1222 | }); 1223 | }); 1224 | 1225 | describe("setUTCMinutes", () => { 1226 | it("sets the minutes in the UTC timezone", () => { 1227 | { 1228 | // 2019-12-31 16:00:00 (UTC) -> ... 1229 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1230 | // ... -> 2019-12-31 16:34:00 (UTC) -> 1231 | date.setUTCMinutes(34); 1232 | expect(utcStr(date)).toBe("2019-12-31T16:34:00.000Z"); 1233 | } 1234 | { 1235 | // 2020-01-01 05:00:00 (UTC) -> ... 1236 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1237 | // ... -> 2020-01-01 05:34:00 (UTC) -> 1238 | date.setUTCMinutes(34); 1239 | expect(utcStr(date)).toBe("2020-01-01T05:34:00.000Z"); 1240 | } 1241 | }); 1242 | 1243 | it("returns the timestamp after setting", () => { 1244 | const date = new TZDate(defaultDateStr, "America/New_York"); 1245 | expect(date.setUTCMinutes(30)).toBe(+date); 1246 | }); 1247 | 1248 | it("allows to set minutes, seconds and milliseconds", () => { 1249 | { 1250 | // 2019-12-31 16:00:00 (UTC) -> ... 1251 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1252 | // ... -> 2019-12-31 16:34:56 (UTC) -> 1253 | date.setUTCMinutes(34, 56, 789); 1254 | expect(date.toISOString()).toBe("2020-01-01T00:34:56.789+08:00"); 1255 | } 1256 | { 1257 | // 2020-01-01 05:00:00 (UTC) -> ... 1258 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1259 | // ... -> 2020-01-01 05:34:56 (UTC) -> 1260 | date.setUTCMinutes(34, 56, 789); 1261 | expect(date.toISOString()).toBe("2020-01-01T00:34:56.789-05:00"); 1262 | } 1263 | }); 1264 | 1265 | it("allows to overflow the hours into the future", () => { 1266 | { 1267 | // 2019-12-31 16:00:00 (UTC) -> ... 1268 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1269 | // ... -> 2019-12-31 18:02:30 (UTC) -> 1270 | date.setUTCMinutes(120, 120, 30000); 1271 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000+08:00"); 1272 | } 1273 | { 1274 | // 2020-01-01 05:00:00 (UTC) -> ... 1275 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1276 | // ... -> 2020-01-01 07:02:30 (UTC) -> 1277 | date.setUTCMinutes(120, 120, 30000); 1278 | expect(date.toISOString()).toBe("2020-01-01T02:02:30.000-05:00"); 1279 | } 1280 | }); 1281 | 1282 | it("allows to overflow the hours into the past", () => { 1283 | { 1284 | // 2019-12-31 16:00:00 (UTC) -> ... 1285 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1286 | // ... -> 2019-12-31 13:57:30 (UTC) -> 1287 | date.setUTCMinutes(-120, -120, -30000); 1288 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000+08:00"); 1289 | } 1290 | { 1291 | // 2020-01-01 05:00:00 (UTC) -> ... 1292 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1293 | // ... -> 2020-01-01 02:57:30 (UTC) -> 1294 | date.setUTCMinutes(-120, -120, -30000); 1295 | expect(date.toISOString()).toBe("2019-12-31T21:57:30.000-05:00"); 1296 | } 1297 | }); 1298 | }); 1299 | }); 1300 | 1301 | describe("seconds", () => { 1302 | describe("getSeconds", () => { 1303 | it("returns the seconds in the timezone", () => { 1304 | const dateStr = "1987-02-10T00:00:30.000Z"; 1305 | expect(new TZDate(dateStr, "America/New_York").getSeconds()).toBe(30); 1306 | expect(new TZDate(dateStr, "Asia/Singapore").getSeconds()).toBe(30); 1307 | }); 1308 | 1309 | it("returns NaN when the date or time zone are invalid", () => { 1310 | expect(new TZDate(NaN, "America/New_York").getSeconds()).toBe(NaN); 1311 | expect(new TZDate(Date.now(), "Etc/Invalid").getSeconds()).toBe(NaN); 1312 | }); 1313 | }); 1314 | 1315 | describe("getUTCSeconds", () => { 1316 | it("returns the seconds in the UTC timezone", () => { 1317 | const dateStr = "1987-02-10T00:00:30.000Z"; 1318 | const beforeGMTDateStr = "1880-02-10T00:00:30.000Z"; 1319 | expect(new TZDate(dateStr, "America/New_York").getUTCSeconds()).toBe( 1320 | 30, 1321 | ); 1322 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCSeconds()).toBe(30); 1323 | expect(new TZDate(beforeGMTDateStr, "America/New_York").getUTCSeconds()).toBe( 1324 | 30 1325 | ); 1326 | expect(new TZDate(beforeGMTDateStr, "Asia/Singapore").getUTCSeconds()).toBe(30); 1327 | }); 1328 | 1329 | it("returns NaN when the date or time zone are invalid", () => { 1330 | expect(new TZDate(NaN, "America/New_York").getUTCSeconds()).toBe(NaN); 1331 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCSeconds()).toBe(NaN); 1332 | }); 1333 | }); 1334 | 1335 | describe("setSeconds", () => { 1336 | it("sets the seconds in the timezone", () => { 1337 | { 1338 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1339 | date.setSeconds(56); 1340 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.000+08:00"); 1341 | } 1342 | { 1343 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1344 | date.setSeconds(56); 1345 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.000-05:00"); 1346 | } 1347 | { 1348 | const date = new TZDate(1880, 0, 1, "Asia/Singapore"); 1349 | date.setSeconds(56); 1350 | expect(date.toISOString()).toBe("1880-01-01T00:00:31.000+06:55"); // 56s - 25s = 31s 1351 | } 1352 | { 1353 | const date = new TZDate(1880, 0, 1, "America/New_York"); 1354 | date.setSeconds(56); 1355 | expect(date.toISOString()).toBe("1880-01-01T00:00:58.000-04:56"); // 56s + 2s = 58s 1356 | } 1357 | }); 1358 | 1359 | it("returns the timestamp after setting", () => { 1360 | const date = new TZDate(defaultDateStr, "America/New_York"); 1361 | expect(date.setSeconds(56)).toBe(+date); 1362 | }); 1363 | 1364 | it("allows to set seconds and milliseconds", () => { 1365 | { 1366 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1367 | date.setSeconds(56, 987); 1368 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.987+08:00"); 1369 | } 1370 | { 1371 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1372 | date.setSeconds(56, 987); 1373 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.987-05:00"); 1374 | } 1375 | }); 1376 | 1377 | it("allows to overflow the minutes into the future", () => { 1378 | { 1379 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1380 | date.setSeconds(120, 30000); 1381 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1382 | // = 02:30 1383 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000+08:00"); 1384 | } 1385 | { 1386 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1387 | date.setSeconds(120, 30000); 1388 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1389 | // = 02:30 1390 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000-05:00"); 1391 | } 1392 | }); 1393 | 1394 | it("allows to overflow the minutes into the past", () => { 1395 | { 1396 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1397 | date.setSeconds(-120, -30000); 1398 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1399 | // = 02:30 1400 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000+08:00"); 1401 | } 1402 | { 1403 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1404 | date.setSeconds(-120, -30000); 1405 | // 120 seconds (2 minutes) + 30000 ms (30 seconds) 1406 | // = 02:30 1407 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000-05:00"); 1408 | } 1409 | }); 1410 | }); 1411 | 1412 | describe("setUTCSeconds", () => { 1413 | it("sets the seconds in the UTC timezone", () => { 1414 | { 1415 | // 2019-12-31 16:00:00 (UTC) -> ... 1416 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1417 | // ... -> 2019-12-31 16:00:56 (UTC) -> 1418 | date.setUTCSeconds(56); 1419 | expect(utcStr(date)).toBe("2019-12-31T16:00:56.000Z"); 1420 | } 1421 | { 1422 | // 2020-01-01 05:00:00 (UTC) -> ... 1423 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1424 | // ... -> 2020-01-01 05:00:56 (UTC) -> 1425 | date.setUTCSeconds(56); 1426 | expect(utcStr(date)).toBe("2020-01-01T05:00:56.000Z"); 1427 | } 1428 | { 1429 | // 1880-12-31 17:04:35 (UTC) -> ... 1430 | const date = new TZDate(1880, 0, 1, "Asia/Singapore"); 1431 | // ... -> 1880-12-31 17:05:56 (UTC) -> 1432 | date.setUTCSeconds(56); 1433 | expect(utcStr(date)).toBe("1879-12-31T17:04:56.000Z"); 1434 | } 1435 | { 1436 | // 1880-01-01 04:56:58 (UTC) -> ... 1437 | const date = new TZDate(1880, 0, 1, "America/New_York"); 1438 | // ... -> 1880-01-01 04:56:56 (UTC) -> 1439 | date.setUTCSeconds(56); 1440 | expect(utcStr(date)).toBe("1880-01-01T04:56:56.000Z"); 1441 | } 1442 | }); 1443 | 1444 | it("returns the timestamp after setting", () => { 1445 | const date = new TZDate(defaultDateStr, "America/New_York"); 1446 | expect(date.setUTCSeconds(56)).toBe(+date); 1447 | }); 1448 | 1449 | it("allows to set seconds and milliseconds", () => { 1450 | { 1451 | // 2019-12-31 16:00:00 (UTC) -> ... 1452 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1453 | // ... -> 2019-12-31 16:00:56 (UTC) -> 1454 | date.setUTCSeconds(56, 789); 1455 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.789+08:00"); 1456 | } 1457 | { 1458 | // 2020-01-01 05:00:00 (UTC) -> ... 1459 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1460 | // ... -> 2020-01-01 05:00:56 (UTC) -> 1461 | date.setUTCSeconds(56, 789); 1462 | expect(date.toISOString()).toBe("2020-01-01T00:00:56.789-05:00"); 1463 | } 1464 | }); 1465 | 1466 | it("allows to overflow the minutes into the future", () => { 1467 | { 1468 | // 2019-12-31 16:00:00 (UTC) -> ... 1469 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1470 | // ... -> 2019-12-31 16:02:30 (UTC) -> 1471 | date.setUTCSeconds(120, 30000); 1472 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000+08:00"); 1473 | } 1474 | { 1475 | // 2020-01-01 05:00:00 (UTC) -> ... 1476 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1477 | // ... -> 2020-01-01 05:02:30 (UTC) -> 1478 | date.setUTCSeconds(120, 30000); 1479 | expect(date.toISOString()).toBe("2020-01-01T00:02:30.000-05:00"); 1480 | } 1481 | }); 1482 | 1483 | it("allows to overflow the minutes into the past", () => { 1484 | { 1485 | // 2019-12-31 16:00:00 (UTC) -> ... 1486 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1487 | // ... -> 2019-12-31 15:57:30 (UTC) -> 1488 | date.setUTCSeconds(-120, -30000); 1489 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000+08:00"); 1490 | } 1491 | { 1492 | // 2020-01-01 05:00:00 (UTC) -> ... 1493 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1494 | // ... -> 2020-01-01 04:57:30 (UTC) -> 1495 | date.setUTCSeconds(-120, -30000); 1496 | expect(date.toISOString()).toBe("2019-12-31T23:57:30.000-05:00"); 1497 | } 1498 | }); 1499 | }); 1500 | }); 1501 | 1502 | describe("milliseconds", () => { 1503 | describe("getMilliseconds", () => { 1504 | it("returns the milliseconds in the timezone", () => { 1505 | const dateStr = "1987-02-10T00:00:00.456Z"; 1506 | expect(new TZDate(dateStr, "America/New_York").getMilliseconds()).toBe( 1507 | 456, 1508 | ); 1509 | expect(new TZDate(dateStr, "Asia/Singapore").getMilliseconds()).toBe( 1510 | 456, 1511 | ); 1512 | }); 1513 | 1514 | it("returns NaN when the date or time zone are invalid", () => { 1515 | expect(new TZDate(NaN, "America/New_York").getMilliseconds()).toBe(NaN); 1516 | expect(new TZDate(Date.now(), "Etc/Invalid").getMilliseconds()).toBe( 1517 | NaN, 1518 | ); 1519 | }); 1520 | }); 1521 | 1522 | describe("getUTCMilliseconds", () => { 1523 | it("returns the milliseconds in the UTC timezone", () => { 1524 | const dateStr = "1987-02-10T00:00:00.456Z"; 1525 | expect( 1526 | new TZDate(dateStr, "America/New_York").getUTCMilliseconds(), 1527 | ).toBe(456); 1528 | expect(new TZDate(dateStr, "Asia/Singapore").getUTCMilliseconds()).toBe( 1529 | 456, 1530 | ); 1531 | }); 1532 | 1533 | it("returns NaN when the date or time zone are invalid", () => { 1534 | expect(new TZDate(NaN, "America/New_York").getUTCMilliseconds()).toBe( 1535 | NaN, 1536 | ); 1537 | expect(new TZDate(Date.now(), "Etc/Invalid").getUTCMilliseconds()).toBe( 1538 | NaN, 1539 | ); 1540 | }); 1541 | }); 1542 | 1543 | describe("setMilliseconds", () => { 1544 | it("sets the milliseconds in the timezone", () => { 1545 | { 1546 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1547 | date.setMilliseconds(789); 1548 | expect(date.toISOString()).toBe("2020-01-01T00:00:00.789+08:00"); 1549 | } 1550 | { 1551 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1552 | date.setMilliseconds(789); 1553 | expect(date.toISOString()).toBe("2020-01-01T00:00:00.789-05:00"); 1554 | } 1555 | }); 1556 | 1557 | it("returns the timestamp after setting", () => { 1558 | const date = new TZDate(defaultDateStr, "America/New_York"); 1559 | expect(date.setMilliseconds(789)).toBe(+date); 1560 | }); 1561 | 1562 | it("allows to overflow the seconds into the future", () => { 1563 | { 1564 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1565 | date.setMilliseconds(1000); 1566 | expect(date.toISOString()).toBe("2020-01-01T00:00:01.000+08:00"); 1567 | } 1568 | { 1569 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1570 | date.setMilliseconds(1000); 1571 | expect(date.toISOString()).toBe("2020-01-01T00:00:01.000-05:00"); 1572 | } 1573 | }); 1574 | 1575 | it("allows to overflow the seconds into the past", () => { 1576 | { 1577 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1578 | date.setMilliseconds(-1000); 1579 | expect(date.toISOString()).toBe("2019-12-31T23:59:59.000+08:00"); 1580 | } 1581 | { 1582 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1583 | date.setMilliseconds(-1000); 1584 | expect(date.toISOString()).toBe("2019-12-31T23:59:59.000-05:00"); 1585 | } 1586 | }); 1587 | }); 1588 | 1589 | describe("setUTCMilliseconds", () => { 1590 | it("sets the milliseconds in the UTC timezone", () => { 1591 | { 1592 | // 2019-12-31 16:00:00 (UTC) -> ... 1593 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1594 | // ... -> 2019-12-31 16:00:00 (UTC) -> 1595 | date.setUTCMilliseconds(789); 1596 | expect(utcStr(date)).toBe("2019-12-31T16:00:00.789Z"); 1597 | } 1598 | { 1599 | // 2020-01-01 05:00:00 (UTC) -> ... 1600 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1601 | // ... -> 2020-01-01 05:00:00 (UTC) -> 1602 | date.setUTCMilliseconds(789); 1603 | expect(utcStr(date)).toBe("2020-01-01T05:00:00.789Z"); 1604 | } 1605 | }); 1606 | 1607 | it("returns the timestamp after setting", () => { 1608 | const date = new TZDate(defaultDateStr, "America/New_York"); 1609 | expect(date.setUTCMilliseconds(789)).toBe(+date); 1610 | }); 1611 | 1612 | it("allows to overflow the seconds into the future", () => { 1613 | { 1614 | // 2019-12-31 16:00:00 (UTC) -> ... 1615 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1616 | // ... -> 2019-12-31 16:00:30 (UTC) -> 1617 | date.setUTCMilliseconds(30000); 1618 | expect(date.toISOString()).toBe("2020-01-01T00:00:30.000+08:00"); 1619 | } 1620 | { 1621 | // 2020-01-01 05:00:00 (UTC) -> ... 1622 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1623 | // ... -> 2019-12-31 15:59:30 (UTC) -> 1624 | date.setUTCMilliseconds(30000); 1625 | expect(date.toISOString()).toBe("2020-01-01T00:00:30.000-05:00"); 1626 | } 1627 | }); 1628 | 1629 | it("allows to overflow the seconds into the past", () => { 1630 | { 1631 | // 2019-12-31 16:00:00 (UTC) -> ... 1632 | const date = new TZDate(2020, 0, 1, "Asia/Singapore"); 1633 | // ... -> 2020-01-01 04:59:30 (UTC) -> 1634 | date.setUTCMilliseconds(-30000); 1635 | expect(date.toISOString()).toBe("2019-12-31T23:59:30.000+08:00"); 1636 | } 1637 | { 1638 | // 2020-01-01 05:00:00 (UTC) -> ... 1639 | const date = new TZDate(2020, 0, 1, "America/New_York"); 1640 | // ... -> 2020-01-01 04:59:30 (UTC) -> 1641 | date.setUTCMilliseconds(-30000); 1642 | expect(date.toISOString()).toBe("2019-12-31T23:59:30.000-05:00"); 1643 | } 1644 | }); 1645 | }); 1646 | }); 1647 | 1648 | describe("time zone", () => { 1649 | describe("withTimeZone", () => { 1650 | it("returns a new date with the given timezone", () => { 1651 | const date = new TZDate(defaultDateStr, "America/New_York"); 1652 | const newDate = date.withTimeZone("Asia/Tokyo"); 1653 | 1654 | expect(date.toISOString()).toBe("1987-02-10T19:00:00.000-05:00"); 1655 | expect(newDate.toISOString()).toBe("1987-02-11T09:00:00.000+09:00"); 1656 | 1657 | const beforeGMTDate = new TZDate(beforeGMTDateStr, "America/New_York"); 1658 | const beforeGMTNewDate = beforeGMTDate.withTimeZone("Asia/Singapore"); 1659 | 1660 | expect(beforeGMTDate.toISOString()).toBe("1880-02-10T19:03:58.000-04:56"); 1661 | expect(beforeGMTNewDate.toISOString()).toBe("1880-02-11T06:55:25.000+06:55"); 1662 | }); 1663 | }); 1664 | 1665 | describe("getTimezoneOffset", () => { 1666 | it("returns the timezone offset", () => { 1667 | expect( 1668 | new TZDate(defaultDateStr, "America/New_York").getTimezoneOffset(), 1669 | ).toBe(300); 1670 | expect( 1671 | new TZDate(defaultDateStr, "Asia/Singapore").getTimezoneOffset(), 1672 | ).toBe(-480); 1673 | expect( 1674 | new TZDate(beforeGMTDateStr, "America/New_York").getTimezoneOffset() 1675 | ).toBe(296); 1676 | expect( 1677 | new TZDate(beforeGMTDateStr, "Asia/Singapore").getTimezoneOffset() 1678 | ).toBe(-415); 1679 | }); 1680 | 1681 | it("returns NaN when the date is invalid", () => { 1682 | expect(new TZDate(NaN, "America/New_York").getTimezoneOffset()).toBe( 1683 | NaN, 1684 | ); 1685 | }); 1686 | }); 1687 | }); 1688 | 1689 | describe("representation", () => { 1690 | describe("[Symbol.toPrimitive]", () => { 1691 | it("returns string representation of the date when the hint is 'string'", () => { 1692 | expect( 1693 | new TZDate(2020, 0, 1, "Asia/Singapore")[Symbol.toPrimitive]( 1694 | "string", 1695 | ), 1696 | ).toBe("Wed Jan 01 2020 00:00:00 GMT+0800 (Singapore Standard Time)"); 1697 | expect( 1698 | new TZDate(2020, 0, 1, "America/New_York")[Symbol.toPrimitive]( 1699 | "string", 1700 | ), 1701 | ).toBe("Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)"); 1702 | }); 1703 | 1704 | it("returns string representation of the date when the hint is 'default'", () => { 1705 | expect( 1706 | new TZDate(2020, 0, 1, "Asia/Singapore")[Symbol.toPrimitive]( 1707 | "default", 1708 | ), 1709 | ).toBe("Wed Jan 01 2020 00:00:00 GMT+0800 (Singapore Standard Time)"); 1710 | expect( 1711 | new TZDate(2020, 0, 1, "America/New_York")[Symbol.toPrimitive]( 1712 | "default", 1713 | ), 1714 | ).toBe("Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)"); 1715 | }); 1716 | 1717 | it("returns number representation of the date when the hint is 'number'", () => { 1718 | const nativeDate = new Date("2020-01-01T00:00:00.000+08:00"); 1719 | expect( 1720 | new TZDate(+nativeDate, "Asia/Singapore")[Symbol.toPrimitive]( 1721 | "number", 1722 | ), 1723 | ).toBe(+nativeDate); 1724 | expect( 1725 | new TZDate(+nativeDate, "America/New_York")[Symbol.toPrimitive]( 1726 | "number", 1727 | ), 1728 | ).toBe(+nativeDate); 1729 | 1730 | const beforeGMTDate = new Date("1880-01-01T00:00:00.000+08:00"); 1731 | expect( 1732 | new TZDate(+beforeGMTDate, "America/New_York")[Symbol.toPrimitive]( 1733 | "number" 1734 | ) 1735 | ).toBe(+beforeGMTDate); 1736 | }); 1737 | }); 1738 | 1739 | describe("toISOString", () => { 1740 | it("returns ISO 8601 formatted date in the timezone", () => { 1741 | expect(new TZDate(2020, 0, 1, "America/New_York").toISOString()).toBe( 1742 | "2020-01-01T00:00:00.000-05:00", 1743 | ); 1744 | expect(new TZDate(2020, 0, 1, "Asia/Singapore").toISOString()).toBe( 1745 | "2020-01-01T00:00:00.000+08:00", 1746 | ); 1747 | expect(new TZDate(2020, 0, 1, "Asia/Kolkata").toISOString()).toBe( 1748 | "2020-01-01T00:00:00.000+05:30", 1749 | ); 1750 | expect(new TZDate(2015, 7, 1, "Asia/Pyongyang").toISOString()).toBe( 1751 | "2015-08-01T00:00:00.000+09:00", 1752 | ); 1753 | expect(new TZDate(2015, 8, 1, "Asia/Pyongyang").toISOString()).toBe( 1754 | "2015-09-01T00:00:00.000+08:30", 1755 | ); 1756 | expect(new TZDate(1880, 0, 1, 22, 25, 54, "America/New_York").toISOString()).toBe( 1757 | "1880-01-01T22:25:54.000-04:56" 1758 | ); 1759 | expect(new TZDate(1880, 0, 1, "America/New_York").toISOString()).toBe( 1760 | "1880-01-01T00:00:00.000-04:56" 1761 | ); 1762 | expect(new TZDate(1880, 0, 1, "Asia/Pyongyang").toISOString()).toBe( 1763 | "1880-01-01T00:00:00.000+08:23" 1764 | ); 1765 | expect(new TZDate(1880, 0, 1, "Europe/Moscow").toISOString()).toBe( 1766 | "1880-01-01T00:00:00.000+02:30" 1767 | ); 1768 | expect(new TZDate(1880, 0, 1, "Europe/Berlin").toISOString()).toBe( 1769 | "1880-01-01T00:00:00.000+00:53" 1770 | ); 1771 | expect(new TZDate(1880, 0, 1, "Europe/Vienna").toISOString()).toBe( 1772 | "1880-01-01T00:00:00.000+01:05" 1773 | ); 1774 | }); 1775 | }); 1776 | 1777 | describe("toJSON", () => { 1778 | it("works the same as toISOString", () => { 1779 | expect(new TZDate(2020, 0, 1, "America/New_York").toJSON()).toBe( 1780 | "2020-01-01T00:00:00.000-05:00", 1781 | ); 1782 | expect(new TZDate(2015, 7, 1, "Asia/Pyongyang").toJSON()).toBe( 1783 | "2015-08-01T00:00:00.000+09:00", 1784 | ); 1785 | expect(new TZDate(2015, 8, 1, "Asia/Pyongyang").toJSON()).toBe( 1786 | "2015-09-01T00:00:00.000+08:30", 1787 | ); 1788 | expect(new TZDate(1880, 0, 1, 22, 25, 54, "America/New_York").toJSON()).toBe( 1789 | "1880-01-01T22:25:54.000-04:56" 1790 | ); 1791 | expect(new TZDate(1880, 0, 1, "America/New_York").toJSON()).toBe( 1792 | "1880-01-01T00:00:00.000-04:56" 1793 | ); 1794 | expect(new TZDate(1880, 0, 1, "Asia/Pyongyang").toJSON()).toBe( 1795 | "1880-01-01T00:00:00.000+08:23" 1796 | ); 1797 | expect(new TZDate(1880, 0, 1, "Europe/Moscow").toJSON()).toBe( 1798 | "1880-01-01T00:00:00.000+02:30" 1799 | ); 1800 | expect(new TZDate(1880, 0, 1, "Europe/Berlin").toJSON()).toBe( 1801 | "1880-01-01T00:00:00.000+00:53" 1802 | ); 1803 | expect(new TZDate(1880, 0, 1, "Europe/Vienna").toJSON()).toBe( 1804 | "1880-01-01T00:00:00.000+01:05" 1805 | ); 1806 | }); 1807 | }); 1808 | 1809 | describe("toString", () => { 1810 | it("returns string representation of the date in the timezone", () => { 1811 | expect(new TZDate(2020, 0, 1, "America/New_York").toString()).toBe( 1812 | "Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)", 1813 | ); 1814 | expect( 1815 | new TZDate("2020-01-01T00:00:00.000Z", "America/New_York").toString(), 1816 | ).toBe("Tue Dec 31 2019 19:00:00 GMT-0500 (Eastern Standard Time)"); 1817 | expect(new TZDate(2020, 5, 1, "America/New_York").toString()).toBe( 1818 | "Mon Jun 01 2020 00:00:00 GMT-0400 (Eastern Daylight Time)", 1819 | ); 1820 | expect(new TZDate(1880, 0, 1, 22, 25, 54, "America/New_York").toString()).toBe( 1821 | "Thu Jan 01 1880 22:25:54 GMT-0456 (GMT-04:56:02)" 1822 | ); 1823 | expect(new TZDate(1880, 0, 1, "America/New_York").toString()).toBe( 1824 | "Thu Jan 01 1880 00:00:00 GMT-0456 (GMT-04:56:02)" 1825 | ); 1826 | expect(new TZDate(1880, 0, 1, "Asia/Pyongyang").toString()).toBe( 1827 | "Thu Jan 01 1880 00:00:00 GMT+0823 (GMT+08:23)" 1828 | ); 1829 | expect(new TZDate(1880, 0, 1, "Europe/Moscow").toString()).toBe( 1830 | "Thu Jan 01 1880 00:00:00 GMT+0230 (GMT+02:30:17)" 1831 | ); 1832 | expect(new TZDate(1880, 0, 1, "Europe/Berlin").toString()).toBe( 1833 | "Thu Jan 01 1880 00:00:00 GMT+0053 (GMT+00:53:28)" 1834 | ); 1835 | expect(new TZDate(1880, 0, 1, "Europe/Vienna").toString()).toBe( 1836 | "Thu Jan 01 1880 00:00:00 GMT+0105 (GMT+01:05:21)" 1837 | ); 1838 | }); 1839 | }); 1840 | 1841 | describe("toDateString", () => { 1842 | it("returns formatted date portion of the in the timezone", () => { 1843 | expect(new TZDate(2020, 0, 1, "America/New_York").toDateString()).toBe( 1844 | "Wed Jan 01 2020", 1845 | ); 1846 | expect( 1847 | new TZDate("2020-01-01T00:00:00Z", "America/New_York").toDateString(), 1848 | ).toBe("Tue Dec 31 2019"); 1849 | expect(new TZDate(1880, 0, 1, 22, 25, 54, "America/New_York").toDateString()).toBe( 1850 | "Thu Jan 01 1880" 1851 | ); 1852 | expect(new TZDate(1880, 0, 1, "America/New_York").toDateString()).toBe( 1853 | "Thu Jan 01 1880" 1854 | ); 1855 | expect(new TZDate(1880, 0, 1, "Asia/Pyongyang").toDateString()).toBe( 1856 | "Thu Jan 01 1880" 1857 | ); 1858 | expect(new TZDate(1880, 0, 1, "Europe/Moscow").toDateString()).toBe( 1859 | "Thu Jan 01 1880" 1860 | ); 1861 | expect(new TZDate(1880, 0, 1, "Europe/Berlin").toDateString()).toBe( 1862 | "Thu Jan 01 1880" 1863 | ); 1864 | expect(new TZDate(1880, 0, 1, "Europe/Vienna").toDateString()).toBe( 1865 | "Thu Jan 01 1880" 1866 | ); 1867 | }); 1868 | }); 1869 | 1870 | describe("toTimeString", () => { 1871 | it("returns formatted time portion of the in the timezone", () => { 1872 | expect(new TZDate(2020, 0, 1, "America/New_York").toTimeString()).toBe( 1873 | "00:00:00 GMT-0500 (Eastern Standard Time)", 1874 | ); 1875 | expect( 1876 | new TZDate( 1877 | "2020-01-01T00:00:00.000Z", 1878 | "America/New_York", 1879 | ).toTimeString(), 1880 | ).toBe("19:00:00 GMT-0500 (Eastern Standard Time)"); 1881 | expect(new TZDate(2020, 5, 1, "America/New_York").toTimeString()).toBe( 1882 | "00:00:00 GMT-0400 (Eastern Daylight Time)", 1883 | ); 1884 | expect(new TZDate(1880, 0, 1, "America/New_York").toTimeString()).toBe( 1885 | "00:00:00 GMT-0456 (GMT-04:56:02)" 1886 | ); 1887 | expect(new TZDate(1880, 0, 1, "Asia/Pyongyang").toTimeString()).toBe( 1888 | "00:00:00 GMT+0823 (GMT+08:23)" 1889 | ); 1890 | expect(new TZDate(1880, 0, 1, "Europe/Moscow").toTimeString()).toBe( 1891 | "00:00:00 GMT+0230 (GMT+02:30:17)" 1892 | ); 1893 | expect(new TZDate(1880, 0, 1, "Europe/Berlin").toTimeString()).toBe( 1894 | "00:00:00 GMT+0053 (GMT+00:53:28)" 1895 | ); 1896 | expect(new TZDate(1880, 0, 1, "Europe/Vienna").toTimeString()).toBe( 1897 | "00:00:00 GMT+0105 (GMT+01:05:21)" 1898 | ); 1899 | }); 1900 | }); 1901 | 1902 | describe("toUTCString", () => { 1903 | it("returns string representation of the date in UTC", () => { 1904 | expect( 1905 | new TZDate( 1906 | "2020-02-11T08:00:00.000Z", 1907 | "America/New_York", 1908 | ).toUTCString(), 1909 | ).toBe("Tue, 11 Feb 2020 08:00:00 GMT"); 1910 | expect( 1911 | new TZDate( 1912 | "2020-02-11T08:00:00.000Z", 1913 | "Asia/Singapore", 1914 | ).toUTCString(), 1915 | ).toBe("Tue, 11 Feb 2020 08:00:00 GMT"); 1916 | expect(new TZDate(1880, 0, 1, "America/New_York").toUTCString()).toBe( 1917 | "Thu, 01 Jan 1880 04:56:02 GMT" 1918 | ); 1919 | expect(new TZDate(1880, 0, 1, "Asia/Pyongyang").toUTCString()).toBe( 1920 | "Wed, 31 Dec 1879 15:37:00 GMT" 1921 | ); 1922 | expect(new TZDate(1880, 0, 1, "Europe/Moscow").toUTCString()).toBe( 1923 | "Wed, 31 Dec 1879 21:29:43 GMT" 1924 | ); 1925 | expect(new TZDate(1880, 0, 1, "Europe/Berlin").toUTCString()).toBe( 1926 | "Wed, 31 Dec 1879 23:06:32 GMT" 1927 | ); 1928 | expect(new TZDate(1880, 0, 1, "Europe/Vienna").toUTCString()).toBe( 1929 | "Wed, 31 Dec 1879 22:54:39 GMT" 1930 | ); 1931 | }); 1932 | }); 1933 | 1934 | describe("toLocaleString", () => { 1935 | it("returns localized date and time in the timezone", () => { 1936 | expect( 1937 | new TZDate(2020, 0, 1, "America/New_York").toLocaleString(), 1938 | ).toBe("1/1/2020, 12:00:00 AM"); 1939 | expect( 1940 | new TZDate( 1941 | "2020-01-01T00:00:00.000Z", 1942 | "America/New_York", 1943 | ).toLocaleString(), 1944 | ).toBe("12/31/2019, 7:00:00 PM"); 1945 | expect( 1946 | new TZDate(2020, 5, 1, "America/New_York").toLocaleString("es-ES", { 1947 | dateStyle: "full", 1948 | timeStyle: "full", 1949 | }), 1950 | ).toBe("lunes, 1 de junio de 2020, 0:00:00 (hora de verano oriental)"); 1951 | expect( 1952 | new TZDate( 1953 | "2020-01-01T02:00:00.000Z", 1954 | "America/New_York", 1955 | ).toLocaleString("es-ES", { 1956 | dateStyle: "full", 1957 | timeStyle: "full", 1958 | }), 1959 | ).toBe( 1960 | "martes, 31 de diciembre de 2019, 21:00:00 (hora estándar oriental)", 1961 | ); 1962 | expect( 1963 | new TZDate( 1964 | "2020-01-01T02:00:00.000Z", 1965 | "America/New_York", 1966 | ).toLocaleString("es-ES", { 1967 | dateStyle: "full", 1968 | timeStyle: "full", 1969 | timeZone: "Asia/Singapore", 1970 | }), 1971 | ).toBe("miércoles, 1 de enero de 2020, 10:00:00 (hora de Singapur)"); 1972 | }); 1973 | }); 1974 | 1975 | describe("toLocaleDateString", () => { 1976 | it("returns localized date portion of the in the timezone", () => { 1977 | expect( 1978 | new TZDate(2020, 0, 1, "America/New_York").toLocaleDateString(), 1979 | ).toBe("1/1/2020"); 1980 | expect( 1981 | new TZDate( 1982 | "2020-01-01T00:00:00.000Z", 1983 | "America/New_York", 1984 | ).toLocaleDateString(), 1985 | ).toBe("12/31/2019"); 1986 | expect( 1987 | new TZDate(2020, 5, 1, "America/New_York").toLocaleDateString( 1988 | "es-ES", 1989 | { dateStyle: "full" }, 1990 | ), 1991 | ).toBe("lunes, 1 de junio de 2020"); 1992 | expect( 1993 | new TZDate( 1994 | "2020-01-01T02:00:00.000Z", 1995 | "America/New_York", 1996 | ).toLocaleDateString("es-ES", { 1997 | dateStyle: "full", 1998 | }), 1999 | ).toBe("martes, 31 de diciembre de 2019"); 2000 | expect( 2001 | new TZDate(2020, 0, 1, 10, "America/New_York").toLocaleDateString( 2002 | "es-ES", 2003 | { dateStyle: "full", timeZone: "Asia/Singapore" }, 2004 | ), 2005 | ).toBe("miércoles, 1 de enero de 2020"); 2006 | }); 2007 | }); 2008 | 2009 | describe("toLocaleTimeString", () => { 2010 | it("returns localized time portion of the in the timezone", () => { 2011 | expect( 2012 | new TZDate(2020, 0, 1, "America/New_York").toLocaleTimeString(), 2013 | ).toBe("12:00:00 AM"); 2014 | expect( 2015 | new TZDate( 2016 | "2020-02-11T00:00:00.000Z", 2017 | "America/New_York", 2018 | ).toLocaleTimeString(), 2019 | ).toBe("7:00:00 PM"); 2020 | expect( 2021 | new TZDate(2020, 5, 1, "America/New_York").toLocaleTimeString( 2022 | "es-ES", 2023 | { timeStyle: "full" }, 2024 | ), 2025 | ).toBe("0:00:00 (hora de verano oriental)"); 2026 | expect( 2027 | new TZDate( 2028 | "2020-01-01T00:00:00.000Z", 2029 | "America/New_York", 2030 | ).toLocaleTimeString("es-ES", { timeStyle: "full" }), 2031 | ).toBe("19:00:00 (hora estándar oriental)"); 2032 | expect( 2033 | new TZDate( 2034 | "2020-01-01T00:00:00.000Z", 2035 | "America/New_York", 2036 | ).toLocaleTimeString("es-ES", { 2037 | timeStyle: "full", 2038 | timeZone: "Asia/Singapore", 2039 | }), 2040 | ).toBe("8:00:00 (hora de Singapur)"); 2041 | }); 2042 | }); 2043 | }); 2044 | 2045 | describe('[Symbol.for("constructDateFrom")]', () => { 2046 | it("constructs a new date from value and the instance time zone", () => { 2047 | const dateStr = "2020-01-01T00:00:00.000Z"; 2048 | const beforeGMTDateStr = "1880-01-01T00:00:00.000Z"; 2049 | const nativeDate = new Date(dateStr); 2050 | const beforeGMTNativeDate = new Date("1880-01-01T00:00:00.000Z"); 2051 | { 2052 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 2053 | const result = date[constructFromSymbol](nativeDate); 2054 | expect(result.toISOString()).toBe("2020-01-01T08:00:00.000+08:00"); 2055 | } 2056 | { 2057 | const date = new TZDate(defaultDateStr, "America/New_York"); 2058 | const result = date[constructFromSymbol](nativeDate); 2059 | expect(result.toISOString()).toBe("2019-12-31T19:00:00.000-05:00"); 2060 | } 2061 | { 2062 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 2063 | const result = date[constructFromSymbol](+nativeDate); 2064 | expect(result.toISOString()).toBe("2020-01-01T08:00:00.000+08:00"); 2065 | } 2066 | { 2067 | const date = new TZDate(defaultDateStr, "America/New_York"); 2068 | const result = date[constructFromSymbol](+nativeDate); 2069 | expect(result.toISOString()).toBe("2019-12-31T19:00:00.000-05:00"); 2070 | } 2071 | { 2072 | const date = new TZDate(beforeGMTDateStr, "America/New_York"); 2073 | const result = date[constructFromSymbol](+beforeGMTNativeDate); 2074 | expect(result.toISOString()).toBe("1879-12-31T19:03:58.000-04:56"); 2075 | } 2076 | { 2077 | const date = new TZDate(beforeGMTDateStr, "Europe/Berlin"); 2078 | const result = date[constructFromSymbol](+beforeGMTNativeDate); 2079 | expect(result.toISOString()).toBe("1880-01-01T00:53:28.000+00:53"); 2080 | } 2081 | { 2082 | const date = new TZDate(defaultDateStr, "Asia/Singapore"); 2083 | const result = date[constructFromSymbol](dateStr); 2084 | expect(result.toISOString()).toBe("2020-01-01T08:00:00.000+08:00"); 2085 | } 2086 | { 2087 | const date = new TZDate(defaultDateStr, "America/New_York"); 2088 | const result = date[constructFromSymbol](dateStr); 2089 | expect(result.toISOString()).toBe("2019-12-31T19:00:00.000-05:00"); 2090 | } 2091 | { 2092 | const date = new TZDate(beforeGMTDateStr, "America/New_York"); 2093 | const result = date[constructFromSymbol](beforeGMTDateStr); 2094 | expect(result.toISOString()).toBe("1879-12-31T19:03:58.000-04:56"); 2095 | } 2096 | { 2097 | const date = new TZDate(beforeGMTDateStr, "Europe/Berlin"); 2098 | const result = date[constructFromSymbol](+beforeGMTNativeDate); 2099 | expect(result.toISOString()).toBe("1880-01-01T00:53:28.000+00:53"); 2100 | } 2101 | }); 2102 | }); 2103 | 2104 | describe("DST", () => { 2105 | describe("setting the DST time", () => { 2106 | describe("DST start", () => { 2107 | describe("default methods", () => { 2108 | it("America/Los_Angeles", () => { 2109 | withDSTStart(laName, (date) => { 2110 | expect(utcStr(date)).toBe("2020-03-08T08:00:00.000Z"); 2111 | }); 2112 | 2113 | // Set on the DST hour 2114 | withDSTStart(laName, (date) => { 2115 | date.setHours(2); 2116 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2117 | }); 2118 | 2119 | // Set on the hour DST moves to 2120 | withDSTStart(laName, (date) => { 2121 | date.setHours(3); 2122 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2123 | }); 2124 | 2125 | // Set after the DST hour 2126 | withDSTStart(laName, (date) => { 2127 | date.setHours(5); 2128 | expect(utcStr(date)).toBe("2020-03-08T12:00:00.000Z"); 2129 | }); 2130 | }); 2131 | 2132 | it("America/New_York", () => { 2133 | withDSTStart(nyName, (date) => { 2134 | expect(utcStr(date)).toBe("2020-03-08T05:00:00.000Z"); 2135 | }); 2136 | 2137 | // Set on the DST hour 2138 | withDSTStart(nyName, (date) => { 2139 | date.setHours(2); 2140 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2141 | }); 2142 | 2143 | // Set on the hour DST moves to 2144 | withDSTStart(nyName, (date) => { 2145 | date.setHours(3); 2146 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2147 | }); 2148 | 2149 | // Set after the DST hour 2150 | withDSTStart(nyName, (date) => { 2151 | date.setHours(5); 2152 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 2153 | }); 2154 | }); 2155 | }); 2156 | 2157 | describe("UTC methods", () => { 2158 | it("America/Los_Angeles", () => { 2159 | // Set on the DST hour 2160 | withDSTStart(laName, (date) => { 2161 | date.setUTCHours(10); 2162 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2163 | }); 2164 | 2165 | // Set after the DST hour 2166 | withDSTStart(laName, (date) => { 2167 | date.setUTCHours(12); 2168 | expect(utcStr(date)).toBe("2020-03-08T12:00:00.000Z"); 2169 | }); 2170 | }); 2171 | 2172 | it("America/New_York", () => { 2173 | // Set on the DST hour 2174 | withDSTStart(nyName, (date) => { 2175 | date.setUTCHours(7); 2176 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2177 | }); 2178 | 2179 | // Set after the DST hour 2180 | withDSTStart(nyName, (date) => { 2181 | date.setUTCHours(9); 2182 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 2183 | }); 2184 | }); 2185 | }); 2186 | }); 2187 | }); 2188 | 2189 | describe("updating to the DST time", () => { 2190 | describe("DST start", () => { 2191 | describe("default methods", () => { 2192 | it("America/Los_Angeles", () => { 2193 | withDSTStart(laName, (date) => { 2194 | date.setHours(1); 2195 | date.setHours(1); 2196 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 2197 | }); 2198 | 2199 | // Update to the same DST hour 2200 | withDSTStart(laName, (date) => { 2201 | date.setHours(2); 2202 | date.setHours(2); 2203 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2204 | }); 2205 | 2206 | // Update to the hour DST moves to 2207 | withDSTStart(laName, (date) => { 2208 | date.setHours(2); 2209 | date.setHours(3); 2210 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2211 | }); 2212 | 2213 | // Update to same DST hour 2214 | withDSTStart(laName, (date) => { 2215 | date.setHours(2); 2216 | date.setHours(date.getHours()); 2217 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2218 | }); 2219 | 2220 | // Update to same hour DST moves to 2221 | withDSTStart(laName, (date) => { 2222 | date.setHours(3); 2223 | date.setHours(date.getHours()); 2224 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2225 | }); 2226 | 2227 | // Update from after the DST hour 2228 | withDSTStart(laName, (date) => { 2229 | date.setHours(5); 2230 | date.setHours(2); 2231 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2232 | }); 2233 | 2234 | // Update to another year with the same DST hour 2235 | withDSTStart(laName, (date) => { 2236 | date.setHours(2); 2237 | date.setFullYear(2015); 2238 | expect(utcStr(date)).toBe("2015-03-08T10:00:00.000Z"); 2239 | }); 2240 | }); 2241 | 2242 | it("America/New_York", () => { 2243 | withDSTStart(nyName, (date) => { 2244 | date.setHours(1); 2245 | date.setHours(1); 2246 | expect(utcStr(date)).toBe("2020-03-08T06:00:00.000Z"); 2247 | }); 2248 | 2249 | // Update to the same DST hour 2250 | withDSTStart(nyName, (date) => { 2251 | date.setHours(2); 2252 | date.setHours(2); 2253 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2254 | }); 2255 | 2256 | // Update to the hour DST moves to 2257 | withDSTStart(nyName, (date) => { 2258 | date.setHours(2); 2259 | date.setHours(3); 2260 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2261 | }); 2262 | 2263 | // Update to same DST hour 2264 | withDSTStart(nyName, (date) => { 2265 | date.setHours(2); 2266 | date.setHours(date.getHours()); 2267 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2268 | }); 2269 | 2270 | // Update to same hour DST moves to 2271 | withDSTStart(nyName, (date) => { 2272 | date.setHours(3); 2273 | date.setHours(date.getHours()); 2274 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2275 | }); 2276 | 2277 | // Update from after the DST hour 2278 | withDSTStart(nyName, (date) => { 2279 | date.setHours(5); 2280 | date.setHours(2); 2281 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2282 | }); 2283 | 2284 | // Update to another year with the same DST hour 2285 | withDSTStart(nyName, (date) => { 2286 | date.setHours(2); 2287 | date.setFullYear(2015); 2288 | expect(utcStr(date)).toBe("2015-03-08T07:00:00.000Z"); 2289 | }); 2290 | }); 2291 | }); 2292 | 2293 | describe("UTC methods", () => { 2294 | it("America/Los_Angeles", () => { 2295 | withDSTStart(laName, (date) => { 2296 | date.setUTCHours(9); 2297 | date.setUTCHours(9); 2298 | expect(utcStr(date)).toBe("2020-03-08T09:00:00.000Z"); 2299 | }); 2300 | 2301 | // Update to the same DST hour 2302 | withDSTStart(laName, (date) => { 2303 | date.setUTCHours(10); 2304 | date.setUTCHours(10); 2305 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2306 | }); 2307 | 2308 | // Update from after the DST hour 2309 | withDSTStart(laName, (date) => { 2310 | date.setUTCHours(12); 2311 | date.setUTCHours(10); 2312 | expect(utcStr(date)).toBe("2020-03-08T10:00:00.000Z"); 2313 | }); 2314 | 2315 | // Update to another year with the same DST hour 2316 | withDSTStart(laName, (date) => { 2317 | date.setUTCHours(10); 2318 | date.setUTCFullYear(2015); 2319 | expect(utcStr(date)).toBe("2015-03-08T10:00:00.000Z"); 2320 | }); 2321 | }); 2322 | 2323 | it("America/New_York", () => { 2324 | withDSTStart(nyName, (date) => { 2325 | date.setUTCHours(6); 2326 | date.setUTCHours(6); 2327 | expect(utcStr(date)).toBe("2020-03-08T06:00:00.000Z"); 2328 | }); 2329 | 2330 | // Update to the same DST hour 2331 | withDSTStart(nyName, (date) => { 2332 | date.setUTCHours(7); 2333 | date.setUTCHours(7); 2334 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2335 | }); 2336 | 2337 | // Update from after the DST hour 2338 | withDSTStart(nyName, (date) => { 2339 | date.setUTCHours(10); 2340 | date.setUTCHours(7); 2341 | expect(utcStr(date)).toBe("2020-03-08T07:00:00.000Z"); 2342 | }); 2343 | 2344 | // Update to another year with the same DST hour 2345 | withDSTStart(nyName, (date) => { 2346 | date.setUTCHours(7); 2347 | date.setUTCFullYear(2015); 2348 | expect(utcStr(date)).toBe("2015-03-08T07:00:00.000Z"); 2349 | }); 2350 | }); 2351 | }); 2352 | }); 2353 | }); 2354 | }); 2355 | }); 2356 | 2357 | function withDSTStart(tz: string, fn: (date: TZDate) => void) { 2358 | fn(new TZDate(2020, 2, 8, tz)); 2359 | } 2360 | 2361 | function withDSTEnd(tz: string, fn: (date: TZDate) => void) { 2362 | fn(new TZDate(2020, 10, 1, tz)); 2363 | } 2364 | 2365 | function utcStr(date: TZDate) { 2366 | return new Date(+date).toISOString(); 2367 | } 2368 | 2369 | const laName = "America/Los_Angeles"; 2370 | const nyName = "America/New_York"; 2371 | const sgName = "Asia/Singapore"; 2372 | --------------------------------------------------------------------------------