Skip to content

Commit

Permalink
change to parseLocalDate(). getting LocalDate from strings with timez…
Browse files Browse the repository at this point in the history
…one can be dangerous because it might parse different timezone dates all as LocalDate
  • Loading branch information
fluentfuture committed Apr 13, 2024
1 parent fadb5df commit 9756a88
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 58 deletions.
10 changes: 10 additions & 0 deletions mug/src/main/java/com/google/mu/collect/PrefixSearchTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ public Builder<K, V> add(List<? extends K> compoundKey, V value) {
return this;
}

/**
* Adds all of {@code prefixMappings} into this builder.
*
* @since 8.0
*/
public Builder<K, V> addAll(Map<? extends List<? extends K>, ? extends V> prefixMappings) {
BiStream.from(prefixMappings).forEachOrdered(this::add);
return this;
}

public PrefixSearchTable<K, V> build() {
return new PrefixSearchTable<>(BiStream.from(nodes).mapValues(Node.Builder::build).toMap());
}
Expand Down
72 changes: 45 additions & 27 deletions mug/src/main/java/com/google/mu/time/DateTimeFormats.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ public final class DateTimeFormats {
.repeatedly();
private static final Substring.RepeatingPattern PLACEHOLDERS =
consecutive(noneOf("<>")).immediatelyBetween("<", INCLUSIVE, ">", INCLUSIVE).repeatedly();

private static final Map<List<?>, DateTimeFormatter> ISO_DATE_FORMATTERS =
BiStream.of(
forExample("2011-12-03"), DateTimeFormatter.ISO_LOCAL_DATE,
forExample("2011-12-03+08:00"), DateTimeFormatter.ISO_DATE,
forExample("2011-12-03-08:00"), DateTimeFormatter.ISO_DATE,
forExample("20111203"), DateTimeFormatter.BASIC_ISO_DATE).toMap();
forExample("2011-12-03-08:00"), DateTimeFormatter.ISO_DATE).toMap();

/** These ISO formats all support optional nanoseconds in the format of ".nnnnnnnnn". */
private static final Map<List<?>, DateTimeFormatter> ISO_DATE_TIME_FORMATTERS =
Expand All @@ -157,28 +157,36 @@ public final class DateTimeFormats {
"Tue, 10 Jun 2008 11:05:30 -0800",
"1 Jun 2008 11:05:30 +0800",
"10 Jun 2008 11:05:30 +0800")
.collect(
toMap(
DateTimeFormats::forExample, ex -> DateTimeFormatter.RFC_1123_DATE_TIME));
.collect(toMap(DateTimeFormats::forExample, ex -> DateTimeFormatter.RFC_1123_DATE_TIME));

private static final Map<List<?>, String> DATE_PREFIXES = BiStream.<List<?>, String>builder()
.add(forExample("2011-12-03"), "yyyy-MM-dd")
.add(forExample("2011-12-3"), "yyyy-MM-d")
.add(forExample("2011/12/03"), "yyyy/MM/dd")
.add(forExample("2011/12/3"), "yyyy/MM/d")
.add(forExample("Jan 11 2011"), "LLL dd yyyy")
.add(forExample("Jan 1 2011"), "LLL d yyyy")
.add(forExample("11 Jan 2011"), "dd LLL yyyy")
.add(forExample("1 Jan 2011"), "d LLL yyyy")
.add(forExample("2011 Jan 1"), "yyyy LLL d")
.add(forExample("2011 Jan 11"), "yyyy LLL dd")
.add(forExample("January 11 2011"), "LLLL dd yyyy")
.add(forExample("January 1 2011"), "LLLL d yyyy")
.add(forExample("11 January 2011"), "dd LLLL yyyy")
.add(forExample("1 January 2011"), "d LLLL yyyy")
.add(forExample("2011 January 1"), "yyyy LLLL d")
.add(forExample("2011 January 11"), "yyyy LLLL dd")
.build()
.toMap();

private static final Map<List<?>, DateTimeFormatter> LOCAL_DATE_FORMATTERS =
BiStream.from(DATE_PREFIXES).mapValues(p -> DateTimeFormatter.ofPattern(p))
.append(forExample("20111203"), DateTimeFormatter.BASIC_ISO_DATE)
.toMap();

