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.
Files changed (39) hide show
  1. package/.github/dependabot.yml +3 -0
  2. package/.github/workflows/publish.yml +0 -4
  3. package/.github/workflows/update-mapping.yml +1 -1
  4. package/.releaserc.json +1 -2
  5. package/dist/cjs/converter.js +78 -0
  6. package/dist/cjs/errors.js +24 -0
  7. package/dist/cjs/index.js +14 -0
  8. package/dist/cjs/info.js +16 -0
  9. package/dist/cjs/mapping/geo.js +11991 -0
  10. package/dist/cjs/mapping/timezones.js +12005 -0
  11. package/dist/esm/converter.js +75 -0
  12. package/dist/{src → esm}/errors.js +5 -3
  13. package/dist/esm/index.js +3 -0
  14. package/dist/esm/info.js +14 -0
  15. package/dist/{src → esm}/mapping/geo.js +5 -3
  16. package/dist/{src → esm}/mapping/timezones.js +3 -1
  17. package/dist/types/converter.d.ts +10 -0
  18. package/jest.config.js +5 -1
  19. package/package.json +24 -5
  20. package/rollup.config.js +53 -0
  21. package/src/converter.ts +49 -25
  22. package/tests/built.test.ts +52 -0
  23. package/tests/converter.test.ts +58 -14
  24. package/tsconfig.json +5 -2
  25. package/dist/scripts/generateMapping.d.ts +0 -2
  26. package/dist/scripts/generateMapping.js +0 -78
  27. package/dist/src/converter.d.ts +0 -10
  28. package/dist/src/converter.js +0 -50
  29. package/dist/src/index.js +0 -3
  30. package/dist/src/info.js +0 -11
  31. package/dist/tests/converter.test.d.ts +0 -1
  32. package/dist/tests/converter.test.js +0 -56
  33. package/dist/tests/info.test.d.ts +0 -1
  34. package/dist/tests/info.test.js +0 -34
  35. /package/dist/{src → types}/errors.d.ts +0 -0
  36. /package/dist/{src → types}/index.d.ts +0 -0
  37. /package/dist/{src → types}/info.d.ts +0 -0
  38. /package/dist/{src → types}/mapping/geo.d.ts +0 -0
  39. /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
- export class UnknownAirportError extends Error {
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
- export class InvalidTimestampError extends Error {
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
- export class UnknownTimezoneError extends Error {
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 };
@@ -0,0 +1,3 @@
1
+ export { convertLocalToUTCByZone, convertToUTC } from './converter.js';
2
+ export { getAirportInfo } from './info.js';
3
+ export { InvalidTimestampError, UnknownAirportError, UnknownTimezoneError } from './errors.js';
@@ -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
- export const geo = {
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": -0.006439, "name": "Tarbes-Lourdes-Pyrénées Airport", "city": "Tarbes-Lourdes-Pyrénées Airport", "country": "FR" },
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": -0.00875, "name": "Stratford International Station", "city": "London", "country": "GB" },
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
- export const timezones = {
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.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
- "types": "dist/index.d.ts",
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
- "dayjs": "^1.11.7"
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
  }
@@ -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 dayjs from 'dayjs';
2
- import utc from 'dayjs/plugin/utc';
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
- // Initialize plugins
12
- dayjs.extend(utc);
13
- dayjs.extend(timezone);
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 8601 (YYYY-MM-DDTHH:mm) at an airport into UTC ISO string.
17
- * @throws UnknownAirportError | InvalidTimestampError
29
+ * Convert a local ISO8601 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
- // 1) Pre-validate timestamp
24
- const localDt = dayjs(localIso, /* no format, ISO parse */);
25
- if (!localDt.isValid()) throw new InvalidTimestampError(localIso);
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
- // 2) Then apply timezone conversion
28
- let dt;
42
+ let zoned: TZDate;
29
43
  try {
30
- dt = dayjs.tz(localIso, tz);
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
- return dt.utc().format(); // "YYYY-MM-DDTHH:mm:ssZ"
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 8601 string in any IANA timezone to UTC ISO string.
41
- * @throws UnknownTimezoneError | InvalidTimestampError
55
+ * Convert a local ISO8601 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
- // 1) Validate timestamp first
48
- const localDt = dayjs(localIso);
49
- if (!localDt.isValid()) throw new InvalidTimestampError(localIso);
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
- // 2) Apply timezone, catching only invalid timezone errors
52
- let dt;
75
+ let zoned: TZDate;
53
76
  try {
54
- dt = dayjs.tz(localIso, timeZone);
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 dt.utc().format();
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
+ });
@@ -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
- // Dynamically find the first 3-letter code not in our mapping
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 3-letter codes are taken?!');
22
+ throw new Error('All codes taken?!');
22
23
  }
23
-
24
24
  const invalidIata = getInvalidIata();
25
25
 
26
- describe('convertToUTC (Day.js)', () => {
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('invalid-format', 'JFK'))
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
- it('throws InvalidTimestampError if dayjs.tz unexpectedly throws', () => {
43
- const orig = (dayjs as any).tz;
44
- (dayjs as any).tz = () => { throw new Error(); };
45
- try {
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
- } finally {
49
- (dayjs as any).tz = orig;
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', 'Invalid/Zone')
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": ["src/**/*", "scripts/**/*", "tests/**/*"]
14
- }
14
+ "include": [
15
+ "src/**/*"
16
+ ]
17
+ }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};