metar-taf-parser 5.1.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,8 +1174,8 @@ 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[];
1174
1180
  /**
1175
1181
  * Just the first part of the TAF message without trends (FM, BECMG, etc)
@@ -1265,6 +1271,8 @@ interface ITAFDated extends ITAF {
1265
1271
  start: Date;
1266
1272
  end: Date;
1267
1273
  };
1274
+ minTemperature?: ITemperatureDated;
1275
+ maxTemperature?: ITemperatureDated;
1268
1276
  trends: TAFTrendDated[];
1269
1277
  }
1270
1278
 
@@ -1297,14 +1305,34 @@ declare class UnexpectedParseError extends ParseError {
1297
1305
  * The initial forecast, extracted from the first line of the TAF, does not have
1298
1306
  * a trend type (FM, BECMG, etc)
1299
1307
  */
1300
- declare type Forecast = Omit<TAFTrendDated, "type"> & Partial<Pick<TAFTrendDated, "type">>;
1301
- 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 {
1302
1327
  station: string;
1303
1328
  issued: Date;
1304
1329
  start: Date;
1305
1330
  end: Date;
1306
1331
  message: string;
1307
1332
  forecast: Forecast[];
1333
+ amendment?: true;
1334
+ maxTemperature?: ITemperatureDated;
1335
+ minTemperature?: ITemperatureDated;
1308
1336
  }
1309
1337
  interface ICompositeForecast {
1310
1338
  /**
@@ -1344,4 +1372,4 @@ declare function parseTAF(rawTAF: string, options?: IMetarTAFParserOptions): ITA
1344
1372
  declare function parseTAF(rawTAF: string, options?: IMetarTAFParserOptionsDated): ITAFDated;
1345
1373
  declare function parseTAFAsForecast(rawTAF: string, options: IMetarTAFParserOptionsDated): IForecastContainer;
1346
1374
 
1347
- 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 };
@@ -2615,6 +2615,18 @@ function tafDatesHydrator(report, date) {
2615
2615
  start: getReportDate(issued, report.validity.startDay, report.validity.startHour),
2616
2616
  end: getReportDate(issued, report.validity.endDay, report.validity.endHour),
2617
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,
2618
2630
  trends: report.trends.map((trend) => ({
2619
2631
  ...trend,
2620
2632
  validity: (() => {
@@ -2638,12 +2650,10 @@ function tafDatesHydrator(report, date) {
2638
2650
 
2639
2651
  function getForecastFromTAF(taf) {
2640
2652
  return {
2641
- issued: taf.issued,
2642
- station: taf.station,
2643
- message: taf.message,
2653
+ ...taf,
2644
2654
  start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour),
2645
2655
  end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour),
2646
- forecast: [makeInitialForecast(taf), ...taf.trends],
2656
+ forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity),
2647
2657
  };
2648
2658
  }
2649
2659
  /**
@@ -2670,6 +2680,80 @@ function makeInitialForecast(taf) {
2670
2680
  },
2671
2681
  };
2672
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
+ }
2673
2757
  class TimestampOutOfBoundsError extends ParseError {
2674
2758
  constructor(message) {
2675
2759
  super(message);
@@ -2685,15 +2769,16 @@ function getCompositeForecastForDate(date, forecastContainer) {
2685
2769
  let base;
2686
2770
  let additional = [];
2687
2771
  for (const forecast of forecastContainer.forecast) {
2688
- if (!forecast.validity.end &&
2689
- forecast.validity.start.getTime() <= date.getTime()) {
2772
+ if (hasImplicitEnd(forecast) &&
2773
+ forecast.start.getTime() <= date.getTime()) {
2690
2774
  // Is FM or initial forecast
2691
2775
  base = forecast;
2692
2776
  }
2693
- if (forecast.validity.end &&
2694
- forecast.validity.end.getTime() - date.getTime() > 0 &&
2695
- forecast.validity.start.getTime() - date.getTime() <= 0) {
2696
- // 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
2697
2782
  additional.push(forecast);
2698
2783
  }
2699
2784
  }
@@ -2701,6 +2786,11 @@ function getCompositeForecastForDate(date, forecastContainer) {
2701
2786
  throw new UnexpectedParseError("Unable to find trend for date");
2702
2787
  return { base, additional };
2703
2788
  }
2789
+ function byIfNeeded(forecast) {
2790
+ if (forecast.type !== WeatherChangeType.BECMG)
2791
+ return {};
2792
+ return { by: forecast.validity.end };
2793
+ }
2704
2794
 
2705
2795
  function parseMetar(rawMetar, options) {
2706
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.1.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": [