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..e1b944c1 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 @@ -4,6 +4,7 @@ import io.github.mivek.command.common.CommonCommandSupplier; import io.github.mivek.command.metar.Command; import io.github.mivek.command.metar.MetarCommandSupplier; +import io.github.mivek.enums.Flag; import io.github.mivek.enums.WeatherChangeType; import io.github.mivek.exception.ErrorCodes; import io.github.mivek.exception.ParseErrorType; @@ -83,7 +84,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 +102,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 +115,32 @@ 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 correction indicator (COR) preceding the delivery time. Other flags and COR 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 correction indicator. + */ + 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 && Flag.COR.name().equals(metarTab[flagIndex])) { + 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..b5f76f70 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 @@ -9,6 +9,8 @@ import io.github.mivek.model.trend.MetarTrend; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -507,4 +509,75 @@ 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()); + } + + @ParameterizedTest + @ValueSource(strings = {"AMD", "CNL", "AUTO", "NIL"}) + void testParseRejectsFlagsOtherThanCorBeforeDeliveryTime(final String flag) { + ParseException e = assertThrows(ParseException.class, () -> parser.parse("MRLB " + flag + " 071700Z 11013KT 9999")); + assertEquals(ParseErrorType.DELIVERY_TIME, e.getType()); + assertEquals(flag, e.getOffendingToken()); + } + }