metar-taf-parser 6.1.3 → 7.0.0

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
@@ -101,11 +101,13 @@ console.log(report.forecast);
101
101
 
102
102
  > ⚠️ **Warning:** Experimental API
103
103
 
104
- Provides all relevant weather conditions for a given timestamp. It returns a `ICompositeForecast` with a `base` and `additional` component. The `base` component is the base weather condition period (type = `FM`, `BECMG`, or `undefined`) - and there will always be one.
104
+ Provides all relevant weather conditions for a given timestamp. It returns an `ICompositeForecast` with a `prevailing` and `supplemental` component. The `prevailing` component is the prevailing weather condition period (type = `FM`, `BECMG`, or `undefined`) - and there will always be one.
105
105
 
106
- The `additional` property is an array of weather condition periods valid for the given timestamp (any `PROB` and/or `TEMPO`)
106
+ The `supplemental` property is an array of weather condition periods valid for the given timestamp (any `PROB`, `TEMPO` and/or `INTER`) - conditions that are ephemeral and/or lower probability.
107
107
 
108
- You will still need to write some logic to use this API to determine what data to use - for example, if `additional[0].visibility` exists, you may want to use it over `base.visibility`.
108
+ You will still need to write some logic to determine what data to use - for example, if `supplemental[0].visibility` exists, you may want to use it over `prevailing.visibility`, or otherwise present it to the user.
109
+
110
+ This function throws a `TimestampOutOfBoundsError` if the provided date is outside of the report validity period.
109
111
 
110
112
  #### Example
111
113
 
@@ -192,6 +192,12 @@ declare enum WeatherChangeType {
192
192
  * visibility two in mist and haze between 0000Z and 0600Z.
193
193
  */
194
194
  TEMPO = "TEMPO",
195
+ /**
196
+ * For periods up to 30 minutes (`INTER` or intermittent).
197
+ *
198
+ * Otherwise, similar to `TEMPO`
199
+ */
200
+ INTER = "INTER",
195
201
  /**
196
202
  * Probability Forecast
197
203
  *
@@ -811,6 +817,9 @@ interface IPrevailingVisibilityRemark extends IBaseRemark {
811
817
 
812
818
  interface ISeaLevelPressureRemark extends IBaseRemark {
813
819
  type: RemarkType.SeaLevelPressure;
820
+ /**
821
+ * Sea level pressure, in hPa/millibars
822
+ */
814
823
  pressure: number;
815
824
  }
816
825
 
