From 4de8e813c7f7d82085fa7c4c2c6b693ac97e587b Mon Sep 17 00:00:00 2001 From: "f.loris" Date: Mon, 7 Sep 2026 20:29:33 +0200 Subject: [PATCH] fix(parser): accept report modifiers before the delivery time A correction indicator is legal both between the report type and the station ("METAR COR LFPG 081130Z") and between the station and the delivery time ("MRLB COR 071700Z"), the latter being the form NOAA publishes. Both were rejected because parseIdentification expected the station and the delivery time at fixed offsets. Reuse parseFlags for the tokens preceding the delivery time, so these reports parse and isCorrected() reports them as corrected, consistent with a COR that follows the delivery time and is already handled by the body of the message. Flag skipping stays disabled at index 0, where only a report type or a station can appear and where AUTO would otherwise shadow a four-letter station. --- .../io/github/mivek/parser/MetarParser.java | 32 ++++++++-- .../github/mivek/parser/MetarParserTest.java | 63 +++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/metarParser-parsers/src/main/java/io/github/mivek/parser/MetarParser.java b/metarParser-parsers/src/main/java/io/github/mivek/parser/MetarParser.java index d055ecdb..b62f8cb5 100644 --- a/metarParser-parsers/src/main/java/io/github/mivek/parser/MetarParser.java +++ b/metarParser-parsers/src/main/java/io/github/mivek/parser/MetarParser.java @@ -83,7 +83,7 @@ public Metar parse(final String code) throws ParseException { } /** - * Parses the report-type prefix, station and delivery time. + * Parses the report-type prefix, the report modifiers, the station and the delivery time. * * @param m the metar being built. * @param metarTab the tokenized message. @@ -101,6 +101,8 @@ private int parseIdentification(final Metar m, final String[] metarTab, final St m.setReportType(io.github.mivek.enums.ReportType.SPECI); startIndex = 1; } + // a modifier may follow the report type, as in "METAR COR LFPG 081130Z" + startIndex = parseLeadingFlags(m, metarTab, startIndex); } if (startIndex >= metarTab.length) { throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.STATION, null, startIndex); @@ -112,11 +114,31 @@ private int parseIdentification(final Metar m, final String[] metarTab, final St m.setStation(stationToken); m.setAirport(getAirportSupplier().get(stationToken)); m.setMessage(code); - if (startIndex + 1 >= metarTab.length) { - throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.DELIVERY_TIME, null, startIndex + 1); + // it may also sit between the station and the delivery time, as in "LFPG COR 081130Z" + int timeIndex = parseLeadingFlags(m, metarTab, startIndex + 1); + if (timeIndex >= metarTab.length) { + throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.DELIVERY_TIME, null, timeIndex); } - parseDeliveryTime(m, metarTab[startIndex + 1], startIndex + 1); - return startIndex + 2; + parseDeliveryTime(m, metarTab[timeIndex], timeIndex); + return timeIndex + 1; + } + + /** + * Consumes the report modifiers (COR, AMD, ...) preceding the delivery time. Modifiers placed + * after the delivery time are consumed by the body of the message instead. + * + * @param m the metar being built. + * @param metarTab the tokenized message. + * @param index the index to start at. + * @return the index of the first token that is not a modifier. + */ + private int parseLeadingFlags(final Metar m, final String[] metarTab, final int index) { + int flagIndex = index; + // index 0 holds the report type or the station, never a modifier + while (flagIndex > 0 && flagIndex < metarTab.length && parseFlags(m, metarTab[flagIndex])) { + flagIndex++; + } + return flagIndex; } /** diff --git a/metarParser-parsers/src/test/java/io/github/mivek/parser/MetarParserTest.java b/metarParser-parsers/src/test/java/io/github/mivek/parser/MetarParserTest.java index 17e4f082..f27151e3 100644 --- a/metarParser-parsers/src/test/java/io/github/mivek/parser/MetarParserTest.java +++ b/metarParser-parsers/src/test/java/io/github/mivek/parser/MetarParserTest.java @@ -507,4 +507,67 @@ void testParseWithoutDeliveryTimeThrowsDeliveryTimeError() { assertEquals(ParseErrorType.DELIVERY_TIME, e.getType()); } + @Test + void testParseWithCorrectionBetweenStationAndDeliveryTime() throws ParseException { + Metar m = parser.parse("MRLB COR 071700Z 11013KT 9999 FEW045 34/22 A2983"); + + assertTrue(m.isCorrected()); + assertEquals("MRLB", m.getStation()); + assertEquals(Integer.valueOf(7), m.getDay()); + assertEquals(17, m.getTime().getHour()); + assertEquals(0, m.getTime().getMinute()); + assertEquals(Integer.valueOf(34), m.getTemperature()); + } + + @Test + void testParseWithCorrectionBetweenReportTypeAndStation() throws ParseException { + Metar m = parser.parse("METAR COR LFPG 081130Z 00000KT 0350 FG SCT000 M01/M01 Q1026"); + + assertTrue(m.isCorrected()); + assertEquals(ReportType.METAR, m.getReportType()); + assertEquals("LFPG", m.getStation()); + assertEquals(Integer.valueOf(8), m.getDay()); + assertEquals(11, m.getTime().getHour()); + assertEquals(30, m.getTime().getMinute()); + } + + @Test + void testParseWithCorrectionAfterDeliveryTime() throws ParseException { + Metar m = parser.parse("MRLB 071700Z COR 11013KT 9999 FEW045 34/22 A2983"); + + assertTrue(m.isCorrected()); + assertEquals(Integer.valueOf(7), m.getDay()); + assertEquals(Integer.valueOf(34), m.getTemperature()); + } + + @Test + void testParseWithoutCorrectionIsNotFlaggedAsCorrected() throws ParseException { + Metar m = parser.parse("MRLB 071700Z 11013KT 9999 FEW045 34/22 A2983"); + + assertFalse(m.isCorrected()); + assertEquals(Integer.valueOf(7), m.getDay()); + } + + @Test + void testParseWithModifierButWithoutDeliveryTimeThrowsDeliveryTimeError() { + ParseException e = assertThrows(ParseException.class, () -> parser.parse("LFPG COR")); + assertEquals(ParseErrorType.DELIVERY_TIME, e.getType()); + assertEquals(2, e.getPosition()); + } + + @Test + void testParseWithModifierButWithoutStationThrowsStationError() { + ParseException e = assertThrows(ParseException.class, () -> parser.parse("METAR COR")); + assertEquals(ParseErrorType.STATION, e.getType()); + assertEquals(2, e.getPosition()); + } + + @Test + void testParseKeepsAutoAsStationWhenItLeadsTheMessage() { + // a modifier is only expected after the report type or the station, never in first position + ParseException e = assertThrows(ParseException.class, () -> parser.parse("AUTO 11013KT 9999")); + assertEquals(ParseErrorType.DELIVERY_TIME, e.getType()); + assertEquals("11013KT", e.getOffendingToken()); + } + }