airport-utils 1.3.29 → 1.4.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/README.md CHANGED
@@ -7,9 +7,9 @@ Convert local ISO 8601 timestamps to UTC using airport IATA codes, with airport
7
7
  - **Local → UTC** conversion only (ISO 8601 in, ISO 8601 UTC out)
8
8
  - Built-in IATA→IANA timezone mapping (OPTD)
9
9
  - Built-in airport geo-data: latitude, longitude, name, city, country, country name
10
- - TypeScript support, Node 20+
10
+ - TypeScript support, Node 22+
11
11
  - Synchronous API with custom error classes
12
- - Day.js (UTC & Timezone plugins) under the hood
12
+ - `@date-fns/tz` under the hood
13
13
  - Daily auto-updated mapping via GitHub Actions
14
14
  - Jest tests with 100% coverage
15
15
  - Automated releases via semantic-release
@@ -38,6 +38,7 @@ try {
38
38
  console.log(utc); // "2025-05-02T18:30:00Z"
39
39
  } catch (err) {
40
40
  // handle UnknownAirportError or InvalidTimestampError
41
+ // InvalidTimestampError is also thrown for nonexistent or ambiguous DST wall-clock times
41
42
  }
42
43
 
43
44
  // Convert local time by zone
@@ -46,6 +47,7 @@ try {
46
47
  console.log(utc2); // "2025-05-02T13:30:00Z"
47
48
  } catch (err) {
48
49
  // handle UnknownTimezoneError or InvalidTimestampError
50
+ // InvalidTimestampError is also thrown for nonexistent or ambiguous DST wall-clock times
49
51
  }
50
52
 
51
53
  // Get full airport info
@@ -69,7 +71,7 @@ try {
69
71
  // Get all airports
70
72
  import { getAllAirports } from 'airport-utils';
71
73
  const airports = getAllAirports();
72
- console.log(airports.length); // > 10000
74
+ console.log(airports.length); // > 8000
73
75
  ```
74
76
 
75
77
  ### API
@@ -141,4 +143,4 @@ npm run update:mapping
141
143
 
142
144
  ## License
143
145
 
144
- MIT
146
+ MIT
@@ -6,6 +6,7 @@ var errors = require('./errors.cjs');
6
6
 
7
7
  const ISO_LOCAL_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
8
8
  const VALID_TIMEZONE_CACHE = new Map();
9
+ const INVALID_LOCAL_INTERVAL_CACHE = new Map();
9
10
  function isLeapYear(year) {
10
11
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
11
12
  }
@@ -54,19 +55,75 @@ function assertValidTimezone(timeZone) {
54
55
  throw new errors.UnknownTimezoneError(timeZone);
55
56
  }
56
57
  }
58
+ function localPartsToNaiveUtcMs(parts) {
59
+ const [year, month, day, hour, minute, second] = parts;
60
+ return Date.UTC(year, month, day, hour, minute, second);
61
+ }
62
+ function resolveUtcMs(localNaiveMs, timeZone) {
63
+ let utcMs = localNaiveMs;
64
+ for (let i = 0; i < 4; i++) {
65
+ const offsetMinutes = tz.tzOffset(timeZone, new Date(utcMs));
66
+ if (!Number.isFinite(offsetMinutes)) {
67
+ throw new RangeError(`Invalid timezone offset for: ${timeZone}`);
68
+ }
69
+ const nextUtcMs = localNaiveMs - offsetMinutes * 60_000;
70
+ if (nextUtcMs === utcMs)
71
+ return utcMs;
72
+ utcMs = nextUtcMs;
73
+ }
74
+ return utcMs;
75
+ }
76
+ function getInvalidLocalIntervals(timeZone, year) {
77
+ const cacheKey = `${timeZone}:${year}`;
78
+ const cached = INVALID_LOCAL_INTERVAL_CACHE.get(cacheKey);
79
+ if (cached)
80
+ return cached;
81
+ const yearStart = Date.UTC(year, 0, 1, 0, 0, 0);
82
+ const yearEnd = Date.UTC(year + 1, 0, 1, 0, 0, 0);
83
+ const transitions = tz.tzScan(timeZone, {
84
+ start: new Date(Date.UTC(year - 1, 11, 31, 0, 0, 0)),
85
+ end: new Date(Date.UTC(year + 1, 0, 2, 0, 0, 0))
86
+ });
87
+ const intervals = transitions
88
+ .map((transition) => {
89
+ const previousOffset = transition.offset - transition.change;
90
+ if (transition.change < 0) {
91
+ return {
92
+ start: transition.date.getTime() + transition.offset * 60_000,
93
+ end: transition.date.getTime() + previousOffset * 60_000
94
+ };
95
+ }
96
+ return {
97
+ start: transition.date.getTime() + previousOffset * 60_000,
98
+ end: transition.date.getTime() + transition.offset * 60_000
99
+ };
100
+ })
101
+ .filter((interval) => interval.end > yearStart && interval.start < yearEnd);
102
+ INVALID_LOCAL_INTERVAL_CACHE.set(cacheKey, intervals);
103
+ return intervals;
104
+ }
105
+ function assertResolvableLocalTime(localIso, timeZone, year, localNaiveMs) {
106
+ const intervals = getInvalidLocalIntervals(timeZone, year);
107
+ for (const interval of intervals) {
108
+ if (localNaiveMs >= interval.start && localNaiveMs < interval.end) {
109
+ throw new errors.InvalidTimestampError(localIso);
110
+ }
111
+ }
112
+ }
57
113
  function toUtcIso(localIso, timeZone, onTimeZoneError) {
58
- const [year, month, day, hour, minute, second] = parseLocalIsoStrict(localIso);
59
- let zoned;
114
+ const parts = parseLocalIsoStrict(localIso);
115
+ const [year] = parts;
116
+ const localNaiveMs = localPartsToNaiveUtcMs(parts);
117
+ assertResolvableLocalTime(localIso, timeZone, year, localNaiveMs);
118
+ let utcMs;
60
119
  try {
61
- zoned = tz.TZDate.tz(timeZone, year, month, day, hour, minute, second);
120
+ utcMs = resolveUtcMs(localNaiveMs, timeZone);
62
121
  }
63
122
  catch (err) {
64
123
  throw onTimeZoneError(err);
65
124
  }
66
- if (isNaN(zoned.getTime()))
67
- throw new errors.InvalidTimestampError(localIso);
68
125
  // Strip ".000" from the ISO string
69
- return new Date(zoned.getTime()).toISOString().replace('.000Z', 'Z');
126
+ return new Date(utcMs).toISOString().replace('.000Z', 'Z');
70
127
  }
71
128
  /**
72
129
  * Convert a local ISO‐8601 string at an airport (IATA) into a UTC ISO string.
package/dist/cjs/info.cjs CHANGED
@@ -4,6 +4,7 @@ var timezones = require('./mapping/timezones.cjs');
4
4
  var geo = require('./mapping/geo.cjs');
5
5
  var errors = require('./errors.cjs');
6
6
 
7
+ const GEO_IATAS = Object.keys(geo.geo);
7
8
  /** @throws UnknownAirportError */
8
9
  function getAirportInfo(iata) {
9
10
  const tz = timezones.timezones[iata];
@@ -14,7 +15,7 @@ function getAirportInfo(iata) {
14
15
  }
15
16
  function getAllAirports() {
16
17
  const all = [];
17
- for (const iata of Object.keys(geo.geo)) {
18
+ for (const iata of GEO_IATAS) {
18
19
  const tz = timezones.timezones[iata];
19
20
  const g = geo.geo[iata];
20
21
  if (!tz || !g)