Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.time.OffsetDateTime;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
Expand All @@ -60,6 +61,12 @@ public class DateUtils {

private static final int MAX_FORMAT_CACHE_SIZE = 64;

/**
* Prefix marking strict-parsing formatters in the format cache, so they never collide with the
* non-strict formatter of an equal pattern.
*/
private static final String STRICT_CACHE_PREFIX = "strict:";

/**
* Is a cache of dates
*/
Expand Down Expand Up @@ -136,7 +143,7 @@ public static LocalDateTime parseLocalDateTime(String dateString, String dateFor
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = switchDateFormat(dateString);
}
return LocalDateTime.parse(dateString, getCacheDateTimeFormat(dateFormat, local));
return LocalDateTime.parse(dateString, getCacheDateTimeFormat(dateFormat, local, true));
}

/**
Expand All @@ -151,7 +158,7 @@ public static LocalDate parseLocalDate(String dateString, String dateFormat, Loc
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = switchDateFormat(dateString);
}
return LocalDate.parse(dateString, getCacheDateTimeFormat(dateFormat, local));
return LocalDate.parse(dateString, getCacheDateTimeFormat(dateFormat, local, true));
}

/**
Expand All @@ -166,7 +173,7 @@ public static LocalTime parseLocalTime(String timeString, String timeFormat, Loc
if (StringUtils.isEmpty(timeFormat)) {
timeFormat = switchTimeFormat(timeString);
}
return LocalTime.parse(timeString, getCacheDateTimeFormat(timeFormat, local));
return LocalTime.parse(timeString, getCacheDateTimeFormat(timeFormat, local, true));
}

/**
Expand Down Expand Up @@ -451,6 +458,10 @@ public static boolean isDate1904(ExcelContentProperty contentProperty, GlobalCon
}

private static DateTimeFormatter getCacheDateTimeFormat(String dateFormat, Locale locale) {
return getCacheDateTimeFormat(dateFormat, locale, false);
}

private static DateTimeFormatter getCacheDateTimeFormat(String dateFormat, Locale locale, boolean strictParsing) {
Locale actualLocale = locale == null ? Locale.getDefault(Locale.Category.FORMAT) : locale;
Map<Locale, Map<String, DateTimeFormatter>> localeCache = DATE_TIME_FORMATTER_THREAD_LOCAL.get();
if (localeCache == null) {
Expand All @@ -462,14 +473,61 @@ private static DateTimeFormatter getCacheDateTimeFormat(String dateFormat, Local
formatCache = MapUtils.newBoundedMap(MAX_FORMAT_CACHE_SIZE);
localeCache.put(actualLocale, formatCache);
}
DateTimeFormatter formatter = formatCache.get(dateFormat);
String cacheKey = strictParsing ? STRICT_CACHE_PREFIX + toStrictParsePattern(dateFormat) : dateFormat;
DateTimeFormatter formatter = formatCache.get(cacheKey);
if (formatter == null) {
formatter = DateTimeFormatter.ofPattern(dateFormat, actualLocale);
formatCache.put(dateFormat, formatter);
String pattern = strictParsing ? cacheKey.substring(STRICT_CACHE_PREFIX.length()) : dateFormat;
formatter = DateTimeFormatter.ofPattern(pattern, actualLocale);
if (strictParsing) {
formatter = formatter.withResolverStyle(ResolverStyle.STRICT);
}
formatCache.put(cacheKey, formatter);
}
return formatter;
}

/**
* Rewrites a date pattern for strict parsing. The year-of-era letter {@code y} cannot be
* resolved without era information that simple patterns do not carry, so it is replaced by the
* proleptic-year letter {@code u} unless the pattern already contains an era section. Quoted
* literals are kept as-is.
*
* @param dateFormat the original pattern
* @return a pattern equivalent to the original one but resolvable under {@code STRICT}
*/
private static String toStrictParsePattern(String dateFormat) {
boolean containsEra = false;
boolean containsYearOfEra = false;
boolean inQuote = false;
for (int i = 0; i < dateFormat.length(); i++) {
char currentChar = dateFormat.charAt(i);
if (currentChar == '\'') {
inQuote = !inQuote;
} else if (!inQuote) {
if (currentChar == 'G') {
containsEra = true;
} else if (currentChar == 'y') {
containsYearOfEra = true;
}
}
}
if (!containsYearOfEra || containsEra) {
return dateFormat;
}
StringBuilder strictPattern = new StringBuilder(dateFormat.length());
inQuote = false;
for (int i = 0; i < dateFormat.length(); i++) {
char currentChar = dateFormat.charAt(i);
if (currentChar == '\'') {
inQuote = !inQuote;
} else if (!inQuote && currentChar == 'y') {
currentChar = 'u';
}
strictPattern.append(currentChar);
}
return strictPattern.toString();
}

