https://bugs.documentfoundation.org/show_bug.cgi?id=172582
--- Comment #5 from [email protected] --- The bibisect boundaries in Comment 4 are the release tags — good = `87b77fad…` is **LibreOffice 7.2.1.2**, bad = `cdeefe45…` is **LibreOffice 7.5.9.2** — so the regression is somewhere in the 7.3–7.5 range. It was introduced by: commit 5ce6de864380f1eabbd78656ff6cc31920c534d2 Author: Eike Rathke <[email protected]> Date: 2022-10-24 Related: tdf#136615 Do not round a DateTime clock format into the next day First shipped in **LibreOffice 7.5.0**. It is a one-file change in `svl/source/numbers/zformat.cxx`, function `SvNumberformat::ImpGetDateTimeOutput()` — the exact code path that renders a cell with a combined date+time number format such as `DD/MM/YYYY HH:MM:SS`. **Before (7.2.x — correct).** For a clock format (`nCntPost == 0`) the fractional day was rounded to whole seconds, and a roll-over guard reconciled the date and the time: nCntPost = rInfo.nCntPost; // 0 for HH:MM:SS double fTime = (fNumber - floor(fNumber)) * 86400.0; fTime = rtl::math::round( fTime, int(nCntPost) ); // round to 0 dp -> whole seconds if (fTime >= 86400.0) { // 86400.0 -> guard fires fTime -= 86400.0; fNumber = floor(fNumber + 0.5) + fTime; // advance to next-day 00:00:00 } rCal.setLocalDateTime( fNumber ); 46132.9999999999: fTime = 0.9999999999 * 86400 = 86399.99999136 round(…, 0) = 86400.0 -> >= 86400 -> fNumber := 46133.0 => 21/04/2026 00:00:00 (date and time agree) **After (7.5.0+ — the regression).** A new `nFirstRounding` rounds a clock format at 7 decimals instead of 0, so the roll-over guard no longer fires: nCntPost = rInfo.nCntPost; // still 0 // For clock format (not []) do not round up to seconds and thus days. nFirstRounding = (rInfo.bThousand ? nCntPost : kTimeSignificantRound); // = 7 double fTime = (fNumber - floor(fNumber)) * 86400.0; fTime = rtl::math::round( fTime, int(nFirstRounding) ); // round to 7 dp if (fTime >= 86400.0) { … } // NOT taken any more rCal.setLocalDateTime( fNumber ); // fNumber still 46132.9999999999 46132.9999999999: fTime = round(86399.99999136, 7) = 86399.9999914 -> < 86400 guard skipped, fNumber unchanged. rCal.setLocalDateTime(46132.9999999999): fM = 46132.9999999999 * 86400000 = 3985891199999.914 round(fM) = 3985891200000 = 46133.0 -> date = 21/04/2026 (rolls up) tools::Time::GetClock(46132.9999999999, …) truncates -> 23:59:59 (no roll-up) => 21/04/2026 23:59:59 (date and time disagree) So the **date** is still taken from `setLocalDateTime()`, which rounds to the nearest millisecond and therefore rolls up to the next day, while the **time** is now derived from the un-adjusted `fNumber` and truncated. The two halves of the same cell end up almost 24 hours apart. This also explains the threshold Takenori Yasuda reports in Comment 2 (~`0.99999999422`, i.e. about half a millisecond before midnight): that is exactly the point at which `setLocalDateTime()`'s round-to-millisecond — `round(fraction × 86 400 000)` — crosses `x.5` ms and rounds the **date** up to the next day, whereas the 7-decimal rounding of the seconds never reaches a full day. The commit is a follow-up to tdf#136615 ("Re-consider date/time parts calculation for functions and formatting"). Its direct trigger was a case Eike Rathke himself raised in that thread: the combined format =TEXT(44858.9999999306;"yyyy-mm-dd hh:mm:ss") was giving `2022-10-25 00:00:00` instead of `2022-10-24 23:59:59`, whereas the `.000` variant correctly gave `2022-10-24 23:59:59.994`. In LibreOffice's model, `hh:mm:ss` is a **clock** format that should *not* round up into the next unit (and hence not into the next day), while `[hh]:mm:ss` is a **duration** format that does round. The patch was intended to make the combined date+time clock format stop rolling into the next day. So fixing the bug can be done by making the date and time halves use the *same* convention. Either (a) derive the date from the same non-rounded value the time is derived from, so a near-midnight value stays on the current day as `20/04/2026 23:59:59`; or (b) round the whole serial to the display precision once, up front, and derive both halves from that single value, giving `21/04/2026 00:00:00`. Either is acceptable; the current mixed behaviour is not. On the other hand, someone handling pure time within a 24 hour "circle", might want the behaviour that all values greater than the maximum value, are rounded to the maximum value, so option (a) is defendable when only showing time, but once dates are included, it is inconsistent with itself: when the time reaches a certain threshold, the rounding rule changes. The bug is relatively obvious with date/time data exported from other systems, because it generated an almost-24-hour error; but if (a) is followed it may instead trigger future frustration for someone to whom an almost-1-second error is unacceptable (e.g. a strictly monotone sub-second timeseries that suddenly develops duplicate values). So the choice between (a) and (b) is not a matter of taste; it is fixed by the number space the value actually lives in. A bare *time-of-day* is a cyclic quantity: it lives on the circle ℝ / 86400ℤ (clock arithmetic, i.e. modular arithmetic), in which the mark "86400 s" and the mark "0 s" are the *same point*. On a bounded, cyclic space there are exactly two self-consistent ways to treat a value that rounds past the top: wrap-around (modular): 23:59:59.9999 -> 00:00:00 carry wraps, as in uint8: 255 + 1 = 0 saturating (clamp): 23:59:59.9999 -> 23:59:59 value pinned below the wall, as in saturating uint8: 255 + 1 = 255 For a *time-only* field with no date, saturating is the sensible choice: wrapping to 00:00:00 would move "almost midnight" to the *start* of the dial and destroy the magnitude. This is the legitimate core of the intuition that an almost-full-day value should not reset to zero. But a *date+time* serial is **not** a point on that circle. It is a point on the **real line** ℝ (days since the epoch): the date is its integer part, the time its fractional part. ℝ is neither bounded nor cyclic — there is no wall and no wrap. Rounding is therefore performed once, on ℝ, and the carry flows naturally out of the seconds and into the integer part, i.e. into the date. That is exactly option (b): round(46132.9999999999 × 86400) / 86400 = 46133.0 -> 21/04/2026 00:00:00 — unambiguous, with no boundary paradox. Option (a) is precisely the error of importing the *saturating* rule of the bounded circle into a value that lives on ℝ. It pins the time below the day wall (23:59:59) even though the number has no wall — which is the very "the rounding rule changes at a threshold" self-inconsistency noted above. The apparent day boundary is an artefact of *displaying only the fractional part*; it is not a boundary of the underlying number. So LibreOffice's present behaviour is a category error: it rounds the **date** on ℝ (`setLocalDateTime`, carry allowed) but rounds the **time** on the circle ℝ / 86400ℤ (carry forbidden). The two halves of one value are rounded in two different number systems. The fix is to choose a single system for the whole value; and since the value demonstrably lives on ℝ, that system is ℝ — option (b). -- You are receiving this mail because: You are the assignee for the bug.
