Browse Source

* 兼容`LocalDate` [Issue #2908](https://github.com/alibaba/easyexcel/issues/2908)

appendLagreFile
Jiaju Zhuang 2 years ago
parent
commit
6ff6fbff13
  1. 2
      README.md
  2. 10
      easyexcel-core/src/main/java/com/alibaba/excel/converters/DefaultConverterLoader.java
  3. 36
      easyexcel-core/src/main/java/com/alibaba/excel/converters/localdate/LocalDateDateConverter.java
  4. 57
      easyexcel-core/src/main/java/com/alibaba/excel/converters/localdate/LocalDateNumberConverter.java
  5. 52
      easyexcel-core/src/main/java/com/alibaba/excel/converters/localdate/LocalDateStringConverter.java
  6. 4
      easyexcel-core/src/main/java/com/alibaba/excel/converters/localdatetime/LocalDateTimeDateConverter.java
  7. 2
      easyexcel-core/src/main/java/com/alibaba/excel/converters/localdatetime/LocalDateTimeNumberConverter.java
  8. 74
      easyexcel-core/src/main/java/com/alibaba/excel/util/DateUtils.java
  9. 11
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterDataListener.java
  10. 8
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterDataTest.java
  11. 3
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterReadData.java
  12. 3
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterWriteData.java
  13. 36
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/util/TestUtil.java
  14. 2
      pom.xml
  15. 4
      update.md

2
README.md

@ -27,7 +27,7 @@ easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId> <artifactId>easyexcel</artifactId>
<version>3.2.0</version> <version>3.2.1</version>
</dependency> </dependency>
``` ```

10
easyexcel-core/src/main/java/com/alibaba/excel/converters/DefaultConverterLoader.java

@ -31,7 +31,10 @@ import com.alibaba.excel.converters.inputstream.InputStreamImageConverter;
import com.alibaba.excel.converters.integer.IntegerBooleanConverter; import com.alibaba.excel.converters.integer.IntegerBooleanConverter;
import com.alibaba.excel.converters.integer.IntegerNumberConverter; import com.alibaba.excel.converters.integer.IntegerNumberConverter;
import com.alibaba.excel.converters.integer.IntegerStringConverter; import com.alibaba.excel.converters.integer.IntegerStringConverter;
import com.alibaba.excel.converters.localdatetime.LocalDateNumberConverter; import com.alibaba.excel.converters.localdate.LocalDateDateConverter;
import com.alibaba.excel.converters.localdate.LocalDateNumberConverter;
import com.alibaba.excel.converters.localdate.LocalDateStringConverter;
import com.alibaba.excel.converters.localdatetime.LocalDateTimeNumberConverter;
import com.alibaba.excel.converters.localdatetime.LocalDateTimeDateConverter; import com.alibaba.excel.converters.localdatetime.LocalDateTimeDateConverter;
import com.alibaba.excel.converters.localdatetime.LocalDateTimeStringConverter; import com.alibaba.excel.converters.localdatetime.LocalDateTimeStringConverter;
import com.alibaba.excel.converters.longconverter.LongBooleanConverter; import com.alibaba.excel.converters.longconverter.LongBooleanConverter;
@ -83,6 +86,9 @@ public class DefaultConverterLoader {
putAllConverter(new DateStringConverter()); putAllConverter(new DateStringConverter());
putAllConverter(new LocalDateNumberConverter()); putAllConverter(new LocalDateNumberConverter());
putAllConverter(new LocalDateStringConverter());
putAllConverter(new LocalDateTimeNumberConverter());
putAllConverter(new LocalDateTimeStringConverter()); putAllConverter(new LocalDateTimeStringConverter());
putAllConverter(new DoubleBooleanConverter()); putAllConverter(new DoubleBooleanConverter());
@ -121,6 +127,7 @@ public class DefaultConverterLoader {
putWriteConverter(new ByteNumberConverter()); putWriteConverter(new ByteNumberConverter());
putWriteConverter(new DateDateConverter()); putWriteConverter(new DateDateConverter());
putWriteConverter(new LocalDateTimeDateConverter()); putWriteConverter(new LocalDateTimeDateConverter());
putWriteConverter(new LocalDateDateConverter());
putWriteConverter(new DoubleNumberConverter()); putWriteConverter(new DoubleNumberConverter());
putWriteConverter(new FloatNumberConverter()); putWriteConverter(new FloatNumberConverter());
putWriteConverter(new IntegerNumberConverter()); putWriteConverter(new IntegerNumberConverter());
@ -139,6 +146,7 @@ public class DefaultConverterLoader {
putWriteStringConverter(new BooleanStringConverter()); putWriteStringConverter(new BooleanStringConverter());
putWriteStringConverter(new ByteStringConverter()); putWriteStringConverter(new ByteStringConverter());
putWriteStringConverter(new DateStringConverter()); putWriteStringConverter(new DateStringConverter());
putWriteStringConverter(new LocalDateStringConverter());
putWriteStringConverter(new LocalDateTimeStringConverter()); putWriteStringConverter(new LocalDateTimeStringConverter());
putWriteStringConverter(new DoubleStringConverter()); putWriteStringConverter(new DoubleStringConverter());
putWriteStringConverter(new FloatStringConverter()); putWriteStringConverter(new FloatStringConverter());

36
easyexcel-core/src/main/java/com/alibaba/excel/converters/localdate/LocalDateDateConverter.java

@ -0,0 +1,36 @@
package com.alibaba.excel.converters.localdate;
import java.time.LocalDate;
import java.time.LocalDateTime;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.DateUtils;
import com.alibaba.excel.util.WorkBookUtil;
/**
* LocalDate and date converter
*
* @author Jiaju Zhuang
*/
public class LocalDateDateConverter implements Converter<LocalDate> {
@Override
public Class<?> supportJavaTypeKey() {
return LocalDate.class;
}
@Override
public WriteCellData<?> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) 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);
return cellData;
}
}

57
easyexcel-core/src/main/java/com/alibaba/excel/converters/localdate/LocalDateNumberConverter.java

@ -0,0 +1,57 @@
package com.alibaba.excel.converters.localdate;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.DateUtils;
import org.apache.poi.ss.usermodel.DateUtil;
/**
* LocalDate and number converter
*
* @author Jiaju Zhuang
*/
public class LocalDateNumberConverter implements Converter<LocalDate> {
@Override
public Class<?> supportJavaTypeKey() {
return LocalDate.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.NUMBER;
}
@Override
public LocalDate convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
return DateUtils.getLocalDate(cellData.getNumberValue().doubleValue(),
globalConfiguration.getUse1904windowing());
} else {
return DateUtils.getLocalDate(cellData.getNumberValue().doubleValue(),
contentProperty.getDateTimeFormatProperty().getUse1904windowing());
}
}
@Override
public WriteCellData<?> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
return new WriteCellData<>(
BigDecimal.valueOf(DateUtil.getExcelDate(value, globalConfiguration.getUse1904windowing())));
} else {
return new WriteCellData<>(BigDecimal.valueOf(
DateUtil.getExcelDate(value, contentProperty.getDateTimeFormatProperty().getUse1904windowing())));
}
}
}

