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
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fetch from 'node-fetch';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
async function generateMapping() {
|
|
6
|
-
const url = 'https://raw.githubusercontent.com/opentraveldata/opentraveldata/master/opentraveldata/optd_por_public.csv';
|
|
7
|
-
const res = await fetch(url);
|
|
8
|
-
if (!res.ok)
|
|
9
|
-
throw new Error(`Fetch failed: ${res.statusText}`);
|
|
10
|
-
const text = await res.text();
|
|
11
|
-
const lines = text.split('\n').filter(l => l.trim());
|
|
12
|
-
const header = lines[0].split('^');
|
|
13
|
-
const idx = {
|
|
14
|
-
iata: header.indexOf('iata_code'),
|
|
15
|
-
tz: header.indexOf('timezone'),
|
|
16
|
-
lat: header.indexOf('latitude'),
|
|
17
|
-
lon: header.indexOf('longitude'),
|
|
18
|
-
name: header.indexOf('name'),
|
|
19
|
-
city: header.indexOf('city_name_list'),
|
|
20
|
-
country: header.indexOf('country_code')
|
|
21
|
-
};
|
|
22
|
-
if (Object.values(idx).some(i => i < 0)) {
|
|
23
|
-
throw new Error('Missing required OPTD columns');
|
|
24
|
-
}
|
|
25
|
-
const timezonesMap = {};
|
|
26
|
-
const geoMap = {};
|
|
27
|
-
for (let i = 1; i < lines.length; i++) {
|
|
28
|
-
const cols = lines[i].split('^');
|
|
29
|
-
const code = cols[idx.iata];
|
|
30
|
-
if (!code || code.length !== 3)
|
|
31
|
-
continue;
|
|
32
|
-
const tz = cols[idx.tz];
|
|
33
|
-
if (tz)
|
|
34
|
-
timezonesMap[code] = tz;
|
|
35
|
-
const lat = parseFloat(cols[idx.lat]);
|
|
36
|
-
const lon = parseFloat(cols[idx.lon]);
|
|
37
|
-
const name = cols[idx.name];
|
|
38
|
-
const city = cols[idx.city].split(',')[0].trim();
|
|
39
|
-
const country = cols[idx.country];
|
|
40
|
-
if (!isNaN(lat) && !isNaN(lon) && name && city && country) {
|
|
41
|
-
geoMap[code] = { latitude: lat, longitude: lon, name, city, country };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const sortedCodes = Object.keys(timezonesMap).sort();
|
|
45
|
-
const sortedTz = Object.fromEntries(sortedCodes.map(c => [c, timezonesMap[c]]));
|
|
46
|
-
const sortedGeo = Object.fromEntries(sortedCodes.filter(c => geoMap[c]).map(c => [c, geoMap[c]]));
|
|
47
|
-
const dir = path.resolve(path.dirname(import.meta.url.replace('file://', '')), '../src/mapping');
|
|
48
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
-
// Write TypeScript modules
|
|
50
|
-
const tzTs = [
|
|
51
|
-
'// generated — do not edit',
|
|
52
|
-
'export const timezones: Record<string, string> = ',
|
|
53
|
-
JSON.stringify(sortedTz, null, 2) + ';',
|
|
54
|
-
].join('\n');
|
|
55
|
-
fs.writeFileSync(path.join(dir, 'timezones.ts'), tzTs + '\n');
|
|
56
|
-
const geoEntries = Object.entries(sortedGeo)
|
|
57
|
-
.map(([code, g]) => ` "${code}": ${JSON.stringify(g)}`)
|
|
58
|
-
.join(',\n');
|
|
59
|
-
const geoTs = [
|
|
60
|
-
'// generated — do not edit',
|
|
61
|
-
'export interface GeoEntry {',
|
|
62
|
-
' latitude: number;',
|
|
63
|
-
' longitude: number;',
|
|
64
|
-
' name: string;',
|
|
65
|
-
' city: string;',
|
|
66
|
-
' country: string;',
|
|
67
|
-
'}', '',
|
|
68
|
-
'export const geo: Record<string, GeoEntry> = {',
|
|
69
|
-
geoEntries,
|
|
70
|
-
'};',
|
|
71
|
-
].join('\n');
|
|
72
|
-
fs.writeFileSync(path.join(dir, 'geo.ts'), geoTs + '\n');
|
|
73
|
-
console.log(`✅ Mappings: ${sortedCodes.length} timezones, ${Object.keys(sortedGeo).length} geo entries`);
|
|
74
|
-
}
|
|
75
|
-
generateMapping().catch(err => {
|
|
76
|
-
console.error(err);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
});
|
package/dist/src/converter.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert local ISO 8601 (YYYY-MM-DDTHH:mm) at an airport into UTC ISO string.
|
|
3
|
-
* @throws UnknownAirportError | InvalidTimestampError
|
|
4
|
-
*/
|
|
5
|
-
export declare function convertToUTC(localIso: string, iata: string): string;
|
|
6
|
-
/**
|
|
7
|
-
* Convert local ISO 8601 string in any IANA timezone to UTC ISO string.
|
|
8
|
-
* @throws UnknownTimezoneError | InvalidTimestampError
|
|
9
|
-
*/
|
|
10
|
-
export declare function convertLocalToUTCByZone(localIso: string, timeZone: string): string;
|
package/dist/src/converter.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import dayjs from 'dayjs';
|
|
2
|
-
import utc from 'dayjs/plugin/utc';
|
|
3
|
-
import timezone from 'dayjs/plugin/timezone';
|
|
4
|
-
import { timezones } from './mapping/timezones';
|
|
5
|
-
import { UnknownAirportError, InvalidTimestampError, UnknownTimezoneError } from './errors';
|
|
6
|
-
// Initialize plugins
|
|
7
|
-
dayjs.extend(utc);
|
|
8
|
-
dayjs.extend(timezone);
|
|
9
|
-
/**
|
|
10
|
-
* Convert local ISO 8601 (YYYY-MM-DDTHH:mm) at an airport into UTC ISO string.
|
|
11
|
-
* @throws UnknownAirportError | InvalidTimestampError
|
|
12
|
-
*/
|
|
13
|
-
export function convertToUTC(localIso, iata) {
|
|
14
|
-
const tz = timezones[iata];
|
|
15
|
-
if (!tz)
|
|
16
|
-
throw new UnknownAirportError(iata);
|
|
17
|
-
// 1) Pre-validate timestamp
|
|
18
|
-
const localDt = dayjs(localIso);
|
|
19
|
-
if (!localDt.isValid())
|
|
20
|
-
throw new InvalidTimestampError(localIso);
|
|
21
|
-
// 2) Then apply timezone conversion
|
|
22
|
-
let dt;
|
|
23
|
-
try {
|
|
24
|
-
dt = dayjs.tz(localIso, tz);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// Shouldn't happen for valid tz, but just in case:
|
|
28
|
-
throw new InvalidTimestampError(localIso);
|
|
29
|
-
}
|
|
30
|
-
return dt.utc().format(); // "YYYY-MM-DDTHH:mm:ssZ"
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Convert local ISO 8601 string in any IANA timezone to UTC ISO string.
|
|
34
|
-
* @throws UnknownTimezoneError | InvalidTimestampError
|
|
35
|
-
*/
|
|
36
|
-
export function convertLocalToUTCByZone(localIso, timeZone) {
|
|
37
|
-
// 1) Validate timestamp first
|
|
38
|
-
const localDt = dayjs(localIso);
|
|
39
|
-
if (!localDt.isValid())
|
|
40
|
-
throw new InvalidTimestampError(localIso);
|
|
41
|
-
// 2) Apply timezone, catching only invalid timezone errors
|
|
42
|
-
let dt;
|
|
43
|
-
try {
|
|
44
|
-
dt = dayjs.tz(localIso, timeZone);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
throw new UnknownTimezoneError(timeZone);
|
|
48
|
-
}
|
|
49
|
-
return dt.utc().format();
|
|
50
|
-
}
|
package/dist/src/index.js
DELETED
package/dist/src/info.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { timezones } from './mapping/timezones';
|
|
2
|
-
import { geo } from './mapping/geo';
|
|
3
|
-
import { UnknownAirportError } from './errors';
|
|
4
|
-
/** @throws UnknownAirportError */
|
|
5
|
-
export function getAirportInfo(iata) {
|
|
6
|
-
const tz = timezones[iata];
|
|
7
|
-
const g = geo[iata];
|
|
8
|
-
if (!tz || !g)
|
|
9
|
-
throw new UnknownAirportError(iata);
|
|
10
|
-
return { timezone: tz, ...g };
|
|
11
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import dayjs from 'dayjs';
|
|
2
|
-
import { convertToUTC, convertLocalToUTCByZone } from '../src/converter';
|
|
3
|
-
import { UnknownAirportError, InvalidTimestampError, UnknownTimezoneError } from '../src/errors';
|
|
4
|
-
import { timezones } from '../src/mapping/timezones';
|
|
5
|
-
// Dynamically find the first 3-letter code not in our mapping
|
|
6
|
-
function getInvalidIata() {
|
|
7
|
-
const existing = new Set(Object.keys(timezones));
|
|
8
|
-
for (let a = 65; a <= 90; a++) {
|
|
9
|
-
for (let b = 65; b <= 90; b++) {
|
|
10
|
-
for (let c = 65; c <= 90; c++) {
|
|
11
|
-
const code = String.fromCharCode(a, b, c);
|
|
12
|
-
if (!existing.has(code))
|
|
13
|
-
return code;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
throw new Error('All 3-letter codes are taken?!');
|
|
18
|
-
}
|
|
19
|
-
const invalidIata = getInvalidIata();
|
|
20
|
-
describe('convertToUTC (Day.js)', () => {
|
|
21
|
-
it('converts JFK local time (UTC–4 in May) correctly', () => {
|
|
22
|
-
expect(convertToUTC('2025-05-02T14:30', 'JFK'))
|
|
23
|
-
.toBe('2025-05-02T18:30:00Z');
|
|
24
|
-
});
|
|
25
|
-
it('throws UnknownAirportError for bad IATA', () => {
|
|
26
|
-
expect(() => convertToUTC('2025-05-02T14:30', invalidIata))
|
|
27
|
-
.toThrow(UnknownAirportError);
|
|
28
|
-
});
|
|
29
|
-
it('throws InvalidTimestampError for malformed timestamp', () => {
|
|
30
|
-
expect(() => convertToUTC('invalid-format', 'JFK'))
|
|
31
|
-
.toThrow(InvalidTimestampError);
|
|
32
|
-
});
|
|
33
|
-
it('throws InvalidTimestampError if dayjs.tz unexpectedly throws', () => {
|
|
34
|
-
const orig = dayjs.tz;
|
|
35
|
-
dayjs.tz = () => { throw new Error(); };
|
|
36
|
-
try {
|
|
37
|
-
expect(() => convertToUTC('2025-05-02T14:30', 'JFK'))
|
|
38
|
-
.toThrow(InvalidTimestampError);
|
|
39
|
-
}
|
|
40
|
-
finally {
|
|
41
|
-
dayjs.tz = orig;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
describe('convertLocalToUTCByZone', () => {
|
|
46
|
-
it('converts London local time to UTC', () => {
|
|
47
|
-
expect(convertLocalToUTCByZone('2025-05-02T14:30:00', 'Europe/London'))
|
|
48
|
-
.toBe('2025-05-02T13:30:00Z');
|
|
49
|
-
});
|
|
50
|
-
it('throws UnknownTimezoneError for invalid tz', () => {
|
|
51
|
-
expect(() => convertLocalToUTCByZone('2025-05-02T14:30:00', 'Invalid/Zone')).toThrow(UnknownTimezoneError);
|
|
52
|
-
});
|
|
53
|
-
it('throws InvalidTimestampError for malformed timestamp', () => {
|
|
54
|
-
expect(() => convertLocalToUTCByZone('bad-format', 'Europe/London')).toThrow(InvalidTimestampError);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/tests/info.test.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { getAirportInfo } from '../src/info';
|
|
2
|
-
import { UnknownAirportError } from '../src/errors';
|
|
3
|
-
import { timezones } from '../src/mapping/timezones';
|
|
4
|
-
import { geo } from '../src/mapping/geo';
|
|
5
|
-
// Dynamically find the first 3-letter code not in our mapping
|
|
6
|
-
function getInvalidIata() {
|
|
7
|
-
const existing = new Set(Object.keys(timezones));
|
|
8
|
-
for (let a = 65; a <= 90; a++) {
|
|
9
|
-
for (let b = 65; b <= 90; b++) {
|
|
10
|
-
for (let c = 65; c <= 90; c++) {
|
|
11
|
-
const code = String.fromCharCode(a, b, c);
|
|
12
|
-
if (!existing.has(code))
|
|
13
|
-
return code;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
throw new Error('All 3-letter codes are taken?!');
|
|
18
|
-
}
|
|
19
|
-
const invalidIata = getInvalidIata();
|
|
20
|
-
describe('getAirportInfo', () => {
|
|
21
|
-
const validCodes = Object.keys(timezones).filter(i => geo[i]);
|
|
22
|
-
const sample = validCodes.length > 0 ? validCodes[0] : 'JFK';
|
|
23
|
-
it('returns full info for a valid IATA', () => {
|
|
24
|
-
const info = getAirportInfo(sample);
|
|
25
|
-
expect(info).toEqual({
|
|
26
|
-
timezone: timezones[sample],
|
|
27
|
-
...geo[sample]
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
it('throws UnknownAirportError for missing IATA', () => {
|
|
31
|
-
expect(() => getAirportInfo(invalidIata))
|
|
32
|
-
.toThrow(UnknownAirportError);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|