private static final PrefixSearchTable<Object, String> PREFIX_TABLE =
PrefixSearchTable.<Object, String>builder()
.add(forExample("2011-12-03"), "yyyy-MM-dd")
.add(forExample("2011-12-3"), "yyyy-MM-d")
.add(forExample("2011/12/03"), "yyyy/MM/dd")
.add(forExample("2011/12/3"), "yyyy/MM/d")
.add(forExample("Jan 11 2011"), "LLL dd yyyy")
.add(forExample("Jan 1 2011"), "LLL d yyyy")
.add(forExample("11 Jan 2011"), "dd LLL yyyy")
.add(forExample("1 Jan 2011"), "d LLL yyyy")
.add(forExample("2011 Jan 1"), "yyyy LLL d")
.add(forExample("2011 Jan 11"), "yyyy LLL dd")
.add(forExample("January 11 2011"), "LLLL dd yyyy")
.add(forExample("January 1 2011"), "LLLL d yyyy")
.add(forExample("11 January 2011"), "dd LLLL yyyy")
.add(forExample("1 January 2011"), "d LLLL yyyy")
.add(forExample("2011 January 1"), "yyyy LLLL d")
.add(forExample("2011 January 11"), "yyyy LLLL dd")
.addAll(DATE_PREFIXES)
.add(forExample("T"), "'T'")
.add(forExample("10:15"), "HH:mm")
.add(forExample("10:15:30"), "HH:mm:ss")
Expand Down Expand Up @@ -287,11 +295,11 @@ private static <T> T parseDateTime(String dateTimeString, TemporalQuery<T> query
}