private static DateFormat getCacheDateFormat(String dateFormat) {
Map<String, SimpleDateFormat> dateFormatMap = DATE_FORMAT_THREAD_LOCAL.get();
if (dateFormatMap == null) {
Expand All @@ -482,6 +540,8 @@ private static DateFormat getCacheDateFormat(String dateFormat) {
}
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
// Strictly match the format, e.g. "2024-02-31" is invalid instead of being normalized to 2024-03-02.
simpleDateFormat.setLenient(false);
dateFormatMap.put(dateFormat, simpleDateFormat);
return simpleDateFormat;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.fesod.sheet.converters.date;

import java.text.ParseException;
import org.apache.fesod.sheet.metadata.data.ReadCellData;
import org.apache.fesod.sheet.testkit.Tags;
import org.apache.fesod.sheet.util.DateUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

/**
* Tests {@link DateStringConverter}.
*/
@Tag(Tags.UNIT)
class DateStringConverterTest {

private final DateStringConverter converter = new DateStringConverter();

@AfterEach
void tearDown() {
DateUtils.removeThreadLocalCache();
}

@Test
void convertToJavaDataRejectsInvalidMonthEndInsteadOfNormalizingIt() {
Assertions.assertThrows(
ParseException.class, () -> converter.convertToJavaData(new ReadCellData<>("2024-02-31"), null, null));
}

@Test
void convertToJavaDataRejectsInvalidLeapDayInsteadOfNormalizingIt() {
Assertions.assertThrows(
ParseException.class, () -> converter.convertToJavaData(new ReadCellData<>("2023-02-29"), null, null));
}

@Test
void convertToJavaDataRejectsInvalidDayInDateTimeInsteadOfNormalizingIt() {
Assertions.assertThrows(
ParseException.class,
() -> converter.convertToJavaData(new ReadCellData<>("2024-02-31 10:20:30"), null, null));
}

@Test
void convertToJavaDataAcceptsValidLeapDay() throws ParseException {
String formatted = DateUtils.format(
converter.convertToJavaData(new ReadCellData<>("2024-02-29"), null, null), "yyyy-MM-dd");
Assertions.assertEquals("2024-02-29", formatted);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.fesod.sheet.converters.localdate;

import java.text.ParseException;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import org.apache.fesod.sheet.metadata.GlobalConfiguration;
import org.apache.fesod.sheet.metadata.data.ReadCellData;
import org.apache.fesod.sheet.metadata.property.DateTimeFormatProperty;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.apache.fesod.sheet.testkit.Tags;
import org.apache.fesod.sheet.util.DateUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

/**
* Tests {@link LocalDateStringConverter}.
*/
@Tag(Tags.UNIT)
class LocalDateStringConverterTest {

private final LocalDateStringConverter converter = new LocalDateStringConverter();

@AfterEach
void tearDown() {
DateUtils.removeThreadLocalCache();
}

@Test
void convertToJavaDataRejectsInvalidMonthEndInsteadOfNormalizingIt() {
Assertions.assertThrows(
DateTimeParseException.class,
() -> converter.convertToJavaData(new ReadCellData<>("2024-02-31"), null, new GlobalConfiguration()));
}

@Test
void convertToJavaDataRejectsInvalidLeapDayInsteadOfNormalizingIt() {
Assertions.assertThrows(
DateTimeParseException.class,
() -> converter.convertToJavaData(new ReadCellData<>("2023-02-29"), null, new GlobalConfiguration()));
}

@Test
void convertToJavaDataAcceptsValidLeapDay() throws ParseException {
LocalDate actual =
converter.convertToJavaData(new ReadCellData<>("2024-02-29"), null, new GlobalConfiguration());
Assertions.assertEquals(LocalDate.of(2024, 2, 29), actual);
}

@Test
void convertToJavaDataRejectsInvalidDayWithCustomPattern() {
ExcelContentProperty contentProperty = new ExcelContentProperty();
contentProperty.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy/MM/dd", null));

Assertions.assertThrows(
DateTimeParseException.class, () -> convert(new ReadCellData<>("2024/02/31"), contentProperty));
}

@Test
void convertToJavaDataAcceptsValidDayWithCustomPattern() throws ParseException {
ExcelContentProperty contentProperty = new ExcelContentProperty();
contentProperty.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy/MM/dd", null));

LocalDate actual = convert(new ReadCellData<>("2024/02/29"), contentProperty);
Assertions.assertEquals(LocalDate.of(2024, 2, 29), actual);
}

private LocalDate convert(ReadCellData<?> cellData, ExcelContentProperty contentProperty) throws ParseException {
return converter.convertToJavaData(cellData, contentProperty, new GlobalConfiguration());
}
}
Loading