@@ -1211,7 +1220,7 @@ interface IBaseTAFTrend extends IAbstractTrend {
1211
1220
  *
1212
1221
  * - FM trends also have `startMinutes`. They **DO NOT** have an explicit end
1213
1222
  * validity (it is implied by the following FM).
1214
- * - All others (PROB, TEMPO, BECMG) have `endDay` and `endHour`.
1223
+ * - All others (PROB, TEMPO, BECMG, INTER) have `endDay` and `endHour`.
1215
1224
  *
1216
1225
  * All properties are allowed to be accessed (as optionals), but if you want
1217
1226
  * type guarantees, you can check the trend type. For example:
@@ -1224,6 +1233,7 @@ interface IBaseTAFTrend extends IAbstractTrend {
1224
1233
  * case WeatherChangeType.PROB:
1225
1234
  * case WeatherChangeType.BECMG:
1226
1235
  * case WeatherChangeType.TEMPO:
1236
+ * case WeatherChangeType.INTER:
1227
1237
  * // trend.validity now has endHour, endDay defined
1228
1238
  * }
1229
1239
  * ```
@@ -1343,17 +1353,16 @@ interface IForecastContainer extends IFlags {
1343
1353
  }
1344
1354
  interface ICompositeForecast {
1345
1355
  /**
1346
- * The base forecast (type is `FM` or initial group)
1356
+ * Prevailing conditions: type is `FM`, `BECMG` or initial conditions (`undefined` type)
1347
1357
  */
1348
- base: Forecast;
1358
+ prevailing: Forecast;
1349
1359
  /**
1350
- * Any forecast here should pre-empt the base forecast. These forecasts may
1351
- * have probabilities of occuring, be temporary, or otherwise notable
1352
- * precipitation events
1360
+ * supplemental forecasts may have probabilities of occuring, be temporary,
1361
+ * or otherwise notable change of conditions. They enhance the prevailing forecast.
1353
1362
  *
1354
- * `type` is (`BECMG`, `TEMPO` or `PROB`)
1363
+ * `type` is (`TEMPO`, `INTER` or `PROB`)
1355
1364
  */
1356
- additional: Forecast[];
1365
+ supplemental: Forecast[];
1357
1366
  }
1358
1367
  declare class TimestampOutOfBoundsError extends ParseError {
1359
1368
  name: string;
@@ -384,6 +384,12 @@ var WeatherChangeType;
384
384
  * visibility two in mist and haze between 0000Z and 0600Z.
385
385
  */
386
386
  WeatherChangeType["TEMPO"] = "TEMPO";
387
+ /**
388
+ * For periods up to 30 minutes (`INTER` or intermittent).
389
+ *
390
+ * Otherwise, similar to `TEMPO`
391
+ */
392
+ WeatherChangeType["INTER"] = "INTER";
387
393
  /**
388
394
  * Probability Forecast
389
395
  *
@@ -1797,7 +1803,6 @@ class MainVisibilityCommand {
1797
1803
  const distance = convertVisibility(matches[1]);
1798
1804
  if (!container.visibility)
1799
1805
  container.visibility = distance;
1800
- container.visibility = { ...container.visibility, ...distance };
1801
1806
  if (matches[2] === "NDV")
1802
1807
  container.visibility.ndv = true;
1803
1808
  return true;
@@ -2159,6 +2164,7 @@ class AbstractParser {
2159
2164
  this.locale = locale;
2160
2165
  this.FM = "FM";
2161
2166
  this.TEMPO = "TEMPO";
2167
+ this.INTER = "INTER";
2162
2168
  this.BECMG = "BECMG";
2163
2169
  this.RMK = "RMK";
2164
2170
  // Safari does not currently support negative lookbehind
@@ -2263,6 +2269,7 @@ class MetarParser extends AbstractParser {
2263
2269
  let i = index + 1;
2264
2270
  while (i < trendParts.length &&
2265
2271
  trendParts[i] !== this.TEMPO &&
2272
+ trendParts[i] !== this.INTER &&
2266
2273
  trendParts[i] !== this.BECMG) {
2267
2274
  if (trendParts[i].startsWith(this.FM) ||
2268
2275
  trendParts[i].startsWith(this.TL) ||
@@ -2306,16 +2313,19 @@ class MetarParser extends AbstractParser {
2306
2313
  metar.nosig = true;
2307
2314
  }
2308
2315
  else if (metarTab[index] === this.TEMPO ||
2316
+ metarTab[index] === this.INTER ||
2309
2317
  metarTab[index] === this.BECMG) {
2318
+ const startIndex = index;
2310
2319
  const trend = {
2311
2320
  type: WeatherChangeType[metarTab[index]],
2312
2321
  weatherConditions: [],
2313
2322
  clouds: [],
2314
2323
  times: [],
2315
2324
  remarks: [],
2316
- raw: input,
2325
+ raw: "",
2317
2326
  };
2318
2327
  index = this.parseTrend(index, trend, metarTab);
2328
+ trend.raw = metarTab.slice(startIndex, index + 1).join(" ");
2319
2329
  metar.trends.push(trend);
2320
2330
  }
2321
2331
  else if (metarTab[index] === this.RMK) {
@@ -2346,6 +2356,27 @@ class TAFParser extends AbstractParser {
2346
2356
  this.TN = "TN";
2347
2357
  _TAFParser_validityPattern.set(this, /^\d{4}\/\d{4}$/);
2348
2358
  }
2359
+ /**
2360
+ * TAF messages can be formatted poorly
2361
+ *
2362
+ * Attempt to handle those situations gracefully
2363
+ */
2364
+ parseMessageStart(input) {
2365
+ let index = 0;
2366
+ if (input[index] === this.TAF)
2367
+ index += 1;
2368
+ if (input[index + 1] === this.TAF)
2369
+ index += 2;
2370
+ const flags1 = findFlags(input[index]);
2371
+ if (flags1)
2372
+ index += 1;
2373
+ if (input[index] === this.TAF)
2374
+ index += 1;
2375
+ const flags2 = findFlags(input[index]);
2376
+ if (flags2)
2377
+ index += 1;
2378
+ return [index, { ...flags1, ...flags2 }];
2379
+ }
2349
2380
  /**
2350
2381
  * the message to parse
2351
2382
  * @param input
@@ -2354,15 +2385,7 @@ class TAFParser extends AbstractParser {
2354
2385
  */
2355
2386
  parse(input) {
2356
2387
  const lines = this.extractLinesTokens(input);
2357
- let index = 0;
2358
- if (lines[0][0] === this.TAF)
2359
- index = 1;
2360
- if (lines[0][1] === this.TAF)
2361
- index = 2;
2362
- const flags = findFlags(lines[0][index]);
2363
- if (flags) {
2364
- index += 1;
2365
- }
2388
+ let [index, flags] = this.parseMessageStart(lines[0]);
2366
2389
  const station = lines[0][index];
2367
2390
  index += 1;
2368
2391
  const time = parseDeliveryTime(lines[0][index]);
@@ -2426,12 +2449,12 @@ class TAFParser extends AbstractParser {
2426
2449
  const singleLine = tafCode.replace(/\n/g, " ");
2427
2450
  const cleanLine = singleLine.replace(/\s{2,}/g, " ");
2428
2451
  const lines = joinProbIfNeeded(cleanLine
2429
- .replace(/\s(?=PROB\d{2}\sTEMPO|TEMPO|BECMG|FM|PROB)/g, "\n")
2452
+ .replace(/\s(?=PROB\d{2}\s(?=TEMPO|INTER)|TEMPO|INTER|BECMG|FM|PROB)/g, "\n")
2430
2453
  .split(/\n/));
2431
2454
  // TODO cleanup
2432
2455
  function joinProbIfNeeded(ls) {
2433
2456
  for (let i = 0; i < ls.length; i++) {
2434
- if (/^PROB\d{2}$/.test(ls[i]) && /^TEMPO/.test(ls[i + 1])) {
2457
+ if (/^PROB\d{2}$/.test(ls[i]) && /^TEMPO|INTER/.test(ls[i + 1])) {
2435
2458
  ls.splice(i, 2, `${ls[i]} ${ls[i + 1]}`);
2436
2459
  }
2437
2460
  }
@@ -2466,7 +2489,8 @@ class TAFParser extends AbstractParser {
2466
2489
  validity,
2467
2490
  raw: lineTokens.join(" "),
2468
2491
  };
2469
- if (lineTokens.length > 1 && lineTokens[1] === this.TEMPO) {
2492
+ if (lineTokens.length > 1 &&
2493
+ (lineTokens[1] === this.TEMPO || lineTokens[1] === this.INTER)) {
2470
2494
  trend = {
2471
2495
  ...this.makeEmptyTAFTrend(),
2472
2496
  type: WeatherChangeType[lineTokens[1]],
@@ -2665,8 +2689,9 @@ function tafDatesHydrator(report, date) {
2665
2689
  }
2666
2690
 
2667
2691
  function getForecastFromTAF(taf) {
2692
+ const { trends, wind, visibility, verticalVisibility, windShear, cavok, remark, remarks, clouds, weatherConditions, initialRaw, validity, ...tafWithoutBaseProperties } = taf;
2668
2693
  return {
2669
- ...taf,
2694
+ ...tafWithoutBaseProperties,
2670
2695
  start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour),
2671
2696
  end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour),
2672
2697
  forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity),
@@ -2719,8 +2744,9 @@ function hydrateEndDates(trends, reportValidity) {
2719
2744
  const currentTrend = trends[i];
2720
2745
  const nextTrend = findNext(i + 1);
2721
2746
  if (!hasImplicitEnd(currentTrend)) {
2747
+ const { validity, ...trend } = currentTrend;
2722
2748
  forecasts.push({
2723
- ...currentTrend,
2749
+ ...trend,
2724
2750
  start: currentTrend.validity.start,
2725
2751
  // Has a type and not a FM/BECMG/undefined, so always has an end
2726
2752
  end: currentTrend.validity.end,
@@ -2728,9 +2754,10 @@ function hydrateEndDates(trends, reportValidity) {
2728
2754
  continue;
2729
2755
  }
2730
2756
  let forecast;
2757
+ const { validity, ...trendWithoutValidity } = currentTrend;
2731
2758
  if (nextTrend === undefined) {
2732
2759
  forecast = hydrateWithPreviousContextIfNeeded({
2733
- ...currentTrend,
2760
+ ...trendWithoutValidity,
2734
2761
  start: currentTrend.validity.start,
2735
2762
  end: reportValidity.end,
2736
2763
  ...byIfNeeded(currentTrend),
@@ -2738,7 +2765,7 @@ function hydrateEndDates(trends, reportValidity) {
2738
2765
  }
2739
2766
  else {
2740
2767
  forecast = hydrateWithPreviousContextIfNeeded({
2741
- ...currentTrend,
2768
+ ...trendWithoutValidity,
2742
2769
  start: currentTrend.validity.start,
2743
2770
  end: new Date(nextTrend.validity.start),
2744
2771
  ...byIfNeeded(currentTrend),
@@ -2760,12 +2787,16 @@ function hydrateWithPreviousContextIfNeeded(forecast, context) {
2760
2787
  context = { ...context };
2761
2788
  delete context.remark;
2762
2789
  context.remarks = [];
2790
+ // vertical visibility should not be carried over, if clouds exist
2791
+ if (forecast.clouds.length)
2792
+ delete context.verticalVisibility;
2763
2793
  forecast = {
2764
2794
  ...context,
2765
2795
  ...forecast,
2766
2796
  };
2767
- if (!forecast.clouds.length)
2797
+ if (!forecast.clouds.length) {
2768
2798
  forecast.clouds = context.clouds;
2799
+ }
2769
2800
  if (!forecast.weatherConditions.length)
2770
2801
  forecast.weatherConditions = context.weatherConditions;
2771
2802
  return forecast;
@@ -2782,25 +2813,25 @@ function getCompositeForecastForDate(date, forecastContainer) {
2782
2813
  if (date.getTime() > forecastContainer.end.getTime() ||
2783
2814
  date.getTime() < forecastContainer.start.getTime())
2784
2815
  throw new TimestampOutOfBoundsError("Provided timestamp is outside the report validity period");
2785
- let base;
2786
- let additional = [];
2816
+ let prevailing;
2817
+ let supplemental = [];
2787
2818
  for (const forecast of forecastContainer.forecast) {
2788
2819
  if (hasImplicitEnd(forecast) &&
2789
2820
  forecast.start.getTime() <= date.getTime()) {
2790
- // Is FM or initial forecast
2791
- base = forecast;
2821
+ // Is FM, BECMG or initial forecast
2822
+ prevailing = forecast;
2792
2823
  }
2793
2824
  if (!hasImplicitEnd(forecast) &&
2794
2825
  forecast.end &&
2795
2826
  forecast.end.getTime() - date.getTime() > 0 &&
2796
2827
  forecast.start.getTime() - date.getTime() <= 0) {
2797
- // Is TEMPO, BECMG etc
2798
- additional.push(forecast);
2828
+ // Is TEMPO, INTER, PROB etc
2829
+ supplemental.push(forecast);
2799
2830
  }
2800
2831
  }
2801
- if (!base)
2832
+ if (!prevailing)
2802
2833
  throw new UnexpectedParseError("Unable to find trend for date");
2803
- return { base, additional };
2834
+ return { prevailing, supplemental };
2804
2835
  }
2805
2836
  function byIfNeeded(forecast) {
2806
2837
  if (forecast.type !== WeatherChangeType.BECMG)
@@ -2822,7 +2853,7 @@ function parse(rawReport, options, parser, datesHydrator) {
2822
2853
  const lang = options?.locale || en;
2823
2854
  try {
2824
2855
  const report = new parser(lang).parse(rawReport);
2825
- if (options && "issued" in options) {
2856
+ if (options && "issued" in options && options.issued) {
2826
2857
  return datesHydrator(report, options.issued);
2827
2858
  }
2828
2859
  return report;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metar-taf-parser",
3
- "version": "6.1.3",
3
+ "version": "7.0.0",
4
4
  "description": "Parse METAR and TAF reports",
5
5
  "homepage": "https://aeharding.github.io/metar-taf-parser",
6
6
  "keywords": [