metar-taf-parser 6.1.4 → 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,6 +2313,7 @@ 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) {
2310
2318
  const startIndex = index;
2311
2319
  const trend = {
@@ -2348,6 +2356,27 @@ class TAFParser extends AbstractParser {
2348
2356
  this.TN = "TN";
2349
2357
  _TAFParser_validityPattern.set(this, /^\d{4}\/\d{4}$/);
2350
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
+ }
2351
2380
  /**
2352
2381
  * the message to parse
2353
2382
  * @param input
@@ -2356,15 +2385,7 @@ class TAFParser extends AbstractParser {
2356
2385
  */
2357
2386
  parse(input) {
2358
2387
  const lines = this.extractLinesTokens(input);
2359
- let index = 0;
2360
- if (lines[0][0] === this.TAF)
2361
- index = 1;
2362
- if (lines[0][1] === this.TAF)
2363
- index = 2;
2364
- const flags = findFlags(lines[0][index]);
2365
- if (flags) {
2366
- index += 1;
2367
- }
2388
+ let [index, flags] = this.parseMessageStart(lines[0]);
2368
2389
  const station = lines[0][index];
2369
2390
  index += 1;
2370
2391
  const time = parseDeliveryTime(lines[0][index]);
@@ -2428,12 +2449,12 @@ class TAFParser extends AbstractParser {
2428
2449
  const singleLine = tafCode.replace(/\n/g, " ");
2429
2450
  const cleanLine = singleLine.replace(/\s{2,}/g, " ");
2430
2451
  const lines = joinProbIfNeeded(cleanLine
2431
- .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")
2432
2453
  .split(/\n/));
2433
2454
  // TODO cleanup
2434
2455
  function joinProbIfNeeded(ls) {
2435
2456
  for (let i = 0; i < ls.length; i++) {
2436
- 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])) {
2437
2458
  ls.splice(i, 2, `${ls[i]} ${ls[i + 1]}`);
2438
2459
  }
2439
2460
  }
@@ -2468,7 +2489,8 @@ class TAFParser extends AbstractParser {
2468
2489
  validity,
2469
2490
  raw: lineTokens.join(" "),
2470
2491
  };
2471
- if (lineTokens.length > 1 && lineTokens[1] === this.TEMPO) {
2492
+ if (lineTokens.length > 1 &&
2493
+ (lineTokens[1] === this.TEMPO || lineTokens[1] === this.INTER)) {
2472
2494
  trend = {
2473
2495
  ...this.makeEmptyTAFTrend(),
2474
2496
  type: WeatherChangeType[lineTokens[1]],
@@ -2667,8 +2689,9 @@ function tafDatesHydrator(report, date) {
2667
2689
  }
2668
2690
 
2669
2691
  function getForecastFromTAF(taf) {
2692
+ const { trends, wind, visibility, verticalVisibility, windShear, cavok, remark, remarks, clouds, weatherConditions, initialRaw, validity, ...tafWithoutBaseProperties } = taf;
2670
2693
  return {
2671
- ...taf,
2694
+ ...tafWithoutBaseProperties,
2672
2695
  start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour),
2673
2696
  end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour),
2674
2697
  forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity),
@@ -2721,8 +2744,9 @@ function hydrateEndDates(trends, reportValidity) {
2721
2744
  const currentTrend = trends[i];
2722
2745
  const nextTrend = findNext(i + 1);
2723
2746
  if (!hasImplicitEnd(currentTrend)) {
2747
+ const { validity, ...trend } = currentTrend;
2724
2748
  forecasts.push({
2725
- ...currentTrend,
2749
+ ...trend,
2726
2750
  start: currentTrend.validity.start,
2727
2751
  // Has a type and not a FM/BECMG/undefined, so always has an end
2728
2752
  end: currentTrend.validity.end,
@@ -2730,9 +2754,10 @@ function hydrateEndDates(trends, reportValidity) {
2730
2754
  continue;
2731
2755
  }
2732
2756
  let forecast;
2757
+ const { validity, ...trendWithoutValidity } = currentTrend;
2733
2758
  if (nextTrend === undefined) {
2734
2759
  forecast = hydrateWithPreviousContextIfNeeded({
2735
- ...currentTrend,
2760
+ ...trendWithoutValidity,
2736
2761
  start: currentTrend.validity.start,
2737
2762
  end: reportValidity.end,
2738
2763
  ...byIfNeeded(currentTrend),
@@ -2740,7 +2765,7 @@ function hydrateEndDates(trends, reportValidity) {
2740
2765
  }
2741
2766
  else {
2742
2767
  forecast = hydrateWithPreviousContextIfNeeded({
2743
- ...currentTrend,
2768
+ ...trendWithoutValidity,
2744
2769
  start: currentTrend.validity.start,
2745
2770
  end: new Date(nextTrend.validity.start),
2746
2771
  ...byIfNeeded(currentTrend),
@@ -2762,12 +2787,16 @@ function hydrateWithPreviousContextIfNeeded(forecast, context) {
2762
2787
  context = { ...context };
2763
2788
  delete context.remark;
2764
2789
  context.remarks = [];
2790
+ // vertical visibility should not be carried over, if clouds exist
2791
+ if (forecast.clouds.length)
2792
+ delete context.verticalVisibility;
2765
2793
  forecast = {
2766
2794
  ...context,
2767
2795
  ...forecast,
2768
2796
  };
2769
- if (!forecast.clouds.length)
2797
+ if (!forecast.clouds.length) {
2770
2798
  forecast.clouds = context.clouds;
2799
+ }
2771
2800
  if (!forecast.weatherConditions.length)
2772
2801
  forecast.weatherConditions = context.weatherConditions;
2773
2802
  return forecast;
@@ -2784,25 +2813,25 @@ function getCompositeForecastForDate(date, forecastContainer) {
2784
2813
  if (date.getTime() > forecastContainer.end.getTime() ||
2785
2814
  date.getTime() < forecastContainer.start.getTime())
2786
2815
  throw new TimestampOutOfBoundsError("Provided timestamp is outside the report validity period");
2787
- let base;
2788
- let additional = [];
2816
+ let prevailing;
2817
+ let supplemental = [];
2789
2818
  for (const forecast of forecastContainer.forecast) {
2790
2819
  if (hasImplicitEnd(forecast) &&
2791
2820
  forecast.start.getTime() <= date.getTime()) {
2792
- // Is FM or initial forecast
2793
- base = forecast;
2821
+ // Is FM, BECMG or initial forecast
2822
+ prevailing = forecast;
2794
2823
  }
2795
2824
  if (!hasImplicitEnd(forecast) &&
2796
2825
  forecast.end &&
2797
2826
  forecast.end.getTime() - date.getTime() > 0 &&
2798
2827
  forecast.start.getTime() - date.getTime() <= 0) {
2799
- // Is TEMPO, BECMG etc
2800
- additional.push(forecast);
2828
+ // Is TEMPO, INTER, PROB etc
2829
+ supplemental.push(forecast);
2801
2830
  }
2802
2831
  }
2803
- if (!base)
2832
+ if (!prevailing)
2804
2833
  throw new UnexpectedParseError("Unable to find trend for date");
2805
- return { base, additional };
2834
+ return { prevailing, supplemental };
2806
2835
  }
2807
2836
  function byIfNeeded(forecast) {
2808
2837
  if (forecast.type !== WeatherChangeType.BECMG)
@@ -2824,7 +2853,7 @@ function parse(rawReport, options, parser, datesHydrator) {
2824
2853
  const lang = options?.locale || en;
2825
2854
  try {
2826
2855
  const report = new parser(lang).parse(rawReport);
2827
- if (options && "issued" in options) {
2856
+ if (options && "issued" in options && options.issued) {
2828
2857
  return datesHydrator(report, options.issued);
2829
2858
  }
2830
2859
  return report;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metar-taf-parser",
3
- "version": "6.1.4",
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": [