metar-taf-parser 5.0.0 → 6.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
@@ -45,7 +45,7 @@ const datedMetar = parseMetar(rawMetarString, { issued });
45
45
 
46
46
  #### `parseTAF`
47
47
 
48
- 👉 **Note:** One of the common use cases for TAF reports is to get relevant forecast data for a given date. Check out [the `Forecast` abstraction](#higher-level-parsing-the-forecast-abstraction) below which may provide TAF data in a more normalized format, depending on your use case.
48
+ > 👉 **Note:** One of the common use cases for TAF reports is to get relevant forecast data for a given `Date`, or display the various forecast groups to the user. Check out [the `Forecast` abstraction](#higher-level-parsing-the-forecast-abstraction) below which may provide TAF data in a more normalized and easier to use format, depending on your use case.
49
49
 
50
50
  ```ts
51
51
  import { parseTAF } from "metar-taf-parser";
@@ -61,11 +61,32 @@ const datedTAF = parseTAF(rawTAFString, { issued });
61
61
 
62
62
  ### Higher level parsing: The Forecast abstraction
63
63
 
64
- TAF reports are a little funky... FM, BECMG, PROB, etc. You may find the `Forecast` abstraction more helpful.
64
+ TAF reports are a little funky... FM, BECMG, PROB, weird validity periods, etc. You may find the higher level `Forecast` abstraction more helpful.
65
+
66
+ ⚠️ **Important:** The `Forecast` abstraction makes some assumptions in order to make it easier to consume the TAF. If you want different behavior, you may want to use the lower level `parseTAF` function directly (see above). Below are some of the assumptions the `Forecast` API makes:
67
+
68
+ 1. The `validity` object found from `parseTAF`'s `trends[]` is too low level, so it is removed. Instead, you will find `start` and `end` on the base `Forecast` object. The end of a `FM` and `BECMG` group is derived from the start of the next `FM`/`BECMG` trend, or the end of the report validity if the last.
69
+
70
+ Additionally, there is a property, `by`, on `BECMG` trends for when conditions are expected to finish transitioning. You will need to type guard `type = BECMG` to access this property.
71
+
72
+ ```ts
73
+ const firstForecast = report.forecast[1];
74
+ if (firstForecast.type === WeatherChangeType.BECMG) {
75
+ // Can now access `by`
76
+ console.log(firstForecast.by);
77
+ }
78
+ ```
79
+
80
+ 2. `BECMG` trends are hydrated with the context of previous trends. For example, if:
81
+
82
+ TAF SBBR 221500Z 2218/2318 15008KT 9999 FEW045
83
+ BECMG 2308/2310 09002KT
84
+
85
+ Then the `BECMG` group will also have visibility and clouds from previously found conditions, with updated winds.
65
86
 
66
87
  #### `parseTAFAsForecast`
67
88
 
68
- Returns a more normalized TAF report. Most notably: while the `parseTAF` function returns initial weather conditions on the base of the returned result (and further conditions on `trends[]`), the `parseTAFAsForecast` function returns the initial weather conditions as the first element of the `forecast[]` property, followed by subsequent trends. This makes it much easier to iterate though.
89
+ Returns a more normalized TAF report than `parseTAF`. Most notably: while the `parseTAF` function returns initial weather conditions on the base of the returned result (and further conditions on `trends[]`), the `parseTAFAsForecast` function returns the initial weather conditions as the first element of the `forecast[]` property (with `type = undefined`), followed by subsequent trends. (For more, please see the above about the forecast abstraction.) This makes it much easier to render a UI similar to the [aviationweather.gov](https://www.aviationweather.gov/taf/data?ids=SBPJ&format=decoded&metars=off&layout=on) TAF decoder.
69
90
 
70
91
  ```ts
71
92
  import { parseTAFAsForecast } from "metar-taf-parser";
@@ -80,11 +101,11 @@ console.log(report.forecast);
80
101
 
81
102
  > ⚠️ **Warning:** Experimental API
82
103
 
83
- 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 (the FM part of the report) - and there will always be one.
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.
84
105
 
85
- The `additional` property is an array of weather condition periods valid for the given timestamp (any `BECMG`, `PROB`, `TEMPO`, etc.)
106
+ The `additional` property is an array of weather condition periods valid for the given timestamp (any `PROB` and/or `TEMPO`)
86
107
 
87
- 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, use it over `base.visibility`.
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`.
88
109
 
89
110
  #### Example
90
111
 
@@ -1045,6 +1045,9 @@ interface IAirport {
1045
1045
  interface IWind {
1046
1046
  speed: number;
1047
1047
  direction: string;
1048
+ /**
1049
+ * If undefined, direction is variable
1050
+ */
1048
1051
  degrees?: number;
1049
1052
  gust?: number;
1050
1053
  minVariation?: number;
@@ -1077,11 +1080,14 @@ interface IWeatherCondition {
1077
1080
  phenomenons: Phenomenon[];
1078
1081
  }
1079
1082
  declare function isWeatherConditionValid(weather: IWeatherCondition): boolean;
1080
- interface ITemperatureDated {
1083
+ interface ITemperature {
1081
1084
  temperature: number;
1082
1085
  day: number;
1083
1086
  hour: number;
1084
1087
  }
1088
+ interface ITemperatureDated extends ITemperature {
1089
+ date: Date;
1090
+ }
1085
1091
  interface IRunwayInfo {
1086
1092
  name: string;
1087
1093
  minRange: number;
@@ -1168,12 +1174,17 @@ interface IMetar extends IAbstractWeatherCode {
1168
1174
  }
1169
1175
  interface ITAF extends IAbstractWeatherCode {
1170
1176
  validity: IValidity;
1171
- maxTemperature?: ITemperatureDated;
1172
- minTemperature?: ITemperatureDated;
1177
+ maxTemperature?: ITemperature;
1178
+ minTemperature?: ITemperature;
1173
1179
  trends: TAFTrend[];
1180
+ /**
1181
+ * Just the first part of the TAF message without trends (FM, BECMG, etc)
1182
+ */
1183
+ initialRaw: string;
1174
1184
  }
1175
1185
  interface IAbstractTrend extends IAbstractWeatherContainer {
1176
1186
  type: WeatherChangeType;
1187
+ raw: string;
1177
1188
  }
1178
1189
  interface IMetarTrendTime extends ITime {
1179
1190
  type: TimeIndicator;
@@ -1260,6 +1271,8 @@ interface ITAFDated extends ITAF {
1260
1271
  start: Date;
1261
1272
  end: Date;
1262
1273
  };
1274
+ minTemperature?: ITemperatureDated;
1275
+ maxTemperature?: ITemperatureDated;
1263
1276
  trends: TAFTrendDated[];
1264
1277
  }
1265
1278
 
@@ -1292,14 +1305,34 @@ declare class UnexpectedParseError extends ParseError {
1292
1305
  * The initial forecast, extracted from the first line of the TAF, does not have
1293
1306
  * a trend type (FM, BECMG, etc)
1294
1307
  */
1295
- declare type Forecast = Omit<TAFTrendDated, "type"> & Partial<Pick<TAFTrendDated, "type">>;
1296
- interface IForecastContainer {
1308
+ declare type ForecastWithoutDates = Omit<TAFTrendDated, "type"> & Partial<Pick<TAFTrendDated, "type">>;
1309
+ declare type ForecastWithoutValidity = Omit<ForecastWithoutDates, "validity">;
1310
+ declare type Forecast = Omit<ForecastWithoutValidity, "type"> & {
1311
+ start: Date;
1312
+ end: Date;
1313
+ } & ({
1314
+ type: Exclude<WeatherChangeType, WeatherChangeType.BECMG> | undefined;
1315
+ } | {
1316
+ type: WeatherChangeType.BECMG;
1317
+ /**
1318
+ * BECMG has a special date, `by`, for when the transition will finish
1319
+ *
1320
+ * For example, a BECMG trend may `start` at 1:00PM and `end` at 5:00PM, but
1321
+ * `by` may be `3:00PM` to denote that conditions will transition from a period of
1322
+ * 1:00PM to 3:00PM
1323
+ */
1324
+ by: Date;
1325
+ });
1326
+ interface IForecastContainer extends IFlags {
1297
1327
  station: string;
1298
1328
  issued: Date;
1299
1329
  start: Date;
1300
1330
  end: Date;
1301
1331
  message: string;
1302
1332
  forecast: Forecast[];
1333
+ amendment?: true;
1334
+ maxTemperature?: ITemperatureDated;
1335
+ minTemperature?: ITemperatureDated;
1303
1336
  }
1304
1337
  interface ICompositeForecast {
1305
1338
  /**
@@ -1339,4 +1372,4 @@ declare function parseTAF(rawTAF: string, options?: IMetarTAFParserOptions): ITA
1339
1372
  declare function parseTAF(rawTAF: string, options?: IMetarTAFParserOptionsDated): ITAFDated;
1340
1373
  declare function parseTAFAsForecast(rawTAF: string, options: IMetarTAFParserOptionsDated): IForecastContainer;
1341
1374
 
1342
- export { CloudQuantity, CloudType, CommandExecutionError, Descriptive, Direction, Distance, DistanceUnit, Forecast, IAbstractTrend, IAbstractValidity, IAbstractWeatherCode, IAbstractWeatherCodeDated, IAbstractWeatherContainer, IAirport, IBaseRemark, IBaseTAFTrend, ICeilingHeightRemark, ICeilingSecondLocationRemark, ICloud, ICompositeForecast, ICountry, IEndValidity, IFMValidity, IFlags, IForecastContainer, IHourlyMaximumMinimumTemperatureRemark, IHourlyMaximumTemperatureRemark, IHourlyMinimumTemperatureRemark, IHourlyPrecipitationAmountRemark, IHourlyPressureRemark, IHourlyTemperatureDewPointRemark, IIceAccretionRemark, IMetar, IMetarTAFParserOptions, IMetarTAFParserOptionsDated, IMetarTrend, IMetarTrendTime, IObscurationRemark, IPrecipitationAmount24HourRemark, IPrecipitationAmount36HourRemark, IPrecipitationBegEndRemark, IPrevailingVisibilityRemark, IRunwayInfo, ISeaLevelPressureRemark, ISecondLocationVisibilityRemark, ISectorVisibilityRemark, ISmallHailSizeRemark, ISnowIncreaseRemark, ISnowPelletsRemark, ISunshineDurationRemark, ISurfaceVisibilityRemark, ITAF, ITAFDated, ITemperatureDated, IThunderStormLocationMovingRemark, IThunderStormLocationRemark, ITime, ITornadicActivityBegEndRemark, ITornadicActivityBegRemark, ITornadicActivityEndRemark, ITowerVisibilityRemark, IUnknownRemark, IValidity, IValidityDated, IVariableSkyHeightRemark, IVariableSkyRemark, IVirgaDirectionRemark, IWaterEquivalentSnowRemark, IWeatherCondition, IWind, IWindPeakCommandRemark, IWindShear, IWindShiftFropaRemark, Intensity, InvalidWeatherStatementError, Locale, ParseError, Phenomenon, Remark, RemarkType, RunwayInfoTrend, RunwayInfoUnit, TAFTrend, TAFTrendDated, TimeIndicator, TimestampOutOfBoundsError, UnexpectedParseError, ValueIndicator, Visibility, WeatherChangeType, getCompositeForecastForDate, isWeatherConditionValid, parseMetar, parseTAF, parseTAFAsForecast };
1375
+ export { CloudQuantity, CloudType, CommandExecutionError, Descriptive, Direction, Distance, DistanceUnit, Forecast, IAbstractTrend, IAbstractValidity, IAbstractWeatherCode, IAbstractWeatherCodeDated, IAbstractWeatherContainer, IAirport, IBaseRemark, IBaseTAFTrend, ICeilingHeightRemark, ICeilingSecondLocationRemark, ICloud, ICompositeForecast, ICountry, IEndValidity, IFMValidity, IFlags, IForecastContainer, IHourlyMaximumMinimumTemperatureRemark, IHourlyMaximumTemperatureRemark, IHourlyMinimumTemperatureRemark, IHourlyPrecipitationAmountRemark, IHourlyPressureRemark, IHourlyTemperatureDewPointRemark, IIceAccretionRemark, IMetar, IMetarTAFParserOptions, IMetarTAFParserOptionsDated, IMetarTrend, IMetarTrendTime, IObscurationRemark, IPrecipitationAmount24HourRemark, IPrecipitationAmount36HourRemark, IPrecipitationBegEndRemark, IPrevailingVisibilityRemark, IRunwayInfo, ISeaLevelPressureRemark, ISecondLocationVisibilityRemark, ISectorVisibilityRemark, ISmallHailSizeRemark, ISnowIncreaseRemark, ISnowPelletsRemark, ISunshineDurationRemark, ISurfaceVisibilityRemark, ITAF, ITAFDated, ITemperature, ITemperatureDated, IThunderStormLocationMovingRemark, IThunderStormLocationRemark, ITime, ITornadicActivityBegEndRemark, ITornadicActivityBegRemark, ITornadicActivityEndRemark, ITowerVisibilityRemark, IUnknownRemark, IValidity, IValidityDated, IVariableSkyHeightRemark, IVariableSkyRemark, IVirgaDirectionRemark, IWaterEquivalentSnowRemark, IWeatherCondition, IWind, IWindPeakCommandRemark, IWindShear, IWindShiftFropaRemark, Intensity, InvalidWeatherStatementError, Locale, ParseError, Phenomenon, Remark, RemarkType, RunwayInfoTrend, RunwayInfoUnit, TAFTrend, TAFTrendDated, TimeIndicator, TimestampOutOfBoundsError, UnexpectedParseError, ValueIndicator, Visibility, WeatherChangeType, getCompositeForecastForDate, isWeatherConditionValid, parseMetar, parseTAF, parseTAFAsForecast };
@@ -2304,6 +2304,7 @@ class MetarParser extends AbstractParser {
2304
2304
  clouds: [],
2305
2305
  times: [],
2306
2306
  remarks: [],
2307
+ raw: input,
2307
2308
  };
2308
2309
  index = this.parseTrend(index, trend, metarTab);
2309
2310
  metar.trends.push(trend);
@@ -2369,6 +2370,7 @@ class TAFParser extends AbstractParser {
2369
2370
  remarks: [],
2370
2371
  clouds: [],
2371
2372
  weatherConditions: [],
2373
+ initialRaw: lines[0].join(" "),
2372
2374
  };
2373
2375
  for (let i = index + 1; i < lines[0].length; i++) {
2374
2376
  const token = lines[0][i];
@@ -2435,6 +2437,7 @@ class TAFParser extends AbstractParser {
2435
2437
  ...this.makeEmptyTAFTrend(),
2436
2438
  type: WeatherChangeType.FM,
2437
2439
  validity: parseFromValidity(lineTokens[0]),
2440
+ raw: lineTokens.join(" "),
2438
2441
  };
2439
2442
  }
2440
2443
  else if (lineTokens[0].startsWith(this.PROB)) {
@@ -2445,12 +2448,14 @@ class TAFParser extends AbstractParser {
2445
2448
  ...this.makeEmptyTAFTrend(),
2446
2449
  type: WeatherChangeType.PROB,
2447
2450
  validity,
2451
+ raw: lineTokens.join(" "),
2448
2452
  };
2449
2453
  if (lineTokens.length > 1 && lineTokens[1] === this.TEMPO) {
2450
2454
  trend = {
2451
2455
  ...this.makeEmptyTAFTrend(),
2452
2456
  type: WeatherChangeType[lineTokens[1]],
2453
2457
  validity,
2458
+ raw: lineTokens.join(" "),
2454
2459
  };
2455
2460
  index = 2;
2456
2461
  }
@@ -2464,6 +2469,7 @@ class TAFParser extends AbstractParser {
2464
2469
  ...this.makeEmptyTAFTrend(),
2465
2470
  type: WeatherChangeType[lineTokens[0]],
2466
2471
  validity,
2472
+ raw: lineTokens.join(" "),
2467
2473
  };
2468
2474
  }
2469
2475
  this.parseTrend(index, lineTokens, trend);
@@ -2609,6 +2615,18 @@ function tafDatesHydrator(report, date) {
2609
2615
  start: getReportDate(issued, report.validity.startDay, report.validity.startHour),
2610
2616
  end: getReportDate(issued, report.validity.endDay, report.validity.endHour),
2611
2617
  },
2618
+ minTemperature: report.minTemperature
2619
+ ? {
2620
+ ...report.minTemperature,
2621
+ date: getReportDate(issued, report.minTemperature.day, report.minTemperature.hour),
2622
+ }
2623
+ : undefined,
2624
+ maxTemperature: report.maxTemperature
2625
+ ? {
2626
+ ...report.maxTemperature,
2627
+ date: getReportDate(issued, report.maxTemperature.day, report.maxTemperature.hour),
2628
+ }
2629
+ : undefined,
2612
2630
  trends: report.trends.map((trend) => ({
2613
2631
  ...trend,
2614
2632
  validity: (() => {
@@ -2632,12 +2650,10 @@ function tafDatesHydrator(report, date) {
2632
2650
 
2633
2651
  function getForecastFromTAF(taf) {
2634
2652
  return {
2635
- issued: taf.issued,
2636
- station: taf.station,
2637
- message: taf.message,
2653
+ ...taf,
2638
2654
  start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour),
2639
2655
  end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour),
2640
- forecast: [makeInitialForecast(taf), ...taf.trends],
2656
+ forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity),
2641
2657
  };
2642
2658
  }
2643
2659
  /**
@@ -2654,6 +2670,7 @@ function makeInitialForecast(taf) {
2654
2670
  remarks: taf.remarks,
2655
2671
  clouds: taf.clouds,
2656
2672
  weatherConditions: taf.weatherConditions,
2673
+ raw: taf.initialRaw,
2657
2674
  validity: {
2658
2675
  // End day/hour are for end of the entire TAF
2659
2676
  startDay: taf.validity.startDay,
@@ -2663,6 +2680,80 @@ function makeInitialForecast(taf) {
2663
2680
  },
2664
2681
  };
2665
2682
  }
2683
+ function hasImplicitEnd({ type }) {
2684
+ return (type === WeatherChangeType.FM ||
2685
+ // BECMG are special - the "end" date in the validity isn't actually
2686
+ // the end date, it's when the change that's "becoming" is expected to
2687
+ // finish transition. The actual "end" date of the BECMG is determined by
2688
+ // the next FM/BECMG/end of the report validity, just like a FM
2689
+ type === WeatherChangeType.BECMG ||
2690
+ // Special case for beginning of report conditions
2691
+ type === undefined);
2692
+ }
2693
+ function hydrateEndDates(trends, reportValidity) {
2694
+ function findNext(index) {
2695
+ for (let i = index; i < trends.length; i++) {
2696
+ if (hasImplicitEnd(trends[i]))
2697
+ return trends[i];
2698
+ }
2699
+ }
2700
+ const forecasts = [];
2701
+ let previouslyHydratedTrend;
2702
+ for (let i = 0; i < trends.length; i++) {
2703
+ const currentTrend = trends[i];
2704
+ const nextTrend = findNext(i + 1);
2705
+ if (!hasImplicitEnd(currentTrend)) {
2706
+ forecasts.push({
2707
+ ...currentTrend,
2708
+ start: currentTrend.validity.start,
2709
+ // Has a type and not a FM/BECMG/undefined, so always has an end
2710
+ end: currentTrend.validity.end,
2711
+ });
2712
+ continue;
2713
+ }
2714
+ let forecast;
2715
+ if (nextTrend === undefined) {
2716
+ forecast = hydrateWithPreviousContextIfNeeded({
2717
+ ...currentTrend,
2718
+ start: currentTrend.validity.start,
2719
+ end: reportValidity.end,
2720
+ ...byIfNeeded(currentTrend),
2721
+ }, previouslyHydratedTrend);
2722
+ }
2723
+ else {
2724
+ forecast = hydrateWithPreviousContextIfNeeded({
2725
+ ...currentTrend,
2726
+ start: currentTrend.validity.start,
2727
+ end: new Date(nextTrend.validity.start),
2728
+ ...byIfNeeded(currentTrend),
2729
+ }, previouslyHydratedTrend);
2730
+ }
2731
+ forecasts.push(forecast);
2732
+ previouslyHydratedTrend = forecast;
2733
+ }
2734
+ return forecasts;
2735
+ }
2736
+ /**
2737
+ * BECMG doesn't always have all the context for the period, so
2738
+ * it needs to be populated
2739
+ */
2740
+ function hydrateWithPreviousContextIfNeeded(forecast, context) {
2741
+ if (forecast.type !== WeatherChangeType.BECMG || !context)
2742
+ return forecast;
2743
+ // Remarks should not be carried over
2744
+ context = { ...context };
2745
+ delete context.remark;
2746
+ context.remarks = [];
2747
+ forecast = {
2748
+ ...context,
2749
+ ...forecast,
2750
+ };
2751
+ if (!forecast.clouds.length)
2752
+ forecast.clouds = context.clouds;
2753
+ if (!forecast.weatherConditions.length)
2754
+ forecast.weatherConditions = context.weatherConditions;
2755
+ return forecast;
2756
+ }
2666
2757
  class TimestampOutOfBoundsError extends ParseError {
2667
2758
  constructor(message) {
2668
2759
  super(message);
@@ -2678,15 +2769,16 @@ function getCompositeForecastForDate(date, forecastContainer) {
2678
2769
  let base;
2679
2770
  let additional = [];
2680
2771
  for (const forecast of forecastContainer.forecast) {
2681
- if (!forecast.validity.end &&
2682
- forecast.validity.start.getTime() <= date.getTime()) {
2772
+ if (hasImplicitEnd(forecast) &&
2773
+ forecast.start.getTime() <= date.getTime()) {
2683
2774
  // Is FM or initial forecast
2684
2775
  base = forecast;
2685
2776
  }
2686
- if (forecast.validity.end &&
2687
- forecast.validity.end.getTime() - date.getTime() > 0 &&
2688
- forecast.validity.start.getTime() - date.getTime() <= 0) {
2689
- // Is BECMG or TEMPO
2777
+ if (!hasImplicitEnd(forecast) &&
2778
+ forecast.end &&
2779
+ forecast.end.getTime() - date.getTime() > 0 &&
2780
+ forecast.start.getTime() - date.getTime() <= 0) {
2781
+ // Is TEMPO, BECMG etc
2690
2782
  additional.push(forecast);
2691
2783
  }
2692
2784
  }
@@ -2694,6 +2786,11 @@ function getCompositeForecastForDate(date, forecastContainer) {
2694
2786
  throw new UnexpectedParseError("Unable to find trend for date");
2695
2787
  return { base, additional };
2696
2788
  }
2789
+ function byIfNeeded(forecast) {
2790
+ if (forecast.type !== WeatherChangeType.BECMG)
2791
+ return {};
2792
+ return { by: forecast.validity.end };
2793
+ }
2697
2794
 
2698
2795
  function parseMetar(rawMetar, options) {
2699
2796
  return parse(rawMetar, options, MetarParser, metarDatesHydrator);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metar-taf-parser",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Parse METAR and TAF reports",
5
5
  "homepage": "https://aeharding.github.io/metar-taf-parser",
6
6
  "keywords": [