diff --git a/src/main/java/liqp/filters/date/BasicDateParser.java b/src/main/java/liqp/filters/date/BasicDateParser.java index 8b25de23..0174e76a 100644 --- a/src/main/java/liqp/filters/date/BasicDateParser.java +++ b/src/main/java/liqp/filters/date/BasicDateParser.java @@ -18,7 +18,7 @@ public abstract class BasicDateParser { - private final List cachedPatterns = new CopyOnWriteArrayList<>(); + protected final List cachedPatterns = new CopyOnWriteArrayList<>(); protected BasicDateParser() { diff --git a/src/main/java/liqp/filters/date/fuzzy/FuzzyDateParser.java b/src/main/java/liqp/filters/date/fuzzy/FuzzyDateParser.java index f991d06a..e4d89ef1 100644 --- a/src/main/java/liqp/filters/date/fuzzy/FuzzyDateParser.java +++ b/src/main/java/liqp/filters/date/fuzzy/FuzzyDateParser.java @@ -7,7 +7,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Stream; import liqp.filters.date.BasicDateParser; import liqp.filters.date.fuzzy.Part.NewPart; import liqp.filters.date.fuzzy.Part.RecognizedPart; @@ -51,17 +53,37 @@ public ZonedDateTime parse(String normalized, Locale locale, ZoneId defaultZone) return date; } - String pattern = guessPattern(normalized, locale); - - TemporalAccessor temporalAccessor = parseUsingPattern(normalized, pattern, locale); - if (temporalAccessor == null) { + GuessingResult guessingResult = guessPattern(normalized, locale, defaultZone); + if (guessingResult == null) { return null; } - storePattern(pattern); - return getFullDateIfPossible(temporalAccessor, defaultZone); + storePattern(guessingResult.pattern); + return getFullDateIfPossible(guessingResult.temporalAccessor, defaultZone); + } + + GuessingResult guessPattern(String normalized, Locale locale, ZoneId defaultZone) { + Stream guessingStream = getGuessingStream(cachedPatterns, normalized, locale, defaultZone); + return getGuessingResult(guessingStream, normalized, locale, defaultZone); + } + + private Stream getGuessingStream(List cachedPatterns, String normalized, + Locale locale, ZoneId defaultZone) { + // [1, 2][1][1][1] => ["1111"], ["2111"] + List> fullPattern = guessVariants(normalized, locale); + + return fullPattern.stream() + .reduce( + Stream.of(""), // Initial + (stream, list) -> stream.flatMap( + combination -> list.stream().map(element -> combination + element) + ), + Stream::concat + ) + .filter(p -> !cachedPatterns.contains(p)); } - String guessPattern(String normalized, Locale locale) { + + protected List> guessVariants(String normalized, Locale locale) { if (locale == null) { locale = Locale.ENGLISH; } @@ -76,19 +98,51 @@ String guessPattern(String normalized, Locale locale) { return reconstructPattern(parts); } - private boolean haveUnrecognized(List parts) { - return parts.stream().anyMatch(p -> p.state() == Part.PartState.NEW); - } - - private String reconstructPattern(List parts) { + private List> reconstructPattern(List parts) { return parts.stream().map(p -> { if (p.state() == Part.PartState.RECOGNIZED) { - return ((RecognizedPart) p).getPattern(); + return ((RecognizedPart) p).getPatterns(); } else if (p.state() == Part.PartState.PUNCTUATION) { - return p.source(); + return newList(p.source()); } else { - return "'" + p.source() + "'"; + return newList("'" + p.source() + "'"); } - }).collect(Collectors.joining()); + }).collect(Collectors.toList()); + } + + private List newList(String pattern) { + List res = new ArrayList<>(); + res.add(pattern); + return res; + } + + private GuessingResult getGuessingResult(Stream guessingStream, String normalized, Locale locale, ZoneId defaultZone) { + return guessingStream + .map(pattern -> { + TemporalAccessor temporalAccessor = parseUsingPattern(normalized, pattern, locale); + if (temporalAccessor != null) { + GuessingResult result = new GuessingResult(); + result.pattern = pattern; + result.temporalAccessor = temporalAccessor; + return result; + } + return null; + }) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + static class GuessingResult { + String pattern; + TemporalAccessor temporalAccessor; + } + private TemporalAccessor getTemporalAccessor(String normalized, Locale locale, + ZoneId defaultZone, Stream guessingStream) { + return null; + } + + private boolean haveUnrecognized(List parts) { + return parts.stream().anyMatch(p -> p.state() == Part.PartState.NEW); } } diff --git a/src/main/java/liqp/filters/date/fuzzy/Part.java b/src/main/java/liqp/filters/date/fuzzy/Part.java index 36cd8dab..c118d4ef 100644 --- a/src/main/java/liqp/filters/date/fuzzy/Part.java +++ b/src/main/java/liqp/filters/date/fuzzy/Part.java @@ -1,5 +1,8 @@ package liqp.filters.date.fuzzy; +import java.util.ArrayList; +import java.util.List; + public interface Part { enum PartState { NEW, @@ -130,6 +133,11 @@ public String source() { public String getPattern() { return pattern; } + public List getPatterns() { + ArrayList res = new ArrayList<>(); + res.add(pattern); + return res; + } @Override public String toString() { diff --git a/src/test/java/liqp/filters/date/fuzzy/FuzzyDateParserParametrizedTest.java b/src/test/java/liqp/filters/date/fuzzy/FuzzyDateParserParametrizedTest.java index c359209b..7ec4c87f 100644 --- a/src/test/java/liqp/filters/date/fuzzy/FuzzyDateParserParametrizedTest.java +++ b/src/test/java/liqp/filters/date/fuzzy/FuzzyDateParserParametrizedTest.java @@ -23,92 +23,92 @@ public class FuzzyDateParserParametrizedTest { @Parameterized.Parameters public static Collection data() { return Arrays.asList(new Object[][]{ -// {null, "1995", "yyyy"}, -// {null, " 1995 ", " yyyy "}, -// {null, " 1995", " yyyy"}, -// {null, "1995 ", "yyyy "}, -// {null, "January 1995", "MMMM yyyy"}, -// {null, "January 1995 ", "MMMM yyyy "}, -// {null, " January 1995", " MMMM yyyy"}, -// {null, " 1995 January", " yyyy MMMM"}, -// {null, "Jan 1995", "MMM yyyy"}, -// {null, "1995 Jan ", "yyyy MMM "}, -// {Locale.GERMAN, "1995 Mai", "yyyy MMMM"}, -// FuzzyDateParser.CLDR_LOADED ? -// new Object[]{ -// Locale.GERMAN, "??1995-----Dez.!", "'??'yyyy-----MMM'!'"} -// : new Object[]{ -// Locale.GERMAN, "??1995-----Dez!", "'??'yyyy-----MMM'!'"} -// , -// {null, "1:23", "H:mm"}, -// {null, "01:23", "HH:mm"}, -// {null, "1:23:45", "H:mm:ss"}, -// {null, "01:23:45", "HH:mm:ss"}, -// {null, "1:23:45.6", "H:mm:ss.S"}, -// {null, "01:23:45.6", "HH:mm:ss.S"}, -// {null, "1:23:45.67", "H:mm:ss.SS"}, -// {null, "1:23:45.678", "H:mm:ss.SSS"}, -// {null, "1:23:45.6789", "H:mm:ss.SSSS"}, -// {null, "1:23:45.67890", "H:mm:ss.SSSSS"}, -// {null, "1:23:45.678901", "H:mm:ss.SSSSSS"}, -// {null, "1:23:45.6789012", "H:mm:ss.SSSSSSS"}, -// {null, "1:23:45.67890123", "H:mm:ss.SSSSSSSS"}, -// {null, "1:23:45.678901234", "H:mm:ss.SSSSSSSSS"}, -// {null, "1:23:45.678901234am", "h:mm:ss.SSSSSSSSSa"}, // correct -// {null, "1:23:45.678901234a", "H:mm:ss.SSSSSSSSS'a'"}, // incorrect -// {null, "1:23:45.678901234p", "H:mm:ss.SSSSSSSSS'p'"}, // incorrect -// {null, "1:23:45.678901234pm", "h:mm:ss.SSSSSSSSSa"}, // correct -// {null, "1:23:45.678901234 pm", "h:mm:ss.SSSSSSSSS a"}, // correct -// {null, " 1:23:45.678", " H:mm:ss.SSS"}, -// {null, " 1:23:45.678 ", " H:mm:ss.SSS "}, -// {null, " 01:23:45.678 ", " HH:mm:ss.SSS "}, -// {null, " 1:23:45.678 am ", " h:mm:ss.SSS a "}, -// {null, " 1:23:45.678 PM ", " h:mm:ss.SSS a "}, -// {null, "12 Jan 1995T01:23:45.678", "'12' MMM yyyy'T'HH:mm:ss.SSS"}, -// {null, "12 AD", "yy GG"}, -// {null, " 12 AD ", " yy GG "}, -// {null, " 12 Anno Domini ", " yy GGGG "}, -// {null, " 12345 Before Christ ", " yyyyy GGGG "}, -// {null, " 1 BC ", " y GG "}, -// {null, "12 January", "'12' MMMM"}, -// {null, " 12 January ", " '12' MMMM "}, -// {null, "12 Jan", "'12' MMM"}, -// {null, " 12 Jan ", " '12' MMM "}, -// -// {null, " 12 BC 12 Jan 01:23:45.678 ", " yy GG '12' MMM HH:mm:ss.SSS "}, -// {null, "12 Jan 01:23:45.678 12 Anno Domini", "'12' MMM HH:mm:ss.SSS yy GGGG"}, -// {null, "Monday", "EEEE"}, -// {null, " Monday ", " EEEE "}, -// {null, "Monday ", "EEEE "}, -// {null, " Monday", " EEEE"}, -// {null, "Mon", "EEE"}, -// {null, " Mon ", " EEE "}, -// {null, " Mon", " EEE"}, -// {null, "Mon ", "EEE "}, -// {Locale.GERMAN, "Montag", "EEEE"}, -// {Locale.GERMAN, " Montag ", " EEEE "}, -// {Locale.GERMAN, "Montag ", "EEEE "}, -// {Locale.GERMAN, " Montag", " EEEE"}, -// FuzzyDateParser.CLDR_LOADED ? -// new Object[]{ -// Locale.GERMAN, "Mo.", "EEE"} -// : new Object[]{ -// Locale.GERMAN, "Mo", "EEE"} -// , + {null, "1995", "yyyy"}, + {null, " 1995 ", " yyyy "}, + {null, " 1995", " yyyy"}, + {null, "1995 ", "yyyy "}, + {null, "January 1995", "MMMM yyyy"}, + {null, "January 1995 ", "MMMM yyyy "}, + {null, " January 1995", " MMMM yyyy"}, + {null, " 1995 January", " yyyy MMMM"}, + {null, "Jan 1995", "MMM yyyy"}, + {null, "1995 Jan ", "yyyy MMM "}, + {Locale.GERMAN, "1995 Mai", "yyyy MMMM"}, + FuzzyDateParser.CLDR_LOADED ? + new Object[]{ + Locale.GERMAN, "??1995-----Dez.!", "'??'yyyy-----MMM'!'"} + : new Object[]{ + Locale.GERMAN, "??1995-----Dez!", "'??'yyyy-----MMM'!'"} + , + {null, "1:23", "H:mm"}, + {null, "01:23", "HH:mm"}, + {null, "1:23:45", "H:mm:ss"}, + {null, "01:23:45", "HH:mm:ss"}, + {null, "1:23:45.6", "H:mm:ss.S"}, + {null, "01:23:45.6", "HH:mm:ss.S"}, + {null, "1:23:45.67", "H:mm:ss.SS"}, + {null, "1:23:45.678", "H:mm:ss.SSS"}, + {null, "1:23:45.6789", "H:mm:ss.SSSS"}, + {null, "1:23:45.67890", "H:mm:ss.SSSSS"}, + {null, "1:23:45.678901", "H:mm:ss.SSSSSS"}, + {null, "1:23:45.6789012", "H:mm:ss.SSSSSSS"}, + {null, "1:23:45.67890123", "H:mm:ss.SSSSSSSS"}, + {null, "1:23:45.678901234", "H:mm:ss.SSSSSSSSS"}, + {null, "1:23:45.678901234am", "h:mm:ss.SSSSSSSSSa"}, // correct + {null, "1:23:45.678901234a", "H:mm:ss.SSSSSSSSS'a'"}, // incorrect + {null, "1:23:45.678901234p", "H:mm:ss.SSSSSSSSS'p'"}, // incorrect + {null, "1:23:45.678901234pm", "h:mm:ss.SSSSSSSSSa"}, // correct + {null, "1:23:45.678901234 pm", "h:mm:ss.SSSSSSSSS a"}, // correct + {null, " 1:23:45.678", " H:mm:ss.SSS"}, + {null, " 1:23:45.678 ", " H:mm:ss.SSS "}, + {null, " 01:23:45.678 ", " HH:mm:ss.SSS "}, + {null, " 1:23:45.678 am ", " h:mm:ss.SSS a "}, + {null, " 1:23:45.678 PM ", " h:mm:ss.SSS a "}, + {null, "12 Jan 1995T01:23:45.678", "'12' MMM yyyy'T'HH:mm:ss.SSS"}, + {null, "12 AD", "yy GG"}, + {null, " 12 AD ", " yy GG "}, + {null, " 12 Anno Domini ", " yy GGGG "}, + {null, " 12345 Before Christ ", " yyyyy GGGG "}, + {null, " 1 BC ", " y GG "}, + {null, "12 January", "'12' MMMM"}, + {null, " 12 January ", " '12' MMMM "}, + {null, "12 Jan", "'12' MMM"}, + {null, " 12 Jan ", " '12' MMM "}, + + {null, " 12 BC 12 Jan 01:23:45.678 ", " yy GG '12' MMM HH:mm:ss.SSS "}, + {null, "12 Jan 01:23:45.678 12 Anno Domini", "'12' MMM HH:mm:ss.SSS yy GGGG"}, + {null, "Monday", "EEEE"}, + {null, " Monday ", " EEEE "}, + {null, "Monday ", "EEEE "}, + {null, " Monday", " EEEE"}, + {null, "Mon", "EEE"}, + {null, " Mon ", " EEE "}, + {null, " Mon", " EEE"}, + {null, "Mon ", "EEE "}, + {Locale.GERMAN, "Montag", "EEEE"}, + {Locale.GERMAN, " Montag ", " EEEE "}, + {Locale.GERMAN, "Montag ", "EEEE "}, + {Locale.GERMAN, " Montag", " EEEE"}, + FuzzyDateParser.CLDR_LOADED ? + new Object[]{ + Locale.GERMAN, "Mo.", "EEE"} + : new Object[]{ + Locale.GERMAN, "Mo", "EEE"} + , // {null, "Monday 17th September 1999 BC at 12:34:56.000 AM", "EEEE '17th' MMMM yyyy GG 'at' h:mm:ss.SSS a"}, -// {null, "2021-1-2", "yyyy-M-d"}, -// {null, "2021-01-2", "yyyy-MM-d"}, -// {null, "2021-1-02", "yyyy-M-dd"}, -// {null, "2024-1-5 08:15 ", "yyyy-M-d HH:mm "}, -// {null, "2024-12-25 14:45 ", "yyyy-MM-dd HH:mm "}, -// {null, "2024-12-25 14:45:30 ", "yyyy-MM-dd HH:mm:ss "}, -// {null, "1/1/23 ", "d/M/yy "}, -// {null, "1/1/2023 ", "d/M/yyyy "}, -// {null, "01/01/23 ", "dd/MM/yy "}, -// {null, "01/01/2023 ", "dd/MM/yyyy "}, -// {null, "1/1/23 12:34 ", "d/M/yy HH:mm "}, -// {null, "1/1/2023 12:34 ", "d/M/yyyy HH:mm "}, -// {null, "01/01/23 12:34 ", "dd/MM/yy HH:mm "}, + {null, "2021-1-2", "yyyy-M-d"}, + {null, "2021-01-2", "yyyy-MM-d"}, + {null, "2021-1-02", "yyyy-M-dd"}, + {null, "2024-1-5 08:15 ", "yyyy-M-d HH:mm "}, + {null, "2024-12-25 14:45 ", "yyyy-MM-dd HH:mm "}, + {null, "2024-12-25 14:45:30 ", "yyyy-MM-dd HH:mm:ss "}, + {null, "1/1/23 ", "d/M/yy "}, + {null, "1/1/2023 ", "d/M/yyyy "}, + {null, "01/01/23 ", "dd/MM/yy "}, + {null, "01/01/2023 ", "dd/MM/yyyy "}, + {null, "1/1/23 12:34 ", "d/M/yy HH:mm "}, + {null, "1/1/2023 12:34 ", "d/M/yyyy HH:mm "}, + {null, "01/01/23 12:34 ", "dd/MM/yy HH:mm "}, }); } @@ -125,7 +125,7 @@ public void shouldParse() { String pattern = null; try { final FuzzyDateParser parser = new FuzzyDateParser(); - pattern = parser.guessPattern(input, locale); + pattern = parser.guessPattern(input, locale, null).pattern; assertEquals(String.format("input is: [%s], expected pattern: [%s], real pattern: [%s]", input, expectedPattern, pattern), expectedPattern, pattern); parsed = parser.parse(input, locale, null); String formatted = parsed.format(DateTimeFormatter.ofPattern(pattern, locale)).toLowerCase(locale);