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:
John McNamara 2025-06-29 14:52:13 +01:00
parent 28615cb8b0
commit fdcacc14bc
13 changed files with 199 additions and 29 deletions

View file

@ -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

View file

@ -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);

View file

@ -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.
*

View file

@ -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;

View file

@ -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;

View file

@ -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.
*/

View file

@ -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);
}
}

View 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);
}

View 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);
}

View 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')

Binary file not shown.

Binary file not shown.

View file

@ -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);