diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateDateConverter.java index 8304cc515..f10303bf2 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateDateConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateDateConverter.java @@ -49,11 +49,7 @@ public WriteCellData convertToExcelData( Date value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { WriteCellData cellData = new WriteCellData<>(value); - String format = null; - if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { - format = contentProperty.getDateTimeFormatProperty().getFormat(); - } - WorkBookUtil.fillDataFormat(cellData, format, DateUtils.defaultDateFormat); + WorkBookUtil.fillDataFormat(cellData, DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration)); return cellData; } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateStringConverter.java index 528050572..57b6111e5 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/date/DateStringConverter.java @@ -67,11 +67,7 @@ public Date convertToJavaData( @Override public WriteCellData convertToExcelData( Date value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { - return new WriteCellData<>(DateUtils.format(value, null)); - } else { - return new WriteCellData<>(DateUtils.format( - value, contentProperty.getDateTimeFormatProperty().getFormat())); - } + return new WriteCellData<>( + DateUtils.format(value, DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration))); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateDateConverter.java index 902fd33f4..3e8204c6c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateDateConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateDateConverter.java @@ -51,11 +51,7 @@ public WriteCellData convertToExcelData( throws Exception { LocalDateTime localDateTime = value == null ? null : value.atTime(0, 0); WriteCellData cellData = new WriteCellData<>(localDateTime); - String format = null; - if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { - format = contentProperty.getDateTimeFormatProperty().getFormat(); - } - WorkBookUtil.fillDataFormat(cellData, format, DateUtils.defaultLocalDateFormat); + WorkBookUtil.fillDataFormat(cellData, DateUtils.resolveLocalDateFormat(contentProperty, globalConfiguration)); return cellData; } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateStringConverter.java index 21bff20ca..89a1359be 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdate/LocalDateStringConverter.java @@ -68,11 +68,9 @@ public LocalDate convertToJavaData( @Override public WriteCellData convertToExcelData( LocalDate value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { - return new WriteCellData<>(DateUtils.format(value, null, globalConfiguration.getLocale())); - } else { - return new WriteCellData<>(DateUtils.format( - value, contentProperty.getDateTimeFormatProperty().getFormat(), globalConfiguration.getLocale())); - } + return new WriteCellData<>(DateUtils.format( + value, + DateUtils.resolveLocalDateFormat(contentProperty, globalConfiguration), + globalConfiguration.getLocale())); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeDateConverter.java index 0f0353044..0afc02e34 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeDateConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeDateConverter.java @@ -49,11 +49,7 @@ public WriteCellData convertToExcelData( LocalDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { WriteCellData cellData = new WriteCellData<>(value); - String format = null; - if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { - format = contentProperty.getDateTimeFormatProperty().getFormat(); - } - WorkBookUtil.fillDataFormat(cellData, format, DateUtils.defaultDateFormat); + WorkBookUtil.fillDataFormat(cellData, DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration)); return cellData; } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeStringConverter.java index d7ab2cab7..6f025397c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localdatetime/LocalDateTimeStringConverter.java @@ -68,11 +68,9 @@ public LocalDateTime convertToJavaData( @Override public WriteCellData convertToExcelData( LocalDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { - return new WriteCellData<>(DateUtils.format(value, null, globalConfiguration.getLocale())); - } else { - return new WriteCellData<>(DateUtils.format( - value, contentProperty.getDateTimeFormatProperty().getFormat(), globalConfiguration.getLocale())); - } + return new WriteCellData<>(DateUtils.format( + value, + DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration), + globalConfiguration.getLocale())); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java index 940f38162..6d6906722 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java @@ -21,7 +21,6 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; -import org.apache.fesod.common.util.StringUtils; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.metadata.GlobalConfiguration; import org.apache.fesod.sheet.metadata.data.WriteCellData; @@ -42,14 +41,7 @@ public WriteCellData convertToExcelData( throws Exception { LocalDateTime localDateTime = value.toLocalDateTime(); WriteCellData cellData = new WriteCellData<>(localDateTime); - String format = null; - if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { - format = contentProperty.getDateTimeFormatProperty().getFormat(); - if (StringUtils.isEmpty(format)) { - format = null; - } - } - WorkBookUtil.fillDataFormat(cellData, format, DateUtils.defaultDateFormat); + WorkBookUtil.fillDataFormat(cellData, DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration)); return cellData; } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/string/StringNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/string/StringNumberConverter.java index 94d72cfbf..9e84d5bb8 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/string/StringNumberConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/string/StringNumberConverter.java @@ -62,7 +62,7 @@ public String convertToJavaData( return DateUtils.format( cellData.getNumberValue(), DateUtils.isDate1904(contentProperty, globalConfiguration), - contentProperty.getDateTimeFormatProperty().getFormat()); + DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration)); } // If there are "NumberFormat", read as number if (contentProperty != null && contentProperty.getNumberFormatProperty() != null) { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/year/YearStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/year/YearStringConverter.java index 4dd2f07e7..8f55a4067 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/year/YearStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/year/YearStringConverter.java @@ -46,20 +46,17 @@ public CellDataTypeEnum supportExcelTypeKey() { public Year convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return DateUtils.parseYear( - cellData.getStringValue(), getYearFormat(contentProperty), globalConfiguration.getLocale()); + cellData.getStringValue(), + DateUtils.resolveYearFormat(contentProperty, globalConfiguration), + globalConfiguration.getLocale()); } @Override public WriteCellData convertToExcelData( Year value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - return new WriteCellData<>( - DateUtils.format(value, getYearFormat(contentProperty), globalConfiguration.getLocale())); - } - - private static String getYearFormat(ExcelContentProperty contentProperty) { - if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { - return null; - } - return contentProperty.getDateTimeFormatProperty().getFormat(); + return new WriteCellData<>(DateUtils.format( + value, + DateUtils.resolveYearFormat(contentProperty, globalConfiguration), + globalConfiguration.getLocale())); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java index 35b63aff4..cf1087974 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java @@ -31,8 +31,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.fesod.common.util.StringUtils; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.converters.ConverterKeyBuild; +import org.apache.fesod.sheet.util.DateUtils; /** * Write/read holder @@ -125,6 +127,37 @@ public AbstractHolder(BasicParameter basicParameter, AbstractHolder prentAbstrac } else { globalConfiguration.setFiledCacheLocation(basicParameter.getFiledCacheLocation()); } + + // Date patterns resolve eagerly: a workbook snapshots the process-wide default once so every cell in it uses + // one consistent value, while sheet/table holders inherit the workbook's snapshot. A blank value is treated as + // "not set" here rather than stored as-is: the resolvers read an empty pattern as a fallback signal, so + // keeping it in the snapshot would let a later global change leak into an in-flight workbook. + if (StringUtils.isBlank(basicParameter.getDefaultDateFormat())) { + globalConfiguration.setDefaultDateFormat( + prentAbstractHolder != null + ? prentAbstractHolder.getGlobalConfiguration().getDefaultDateFormat() + : DateUtils.getDefaultDateFormat()); + } else { + globalConfiguration.setDefaultDateFormat(basicParameter.getDefaultDateFormat()); + } + + if (StringUtils.isBlank(basicParameter.getDefaultLocalDateFormat())) { + globalConfiguration.setDefaultLocalDateFormat( + prentAbstractHolder != null + ? prentAbstractHolder.getGlobalConfiguration().getDefaultLocalDateFormat() + : DateUtils.getDefaultLocalDateFormat()); + } else { + globalConfiguration.setDefaultLocalDateFormat(basicParameter.getDefaultLocalDateFormat()); + } + + if (StringUtils.isBlank(basicParameter.getDefaultYearFormat())) { + globalConfiguration.setDefaultYearFormat( + prentAbstractHolder != null + ? prentAbstractHolder.getGlobalConfiguration().getDefaultYearFormat() + : DateUtils.getDefaultYearFormat()); + } else { + globalConfiguration.setDefaultYearFormat(basicParameter.getDefaultYearFormat()); + } } @Override diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java index de25c3c08..764ca0a82 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java @@ -31,6 +31,7 @@ import java.util.Objects; import java.util.function.Consumer; import org.apache.fesod.common.util.ListUtils; +import org.apache.fesod.common.util.StringUtils; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CacheLocationEnum; @@ -184,6 +185,72 @@ public T autoStrip(Boolean autoStrip) { return self(); } + /** + * Default pattern for {@code Date}, {@code LocalDateTime} and {@code OffsetDateTime} fields without a + * {@code @DateTimeFormat} pattern. + *

+ * default is the process-wide default ({@link org.apache.fesod.sheet.util.DateUtils#getDefaultDateFormat()}), + * {@code yyyy-MM-dd HH:mm:ss} unless changed. + *

+ * Passing {@code null} leaves the pattern unset, so the read/write context inherits the process-wide default. An + * empty or whitespace-only pattern is rejected rather than silently treated as "not set". + * + * @param defaultDateFormat the pattern, or {@code null} to inherit the process-wide default + * @return + * @throws IllegalArgumentException if the pattern is empty or whitespace-only + */ + public T defaultDateFormat(String defaultDateFormat) { + parameter().setDefaultDateFormat(requireNonBlankPattern(defaultDateFormat, "defaultDateFormat")); + return self(); + } + + /** + * Default pattern for {@code LocalDate} fields without a {@code @DateTimeFormat} pattern. + *

+ * default is the process-wide default ({@link org.apache.fesod.sheet.util.DateUtils#getDefaultLocalDateFormat()}), + * {@code yyyy-MM-dd} unless changed. + *

+ * Passing {@code null} leaves the pattern unset, so the read/write context inherits the process-wide default. An + * empty or whitespace-only pattern is rejected rather than silently treated as "not set". + * + * @param defaultLocalDateFormat the pattern, or {@code null} to inherit the process-wide default + * @return + * @throws IllegalArgumentException if the pattern is empty or whitespace-only + */ + public T defaultLocalDateFormat(String defaultLocalDateFormat) { + parameter().setDefaultLocalDateFormat(requireNonBlankPattern(defaultLocalDateFormat, "defaultLocalDateFormat")); + return self(); + } + + /** + * Default pattern for {@code Year} fields without a {@code @DateTimeFormat} pattern. + *

+ * default is the process-wide default ({@link org.apache.fesod.sheet.util.DateUtils#getDefaultYearFormat()}), + * {@code yyyy} unless changed. + *

+ * Passing {@code null} leaves the pattern unset, so the read/write context inherits the process-wide default. An + * empty or whitespace-only pattern is rejected rather than silently treated as "not set". + * + * @param defaultYearFormat the pattern, or {@code null} to inherit the process-wide default + * @return + * @throws IllegalArgumentException if the pattern is empty or whitespace-only + */ + public T defaultYearFormat(String defaultYearFormat) { + parameter().setDefaultYearFormat(requireNonBlankPattern(defaultYearFormat, "defaultYearFormat")); + return self(); + } + + /** + * Rejects an empty or whitespace-only pattern while keeping {@code null} as "not set", which is how the sibling + * builder parameters behave: the holder then falls back to the parent or process-wide default. + */ + private static String requireNonBlankPattern(String format, String name) { + if (format != null && StringUtils.isBlank(format)) { + throw new IllegalArgumentException(name + " can not be blank"); + } + return format; + } + @SuppressWarnings("unchecked") protected T self() { return (T) this; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java index 19a2bece0..6d3c22985 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java @@ -89,4 +89,21 @@ public class BasicParameter { * Automatic strip includes sheet name and content */ private Boolean autoStrip; + /** + * Default pattern for {@code Date}, {@code LocalDateTime} and {@code OffsetDateTime} fields without a + * {@code @DateTimeFormat} pattern; {@code null} falls back to the process-wide default + * ({@link org.apache.fesod.sheet.util.DateUtils#setDefaultDateFormat(String)}). + */ + private String defaultDateFormat; + /** + * Default pattern for {@code LocalDate} fields without a {@code @DateTimeFormat} pattern; {@code null} falls back + * to the process-wide default + * ({@link org.apache.fesod.sheet.util.DateUtils#setDefaultLocalDateFormat(String)}). + */ + private String defaultLocalDateFormat; + /** + * Default pattern for {@code Year} fields without a {@code @DateTimeFormat} pattern; {@code null} falls back to + * the process-wide default ({@link org.apache.fesod.sheet.util.DateUtils#setDefaultYearFormat(String)}). + */ + private String defaultYearFormat; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java index 6a894dc57..e8bb0b4ac 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java @@ -77,6 +77,28 @@ public class GlobalConfiguration { */ private Boolean autoStrip; + /** + * Per-call default pattern for {@code Date}, {@code LocalDateTime} and {@code OffsetDateTime} fields without a + * {@code @DateTimeFormat} pattern; {@code null} falls back to the process-wide default + * ({@link org.apache.fesod.sheet.util.DateUtils#getDefaultDateFormat()}, snapshotted when the context is created). + */ + private String defaultDateFormat; + + /** + * Per-call default pattern for {@code LocalDate} fields without a {@code @DateTimeFormat} pattern; {@code null} + * falls back to the process-wide default + * ({@link org.apache.fesod.sheet.util.DateUtils#getDefaultLocalDateFormat()}, snapshotted when the context is + * created). + */ + private String defaultLocalDateFormat; + + /** + * Per-call default pattern for {@code Year} fields without a {@code @DateTimeFormat} pattern; {@code null} falls + * back to the process-wide default + * ({@link org.apache.fesod.sheet.util.DateUtils#getDefaultYearFormat()}, snapshotted when the context is created). + */ + private String defaultYearFormat; + public GlobalConfiguration() { this.autoTrim = Boolean.TRUE; this.autoStrip = Boolean.FALSE; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvSheet.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvSheet.java index af9c2f6c7..626e89253 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvSheet.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvSheet.java @@ -709,7 +709,10 @@ private String buildCellValue(CsvCell csvCell) { } // date if (dataFormat == null) { - dataFormatString = DateUtils.defaultDateFormat; + String defaultDateFormat = csvWorkbook.getDefaultDateFormat(); + dataFormatString = !StringUtils.isEmpty(defaultDateFormat) + ? defaultDateFormat + : DateUtils.getDefaultDateFormat(); dataFormat = csvWorkbook.createDataFormat().getFormat(dataFormatString); } if (dataFormatString == null) { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvWorkbook.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvWorkbook.java index 628767696..014b29cd4 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvWorkbook.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/csv/CsvWorkbook.java @@ -78,6 +78,12 @@ public class CsvWorkbook implements Workbook { */ private Locale locale; + /** + * Per-call default pattern for date values without an explicit cell style format; {@code null} falls back to the + * process-wide default ({@link org.apache.fesod.sheet.util.DateUtils#getDefaultDateFormat()}). + */ + private String defaultDateFormat; + /** * Whether to use scientific Format. *

diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java index 062d890ac..49b4046eb 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java @@ -90,13 +90,45 @@ public class DateUtils { public static final String TIME_FORMAT_8 = "HH:mm:ss"; private static final String MINUS = "-"; - public static String defaultDateFormat = DATE_FORMAT_19; + /** + * Built-in default pattern for {@code java.time.Year} values ({@value}). + */ + public static final String DEFAULT_YEAR_FORMAT = "yyyy"; - public static String defaultLocalDateFormat = DATE_FORMAT_10; + /** + * Built-in default pattern for time-only values ({@value}). + */ + public static final String DEFAULT_LOCAL_TIME_FORMAT = TIME_FORMAT_8; - public static String defaultYearFormat = "yyyy"; + /** + * Process-wide default pattern for {@code Date}, {@code LocalDateTime} and {@code OffsetDateTime} values when no + * explicit pattern applies. Each read/write context snapshots this value at creation time, so a change only + * affects contexts created afterwards. + * + * @deprecated direct field writes make an in-flight workbook nondeterministically mixed between the old and the + * new value. Use {@link #setDefaultDateFormat(String)} to change the process-wide default, the + * per-call {@code defaultDateFormat(...)} builder parameter, or a {@code @DateTimeFormat} field + * annotation. This field will be removed in the next major version. + */ + @Deprecated + public static volatile String defaultDateFormat = DATE_FORMAT_19; - public static final String DEFAULT_LOCAL_TIME_FORMAT = TIME_FORMAT_8; + /** + * Process-wide default pattern for {@code LocalDate} values when no explicit pattern applies; snapshot semantics + * as described on {@link #defaultDateFormat}. + * + * @deprecated use {@link #setDefaultLocalDateFormat(String)}, the per-call {@code defaultLocalDateFormat(...)} + * builder parameter, or a {@code @DateTimeFormat} field annotation. This field will be removed in + * the next major version. + */ + @Deprecated + public static volatile String defaultLocalDateFormat = DATE_FORMAT_10; + + /** + * Process-wide default pattern for {@code Year} values when no explicit pattern applies; snapshot semantics as + * described on {@link #defaultDateFormat}. + */ + private static volatile String defaultYearFormat = DEFAULT_YEAR_FORMAT; public static final int SECONDS_PER_MINUTE = 60; public static final int MINUTES_PER_HOUR = 60; @@ -109,6 +141,78 @@ public class DateUtils { private DateUtils() {} + /** + * Returns the process-wide default pattern for {@code Date}, {@code LocalDateTime} and {@code OffsetDateTime} + * values. + */ + public static String getDefaultDateFormat() { + return defaultDateFormat; + } + + /** + * Sets the process-wide default pattern for {@code Date}, {@code LocalDateTime} and {@code OffsetDateTime} + * values. The new value applies to every read/write context created afterwards; an already running context keeps + * the pattern it snapshotted at creation. + * + * @param format the pattern, must not be blank + * @throws IllegalArgumentException if the pattern is blank + */ + public static void setDefaultDateFormat(String format) { + defaultDateFormat = requireNonBlank(format, "default date format"); + } + + /** + * Returns the process-wide default pattern for {@code LocalDate} values. + */ + public static String getDefaultLocalDateFormat() { + return defaultLocalDateFormat; + } + + /** + * Sets the process-wide default pattern for {@code LocalDate} values; snapshot semantics as described on + * {@link #setDefaultDateFormat(String)}. + * + * @param format the pattern, must not be blank + * @throws IllegalArgumentException if the pattern is blank + */ + public static void setDefaultLocalDateFormat(String format) { + defaultLocalDateFormat = requireNonBlank(format, "default local date format"); + } + + /** + * Returns the process-wide default pattern for {@code Year} values. + */ + public static String getDefaultYearFormat() { + return defaultYearFormat; + } + + /** + * Sets the process-wide default pattern for {@code Year} values; snapshot semantics as described on + * {@link #setDefaultDateFormat(String)}. + * + * @param format the pattern, must not be blank + * @throws IllegalArgumentException if the pattern is blank + */ + public static void setDefaultYearFormat(String format) { + defaultYearFormat = requireNonBlank(format, "default year format"); + } + + /** + * Restores every process-wide default date pattern to its built-in value. + */ + public static void resetDefaultDateFormats() { + defaultDateFormat = DATE_FORMAT_19; + defaultLocalDateFormat = DATE_FORMAT_10; + defaultYearFormat = DEFAULT_YEAR_FORMAT; + } + + private static String requireNonBlank(String format, String name) { + if (StringUtils.isBlank(format)) { + throw new IllegalArgumentException(name + " can not be blank"); + } + return format; + } + /** * convert string to date * @@ -450,6 +554,81 @@ public static boolean isDate1904(ExcelContentProperty contentProperty, GlobalCon return globalUse1904windowing != null && globalUse1904windowing; } + /** + * Resolves the pattern for a datetime-typed value ({@code Date}, {@code LocalDateTime}, {@code OffsetDateTime}): + * a non-empty {@code @DateTimeFormat} pattern wins, otherwise the per-call configuration, otherwise the + * process-wide default. Types whose string form has its own canonical representation (for example ISO-based + * formats) should keep that representation instead of routing through this helper. + * + * @param contentProperty the field's content property, may be null + * @param globalConfiguration the per-call configuration, may be null + * @return the resolved pattern, never null or empty + */ + public static String resolveDateTimeFormat( + ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + String format = annotationFormat(contentProperty); + if (format != null) { + return format; + } + if (globalConfiguration != null && !StringUtils.isEmpty(globalConfiguration.getDefaultDateFormat())) { + return globalConfiguration.getDefaultDateFormat(); + } + return getDefaultDateFormat(); + } + + /** + * Resolves the pattern for a {@code LocalDate} value; precedence as described on + * {@link #resolveDateTimeFormat(ExcelContentProperty, GlobalConfiguration)}. + * + * @param contentProperty the field's content property, may be null + * @param globalConfiguration the per-call configuration, may be null + * @return the resolved pattern, never null or empty + */ + public static String resolveLocalDateFormat( + ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + String format = annotationFormat(contentProperty); + if (format != null) { + return format; + } + if (globalConfiguration != null && !StringUtils.isEmpty(globalConfiguration.getDefaultLocalDateFormat())) { + return globalConfiguration.getDefaultLocalDateFormat(); + } + return getDefaultLocalDateFormat(); + } + + /** + * Resolves the pattern for a {@code Year} value; precedence as described on + * {@link #resolveDateTimeFormat(ExcelContentProperty, GlobalConfiguration)}. + * + * @param contentProperty the field's content property, may be null + * @param globalConfiguration the per-call configuration, may be null + * @return the resolved pattern, never null or empty + */ + public static String resolveYearFormat( + ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + String format = annotationFormat(contentProperty); + if (format != null) { + return format; + } + if (globalConfiguration != null && !StringUtils.isEmpty(globalConfiguration.getDefaultYearFormat())) { + return globalConfiguration.getDefaultYearFormat(); + } + return getDefaultYearFormat(); + } + + /** + * Extracts the {@code @DateTimeFormat} pattern of a field, treating null and empty as "not set". + */ + private static String annotationFormat(ExcelContentProperty contentProperty) { + if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { + String format = contentProperty.getDateTimeFormatProperty().getFormat(); + if (!StringUtils.isEmpty(format)) { + return format; + } + } + return null; + } + private static DateTimeFormatter getCacheDateTimeFormat(String dateFormat, Locale locale) { Locale actualLocale = locale == null ? Locale.getDefault(Locale.Category.FORMAT) : locale; Map> localeCache = DATE_TIME_FORMATTER_THREAD_LOCAL.get(); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java index 244384205..3f3af59e7 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java @@ -106,6 +106,8 @@ public static void createWorkBook(WriteWorkbookHolder writeWorkbookHolder) throw writeWorkbookHolder.getGlobalConfiguration().getUseScientificFormat(), writeWorkbookHolder.getCharset(), writeWorkbookHolder.getWithBom()); + csvWorkbook.setDefaultDateFormat( + writeWorkbookHolder.getGlobalConfiguration().getDefaultDateFormat()); if (writeWorkbookHolder.getWriteWorkbook().getCsvFormat() != null) { csvWorkbook.setCsvFormat( writeWorkbookHolder.getWriteWorkbook().getCsvFormat()); @@ -148,6 +150,10 @@ public static Cell createCell(Row row, int colNum, String cellValue) { return cell; } + public static void fillDataFormat(WriteCellData cellData, String format) { + fillDataFormat(cellData, format, null); + } + public static void fillDataFormat(WriteCellData cellData, String format, String defaultFormat) { if (cellData.getWriteCellStyle() == null) { cellData.setWriteCellStyle(new WriteCellStyle()); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java index 48b0fc596..4ad4102a1 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java @@ -315,12 +315,11 @@ protected WriteCellData convert(CellWriteHandlerContext cellWriteHandlerConte private void fillProperty(WriteCellData cellDataValue, ExcelContentProperty excelContentProperty) { switch (cellDataValue.getType()) { case DATE: - String dateFormat = null; - if (excelContentProperty != null && excelContentProperty.getDateTimeFormatProperty() != null) { - dateFormat = - excelContentProperty.getDateTimeFormatProperty().getFormat(); - } - WorkBookUtil.fillDataFormat(cellDataValue, dateFormat, DateUtils.defaultDateFormat); + WorkBookUtil.fillDataFormat( + cellDataValue, + DateUtils.resolveDateTimeFormat( + excelContentProperty, + writeContext.currentWriteHolder().globalConfiguration())); return; case NUMBER: String numberFormat = null; diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index 11d665a49..cd296b2bb 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -174,7 +174,7 @@ void dateConverterFallsBackToDefaultFormatForEmptyDateTimeFormat() throws Except property.setDateTimeFormatProperty(new DateTimeFormatProperty("", false)); WriteCellData result = converter.convertToExcelData(VALUE, property, new GlobalConfiguration()); Assertions.assertEquals( - DateUtils.defaultDateFormat, + DateUtils.getDefaultDateFormat(), result.getWriteCellStyle().getDataFormatData().getFormat()); } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java index 3688ead82..8cfeb10d4 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java @@ -42,15 +42,10 @@ public CellDataTypeEnum supportExcelTypeKey() { public WriteCellData convertToExcelData( Timestamp value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { WriteCellData cellData = new WriteCellData<>(); - String cellValue; - if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { - cellValue = DateUtils.format(value.toLocalDateTime(), null, globalConfiguration.getLocale()); - } else { - cellValue = DateUtils.format( - value.toLocalDateTime(), - contentProperty.getDateTimeFormatProperty().getFormat(), - globalConfiguration.getLocale()); - } + String cellValue = DateUtils.format( + value.toLocalDateTime(), + DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration), + globalConfiguration.getLocale()); cellData.setType(CellDataTypeEnum.STRING); cellData.setStringValue(cellValue); cellData.setData(cellValue); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java index 0f344f7e1..0df29c23d 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java @@ -396,7 +396,7 @@ void test_fieldShadowing_subclassWithoutAnnotation(ExcelFormat format) throws Ex .row(1) // col 0 = childDate: subclass shadows parent, no @DateTimeFormat inherited .cell(0) - .hasDataFormatString(DateUtils.defaultDateFormat) + .hasDataFormatString(DateUtils.getDefaultDateFormat()) .and() // col 1 = childDoubleValue: subclass shadows parent, no @NumberFormat inherited .cell(1) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/format/DefaultDateFormatWriteTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/format/DefaultDateFormatWriteTest.java new file mode 100644 index 000000000..e134ea6c2 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/format/DefaultDateFormatWriteTest.java @@ -0,0 +1,212 @@ +/* + * 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.format; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Year; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.assertions.ExcelAssertions; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.util.DateUtils; +import org.apache.fesod.sheet.write.handler.RowWriteHandler; +import org.apache.fesod.sheet.write.handler.context.RowWriteHandlerContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * End-to-end coverage of the default date patterns: per-call builder parameters, the process-wide setters, and the + * workbook-level snapshot semantics (a mid-write global change must not affect the running workbook). + */ +@Tag(Tags.FORMAT) +public class DefaultDateFormatWriteTest extends AbstractExcelTest { + + private static final String PARAM_DATE_TIME_FORMAT = "yyyy/MM/dd HH:mm:ss"; + + private static final String PARAM_LOCAL_DATE_FORMAT = "yyyy/MM/dd"; + + private static final String PARAM_YEAR_FORMAT = "yy"; + + @AfterEach + void resetDefaults() { + DateUtils.resetDefaultDateFormats(); + } + + @Test + void perCallBuilderParametersApplyToXlsxWrite() throws IOException { + File file = createTempFile(ExcelFormat.XLSX); + + FesodSheet.write(file) + .defaultDateFormat(PARAM_DATE_TIME_FORMAT) + .defaultLocalDateFormat(PARAM_LOCAL_DATE_FORMAT) + .defaultYearFormat(PARAM_YEAR_FORMAT) + .head(DateRow.class) + .sheet() + .doWrite(rows()); + + ExcelAssertions.assertThat(file) + .sheet(0) + .row(1) + .cell(0) + .hasDataFormatString(PARAM_DATE_TIME_FORMAT) + .and() + .cell(1) + .hasDataFormatString(PARAM_DATE_TIME_FORMAT) + .and() + .cell(2) + .hasDataFormatString(PARAM_LOCAL_DATE_FORMAT) + .and() + .cell(3) + .hasStringValue("26"); + } + + @Test + void globalSettersApplyToXlsxWrite() throws IOException { + DateUtils.setDefaultDateFormat(PARAM_DATE_TIME_FORMAT); + DateUtils.setDefaultLocalDateFormat(PARAM_LOCAL_DATE_FORMAT); + DateUtils.setDefaultYearFormat(PARAM_YEAR_FORMAT); + File file = createTempFile(ExcelFormat.XLSX); + + FesodSheet.write(file).head(DateRow.class).sheet().doWrite(rows()); + + ExcelAssertions.assertThat(file) + .sheet(0) + .row(1) + .cell(0) + .hasDataFormatString(PARAM_DATE_TIME_FORMAT) + .and() + .cell(1) + .hasDataFormatString(PARAM_DATE_TIME_FORMAT) + .and() + .cell(2) + .hasDataFormatString(PARAM_LOCAL_DATE_FORMAT) + .and() + .cell(3) + .hasStringValue("26"); + } + + @Test + void midWriteGlobalChangeDoesNotAffectRunningWorkbook() throws IOException { + File file = createTempFile(ExcelFormat.XLSX); + + FesodSheet.write(file) + .registerWriteHandler(new GlobalFlippingRowHandler()) + .head(DateRow.class) + .sheet() + .doWrite(rows()); + + // Both rows must carry the snapshot taken at context creation, not the flipped global value. + ExcelAssertions.assertThat(file) + .sheet(0) + .row(1) + .cell(0) + .hasDataFormatString(DateUtils.DATE_FORMAT_19) + .and() + .and() + .row(2) + .cell(0) + .hasDataFormatString(DateUtils.DATE_FORMAT_19); + } + + @Test + void csvWriteUsesPerCallDefault() throws IOException { + File file = createTempFile("defaults", ExcelFormat.CSV); + + FesodSheet.write(file) + .defaultDateFormat(PARAM_DATE_TIME_FORMAT) + .head(DateRow.class) + .sheet() + .doWrite(rows()); + + String csv = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + Assertions.assertTrue(csv.contains("2026/01/15 10:30:00")); + Assertions.assertTrue(csv.contains("2026/01/15")); + } + + @Test + void builderParametersRejectBlankPatterns() throws IOException { + File file = createTempFile(ExcelFormat.XLSX); + + Assertions.assertThrows(IllegalArgumentException.class, () -> { + FesodSheet.write(file).defaultDateFormat(""); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + FesodSheet.write(file).defaultLocalDateFormat(" "); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + FesodSheet.write(file).defaultYearFormat("\t"); + }); + + // null keeps its "not set" meaning, matching the sibling builder parameters. + Assertions.assertDoesNotThrow(() -> { + FesodSheet.write(file).defaultDateFormat(null); + }); + } + + @Getter + @Setter + static class DateRow { + private Date date; + private LocalDateTime dateTime; + private LocalDate localDate; + private Year year; + } + + /** + * Flips the process-wide default after the first data row was written; the running workbook must keep using the + * pattern it snapshotted at context creation. + */ + static class GlobalFlippingRowHandler implements RowWriteHandler { + @Override + public void afterRowDispose(RowWriteHandlerContext context) { + if (Integer.valueOf(1).equals(context.getRowIndex())) { + DateUtils.setDefaultDateFormat("yyyyMMddHHmmss"); + } + } + } + + private static List rows() { + return Arrays.asList(row(15), row(16)); + } + + private static DateRow row(int day) { + DateRow row = new DateRow(); + row.setDate(new GregorianCalendar(2026, Calendar.JANUARY, day, 10, 30, 0).getTime()); + row.setDateTime(LocalDateTime.of(2026, 1, day, 10, 30, 0)); + row.setLocalDate(LocalDate.of(2026, 1, day)); + row.setYear(Year.of(2026)); + return row; + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsDefaultFormatTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsDefaultFormatTest.java new file mode 100644 index 000000000..fc078478e --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsDefaultFormatTest.java @@ -0,0 +1,223 @@ +/* + * 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.util; + +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +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.write.metadata.WriteSheet; +import org.apache.fesod.sheet.write.metadata.WriteWorkbook; +import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder; +import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder; +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 the default date pattern tiering: {@code @DateTimeFormat} annotation over per-call configuration over the + * process-wide default, plus the workbook-level snapshot semantics of {@link DateUtils} defaults. + */ +@Tag(Tags.UNIT) +class DateUtilsDefaultFormatTest { + + private static final String ANNOTATION_FORMAT = "yyyy.MM.dd"; + + private static final String PARAM_FORMAT = "yyyy/MM/dd"; + + private static final String GLOBAL_FORMAT = "yyyyMMdd"; + + @AfterEach + void resetDefaults() { + DateUtils.resetDefaultDateFormats(); + } + + @Test + void builtInDefaultsAreExposed() { + Assertions.assertEquals(DateUtils.DATE_FORMAT_19, DateUtils.getDefaultDateFormat()); + Assertions.assertEquals(DateUtils.DATE_FORMAT_10, DateUtils.getDefaultLocalDateFormat()); + Assertions.assertEquals(DateUtils.DEFAULT_YEAR_FORMAT, DateUtils.getDefaultYearFormat()); + } + + @Test + void settersRejectBlankPatterns() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DateUtils.setDefaultDateFormat(null)); + Assertions.assertThrows(IllegalArgumentException.class, () -> DateUtils.setDefaultLocalDateFormat(" ")); + Assertions.assertThrows(IllegalArgumentException.class, () -> DateUtils.setDefaultYearFormat("")); + } + + @Test + void resetRestoresBuiltInPatterns() { + DateUtils.setDefaultDateFormat(PARAM_FORMAT); + DateUtils.setDefaultLocalDateFormat(PARAM_FORMAT); + DateUtils.setDefaultYearFormat(PARAM_FORMAT); + + DateUtils.resetDefaultDateFormats(); + + Assertions.assertEquals(DateUtils.DATE_FORMAT_19, DateUtils.getDefaultDateFormat()); + Assertions.assertEquals(DateUtils.DATE_FORMAT_10, DateUtils.getDefaultLocalDateFormat()); + Assertions.assertEquals(DateUtils.DEFAULT_YEAR_FORMAT, DateUtils.getDefaultYearFormat()); + } + + @Test + void annotationWinsOverPerCallConfigurationAndGlobalDefault() { + GlobalConfiguration globalConfiguration = perCallConfiguration(PARAM_FORMAT); + DateUtils.setDefaultDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultLocalDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultYearFormat(GLOBAL_FORMAT); + ExcelContentProperty contentProperty = contentProperty(ANNOTATION_FORMAT); + + Assertions.assertEquals( + ANNOTATION_FORMAT, DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration)); + Assertions.assertEquals( + ANNOTATION_FORMAT, DateUtils.resolveLocalDateFormat(contentProperty, globalConfiguration)); + Assertions.assertEquals(ANNOTATION_FORMAT, DateUtils.resolveYearFormat(contentProperty, globalConfiguration)); + } + + @Test + void emptyAnnotationFallsThroughToLowerTiers() { + GlobalConfiguration globalConfiguration = perCallConfiguration(PARAM_FORMAT); + ExcelContentProperty contentProperty = contentProperty(""); + + Assertions.assertEquals(PARAM_FORMAT, DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration)); + Assertions.assertEquals(PARAM_FORMAT, DateUtils.resolveLocalDateFormat(contentProperty, globalConfiguration)); + Assertions.assertEquals(PARAM_FORMAT, DateUtils.resolveYearFormat(contentProperty, globalConfiguration)); + } + + @Test + void perCallConfigurationWinsOverGlobalDefault() { + GlobalConfiguration globalConfiguration = perCallConfiguration(PARAM_FORMAT); + DateUtils.setDefaultDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultLocalDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultYearFormat(GLOBAL_FORMAT); + ExcelContentProperty contentProperty = contentProperty(null); + + Assertions.assertEquals(PARAM_FORMAT, DateUtils.resolveDateTimeFormat(contentProperty, globalConfiguration)); + Assertions.assertEquals(PARAM_FORMAT, DateUtils.resolveLocalDateFormat(contentProperty, globalConfiguration)); + Assertions.assertEquals(PARAM_FORMAT, DateUtils.resolveYearFormat(contentProperty, globalConfiguration)); + } + + @Test + void globalDefaultIsUsedWithoutAnnotationOrPerCallConfiguration() { + DateUtils.setDefaultDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultLocalDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultYearFormat(GLOBAL_FORMAT); + ExcelContentProperty contentProperty = contentProperty(null); + + Assertions.assertEquals( + GLOBAL_FORMAT, DateUtils.resolveDateTimeFormat(contentProperty, new GlobalConfiguration())); + Assertions.assertEquals( + GLOBAL_FORMAT, DateUtils.resolveLocalDateFormat(contentProperty, new GlobalConfiguration())); + Assertions.assertEquals(GLOBAL_FORMAT, DateUtils.resolveYearFormat(contentProperty, new GlobalConfiguration())); + } + + @Test + void nullArgumentsResolveToGlobalDefault() { + DateUtils.setDefaultDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultLocalDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultYearFormat(GLOBAL_FORMAT); + + Assertions.assertEquals(GLOBAL_FORMAT, DateUtils.resolveDateTimeFormat(null, null)); + Assertions.assertEquals(GLOBAL_FORMAT, DateUtils.resolveLocalDateFormat(null, null)); + Assertions.assertEquals(GLOBAL_FORMAT, DateUtils.resolveYearFormat(null, null)); + } + + @Test + void workbookHolderSnapshotsGlobalDefaultOnce() { + DateUtils.setDefaultDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultLocalDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultYearFormat(GLOBAL_FORMAT); + + WriteWorkbookHolder workbookHolder = new WriteWorkbookHolder(new WriteWorkbook()); + + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultDateFormat()); + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultLocalDateFormat()); + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultYearFormat()); + + // The workbook keeps its snapshot; a global change only affects contexts created afterwards. + DateUtils.setDefaultDateFormat(PARAM_FORMAT); + WriteSheetHolder sheetHolder = new WriteSheetHolder(new WriteSheet(), workbookHolder); + + Assertions.assertEquals( + GLOBAL_FORMAT, sheetHolder.getGlobalConfiguration().getDefaultDateFormat()); + } + + @Test + void workbookParameterOverridesGlobalSnapshot() { + DateUtils.setDefaultDateFormat(GLOBAL_FORMAT); + WriteWorkbook writeWorkbook = new WriteWorkbook(); + writeWorkbook.setDefaultDateFormat(PARAM_FORMAT); + + WriteWorkbookHolder workbookHolder = new WriteWorkbookHolder(writeWorkbook); + + Assertions.assertEquals( + PARAM_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultDateFormat()); + } + + @Test + void blankPerCallFormatsFallBackToTheSnapshot() { + DateUtils.setDefaultDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultLocalDateFormat(GLOBAL_FORMAT); + DateUtils.setDefaultYearFormat(GLOBAL_FORMAT); + + WriteWorkbook writeWorkbook = new WriteWorkbook(); + writeWorkbook.setDefaultDateFormat(""); + writeWorkbook.setDefaultLocalDateFormat(" "); + writeWorkbook.setDefaultYearFormat("\t"); + WriteWorkbookHolder workbookHolder = new WriteWorkbookHolder(writeWorkbook); + + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultDateFormat()); + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultLocalDateFormat()); + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultYearFormat()); + + // A blank per-call value must not leave the snapshot open to a later global change. + DateUtils.setDefaultDateFormat(PARAM_FORMAT); + DateUtils.setDefaultLocalDateFormat(PARAM_FORMAT); + DateUtils.setDefaultYearFormat(PARAM_FORMAT); + + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultDateFormat()); + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultLocalDateFormat()); + Assertions.assertEquals( + GLOBAL_FORMAT, workbookHolder.getGlobalConfiguration().getDefaultYearFormat()); + } + + private static ExcelContentProperty contentProperty(String format) { + ExcelContentProperty contentProperty = new ExcelContentProperty(); + contentProperty.setDateTimeFormatProperty(new DateTimeFormatProperty(format, null)); + return contentProperty; + } + + private static GlobalConfiguration perCallConfiguration(String format) { + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + globalConfiguration.setDefaultDateFormat(format); + globalConfiguration.setDefaultLocalDateFormat(format); + globalConfiguration.setDefaultYearFormat(format); + return globalConfiguration; + } +} diff --git a/website/docs/sheet/help/parameter.md b/website/docs/sheet/help/parameter.md index 1c4de33c7..c1e2a26bf 100644 --- a/website/docs/sheet/help/parameter.md +++ b/website/docs/sheet/help/parameter.md @@ -127,6 +127,9 @@ All parameters inherit from `BasicParameter`. | customConverterList | Empty | Many converters are loaded by default. You can add unsupported fields here. | | autoTrim | true | Automatically trims the header, reads data, etc. | | use1904windowing | false | Time is stored as a double-precision floating-point number starting from 1900, but sometimes the default start date is 1904. So setting this value changes the default start date to 1904. | +| defaultDateFormat | yyyy-MM-dd HH:mm:ss | Default pattern for `Date`, `LocalDateTime` and `OffsetDateTime` fields without a `@DateTimeFormat` pattern. Falls back to the process-wide default (`DateUtils.setDefaultDateFormat`). See [write format - Default Date Formats](../write/format.md#default-date-formats). | +| defaultLocalDateFormat | yyyy-MM-dd | Default pattern for `LocalDate` fields without a `@DateTimeFormat` pattern. Falls back to the process-wide default (`DateUtils.setDefaultLocalDateFormat`). | +| defaultYearFormat | yyyy | Default pattern for `Year` fields without a `@DateTimeFormat` pattern. Falls back to the process-wide default (`DateUtils.setDefaultYearFormat`). | | useScientificFormat | false | When converting numbers to text, whether to use scientific notation for large values. | | locale | Empty | DeepL Translate_ The world's most accurate translator | | filedCacheLocation | THREAD_LOCAL | Parsing the fields of the class will have a cache. Before, it was placed globally in a Map. After 3.3.0, it is placed in ThreadLocal by default, meaning that each read and write will reparse the class. You can reflectively modify the annotations of the class, and concurrent scenarios will not affect each other.
THREAD_LOCAL: Default, the cache will be cleared each time, but not the same time.
MEMORY: Placed in global memory, theoretically better performance, but cannot modify the exported object through reflection, exclusion, etc.
NONE: No caching, performance will degrade, consider using it when reading and writing at the same time, and you need to reflectively exclude, modify objects, etc. | diff --git a/website/docs/sheet/read/format.md b/website/docs/sheet/read/format.md index 83132907f..417775fde 100644 --- a/website/docs/sheet/read/format.md +++ b/website/docs/sheet/read/format.md @@ -98,3 +98,14 @@ Converts number cells to formatted strings when the field type is `String`. |:---|:---|:---| | `value` | Empty | Number format pattern, refer to `java.text.DecimalFormat` | | `roundingMode` | `HALF_UP` | Rounding mode when formatting | + +--- + +## Default Date Formats on Read + +Reading rarely needs a default pattern: `String` cells are parsed by length-based pattern detection, and date-formatted numeric cells use the format stored in the file. The default patterns matter on read in two edge cases: + +- `Year` fields: a `String` cell is parsed with the resolved `Year` pattern (`@DateTimeFormat` over the per-call `defaultYearFormat` parameter over the process-wide default, `yyyy`). +- `String` fields with an empty `@DateTimeFormat` reading a numeric date cell: the resolved date pattern renders the value. + +The full cascade, the builder parameters and the process-wide setters are documented in [write format - Default Date Formats](../write/format.md#default-date-formats); each read context snapshots the process-wide defaults at creation. diff --git a/website/docs/sheet/write/format.md b/website/docs/sheet/write/format.md index eb48f5a60..4bb4ae545 100644 --- a/website/docs/sheet/write/format.md +++ b/website/docs/sheet/write/format.md @@ -77,3 +77,60 @@ public void converterWrite() { + +## Default Date Formats + +### Overview + +When a date-typed field carries no `@DateTimeFormat` pattern, Fesod resolves its pattern through a three-tier cascade: + +| Priority | Source | Scope | +| --- | --- | --- | +| 1 | `@DateTimeFormat("...")` | per field | +| 2 | `.defaultDateFormat(...)` builder parameter | per read/write call | +| 3 | `DateUtils.setDefaultDateFormat(...)` | process-wide | +| — | built-in constant | `yyyy-MM-dd HH:mm:ss` | + +Three builder parameters cover the date types: + +| Builder parameter | Applies to | Built-in constant | +| --- | --- | --- | +| `defaultDateFormat` | `Date`, `LocalDateTime`, `OffsetDateTime` | `yyyy-MM-dd HH:mm:ss` | +| `defaultLocalDateFormat` | `LocalDate` | `yyyy-MM-dd` | +| `defaultYearFormat` | `Year` | `yyyy` | + +### Per-Call Default + +```java +FesodSheet.write(fileName, OrderData.class) + .defaultDateFormat("yyyy/MM/dd HH:mm:ss") + .defaultLocalDateFormat("yyyy/MM/dd") + .sheet() + .doWrite(data()); +``` + +The parameters are also available on the read side (`FesodSheet.read(...)`), which mainly affects `Year` parsing when no pattern is annotated. + +### Process-Wide Default + +Applications that want one default everywhere can set it once at startup instead of annotating every field or repeating the builder parameter: + +```java +DateUtils.setDefaultDateFormat("yyyy/MM/dd HH:mm:ss"); +DateUtils.setDefaultLocalDateFormat("yyyy/MM/dd"); +DateUtils.setDefaultYearFormat("yyyy"); +``` + +The setters reject blank patterns and `DateUtils.resetDefaultDateFormats()` restores the built-in values. + +### Snapshot Semantics + +Each read/write context snapshots the process-wide default when it is created, so every cell of one workbook is written with one consistent pattern. Changing the process-wide default mid-run only affects contexts created afterwards — it never leaves an in-flight workbook half-formatted with the old value and half with the new one. + +:::note Migration from `DateUtils.defaultDateFormat` field writes +The public static fields `DateUtils.defaultDateFormat` and `DateUtils.defaultLocalDateFormat` (inherited from the EasyExcel lineage, never documented) are deprecated in favor of the setters above; they still work and are now `volatile`, but direct field writes make an in-flight workbook nondeterministically mixed. `DateUtils.defaultYearFormat` never shipped in a release and was replaced by the setter outright. The fields will be removed in the next major version. +::: + +:::info For converter contributors +New converters that need a default pattern should take a `GlobalConfiguration` field plus a `DateUtils.resolve*Format(contentProperty, globalConfiguration)` call instead of introducing new public static mutable state on `DateUtils`. Types whose string form has a canonical representation (for example ISO-based formats) may keep that representation instead of joining the cascade. +::: diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md index 83a05e5a0..9ecba595c 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md @@ -126,7 +126,10 @@ WriteWorkbook --|> WriteBasicParameter | clazz | 空 | 与 `head` 二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据 | | customConverterList | 空 | 默认加载了很多转换器,这里可以加入不支持的字段 | | autoTrim | true | 会对表头、读取数据等进行自动去除前后空格 | -| use1904windowing | false | 电子表格中时间是存储 1900 年起的一个双精度浮点数,但是有时候默认开始日期是 1904,所以设置这个值改成默认 1904 年开始 | +| use1904windowing | false | 电子表格中时间是存储 1900 年起的一个双精度浮点数,但是有时候默认开始日期是 1904,所以设置这个值改成默认 1904 年开始 | +| defaultDateFormat | yyyy-MM-dd HH:mm:ss | `Date`、`LocalDateTime`、`OffsetDateTime` 字段没有 `@DateTimeFormat` 格式时的默认格式。未设置时回退到进程级默认值(`DateUtils.setDefaultDateFormat`)。见[写侧格式文档 - 默认日期格式](../write/format.md#默认日期格式) | +| defaultLocalDateFormat | yyyy-MM-dd | `LocalDate` 字段没有 `@DateTimeFormat` 格式时的默认格式。未设置时回退到进程级默认值(`DateUtils.setDefaultLocalDateFormat`) | +| defaultYearFormat| yyyy| `Year` 字段没有 `@DateTimeFormat` 格式时的默认格式。未设置时回退到进程级默认值(`DateUtils.setDefaultYearFormat`) | | useScientificFormat | false | 数字转文本的时候在较大的数值的是否是否采用科学计数法 | | locale | 空 | 此参数用于格式化日期和数字 | | filedCacheLocation | THREAD_LOCAL | 解析 class 的 field 会有缓存,默认放到 ThreadLocal ,也就是说每次读写都会重新解析 class,可以反射修改 class 的注解,并发场景不会相互影响
THREAD_LOCAL:默认,每次读写都会缓存,但是不是同一次不会影响
MEMORY:放到全局的内存里面,理论上性能会更好,但是无法通过反射、排除等方法修改导出的对象
NONE:不缓存,性能会变差,涉及到读的同时要写,而且还要反射、排除等方法去修改对象的情况下可以考虑使用。 | diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/format.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/format.md index 53c73d4ef..1d071bb2f 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/format.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/format.md @@ -98,3 +98,14 @@ public void converterRead() { |:---|:---|:---| | `value` | 空 | 数字格式模式,参考 `java.text.DecimalFormat` | | `roundingMode` | `HALF_UP` | 格式化时的舍入模式 | + +--- + +## 读侧的默认日期格式 + +读取通常不需要默认格式:`String` 单元格按长度嗅探模式解析,带日期格式的数字单元格使用文件中存储的格式。默认格式在读侧只影响两个边缘场景: + +- `Year` 字段:`String` 单元格按解析出的 `Year` 格式解析(`@DateTimeFormat` 优先,其次是按调用的 `defaultYearFormat` 参数,最后是进程级默认值 `yyyy`)。 +- 带空 `@DateTimeFormat` 的 `String` 字段读取数字日期单元格:按解析出的日期格式渲染值。 + +完整的级联规则、构建器参数与进程级 setter 见[写侧文档 - 默认日期格式](../write/format.md#默认日期格式);每个读上下文在创建时对进程级默认值做快照。 diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/format.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/format.md index 5ede970a3..60a72d7e5 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/format.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/format.md @@ -77,3 +77,60 @@ public void converterWrite() { + +## 默认日期格式 + +### 概述 + +当日期类型字段没有 `@DateTimeFormat` 格式时,Fesod 按三层级联解析其格式: + +| 优先级 | 来源 | 作用域 | +| --- | --- | --- | +| 1 | `@DateTimeFormat("...")` | 单个字段 | +| 2 | `.defaultDateFormat(...)` 构建器参数 | 单次读/写调用 | +| 3 | `DateUtils.setDefaultDateFormat(...)` | 进程级 | +| — | 内置常量 | `yyyy-MM-dd HH:mm:ss` | + +三个构建器参数分别对应不同日期类型: + +| 构建器参数 | 适用类型 | 内置常量 | +| --- | --- | --- | +| `defaultDateFormat` | `Date`、`LocalDateTime`、`OffsetDateTime` | `yyyy-MM-dd HH:mm:ss` | +| `defaultLocalDateFormat` | `LocalDate` | `yyyy-MM-dd` | +| `defaultYearFormat` | `Year` | `yyyy` | + +### 按调用设置默认值 + +```java +FesodSheet.write(fileName, OrderData.class) + .defaultDateFormat("yyyy/MM/dd HH:mm:ss") + .defaultLocalDateFormat("yyyy/MM/dd") + .sheet() + .doWrite(data()); +``` + +该参数在读侧(`FesodSheet.read(...)`)同样可用,主要影响未标注格式时 `Year` 的解析。 + +### 进程级默认值 + +希望全局统一默认格式的应用,可以在启动时设置一次,而不必逐字段加注解或逐调用传参数: + +```java +DateUtils.setDefaultDateFormat("yyyy/MM/dd HH:mm:ss"); +DateUtils.setDefaultLocalDateFormat("yyyy/MM/dd"); +DateUtils.setDefaultYearFormat("yyyy"); +``` + +setter 会拒绝空白格式,`DateUtils.resetDefaultDateFormats()` 可恢复内置值。 + +### 快照语义 + +每个读/写上下文在创建时对进程级默认值做一次快照,因此同一个工作簿的所有单元格使用一致的格式。运行中途修改进程级默认值只影响之后新建的上下文,绝不会让正在写入的工作簿出现新旧格式混杂。 + +:::note 从 `DateUtils.defaultDateFormat` 字段赋值迁移 +继承自 EasyExcel 血统、从未写入文档的公共静态字段 `DateUtils.defaultDateFormat` 与 `DateUtils.defaultLocalDateFormat` 已标记废弃,推荐改用上述 setter;字段仍然有效且现在是 `volatile` 的,但直接写字段会让进行中的工作簿不确定地混杂新旧值。`DateUtils.defaultYearFormat` 尚未随任何版本发布,已直接改为 setter。字段将在下一个大版本移除。 +::: + +:::info 转换器贡献者须知 +需要默认格式的新转换器应当使用 `GlobalConfiguration` 字段配合 `DateUtils.resolve*Format(contentProperty, globalConfiguration)`,而不是在 `DateUtils` 上新增公共静态可变状态。字符串形式有规范表示的类型(例如基于 ISO 的格式)可以保留该表示、不接入级联。 +:::