airport-utils 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +3 -0
- package/.github/workflows/publish.yml +0 -4
- package/.github/workflows/update-mapping.yml +1 -1
- package/.releaserc.json +1 -2
- package/dist/cjs/converter.js +78 -0
- package/dist/cjs/errors.js +24 -0
- package/dist/cjs/index.js +14 -0
- package/dist/cjs/info.js +16 -0
- package/dist/cjs/mapping/geo.js +11991 -0
- package/dist/cjs/mapping/timezones.js +12005 -0
- package/dist/esm/converter.js +75 -0
- package/dist/{src → esm}/errors.js +5 -3
- package/dist/esm/index.js +3 -0
- package/dist/esm/info.js +14 -0
- package/dist/{src → esm}/mapping/geo.js +5 -3
- package/dist/{src → esm}/mapping/timezones.js +3 -1
- package/dist/types/converter.d.ts +10 -0
- package/jest.config.js +5 -1
- package/package.json +24 -5
- package/rollup.config.js +53 -0
- package/src/converter.ts +49 -25
- package/tests/built.test.ts +52 -0
- package/tests/converter.test.ts +58 -14
- package/tsconfig.json +5 -2
- package/dist/scripts/generateMapping.d.ts +0 -2
- package/dist/scripts/generateMapping.js +0 -78
- package/dist/src/converter.d.ts +0 -10
- package/dist/src/converter.js +0 -50
- package/dist/src/index.js +0 -3
- package/dist/src/info.js +0 -11
- package/dist/tests/converter.test.d.ts +0 -1
- package/dist/tests/converter.test.js +0 -56
- package/dist/tests/info.test.d.ts +0 -1
- package/dist/tests/info.test.js +0 -34
- /package/dist/{src → types}/errors.d.ts +0 -0
- /package/dist/{src → types}/index.d.ts +0 -0
- /package/dist/{src → types}/info.d.ts +0 -0
- /package/dist/{src → types}/mapping/geo.d.ts +0 -0
- /package/dist/{src → types}/mapping/timezones.d.ts +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { parseISO } from 'date-fns';
|
|
2
|
+
import { TZDate } from '@date-fns/tz';
|
|
3
|
+
import { timezones } from './mapping/timezones.js';
|
|
4
|
+
import { UnknownAirportError, InvalidTimestampError, UnknownTimezoneError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
const ISO_LOCAL_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
7
|
+
function parseLocalIso(localIso) {
|
|
8
|
+
const m = ISO_LOCAL_RE.exec(localIso);
|
|
9
|
+
if (!m)
|
|
10
|
+
throw new InvalidTimestampError(localIso);
|
|
11
|
+
const [, Y, Mo, D, h, mi, s] = m;
|
|
12
|
+
return [
|
|
13
|
+
Number(Y),
|
|
14
|
+
Number(Mo) - 1,
|
|
15
|
+
Number(D),
|
|
16
|
+
Number(h),
|
|
17
|
+
Number(mi),
|
|
18
|
+
s ? Number(s) : 0
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Convert a local ISO‐8601 string at an airport (IATA) into a UTC ISO string.
|
|
23
|
+
* Always emits "YYYY-MM-DDTHH:mm:ssZ" (no milliseconds).
|
|
24
|
+
*/
|
|
25
|
+
function convertToUTC(localIso, iata) {
|
|
26
|
+
const tz = timezones[iata];
|
|
27
|
+
if (!tz)
|
|
28
|
+
throw new UnknownAirportError(iata);
|
|
29
|
+
// Quick semantic check
|
|
30
|
+
const base = parseISO(localIso);
|
|
31
|
+
if (isNaN(base.getTime()))
|
|
32
|
+
throw new InvalidTimestampError(localIso);
|
|
33
|
+
const [year, month, day, hour, minute, second] = parseLocalIso(localIso);
|
|
34
|
+
let zoned;
|
|
35
|
+
try {
|
|
36
|
+
zoned = TZDate.tz(tz, year, month, day, hour, minute, second);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new InvalidTimestampError(localIso);
|
|
40
|
+
}
|
|
41
|
+
if (isNaN(zoned.getTime()))
|
|
42
|
+
throw new InvalidTimestampError(localIso);
|
|
43
|
+
// Strip ".000" from the ISO string
|
|
44
|
+
return new Date(zoned.getTime()).toISOString().replace('.000Z', 'Z');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert a local ISO‐8601 string in any IANA timezone into a UTC ISO string.
|
|
48
|
+
* Always emits "YYYY-MM-DDTHH:mm:ssZ" (no milliseconds).
|
|
49
|
+
*/
|
|
50
|
+
function convertLocalToUTCByZone(localIso, timeZone) {
|
|
51
|
+
// Validate timezone
|
|
52
|
+
try {
|
|
53
|
+
new Intl.DateTimeFormat('en-US', { timeZone }).format();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new UnknownTimezoneError(timeZone);
|
|
57
|
+
}
|
|
58
|
+
// Quick semantic check
|
|
59
|
+
const base = parseISO(localIso);
|
|
60
|
+
if (isNaN(base.getTime()))
|
|
61
|
+
throw new InvalidTimestampError(localIso);
|
|
62
|
+
const [year, month, day, hour, minute, second] = parseLocalIso(localIso);
|
|
63
|
+
let zoned;
|
|
64
|
+
try {
|
|
65
|
+
zoned = TZDate.tz(timeZone, year, month, day, hour, minute, second);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new UnknownTimezoneError(timeZone);
|
|
69
|
+
}
|
|
70
|
+
if (isNaN(zoned.getTime()))
|
|
71
|
+
throw new InvalidTimestampError(localIso);
|
|
72
|
+
return new Date(zoned.getTime()).toISOString().replace('.000Z', 'Z');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { convertLocalToUTCByZone, convertToUTC };
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
class UnknownAirportError extends Error {
|
|
2
2
|
constructor(iata) {
|
|
3
3
|
super(`Unknown airport IATA code: ${iata}`);
|
|
4
4
|
this.name = 'UnknownAirportError';
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
class InvalidTimestampError extends Error {
|
|
8
8
|
constructor(ts) {
|
|
9
9
|
super(`Invalid ISO 8601 timestamp: ${ts}`);
|
|
10
10
|
this.name = 'InvalidTimestampError';
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
class UnknownTimezoneError extends Error {
|
|
14
14
|
constructor(tz) {
|
|
15
15
|
super(`Unknown timezone: ${tz}`);
|
|
16
16
|
this.name = 'UnknownTimezoneError';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
export { InvalidTimestampError, UnknownAirportError, UnknownTimezoneError };
|
package/dist/esm/info.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { timezones } from './mapping/timezones.js';
|
|
2
|
+
import { geo } from './mapping/geo.js';
|
|
3
|
+
import { UnknownAirportError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
/** @throws UnknownAirportError */
|
|
6
|
+
function getAirportInfo(iata) {
|
|
7
|
+
const tz = timezones[iata];
|
|
8
|
+
const g = geo[iata];
|
|
9
|
+
if (!tz || !g)
|
|
10
|
+
throw new UnknownAirportError(iata);
|
|
11
|
+
return { timezone: tz, ...g };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { getAirportInfo };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const geo = {
|
|
2
2
|
"AAA": { "latitude": -17.41667, "longitude": -145.5, "name": "Anaa", "city": "Anaa", "country": "PF" },
|
|
3
3
|
"AAB": { "latitude": -26.76408, "longitude": 141.02853, "name": "Arrabury", "city": "Arrabury", "country": "AU" },
|
|
4
4
|
"AAC": { "latitude": 31.13159, "longitude": 33.79844, "name": "Arīsh", "city": "Arīsh", "country": "EG" },
|
|
@@ -4754,7 +4754,7 @@ export const geo = {
|
|
|
4754
4754
|
"LDA": { "latitude": 25.00447, "longitude": 88.14573, "name": "Malda", "city": "Malda", "country": "IN" },
|
|
4755
4755
|
"LDB": { "latitude": -23.31028, "longitude": -51.16278, "name": "Londrina", "city": "Londrina", "country": "BR" },
|
|
4756
4756
|
"LDC": { "latitude": -20.45, "longitude": 149.1, "name": "Lindeman Island Airport", "city": "Lindeman Island Airport", "country": "AU" },
|
|
4757
|
-
"LDE": { "latitude": 43.178675, "longitude": -
|
|
4757
|
+
"LDE": { "latitude": 43.178675, "longitude": -6439e-6, "name": "Tarbes-Lourdes-Pyrénées Airport", "city": "Tarbes-Lourdes-Pyrénées Airport", "country": "FR" },
|
|
4758
4758
|
"LDG": { "latitude": 64.8977, "longitude": 45.7655, "name": "Leshukonskoye", "city": "Leshukonskoye", "country": "RU" },
|
|
4759
4759
|
"LDH": { "latitude": -31.55938, "longitude": 159.08581, "name": "Lord Howe Island", "city": "Lord Howe Island", "country": "AU" },
|
|
4760
4760
|
"LDI": { "latitude": -10, "longitude": 39.71667, "name": "Lindi", "city": "Lindi", "country": "TZ" },
|
|
@@ -10490,7 +10490,7 @@ export const geo = {
|
|
|
10490
10490
|
"XOC": { "latitude": 40.40402, "longitude": -3.68823, "name": "Madrid Atocha Railway Station", "city": "Madrid", "country": "ES" },
|
|
10491
10491
|
"XOD": { "latitude": 62.6, "longitude": 9.683, "name": "Oppdal NO Railway Station", "city": "Oppdal NO Railway Station", "country": "NO" },
|
|
10492
10492
|
"XOE": { "latitude": -26.93, "longitude": -49.05, "name": "Sao Jose", "city": "Sao Jose", "country": "BR" },
|
|
10493
|
-
"XOF": { "latitude": 51.5448, "longitude": -
|
|
10493
|
+
"XOF": { "latitude": 51.5448, "longitude": -875e-5, "name": "Stratford International Station", "city": "London", "country": "GB" },
|
|
10494
10494
|
"XOG": { "latitude": 44.140481, "longitude": 4.866717, "name": "Orange-Caritat", "city": "Orange-Caritat", "country": "FR" },
|
|
10495
10495
|
"XOH": { "latitude": 47.45, "longitude": 12.39, "name": "Kitzbuehel AT Bus Station", "city": "Salzburg AT Kitzbuehel Rail", "country": "AT" },
|
|
10496
10496
|
"XOI": { "latitude": 47.01257, "longitude": 10.29179, "name": "Ischgl", "city": "Ischgl", "country": "AT" },
|
|
@@ -11985,3 +11985,5 @@ export const geo = {
|
|
|
11985
11985
|
"ZZW": { "latitude": -32, "longitude": 147, "name": "Day Trip Mystery", "city": "Day Trip Mystery", "country": "AU" },
|
|
11986
11986
|
"ZZZ": { "latitude": 51.88, "longitude": 0.2333, "name": "Greenwich mean TM", "city": "London", "country": "GB" }
|
|
11987
11987
|
};
|
|
11988
|
+
|
|
11989
|
+
export { geo };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// generated — do not edit
|
|
2
|
-
|
|
2
|
+
const timezones = {
|
|
3
3
|
"AAA": "Pacific/Tahiti",
|
|
4
4
|
"AAB": "Australia/Brisbane",
|
|
5
5
|
"AAC": "Africa/Cairo",
|
|
@@ -11999,3 +11999,5 @@ export const timezones = {
|
|
|
11999
11999
|
"ZZW": "Australia/Sydney",
|
|
12000
12000
|
"ZZZ": "Europe/London"
|
|
12001
12001
|
};
|
|
12002
|
+
|
|
12003
|
+
export { timezones };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a local ISO‐8601 string at an airport (IATA) into a UTC ISO string.
|
|
3
|
+
* Always emits "YYYY-MM-DDTHH:mm:ssZ" (no milliseconds).
|
|
4
|
+
*/
|
|
5
|
+
export declare function convertToUTC(localIso: string, iata: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Convert a local ISO‐8601 string in any IANA timezone into a UTC ISO string.
|
|
8
|
+
* Always emits "YYYY-MM-DDTHH:mm:ssZ" (no milliseconds).
|
|
9
|
+
*/
|
|
10
|
+
export declare function convertLocalToUTCByZone(localIso: string, timeZone: string): string;
|
package/jest.config.js
CHANGED
|
@@ -7,5 +7,9 @@ export default {
|
|
|
7
7
|
global: { branches: 100, functions: 100, lines: 100, statements: 100 }
|
|
8
8
|
},
|
|
9
9
|
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
10
|
-
transform: { '^.+\\.ts$': 'ts-jest' }
|
|
10
|
+
transform: { '^.+\\.ts$': 'ts-jest' },
|
|
11
|
+
coveragePathIgnorePatterns: [
|
|
12
|
+
'/node_modules/',
|
|
13
|
+
'/dist/'
|
|
14
|
+
]
|
|
11
15
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "airport-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Convert local ISO 8601 timestamps to UTC using airport IATA codes, with airport geo-data",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"
|
|
5
|
+
"main": "dist/cjs/index.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"require": "./dist/cjs/index.js",
|
|
10
|
+
"import": "./dist/esm/index.js"
|
|
11
|
+
},
|
|
7
12
|
"type": "module",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/elipeF/airport-utils.git"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
8
20
|
"scripts": {
|
|
9
|
-
"build": "tsc",
|
|
21
|
+
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist/types",
|
|
22
|
+
"build:js": "rollup -c",
|
|
23
|
+
"build": "npm run build:types && npm run build:js",
|
|
10
24
|
"test": "jest --coverage",
|
|
11
25
|
"prepublishOnly": "npm run build",
|
|
12
26
|
"update:mapping": "node --loader ts-node/esm scripts/generateMapping.ts"
|
|
@@ -22,9 +36,12 @@
|
|
|
22
36
|
],
|
|
23
37
|
"license": "MIT",
|
|
24
38
|
"dependencies": {
|
|
25
|
-
"
|
|
39
|
+
"@date-fns/tz": "^1.2.0",
|
|
40
|
+
"date-fns": "^4.1.0"
|
|
26
41
|
},
|
|
27
42
|
"devDependencies": {
|
|
43
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
44
|
+
"@rollup/plugin-typescript": "^12.1.2",
|
|
28
45
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
29
46
|
"@semantic-release/github": "^11.0.2",
|
|
30
47
|
"@semantic-release/npm": "^12.0.1",
|
|
@@ -34,9 +51,11 @@
|
|
|
34
51
|
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
35
52
|
"jest": "^29.0.0",
|
|
36
53
|
"node-fetch": "^3.3.2",
|
|
54
|
+
"rollup": "^4.40.2",
|
|
37
55
|
"semantic-release": "^24.2.3",
|
|
38
56
|
"ts-jest": "^29.0.0",
|
|
39
57
|
"ts-node": "^10.9.1",
|
|
58
|
+
"tslib": "^2.8.1",
|
|
40
59
|
"typescript": "^5.0.0"
|
|
41
60
|
}
|
|
42
61
|
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
+
import typescript from '@rollup/plugin-typescript';
|
|
3
|
+
import pkg from './package.json' assert { type: 'json' };
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const external = [
|
|
7
|
+
...Object.keys(pkg.dependencies || {}),
|
|
8
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export default [
|
|
12
|
+
// ESM build
|
|
13
|
+
{
|
|
14
|
+
input: 'src/index.ts',
|
|
15
|
+
external,
|
|
16
|
+
plugins: [
|
|
17
|
+
resolve(),
|
|
18
|
+
typescript({
|
|
19
|
+
tsconfig: './tsconfig.json',
|
|
20
|
+
declaration: false,
|
|
21
|
+
compilerOptions: { outDir: undefined }
|
|
22
|
+
})
|
|
23
|
+
],
|
|
24
|
+
output: {
|
|
25
|
+
dir: 'dist/esm',
|
|
26
|
+
format: 'esm',
|
|
27
|
+
preserveModules: true,
|
|
28
|
+
preserveModulesRoot: 'src',
|
|
29
|
+
entryFileNames: '[name].js'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// CJS build
|
|
34
|
+
{
|
|
35
|
+
input: 'src/index.ts',
|
|
36
|
+
external,
|
|
37
|
+
plugins: [
|
|
38
|
+
resolve(),
|
|
39
|
+
typescript({
|
|
40
|
+
tsconfig: './tsconfig.json',
|
|
41
|
+
declaration: false,
|
|
42
|
+
compilerOptions: { outDir: undefined }
|
|
43
|
+
})
|
|
44
|
+
],
|
|
45
|
+
output: {
|
|
46
|
+
dir: 'dist/cjs',
|
|
47
|
+
format: 'cjs',
|
|
48
|
+
preserveModules: true,
|
|
49
|
+
preserveModulesRoot: 'src',
|
|
50
|
+
entryFileNames: '[name].js'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
];
|
package/src/converter.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import timezone from 'dayjs/plugin/timezone';
|
|
1
|
+
import { parseISO } from 'date-fns';
|
|
2
|
+
import { TZDate } from '@date-fns/tz';
|
|
4
3
|
import { timezones } from './mapping/timezones';
|
|
5
4
|
import {
|
|
6
5
|
UnknownAirportError,
|
|
@@ -8,53 +7,78 @@ import {
|
|
|
8
7
|
UnknownTimezoneError
|
|
9
8
|
} from './errors';
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const ISO_LOCAL_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
11
|
+
|
|
12
|
+
function parseLocalIso(
|
|
13
|
+
localIso: string
|
|
14
|
+
): [number, number, number, number, number, number] {
|
|
15
|
+
const m = ISO_LOCAL_RE.exec(localIso);
|
|
16
|
+
if (!m) throw new InvalidTimestampError(localIso);
|
|
17
|
+
const [, Y, Mo, D, h, mi, s] = m;
|
|
18
|
+
return [
|
|
19
|
+
Number(Y),
|
|
20
|
+
Number(Mo) - 1,
|
|
21
|
+
Number(D),
|
|
22
|
+
Number(h),
|
|
23
|
+
Number(mi),
|
|
24
|
+
s ? Number(s) : 0
|
|
25
|
+
];
|
|
26
|
+
}
|
|
14
27
|
|
|
15
28
|
/**
|
|
16
|
-
* Convert local ISO
|
|
17
|
-
*
|
|
29
|
+
* Convert a local ISO‐8601 string at an airport (IATA) into a UTC ISO string.
|
|
30
|
+
* Always emits "YYYY-MM-DDTHH:mm:ssZ" (no milliseconds).
|
|
18
31
|
*/
|
|
19
32
|
export function convertToUTC(localIso: string, iata: string): string {
|
|
20
33
|
const tz = timezones[iata];
|
|
21
34
|
if (!tz) throw new UnknownAirportError(iata);
|
|
22
35
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
36
|
+
// Quick semantic check
|
|
37
|
+
const base = parseISO(localIso);
|
|
38
|
+
if (isNaN(base.getTime())) throw new InvalidTimestampError(localIso);
|
|
39
|
+
|
|
40
|
+
const [year, month, day, hour, minute, second] = parseLocalIso(localIso);
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
let dt;
|
|
42
|
+
let zoned: TZDate;
|
|
29
43
|
try {
|
|
30
|
-
|
|
44
|
+
zoned = TZDate.tz(tz, year, month, day, hour, minute, second);
|
|
31
45
|
} catch {
|
|
32
|
-
// Shouldn't happen for valid tz, but just in case:
|
|
33
46
|
throw new InvalidTimestampError(localIso);
|
|
34
47
|
}
|
|
48
|
+
if (isNaN(zoned.getTime())) throw new InvalidTimestampError(localIso);
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
// Strip ".000" from the ISO string
|
|
51
|
+
return new Date(zoned.getTime()).toISOString().replace('.000Z', 'Z');
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
/**
|
|
40
|
-
* Convert local ISO
|
|
41
|
-
*
|
|
55
|
+
* Convert a local ISO‐8601 string in any IANA timezone into a UTC ISO string.
|
|
56
|
+
* Always emits "YYYY-MM-DDTHH:mm:ssZ" (no milliseconds).
|
|
42
57
|
*/
|
|
43
58
|
export function convertLocalToUTCByZone(
|
|
44
59
|
localIso: string,
|
|
45
60
|
timeZone: string
|
|
46
61
|
): string {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
// Validate timezone
|
|
63
|
+
try {
|
|
64
|
+
new Intl.DateTimeFormat('en-US', { timeZone }).format();
|
|
65
|
+
} catch {
|
|
66
|
+
throw new UnknownTimezoneError(timeZone);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Quick semantic check
|
|
70
|
+
const base = parseISO(localIso);
|
|
71
|
+
if (isNaN(base.getTime())) throw new InvalidTimestampError(localIso);
|
|
72
|
+
|
|
73
|
+
const [year, month, day, hour, minute, second] = parseLocalIso(localIso);
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
let dt;
|
|
75
|
+
let zoned: TZDate;
|
|
53
76
|
try {
|
|
54
|
-
|
|
77
|
+
zoned = TZDate.tz(timeZone, year, month, day, hour, minute, second);
|
|
55
78
|
} catch {
|
|
56
79
|
throw new UnknownTimezoneError(timeZone);
|
|
57
80
|
}
|
|
81
|
+
if (isNaN(zoned.getTime())) throw new InvalidTimestampError(localIso);
|
|
58
82
|
|
|
59
|
-
return
|
|
83
|
+
return new Date(zoned.getTime()).toISOString().replace('.000Z', 'Z');
|
|
60
84
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// Test CommonJS build
|
|
5
|
+
describe('CommonJS build (dist/cjs)', () => {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
7
|
+
const cjs = require('../dist/cjs/index.js') as any;
|
|
8
|
+
const { convertToUTC, convertLocalToUTCByZone, getAirportInfo } = cjs;
|
|
9
|
+
|
|
10
|
+
it('convertToUTC works in CommonJS build', () => {
|
|
11
|
+
expect(convertToUTC('2025-05-02T14:30', 'JFK')).toBe('2025-05-02T18:30:00Z');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('convertLocalToUTCByZone works in CommonJS build', () => {
|
|
15
|
+
expect(convertLocalToUTCByZone('2025-05-02T14:30:00', 'Europe/London'))
|
|
16
|
+
.toBe('2025-05-02T13:30:00Z');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('getAirportInfo works in CommonJS build', () => {
|
|
20
|
+
const info = getAirportInfo('JFK');
|
|
21
|
+
expect(info).toHaveProperty('timezone');
|
|
22
|
+
expect(info).toHaveProperty('latitude');
|
|
23
|
+
expect(info).toHaveProperty('longitude');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Test ESM build using a subprocess to dynamically import the file
|
|
28
|
+
describe('ESM build (dist/esm)', () => {
|
|
29
|
+
const esmPath = path.resolve(__dirname, '../dist/esm/index.js');
|
|
30
|
+
const fileUrl = 'file://' + esmPath;
|
|
31
|
+
|
|
32
|
+
it('convertToUTC works in ESM build', () => {
|
|
33
|
+
const cmd = `node -e "(async()=>{ const m = await import('${fileUrl}'); console.log(m.convertToUTC('2025-05-02T14:30','JFK')); })()"`;
|
|
34
|
+
const result = execSync(cmd, { encoding: 'utf-8' }).trim();
|
|
35
|
+
expect(result).toBe('2025-05-02T18:30:00Z');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('convertLocalToUTCByZone works in ESM build', () => {
|
|
39
|
+
const cmd = `node -e "(async()=>{ const m = await import('${fileUrl}'); console.log(m.convertLocalToUTCByZone('2025-05-02T14:30:00','Europe/London')); })()"`;
|
|
40
|
+
const result = execSync(cmd, { encoding: 'utf-8' }).trim();
|
|
41
|
+
expect(result).toBe('2025-05-02T13:30:00Z');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('getAirportInfo works in ESM build', () => {
|
|
45
|
+
const cmd = `node -e "(async()=>{ const m = await import('${fileUrl}'); console.log(JSON.stringify(m.getAirportInfo('JFK'))); })()"`;
|
|
46
|
+
const result = execSync(cmd, { encoding: 'utf-8' }).trim();
|
|
47
|
+
const info = JSON.parse(result);
|
|
48
|
+
expect(info).toHaveProperty('timezone');
|
|
49
|
+
expect(info).toHaveProperty('latitude');
|
|
50
|
+
expect(info).toHaveProperty('longitude');
|
|
51
|
+
});
|
|
52
|
+
});
|
package/tests/converter.test.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import dayjs from 'dayjs';
|
|
2
1
|
import { convertToUTC, convertLocalToUTCByZone } from '../src/converter';
|
|
3
2
|
import {
|
|
4
3
|
UnknownAirportError,
|
|
@@ -7,7 +6,9 @@ import {
|
|
|
7
6
|
} from '../src/errors';
|
|
8
7
|
import { timezones } from '../src/mapping/timezones';
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
const { TZDate } = require('@date-fns/tz');
|
|
10
|
+
|
|
11
|
+
// Dynamically find an invalid 3-letter IATA
|
|
11
12
|
function getInvalidIata(): string {
|
|
12
13
|
const existing = new Set(Object.keys(timezones));
|
|
13
14
|
for (let a = 65; a <= 90; a++) {
|
|
@@ -18,12 +19,11 @@ function getInvalidIata(): string {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
throw new Error('All
|
|
22
|
+
throw new Error('All codes taken?!');
|
|
22
23
|
}
|
|
23
|
-
|
|
24
24
|
const invalidIata = getInvalidIata();
|
|
25
25
|
|
|
26
|
-
describe('convertToUTC
|
|
26
|
+
describe('convertToUTC', () => {
|
|
27
27
|
it('converts JFK local time (UTC–4 in May) correctly', () => {
|
|
28
28
|
expect(convertToUTC('2025-05-02T14:30', 'JFK'))
|
|
29
29
|
.toBe('2025-05-02T18:30:00Z');
|
|
@@ -35,19 +35,34 @@ describe('convertToUTC (Day.js)', () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it('throws InvalidTimestampError for malformed timestamp', () => {
|
|
38
|
-
expect(() => convertToUTC('
|
|
38
|
+
expect(() => convertToUTC('not-a-timestamp', 'JFK'))
|
|
39
|
+
.toThrow(InvalidTimestampError);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('throws InvalidTimestampError for fractional seconds format', () => {
|
|
43
|
+
expect(() => convertToUTC('2025-05-02T14:30:00.123', 'JFK'))
|
|
39
44
|
.toThrow(InvalidTimestampError);
|
|
40
45
|
});
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
(
|
|
45
|
-
|
|
47
|
+
describe('error branches', () => {
|
|
48
|
+
const original = TZDate.tz;
|
|
49
|
+
afterEach(() => { TZDate.tz = original; });
|
|
50
|
+
|
|
51
|
+
it('throws InvalidTimestampError if TZDate.tz throws', () => {
|
|
52
|
+
TZDate.tz = () => { throw new Error('forced'); };
|
|
46
53
|
expect(() => convertToUTC('2025-05-02T14:30', 'JFK'))
|
|
47
54
|
.toThrow(InvalidTimestampError);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('throws InvalidTimestampError if TZDate.tz returns invalid Date', () => {
|
|
58
|
+
TZDate.tz = () => {
|
|
59
|
+
const d = new Date(NaN);
|
|
60
|
+
Object.setPrototypeOf(d, TZDate.prototype);
|
|
61
|
+
return d;
|
|
62
|
+
};
|
|
63
|
+
expect(() => convertToUTC('2025-05-02T14:30', 'JFK'))
|
|
64
|
+
.toThrow(InvalidTimestampError);
|
|
65
|
+
});
|
|
51
66
|
});
|
|
52
67
|
});
|
|
53
68
|
|
|
@@ -59,7 +74,7 @@ describe('convertLocalToUTCByZone', () => {
|
|
|
59
74
|
|
|
60
75
|
it('throws UnknownTimezoneError for invalid tz', () => {
|
|
61
76
|
expect(() =>
|
|
62
|
-
convertLocalToUTCByZone('2025-05-02T14:30:00', '
|
|
77
|
+
convertLocalToUTCByZone('2025-05-02T14:30:00', 'Not/A_Zone')
|
|
63
78
|
).toThrow(UnknownTimezoneError);
|
|
64
79
|
});
|
|
65
80
|
|
|
@@ -68,4 +83,33 @@ describe('convertLocalToUTCByZone', () => {
|
|
|
68
83
|
convertLocalToUTCByZone('bad-format', 'Europe/London')
|
|
69
84
|
).toThrow(InvalidTimestampError);
|
|
70
85
|
});
|
|
86
|
+
|
|
87
|
+
it('throws InvalidTimestampError for fractional seconds format', () => {
|
|
88
|
+
expect(() =>
|
|
89
|
+
convertLocalToUTCByZone('2025-05-02T14:30:00.123', 'Europe/London')
|
|
90
|
+
).toThrow(InvalidTimestampError);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('error branches', () => {
|
|
94
|
+
const original = TZDate.tz;
|
|
95
|
+
afterEach(() => { TZDate.tz = original; });
|
|
96
|
+
|
|
97
|
+
it('throws UnknownTimezoneError if TZDate.tz throws for valid zone', () => {
|
|
98
|
+
TZDate.tz = () => { throw new RangeError('forced'); };
|
|
99
|
+
expect(() =>
|
|
100
|
+
convertLocalToUTCByZone('2025-05-02T14:30:00', 'Europe/London')
|
|
101
|
+
).toThrow(UnknownTimezoneError);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('throws InvalidTimestampError if TZDate.tz returns invalid Date', () => {
|
|
105
|
+
TZDate.tz = () => {
|
|
106
|
+
const d = new Date(NaN);
|
|
107
|
+
Object.setPrototypeOf(d, TZDate.prototype);
|
|
108
|
+
return d;
|
|
109
|
+
};
|
|
110
|
+
expect(() =>
|
|
111
|
+
convertLocalToUTCByZone('2025-05-02T14:30:00', 'Europe/London')
|
|
112
|
+
).toThrow(InvalidTimestampError);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
71
115
|
});
|
package/tsconfig.json
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "node",
|
|
6
6
|
"declaration": true,
|
|
7
|
+
"rootDir": "src",
|
|
7
8
|
"outDir": "dist",
|
|
8
9
|
"strict": true,
|
|
9
10
|
"esModuleInterop": true,
|
|
10
11
|
"forceConsistentCasingInFileNames": true,
|
|
11
12
|
"skipLibCheck": true
|
|
12
13
|
},
|
|
13
|
-
"include": [
|
|
14
|
-
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*"
|
|
16
|
+
]
|
|
17
|
+
}
|