/**
* Parses {@code dateString} to {@link LocalDate}. {@code dateString} could be in the format
* of {@link DateTimeFormatter#ISO_DATE}, {@link DateTimeFormatter#BASIC_ISO_DATE}
* or dates with natural month names like "Jan" or "January".
* Parses {@code dateString} as {@link LocalDate}.
*
* <p>If {@code dateString} has valid time and timezone, they'll be ignored.
* <p>Acceptable formats include dates like "2024/04/11", "2024-04-11", "2024 April 11",
* "Apr 11 2024", "20240401", or even with "10/30/2024", "30/01/2024" etc. as long as it's
* not ambiguous.
*
* <p>Prefer to pre-construct a {@link DateTimeFormatter} using {@link #formatOf} to get
* better performance and earlier error report in case the example date time string cannot
Expand All @@ -300,8 +308,12 @@ private static <T> T parseDateTime(String dateTimeString, TemporalQuery<T> query
* @throws DateTimeException if {@code dateTimeString} cannot be parsed to {@link LocalDate}
* @since 8.0
*/
public static LocalDate parseToLocalDate(String dateString) {
return parseDateTime(dateString, LocalDate::from);
public static LocalDate parseLocalDate(String dateString) {
List<?> signature = forExample(dateString);
return lookup(LOCAL_DATE_FORMATTERS, signature)
.orElseGet(() -> LocalDateRule.resolveFormat(signature)
.orElseThrow(() -> new DateTimeException("unsupported local date: " + dateString)))
.parse(dateString, LocalDate::from);
}

/**
Expand Down Expand Up @@ -465,6 +477,12 @@ static BiOptional<List<Object>, String> resolve(List<?> signature) {
.findFirst();
}

static Optional<DateTimeFormatter> resolveFormat(List<?> signature) {
return resolve(signature)
.filter((prefix, p) -> prefix.size() == 5)
.map((prefix, p) -> DateTimeFormatter.ofPattern(p));
}

private final Predicate<List<?>> predicate;
private final String format;

Expand Down
62 changes: 31 additions & 31 deletions mug/src/test/java/com/google/mu/time/DateTimeFormatsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -733,80 +733,80 @@ public void parseToInstant_invalid()

@Test
public void parseLocalDate_basicIsoDate() {
assertThat(DateTimeFormats.parseToLocalDate("20211020")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseToLocalDate("20211001")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseToLocalDate("20210101")).isEqualTo(LocalDate.of(2021, 1, 1));
assertThat(DateTimeFormats.parseLocalDate("20211020")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseLocalDate("20211001")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseLocalDate("20210101")).isEqualTo(LocalDate.of(2021, 1, 1));
}

@Test
public void parseLocalDate_isoDate() {
assertThat(DateTimeFormats.parseToLocalDate("2021-10-20")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseToLocalDate("2021-10-01")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseToLocalDate("2021-01-01")).isEqualTo(LocalDate.of(2021, 1, 1));
assertThat(DateTimeFormats.parseToLocalDate("2021-01-2")).isEqualTo(LocalDate.of(2021, 1, 2));
assertThat(DateTimeFormats.parseLocalDate("2021-10-20")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseLocalDate("2021-10-01")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseLocalDate("2021-01-01")).isEqualTo(LocalDate.of(2021, 1, 1));
assertThat(DateTimeFormats.parseLocalDate("2021-01-2")).isEqualTo(LocalDate.of(2021, 1, 2));
}

@Test
public void parseLocalDate_euDate_mmddyyyy() {
assertThat(DateTimeFormats.parseToLocalDate("10-30-2021")).isEqualTo(LocalDate.of(2021, 10, 30));
assertThat(DateTimeFormats.parseToLocalDate("1-30-2021")).isEqualTo(LocalDate.of(2021, 1, 30));
assertThat(DateTimeFormats.parseToLocalDate("10/20/2021")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseToLocalDate("1/20/2021")).isEqualTo(LocalDate.of(2021, 1, 20));
assertThat(DateTimeFormats.parseLocalDate("10-30-2021")).isEqualTo(LocalDate.of(2021, 10, 30));
assertThat(DateTimeFormats.parseLocalDate("1-30-2021")).isEqualTo(LocalDate.of(2021, 1, 30));
assertThat(DateTimeFormats.parseLocalDate("10/20/2021")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseLocalDate("1/20/2021")).isEqualTo(LocalDate.of(2021, 1, 20));
}

@Test
public void parseLocalDate_euDate_ddmmyyyy() {
assertThat(DateTimeFormats.parseToLocalDate("30-10-2021")).isEqualTo(LocalDate.of(2021, 10, 30));
assertThat(DateTimeFormats.parseToLocalDate("20/10/2021")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseLocalDate("30-10-2021")).isEqualTo(LocalDate.of(2021, 10, 30));
assertThat(DateTimeFormats.parseLocalDate("20/10/2021")).isEqualTo(LocalDate.of(2021, 10, 20));
}

@Test
public void parseLocalDate_withMonthName_yyyymmdd() {
assertThat(DateTimeFormats.parseToLocalDate("2021 Oct 20")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseToLocalDate("2021 October 1")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseLocalDate("2021 Oct 20")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseLocalDate("2021 October 1")).isEqualTo(LocalDate.of(2021, 10, 1));
}

@Test
public void parseLocalDate_withMonthName_mmddyyyy() {
assertThat(DateTimeFormats.parseToLocalDate("Oct 20 2021")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseToLocalDate("October 1 2021")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseLocalDate("Oct 20 2021")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseLocalDate("October 1 2021")).isEqualTo(LocalDate.of(2021, 10, 1));
}

@Test
public void parseLocalDate_isoDateWithSlash() {
assertThat(DateTimeFormats.parseToLocalDate("2021/10/20")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseToLocalDate("2021/10/01")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseToLocalDate("2021/01/01")).isEqualTo(LocalDate.of(2021, 1, 1));
assertThat(DateTimeFormats.parseToLocalDate("2021/01/2")).isEqualTo(LocalDate.of(2021, 1, 2));
assertThat(DateTimeFormats.parseLocalDate("2021/10/20")).isEqualTo(LocalDate.of(2021, 10, 20));
assertThat(DateTimeFormats.parseLocalDate("2021/10/01")).isEqualTo(LocalDate.of(2021, 10, 1));
assertThat(DateTimeFormats.parseLocalDate("2021/01/01")).isEqualTo(LocalDate.of(2021, 1, 1));
assertThat(DateTimeFormats.parseLocalDate("2021/01/2")).isEqualTo(LocalDate.of(2021, 1, 2));
}

@Test
public void parseLocalDate_instantHasNoDate() {
Instant time = OffsetDateTime.of(2024, 4, 1, 10, 05, 30, 0, ZoneOffset.UTC).toInstant();
assertThrows(
DateTimeException.class,
() -> DateTimeFormats.parseToLocalDate(time.toString()));
() -> DateTimeFormats.parseLocalDate(time.toString()));
}

@Test
public void parseLocalDate_fromZonedDateTimeString() {
public void parseLocalDate_cannotParseZonedDateTimeStringToLocalDate() {
ZonedDateTime time = ZonedDateTime.of(2024, 4, 1, 10, 05, 30, 0, ZoneId.of("America/New_York"));
assertThat(DateTimeFormats.parseToLocalDate(time.toString())).isEqualTo(LocalDate.of(2024, 4, 1));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseLocalDate(time.toString()));
}

@Test
public void parseLocalDate_fromOffsetDateTimeString() {
public void parseLocalDate_cannotParseOffsetDateTimeStringToLocalDate() {
OffsetDateTime time = OffsetDateTime.of(2024, 4, 1, 10, 05, 30, 0, ZoneOffset.of("-08:30"));
assertThat(DateTimeFormats.parseToLocalDate(time.toString())).isEqualTo(LocalDate.of(2024, 4, 1));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseLocalDate(time.toString()));
}

@Test
public void parseLocalDate_incorrectDate() {
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseToLocalDate("20213001"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseToLocalDate("2021/30/01"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseToLocalDate("2021-30-01"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseToLocalDate("01-01-2021"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseToLocalDate("01/01/2021"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseLocalDate("20213001"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseLocalDate("2021/30/01"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseLocalDate("2021-30-01"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseLocalDate("01-01-2021"));
assertThrows(DateTimeException.class, () -> DateTimeFormats.parseLocalDate("01/01/2021"));
}

@Test
Expand Down

0 comments on commit 9756a88

Please sign in to comment.