https://bugs.documentfoundation.org/show_bug.cgi?id=172582
Bug ID: 172582
Summary: Date time representation has a 23:23:59 error due to
rounding, with floats close to midnight.
Product: LibreOffice
Version: unspecified
Hardware: All
OS: All
Status: UNCONFIRMED
Severity: normal
Priority: medium
Component: Calc
Assignee: [email protected]
Reporter: [email protected]
Description:
When a cell contains a datetime serial number whose value is a tiny fraction
below midnight (e.g. `46132.9999999999`), LibreOffice Calc displays the
**date** component correctly (rounds up to the next day) while the **time**
component is wrong (shows `23:59:59` instead of `00:00:00`). The same value in
Microsoft Excel displays both components consistently as `00:00:00`.
Steps to Reproduce:
1. Open LibreOffice Calc with a new, empty spreadsheet.
2. Click on cell **A1**.
3. Type the following value exactly and press Enter:
46132.9999999999
4. With A1 still selected, open **Format > Cells > Numbers** and apply the
format:
DD/MM/YYYY HH:MM:SS
5. Read the displayed value in A1.
Actual Results:
21/04/2026 23:59:59
The date component is correct (21 April 2026) but the time component is wrong
(23:59:59 instead of 00:00:00). The apparent gap between the two displayed
values is 86 399 seconds, exactly 24 hours minus one second.
Expected Results:
21/04/2026 00:00:00
The value `46132.9999999999` is less than one millisecond below midnight at the
start of 21 April 2026. It should round to midnight and display as `00:00:00`.
This is the behaviour of Microsoft Excel and of simple round-before-split
logic.
Reproducible: Always
User Profile Reset: No
Additional Info:
LibreOffice uses **two separate code paths** to extract the date and time
components of a serial number, and they apply different rounding strategies.
1) Date path : i18npool/source/calendar/calendar_gregorian.cxx,
setLocalDateTime()
double fM = fTimeInDays * U_MILLIS_PER_DAY; // multiply by 86 400 000
double fR = rtl::math::round( fM ); // round to nearest millisecond
For `46132.9999999999`, multiplying by 86 400 000 gives `3 985 891 199
999.914`, which rounds up to `3 985 891 200 000` ms = exactly `46133.0` > **21
April 2026** (correct).
2) Time path : tools/source/datetime/ttime.cxx, GetClock()
const double fTime = fTimeInDays - rtl::math::approxFloor(fTimeInDays);
approxFloor is defined in include/rtl/math.hxx:
inline double approxFloor(double a)
{
return floor( approxValue( a ));
}
which delegates to `rtl_math_approxValue` in `sal/rtl/math.cxx`:
double SAL_CALL rtl_math_approxValue(double fValue) noexcept
{
const double fBigInt = 0x1p41; // 2^41 -> only 11 bits left for fractional
part
if (fValue == 0.0 || !std::isfinite(fValue) || fValue > fBigInt)
return fValue;
double fOrigValue = fValue;
bool bSign = std::signbit(fValue);
if (bSign)
fValue = -fValue;
if (isRepresentableInteger(fValue) || getBitsInFracPart(fValue) <= 11)
return fOrigValue;
int nExp = static_cast<int>(floor(log10(fValue)));
nExp = 14 - nExp; // target 14 significant digits
double fExpValue = getN10Exp(abs(nExp));
if (nExp < 0) fValue /= fExpValue;
else fValue *= fExpValue;
if (!std::isfinite(fValue)) return fOrigValue;
fValue = std::round(fValue); // round to 14 sig. digits
if (nExp < 0) fValue *= fExpValue;
else fValue /= fExpValue;
if (!std::isfinite(fValue)) return fOrigValue;
return bSign ? -fValue : fValue;
}
The key line is nExp = 14 - nExp: it scales the value so that `std::round`
lands at exactly 14 significant decimal digits. For `46132.9999999999`, which
already has 14 significant digits, the value passes through essentially
unchanged. `floor()` then sees `46132.9999999999` and returns `46132` — one day
short of midnight:
approxValue(46132.9999999999) = 46132.9999999999 ← 14 sig. digits, still <
46133
floor(46132.9999999999) = 46132
fTime = 46132.9999999999 − 46132 = 0.9999999999
0.9999999999 × 86400 = 86399.999... seconds → truncated to 86399 = 23:59:59
(wrong)
The function then deliberately **truncates** (not rounds) the seconds to avoid
rolling over to `24:00:00`. Its own source comment reads:
> *"do not round values (specifically not up), but truncate … so 23:59:59.99 is
> still
> 23:59:59 and not 24:00:00 (or even 00:00:00 which Excel does)"*
(Explanation generated by an AI. By making the AI prove the hypothesis of why
the bug occurs, the downloaded source code was analyzed).
--
You are receiving this mail because:
You are the assignee for the bug.