52
easyexcel-core/src/main/java/com/alibaba/excel/converters/localdate/LocalDateStringConverter.java

@ -0,0 +1,52 @@
package com.alibaba.excel.converters.localdate;
import java.text.ParseException;
import java.time.LocalDate;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.DateUtils;
/**
* LocalDate and string converter
*
* @author Jiaju Zhuang
*/
public class LocalDateStringConverter implements Converter<LocalDate> {
@Override
public Class<?> supportJavaTypeKey() {
return LocalDate.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public LocalDate convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) throws ParseException {
if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
return DateUtils.parseLocalDate(cellData.getStringValue(), null, globalConfiguration.getLocale());
} else {
return DateUtils.parseLocalDate(cellData.getStringValue(),
contentProperty.getDateTimeFormatProperty().getFormat(), globalConfiguration.getLocale());
}
}
@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()));
}
}
}

4
easyexcel-core/src/main/java/com/alibaba/excel/converters/localdatetime/LocalDateTimeDateConverter.java

@ -10,13 +10,13 @@ import com.alibaba.excel.util.DateUtils;
import com.alibaba.excel.util.WorkBookUtil; import com.alibaba.excel.util.WorkBookUtil;
/** /**
* Date and date converter * LocalDateTime and date converter
* *
* @author Jiaju Zhuang * @author Jiaju Zhuang
*/ */
public class LocalDateTimeDateConverter implements Converter<LocalDateTime> { public class LocalDateTimeDateConverter implements Converter<LocalDateTime> {
@Override @Override
public Class<LocalDateTime> supportJavaTypeKey() { public Class<?> supportJavaTypeKey() {
return LocalDateTime.class; return LocalDateTime.class;
} }

2
easyexcel-core/src/main/java/com/alibaba/excel/converters/localdatetime/LocalDateNumberConverter.java → easyexcel-core/src/main/java/com/alibaba/excel/converters/localdatetime/LocalDateTimeNumberConverter.java

@ -18,7 +18,7 @@ import org.apache.poi.ss.usermodel.DateUtil;
* *
* @author Jiaju Zhuang * @author Jiaju Zhuang
*/ */
public class LocalDateNumberConverter implements Converter<LocalDateTime> { public class LocalDateTimeNumberConverter implements Converter<LocalDateTime> {
@Override @Override
public Class<?> supportJavaTypeKey() { public Class<?> supportJavaTypeKey() {

74
easyexcel-core/src/main/java/com/alibaba/excel/util/DateUtils.java

@ -4,7 +4,9 @@ import java.math.BigDecimal;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Date; import java.util.Date;
@ -59,6 +61,8 @@ public class DateUtils {
public static String defaultDateFormat = DATE_FORMAT_19; public static String defaultDateFormat = DATE_FORMAT_19;
public static String defaultLocalDateFormat = DATE_FORMAT_10;
private DateUtils() {} private DateUtils() {}
/** /**
@ -95,6 +99,25 @@ public class DateUtils {
} }
} }
/**
* convert string to date
*
* @param dateString
* @param dateFormat
* @param local
* @return
*/
public static LocalDate parseLocalDate(String dateString, String dateFormat, Locale local) {
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = switchDateFormat(dateString);
}
if (local == null) {
return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(dateFormat));
} else {
return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(dateFormat, local));
}
}
/** /**
* convert string to date * convert string to date
* *
@ -188,6 +211,38 @@ public class DateUtils {
} }
} }
/**
* Format date
*
* @param date
* @param dateFormat
* @return
*/
public static String format(LocalDate date, String dateFormat) {
return format(date, dateFormat, null);
}
/**
* Format date
*
* @param date
* @param dateFormat
* @return
*/
public static String format(LocalDate date, String dateFormat, Locale local) {
if (date == null) {
return null;
}
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = defaultLocalDateFormat;
}
if (local == null) {
return date.format(DateTimeFormatter.ofPattern(dateFormat));
} else {
return date.format(DateTimeFormatter.ofPattern(dateFormat, local));
}
}
/** /**
* Format date * Format date
* *
@ -271,6 +326,25 @@ public class DateUtils {
return DateUtil.getLocalDateTime(date, use1904windowing, true); return DateUtil.getLocalDateTime(date, use1904windowing, true);
} }
/**
* Given an Excel date with either 1900 or 1904 date windowing,
* converts it to a java.time.LocalDate.
*
* Excel Dates and Times are stored without any timezone
* information. If you know (through other means) that your file
* uses a different TimeZone to the system default, you can use
* this version of the getJavaDate() method to handle it.
*
* @param date The Excel date.
* @param use1904windowing true if date uses 1904 windowing,
* or false if using 1900 date windowing.
* @return Java representation of the date, or null if date is not a valid Excel date
*/
public static LocalDate getLocalDate(double date, boolean use1904windowing) {
LocalDateTime localDateTime = getLocalDateTime(date, use1904windowing);
return localDateTime == null ? null : localDateTime.toLocalDate();
}
/** /**
* Determine if it is a date format. * Determine if it is a date format.
* *

11
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterDataListener.java

@ -3,9 +3,11 @@ package com.alibaba.easyexcel.test.core.converter;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.text.ParseException; import java.text.ParseException;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.alibaba.easyexcel.test.util.TestUtil;
import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelCommonException; import com.alibaba.excel.exception.ExcelCommonException;
@ -32,12 +34,9 @@ public class ConverterDataListener extends AnalysisEventListener<ConverterReadDa
public void doAfterAllAnalysed(AnalysisContext context) { public void doAfterAllAnalysed(AnalysisContext context) {
Assert.assertEquals(list.size(), 1); Assert.assertEquals(list.size(), 1);
ConverterReadData data = list.get(0); ConverterReadData data = list.get(0);
try { Assert.assertEquals(TestUtil.TEST_DATE, data.getDate());
Assert.assertEquals(DateUtils.parseDate("2020-01-01 01:01:01"), data.getDate()); Assert.assertEquals(TestUtil.TEST_LOCAL_DATE, data.getLocalDate());
} catch (ParseException e) { Assert.assertEquals(TestUtil.TEST_LOCAL_DATE_TIME, data.getLocalDateTime());
throw new ExcelCommonException("Test Exception", e);
}
Assert.assertEquals(DateUtils.parseLocalDateTime("2020-01-01 01:01:01", null, null), data.getLocalDateTime());
Assert.assertEquals(data.getBooleanData(), Boolean.TRUE); Assert.assertEquals(data.getBooleanData(), Boolean.TRUE);
Assert.assertEquals(data.getBigDecimal().doubleValue(), BigDecimal.ONE.doubleValue(), 0.0); Assert.assertEquals(data.getBigDecimal().doubleValue(), BigDecimal.ONE.doubleValue(), 0.0);
Assert.assertEquals(data.getBigInteger().intValue(), BigInteger.ONE.intValue(), 0.0); Assert.assertEquals(data.getBigInteger().intValue(), BigInteger.ONE.intValue(), 0.0);

8
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterDataTest.java

@ -4,10 +4,13 @@ import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.alibaba.easyexcel.test.util.TestFileUtil; import com.alibaba.easyexcel.test.util.TestFileUtil;
import com.alibaba.easyexcel.test.util.TestUtil;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.DateUtils; import com.alibaba.excel.util.DateUtils;
@ -112,8 +115,9 @@ public class ConverterDataTest {
private List<ConverterWriteData> data() throws Exception { private List<ConverterWriteData> data() throws Exception {
List<ConverterWriteData> list = new ArrayList<ConverterWriteData>(); List<ConverterWriteData> list = new ArrayList<ConverterWriteData>();
ConverterWriteData converterWriteData = new ConverterWriteData(); ConverterWriteData converterWriteData = new ConverterWriteData();
converterWriteData.setDate(DateUtils.parseDate("2020-01-01 01:01:01")); converterWriteData.setDate(TestUtil.TEST_DATE);
converterWriteData.setLocalDateTime(DateUtils.parseLocalDateTime("2020-01-01 01:01:01", null, null)); converterWriteData.setLocalDate(TestUtil.TEST_LOCAL_DATE);
converterWriteData.setLocalDateTime(TestUtil.TEST_LOCAL_DATE_TIME);
converterWriteData.setBooleanData(Boolean.TRUE); converterWriteData.setBooleanData(Boolean.TRUE);
converterWriteData.setBigDecimal(BigDecimal.ONE); converterWriteData.setBigDecimal(BigDecimal.ONE);
converterWriteData.setBigInteger(BigInteger.ONE); converterWriteData.setBigInteger(BigInteger.ONE);

3
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterReadData.java

@ -2,6 +2,7 @@ package com.alibaba.easyexcel.test.core.converter;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
@ -22,6 +23,8 @@ public class ConverterReadData {
@ExcelProperty("日期") @ExcelProperty("日期")
private Date date; private Date date;
@ExcelProperty("本地日期") @ExcelProperty("本地日期")
private LocalDate localDate;
@ExcelProperty("本地日期时间")
private LocalDateTime localDateTime; private LocalDateTime localDateTime;
@ExcelProperty("布尔") @ExcelProperty("布尔")
private Boolean booleanData; private Boolean booleanData;

3
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/converter/ConverterWriteData.java

@ -2,6 +2,7 @@ package com.alibaba.easyexcel.test.core.converter;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
@ -22,6 +23,8 @@ public class ConverterWriteData {
@ExcelProperty("日期") @ExcelProperty("日期")
private Date date; private Date date;
@ExcelProperty("本地日期") @ExcelProperty("本地日期")
private LocalDate localDate;
@ExcelProperty("本地日期时间")
private LocalDateTime localDateTime; private LocalDateTime localDateTime;
@ExcelProperty("布尔") @ExcelProperty("布尔")
private Boolean booleanData; private Boolean booleanData;

36
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/util/TestUtil.java

@ -0,0 +1,36 @@
package com.alibaba.easyexcel.test.util;
import java.io.File;
import java.io.InputStream;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import com.alibaba.excel.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
/**
* test util
*
* @author Jiaju Zhuang
*/
@Slf4j
public class TestUtil {
public static final Date TEST_DATE;
public static final LocalDate TEST_LOCAL_DATE = LocalDate.of(2020, 1, 1);
public static final LocalDateTime TEST_LOCAL_DATE_TIME = LocalDateTime.of(2020, 1, 1, 1, 1, 1);
static {
try {
TEST_DATE = DateUtils.parseDate("2020-01-01 01:01:01");
} catch (ParseException e) {
log.error("init TestUtil error.", e);
throw new RuntimeException(e);
}
}
}

2
pom.xml

@ -20,7 +20,7 @@
<properties> <properties>
<revision>3.2.0</revision> <revision>3.2.1</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>1.8</jdk.version> <jdk.version>1.8</jdk.version>
<gpg.skip>true</gpg.skip> <gpg.skip>true</gpg.skip>

4
update.md

@ -1,3 +1,7 @@
# 3.2.1
* 兼容`LocalDate` [Issue #2908](https://github.com/alibaba/easyexcel/issues/2908)
# 3.2.0 # 3.2.0
* 修复部分xlsx读取日期可能相差1秒的bug [Issue #1956](https://github.com/alibaba/easyexcel/issues/1956) * 修复部分xlsx读取日期可能相差1秒的bug [Issue #1956](https://github.com/alibaba/easyexcel/issues/1956)

Loading…
Cancel
Save