mirror of
https://github.com/jmcnamara/libxlsxwriter.git
synced 2026-05-15 14:15:54 -06:00
workbook: add support for 1904 date epoch
Add, or rather make public, support for the Excel 1904 date epoch. See #483
This commit is contained in:
parent
28615cb8b0
commit
fdcacc14bc
13 changed files with 199 additions and 29 deletions
|
|
@ -212,9 +212,6 @@ enum lxw_custom_property_types {
|
|||
/* GUID string length. */
|
||||
#define LXW_GUID_LENGTH sizeof("{12345678-1234-1234-1234-1234567890AB}\0")
|
||||
|
||||
#define LXW_EPOCH_1900 0
|
||||
#define LXW_EPOCH_1904 1
|
||||
|
||||
#define LXW_UINT32_T_LENGTH sizeof("4294967296")
|
||||
#define LXW_FILENAME_LENGTH 128
|
||||
#define LXW_IGNORE 1
|
||||
|
|
|
|||
|
|
@ -196,8 +196,21 @@ uint16_t lxw_name_to_col_2(const char *col_str);
|
|||
*/
|
||||
double lxw_datetime_to_excel_datetime(lxw_datetime *datetime);
|
||||
|
||||
double lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime,
|
||||
uint8_t date_1904);
|
||||
/**
|
||||
* @brief Converts a #lxw_datetime to an Excel datetime number with 1900/1904
|
||||
* epoch.
|
||||
*
|
||||
* This function is similar to `lxw_datetime_to_excel_datetime()` but it allows
|
||||
* you to specify whether to use the 1900 or 1904 epoch. See also the
|
||||
* `workbook_use_1904_epoch()` function.
|
||||
*
|
||||
* @param datetime A pointer to a #lxw_datetime struct.
|
||||
* @param use_1904_epoch A flag to indicate whether to use the 1904 epoch (true)
|
||||
* or the 1900 epoch (false).
|
||||
*
|
||||
*/
|
||||
double lxw_datetime_to_excel_date_with_epoch(lxw_datetime *datetime,
|
||||
uint8_t use_1904_epoch);
|
||||
|
||||
/**
|
||||
* @brief Converts a unix datetime to an Excel datetime number.
|
||||
|
|
@ -217,7 +230,21 @@ double lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime,
|
|||
*/
|
||||
double lxw_unixtime_to_excel_date(int64_t unixtime);
|
||||
|
||||
double lxw_unixtime_to_excel_date_epoch(int64_t unixtime, uint8_t date_1904);
|
||||
/**
|
||||
* @brief Converts a unix datetime to an Excel datetime number with 1900/1904
|
||||
* epoch.
|
||||
*
|
||||
* This function is similar to `lxw_unixtime_to_excel_date()` but it allows
|
||||
* you to specify whether to use the 1900 or 1904 epoch. See also the
|
||||
* `workbook_use_1904_epoch()` function.
|
||||
*
|
||||
* @param unixtime Unix time (seconds since 1970-01-01)
|
||||
* @param use_1904_epoch A flag to indicate whether to use the 1904 epoch (true)
|
||||
* or the 1900 epoch (false).
|
||||
*
|
||||
*/
|
||||
double lxw_unixtime_to_excel_date_with_epoch(int64_t unixtime,
|
||||
uint8_t use_1904_epoch);
|
||||
|
||||
char *lxw_strdup(const char *str);
|
||||
char *lxw_strdup_formula(const char *formula);
|
||||
|
|
|
|||
|
|
@ -352,6 +352,8 @@ typedef struct lxw_workbook {
|
|||
char *vba_project_signature;
|
||||
char *vba_codename;
|
||||
|
||||
uint8_t use_1904_epoch;
|
||||
|
||||
lxw_format *default_url_format;
|
||||
|
||||
} lxw_workbook;
|
||||
|
|
@ -1071,6 +1073,27 @@ lxw_error workbook_set_vba_name(lxw_workbook *workbook, const char *name);
|
|||
*/
|
||||
void workbook_read_only_recommended(lxw_workbook *workbook);
|
||||
|
||||
/**
|
||||
* @brief Set the workbook to use the 1904 epoch.
|
||||
*
|
||||
* @param workbook Pointer to a lxw_workbook instance.
|
||||
*
|
||||
* The `%workbook_use_1904_epoch()` function can be used to set the workbook to
|
||||
* use the 1904 epoch instead of the default 1900 epoch.
|
||||
*
|
||||
* Excel supports two date epochs. The first based on 1900-01-01 is the default
|
||||
* for all Windows versions of Excel and for recent versions of Excel for macOS.
|
||||
* Older versions of Excel for macOS used a 1904-01-01 epoch. The 1904 epoch can
|
||||
* be set for compatibility with older versions of Excel or to work around the
|
||||
* Excel limitation of not being able to handle negative times.
|
||||
*
|
||||
* @code
|
||||
* workbook_use_1904_epoch(workbook);
|
||||
* @endcode
|
||||
*
|
||||
*/
|
||||
void workbook_use_1904_epoch(lxw_workbook *workbook);
|
||||
|
||||
/**
|
||||
* @brief Set the size of a workbook window.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2285,6 +2285,8 @@ typedef struct lxw_worksheet {
|
|||
char *ignore_calculated_column;
|
||||
char *ignore_two_digit_text_year;
|
||||
|
||||
uint8_t use_1904_epoch;
|
||||
|
||||
uint16_t excel_version;
|
||||
|
||||
lxw_object_properties **header_footer_objs[LXW_HEADER_FOOTER_OBJS_MAX];
|
||||
|
|
@ -2318,6 +2320,7 @@ typedef struct lxw_worksheet_init_data {
|
|||
const char *tmpdir;
|
||||
lxw_format *default_url_format;
|
||||
uint16_t max_url_length;
|
||||
uint8_t use_1904_epoch;
|
||||
|
||||
} lxw_worksheet_init_data;
|
||||
|
||||
|
|
|
|||
|
|
@ -337,7 +337,8 @@ lxw_name_to_col_2(const char *col_str)
|
|||
* or 1904 epoch.
|
||||
*/
|
||||
double
|
||||
lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime, uint8_t date_1904)
|
||||
lxw_datetime_to_excel_date_with_epoch(lxw_datetime *datetime,
|
||||
uint8_t use_1904_epoch)
|
||||
{
|
||||
int year = datetime->year;
|
||||
int month = datetime->month;
|
||||
|
|
@ -346,8 +347,8 @@ lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime, uint8_t date_1904)
|
|||
int min = datetime->min;
|
||||
double sec = datetime->sec;
|
||||
double seconds;
|
||||
int epoch = date_1904 ? 1904 : 1900;
|
||||
int offset = date_1904 ? 4 : 0;
|
||||
int epoch = use_1904_epoch ? 1904 : 1900;
|
||||
int offset = use_1904_epoch ? 4 : 0;
|
||||
int norm = 300;
|
||||
int range;
|
||||
/* Set month days and check for leap year. */
|
||||
|
|
@ -358,7 +359,7 @@ lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime, uint8_t date_1904)
|
|||
|
||||
/* For times without dates set the default date for the epoch. */
|
||||
if (!year) {
|
||||
if (!date_1904) {
|
||||
if (!use_1904_epoch) {
|
||||
year = 1899;
|
||||
month = 12;
|
||||
day = 31;
|
||||
|
|
@ -374,7 +375,7 @@ lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime, uint8_t date_1904)
|
|||
seconds = (hour * 60 * 60 + min * 60 + sec) / (24 * 60 * 60.0);
|
||||
|
||||
/* Special cases for Excel dates in the 1900 epoch. */
|
||||
if (!date_1904) {
|
||||
if (!use_1904_epoch) {
|
||||
/* Excel 1900 epoch. */
|
||||
if (year == 1899 && month == 12 && day == 31)
|
||||
return seconds;
|
||||
|
|
@ -423,7 +424,7 @@ lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime, uint8_t date_1904)
|
|||
days -= leap;
|
||||
|
||||
/* Adjust for Excel erroneously treating 1900 as a leap year. */
|
||||
if (!date_1904 && days > 59)
|
||||
if (!use_1904_epoch && days > 59)
|
||||
days++;
|
||||
|
||||
return days + seconds;
|
||||
|
|
@ -435,7 +436,7 @@ lxw_datetime_to_excel_date_epoch(lxw_datetime *datetime, uint8_t date_1904)
|
|||
double
|
||||
lxw_datetime_to_excel_datetime(lxw_datetime *datetime)
|
||||
{
|
||||
return lxw_datetime_to_excel_date_epoch(datetime, LXW_FALSE);
|
||||
return lxw_datetime_to_excel_date_with_epoch(datetime, LXW_FALSE);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -445,7 +446,7 @@ lxw_datetime_to_excel_datetime(lxw_datetime *datetime)
|
|||
double
|
||||
lxw_unixtime_to_excel_date(int64_t unixtime)
|
||||
{
|
||||
return lxw_unixtime_to_excel_date_epoch(unixtime, LXW_FALSE);
|
||||
return lxw_unixtime_to_excel_date_with_epoch(unixtime, LXW_FALSE);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -453,14 +454,15 @@ lxw_unixtime_to_excel_date(int64_t unixtime)
|
|||
* 1900 or 1904 epoch.
|
||||
*/
|
||||
double
|
||||
lxw_unixtime_to_excel_date_epoch(int64_t unixtime, uint8_t date_1904)
|
||||
lxw_unixtime_to_excel_date_with_epoch(int64_t unixtime,
|
||||
uint8_t use_1904_epoch)
|
||||
{
|
||||
double excel_datetime = 0.0;
|
||||
double epoch = date_1904 ? 24107.0 : 25568.0;
|
||||
double epoch = use_1904_epoch ? 24107.0 : 25568.0;
|
||||
|
||||
excel_datetime = epoch + (unixtime / (24 * 60 * 60.0));
|
||||
|
||||
if (!date_1904 && excel_datetime >= 60.0)
|
||||
if (!use_1904_epoch && excel_datetime >= 60.0)
|
||||
excel_datetime = excel_datetime + 1.0;
|
||||
|
||||
return excel_datetime;
|
||||
|
|
|
|||
|
|
@ -1614,6 +1614,9 @@ _write_workbook_pr(lxw_workbook *self)
|
|||
if (self->vba_codename)
|
||||
LXW_PUSH_ATTRIBUTES_STR("codeName", self->vba_codename);
|
||||
|
||||
if (self->use_1904_epoch)
|
||||
LXW_PUSH_ATTRIBUTES_STR("date1904", "1");
|
||||
|
||||
LXW_PUSH_ATTRIBUTES_STR("defaultThemeVersion", "124226");
|
||||
|
||||
lxw_xml_empty_tag(self->file, "workbookPr", &attributes);
|
||||
|
|
@ -1994,7 +1997,8 @@ workbook_add_worksheet(lxw_workbook *self, const char *sheetname)
|
|||
lxw_worksheet *worksheet = NULL;
|
||||
lxw_worksheet_name *worksheet_name = NULL;
|
||||
lxw_error error;
|
||||
lxw_worksheet_init_data init_data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
lxw_worksheet_init_data init_data =
|
||||
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
char *new_name = NULL;
|
||||
|
||||
if (sheetname) {
|
||||
|
|
@ -2035,6 +2039,7 @@ workbook_add_worksheet(lxw_workbook *self, const char *sheetname)
|
|||
init_data.tmpdir = self->options.tmpdir;
|
||||
init_data.default_url_format = self->default_url_format;
|
||||
init_data.max_url_length = self->max_url_length;
|
||||
init_data.use_1904_epoch = self->use_1904_epoch;
|
||||
|
||||
/* Create a new worksheet object. */
|
||||
worksheet = lxw_worksheet_new(&init_data);
|
||||
|
|
@ -2078,7 +2083,8 @@ workbook_add_chartsheet(lxw_workbook *self, const char *sheetname)
|
|||
lxw_chartsheet *chartsheet = NULL;
|
||||
lxw_chartsheet_name *chartsheet_name = NULL;
|
||||
lxw_error error;
|
||||
lxw_worksheet_init_data init_data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
lxw_worksheet_init_data init_data =
|
||||
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
char *new_name = NULL;
|
||||
|
||||
if (sheetname) {
|
||||
|
|
@ -2858,6 +2864,15 @@ workbook_read_only_recommended(lxw_workbook *self)
|
|||
self->read_only = 2;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use the 1904 epoch for dates in the workbook.
|
||||
*/
|
||||
void
|
||||
workbook_use_1904_epoch(lxw_workbook *self)
|
||||
{
|
||||
self->use_1904_epoch = LXW_TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the size of a workbook window.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ lxw_worksheet_new(lxw_worksheet_init_data *init_data)
|
|||
worksheet->first_sheet = init_data->first_sheet;
|
||||
worksheet->default_url_format = init_data->default_url_format;
|
||||
worksheet->max_url_length = init_data->max_url_length;
|
||||
worksheet->use_1904_epoch = init_data->use_1904_epoch;
|
||||
}
|
||||
|
||||
return worksheet;
|
||||
|
|
@ -8331,7 +8332,12 @@ worksheet_write_datetime(lxw_worksheet *self,
|
|||
if (err)
|
||||
return err;
|
||||
|
||||
excel_date = lxw_datetime_to_excel_date_epoch(datetime, LXW_EPOCH_1900);
|
||||
printf("worksheet_write_datetime(): %d-%02d-%02d - 1904: %d\n",
|
||||
datetime->year, datetime->month, datetime->day,
|
||||
self->use_1904_epoch);
|
||||
|
||||
excel_date =
|
||||
lxw_datetime_to_excel_date_with_epoch(datetime, self->use_1904_epoch);
|
||||
|
||||
cell = _new_number_cell(row_num, col_num, excel_date, format);
|
||||
|
||||
|
|
@ -8357,7 +8363,8 @@ worksheet_write_unixtime(lxw_worksheet *self,
|
|||
if (err)
|
||||
return err;
|
||||
|
||||
excel_date = lxw_unixtime_to_excel_date_epoch(unixtime, LXW_EPOCH_1900);
|
||||
excel_date =
|
||||
lxw_unixtime_to_excel_date_with_epoch(unixtime, self->use_1904_epoch);
|
||||
|
||||
cell = _new_number_cell(row_num, col_num, excel_date, format);
|
||||
|
||||
|
|
@ -11363,16 +11370,16 @@ worksheet_data_validation_range(lxw_worksheet *self, lxw_row_t first_row,
|
|||
|| validation->validate == LXW_VALIDATION_TYPE_TIME) {
|
||||
if (is_between) {
|
||||
copy->value_number =
|
||||
lxw_datetime_to_excel_date_epoch
|
||||
(&validation->minimum_datetime, LXW_EPOCH_1900);
|
||||
lxw_datetime_to_excel_date_with_epoch
|
||||
(&validation->minimum_datetime, self->use_1904_epoch);
|
||||
copy->maximum_number =
|
||||
lxw_datetime_to_excel_date_epoch
|
||||
(&validation->maximum_datetime, LXW_EPOCH_1900);
|
||||
lxw_datetime_to_excel_date_with_epoch
|
||||
(&validation->maximum_datetime, self->use_1904_epoch);
|
||||
}
|
||||
else {
|
||||
copy->value_number =
|
||||
lxw_datetime_to_excel_date_epoch(&validation->value_datetime,
|
||||
LXW_EPOCH_1900);
|
||||
lxw_datetime_to_excel_date_with_epoch
|
||||
(&validation->value_datetime, self->use_1904_epoch);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
37
test/functional/src/test_date_1904_01.c
Normal file
37
test/functional/src/test_date_1904_01.c
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*****************************************************************************
|
||||
* Test cases for libxlsxwriter.
|
||||
*
|
||||
* Test to compare output against Excel files.
|
||||
*
|
||||
* Copyright 2014-2025, John McNamara, jmcnamara@cpan.org
|
||||
*
|
||||
*/
|
||||
|
||||
#include "xlsxwriter.h"
|
||||
|
||||
int main()
|
||||
{
|
||||
lxw_workbook *workbook = workbook_new("test_date_1904_01.xlsx");
|
||||
lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
|
||||
|
||||
lxw_format *format = workbook_add_format(workbook);
|
||||
format_set_num_format_index(format, 14);
|
||||
|
||||
lxw_datetime datetime1 = {1900, 1, 1, 0, 0, 0.0};
|
||||
lxw_datetime datetime2 = {1902, 9, 26, 0, 0, 0.0};
|
||||
lxw_datetime datetime3 = {1913, 9, 8, 0, 0, 0.0};
|
||||
lxw_datetime datetime4 = {1927, 5, 18, 0, 0, 0.0};
|
||||
lxw_datetime datetime5 = {2173, 10, 14, 0, 0, 0.0};
|
||||
lxw_datetime datetime6 = {4637, 11, 26, 0, 0, 0.0};
|
||||
|
||||
worksheet_set_column(worksheet, 0, 0, 12, NULL);
|
||||
|
||||
worksheet_write_datetime(worksheet, CELL("A1"), &datetime1, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A2"), &datetime2, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A3"), &datetime3, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A4"), &datetime4, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A5"), &datetime5, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A6"), &datetime6, format);
|
||||
|
||||
return workbook_close(workbook);
|
||||
}
|
||||
38
test/functional/src/test_date_1904_02.c
Normal file
38
test/functional/src/test_date_1904_02.c
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*****************************************************************************
|
||||
* Test cases for libxlsxwriter.
|
||||
*
|
||||
* Test to compare output against Excel files.
|
||||
*
|
||||
* Copyright 2014-2025, John McNamara, jmcnamara@cpan.org
|
||||
*
|
||||
*/
|
||||
|
||||
#include "xlsxwriter.h"
|
||||
|
||||
int main() {
|
||||
lxw_workbook *workbook = workbook_new("test_date_1904_02.xlsx");
|
||||
workbook_use_1904_epoch(workbook);
|
||||
|
||||
lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
|
||||
|
||||
lxw_format *format = workbook_add_format(workbook);
|
||||
format_set_num_format_index(format, 14);
|
||||
|
||||
lxw_datetime datetime1 = {1904, 1, 1, 0, 0, 0.0};
|
||||
lxw_datetime datetime2 = {1906, 9, 27, 0, 0, 0.0};
|
||||
lxw_datetime datetime3 = {1917, 9, 9, 0, 0, 0.0};
|
||||
lxw_datetime datetime4 = {1931, 5, 19, 0, 0, 0.0};
|
||||
lxw_datetime datetime5 = {2177, 10, 15, 0, 0, 0.0};
|
||||
lxw_datetime datetime6 = {4641, 11, 27, 0, 0, 0.0};
|
||||
|
||||
worksheet_set_column(worksheet, 0, 0, 12, NULL);
|
||||
|
||||
worksheet_write_datetime(worksheet, CELL("A1"), &datetime1, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A2"), &datetime2, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A3"), &datetime3, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A4"), &datetime4, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A5"), &datetime5, format);
|
||||
worksheet_write_datetime(worksheet, CELL("A6"), &datetime6, format);
|
||||
|
||||
return workbook_close(workbook);
|
||||
}
|
||||
21
test/functional/test_date_1904.py
Normal file
21
test/functional/test_date_1904.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
###############################################################################
|
||||
#
|
||||
# Tests for libxlsxwriter.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
# Copyright 2014-2025, John McNamara, jmcnamara@cpan.org.
|
||||
#
|
||||
|
||||
import base_test_class
|
||||
|
||||
class TestCompareXLSXFiles(base_test_class.XLSXBaseTest):
|
||||
"""
|
||||
Test file created with libxlsxwriter against a file created by Excel.
|
||||
|
||||
"""
|
||||
|
||||
def test_date_1904_01(self):
|
||||
self.run_exe_test('test_date_1904_01')
|
||||
|
||||
def test_date_1904_02(self):
|
||||
self.run_exe_test('test_date_1904_02')
|
||||
BIN
test/functional/xlsx_files/date_1904_01.xlsx
Normal file
BIN
test/functional/xlsx_files/date_1904_01.xlsx
Normal file
Binary file not shown.
BIN
test/functional/xlsx_files/date_1904_02.xlsx
Normal file
BIN
test/functional/xlsx_files/date_1904_02.xlsx
Normal file
Binary file not shown.
|
|
@ -108,7 +108,7 @@
|
|||
datetime->month = _month; \
|
||||
datetime->day = _day; \
|
||||
\
|
||||
got = lxw_datetime_to_excel_date_epoch(datetime, 1); \
|
||||
got = lxw_datetime_to_excel_date_with_epoch(datetime, 1); \
|
||||
\
|
||||
ASSERT_DBL_NEAR(exp, got); \
|
||||
free(datetime);
|
||||
|
|
@ -132,6 +132,6 @@
|
|||
ASSERT_DBL_NEAR(exp, got);
|
||||
|
||||
#define TEST_UNIXTIME_1904(_unixtime, exp) \
|
||||
got = lxw_unixtime_to_excel_date_epoch(_unixtime, 1); \
|
||||
got = lxw_unixtime_to_excel_date_with_epoch(_unixtime, 1); \
|
||||
ASSERT_DBL_NEAR(exp, got);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue