include/tools/duration.hxx | 30 ++++++++++- sc/source/core/tool/interpr5.cxx | 22 +++++++- tools/qa/cppunit/test_duration.cxx | 78 +++++++++++++++++++++++++++++ tools/source/datetime/duration.cxx | 97 ++++++++++++++++++++++++++++++++----- tools/source/datetime/ttime.cxx | 12 ++++ 5 files changed, 221 insertions(+), 18 deletions(-)
New commits: commit c17641839855ba88b1c071c4f5e4533725ff2dbb Author: Eike Rathke <er...@redhat.com> AuthorDate: Mon Aug 7 16:37:26 2023 +0200 Commit: Adolfo Jayme Barrientos <fit...@ubuntu.com> CommitDate: Wed Aug 16 13:13:14 2023 +0200 Resolves: tdf#127334 Backport tools::Duration ScInterpreter::CalculateAddSub() All tools::Duration implementation for completeness. This is a combination of 5 (+2 typo) commits. Introduce tools::Duration(sal_Int32 nDays, const Time& rTime) ctor xChange-Id: If002e04536149b49b2249103ac914d17dec3fae6 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/153409 Reviewed-by: Eike Rathke <er...@redhat.com> Tested-by: Jenkins (cherry picked from commit 986c2d86a7b53a6599d014db7327f47cb33d4fea) Introduce tools::Duration individual time values ctor xChange-Id: I516d3727cbcf6667b32dc963febbf4b753ef6a91 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/153497 Reviewed-by: Eike Rathke <er...@redhat.com> Tested-by: Jenkins (cherry picked from commit c968d8989004301b49d67a093a6eb8a629533837) Clamp and assert maximum hours value in Time::init() xChange-Id: Ia777222f3c797b90663b55499a57025e410b1d70 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/153407 Reviewed-by: Eike Rathke <er...@redhat.com> Tested-by: Jenkins (cherry picked from commit b07d72c6c1075efa6b64c67758566426c22c5225) Use tools::Duration in ScInterpreter::CalculateAddSub() ... for all (date+)time inflicted operands. xChange-Id: I93043d912867e2ef7d4af271b5c4566a3ffb4ef9 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/153291 Reviewed-by: Eike Rathke <er...@redhat.com> Tested-by: Jenkins (cherry picked from commit 174a72f3fd50d1146d6bedd4cc2a1971aa33be67) Resolves: tdf#127334 Increase tools::Duration accuracy epsilon unsharpness ... when converting from double, i.e. to 300 nanoseconds. Empirically determined.. xChange-Id: I92c43b5f244923363af5d44bece9c155126ca343 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/155324 Reviewed-by: Eike Rathke <er...@redhat.com> Tested-by: Jenkins (cherry picked from commit 46e672db8002e7aaac881bee65b5c50c4e14c666) Change-Id: I92c43b5f244923363af5d44bece9c155126ca343 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/155427 Tested-by: Jenkins Reviewed-by: Adolfo Jayme Barrientos <fit...@ubuntu.com> diff --git a/include/tools/duration.hxx b/include/tools/duration.hxx index 83b9d12a77b3..9fae80d1d7c9 100644 --- a/include/tools/duration.hxx +++ b/include/tools/duration.hxx @@ -31,8 +31,30 @@ public: minutes and seconds values here though. */ Duration(const Time& rStart, const Time& rEnd); - /** Difference in days, like DateTime()-DateTime(). */ - explicit Duration(double fTimeInDays); + /** Difference in days, like DateTime()-DateTime(). + + @param nAccuracyEpsilonNanoseconds + Round for example by 1 nanosecond if it's just 1 off to a + second, i.e. 0999999999 or 0000000001. This can be loosened if + necessary. For example, if fTimeInDays is a date+time in + "today's" range with a significant seconds resolution, an + accuracy epsilon (=unsharpness) of ~300 is required. Hence default. + Must be 0 <= nAccuracyEpsilonNanoseconds <= Time::nanoSecPerSec - 1. + */ + explicit Duration(double fTimeInDays, sal_uInt64 nAccuracyEpsilonNanoseconds = 300); + + /** Time can be a limited duration as well and can have out-of-range + values, it will be normalized. Sign of both days and Time must be equal + unless one is 0. */ + Duration(sal_Int32 nDays, const Time& rTime); + + /** Individual time values can be out-of-range, all will be normalized. + Additionally, the resulting time overall hour value is not restricted + to sal_uInt16 like it is with Time, as values >=24 flow over into days. + For a negative duration only a negative nDays can be given, thus a + negative duration of less than one day is not possible. */ + Duration(sal_Int32 nDays, sal_uInt32 nHours, sal_uInt32 nMinutes, sal_uInt32 nSeconds, + sal_uInt64 nNanoseconds); bool IsNegative() const { return mnDays < 0 || maTime.GetTime() < 0; } sal_Int32 GetDays() const { return mnDays; } @@ -55,6 +77,10 @@ private: /** Internal days and Time values. */ Duration(sal_Int32 nDays, sal_Int64 nTime); + /** Prerequisite: mnDays is already set. */ + void Normalize(sal_uInt64 nHours, sal_uInt64 nMinutes, sal_uInt64 nSeconds, + sal_uInt64 nNanoseconds, bool bNegative); + /** Prerequisite: mnDays is already correctly set and absolute value of nanoseconds less than one day. */ void ApplyTime(sal_Int64 nNS); diff --git a/sc/source/core/tool/interpr5.cxx b/sc/source/core/tool/interpr5.cxx index b27e27c5e188..dcb5ee6ea343 100644 --- a/sc/source/core/tool/interpr5.cxx +++ b/sc/source/core/tool/interpr5.cxx @@ -27,6 +27,7 @@ #include <unotools/bootstrap.hxx> #include <svl/zforlist.hxx> +#include <tools/duration.hxx> #include <interpre.hxx> #include <global.hxx> @@ -1220,6 +1221,7 @@ void ScInterpreter::CalculateAddSub(bool _bSub) double fVal1 = 0.0, fVal2 = 0.0; SvNumFormatType nFmt1, nFmt2; nFmt1 = nFmt2 = SvNumFormatType::UNDEFINED; + bool bDuration = false; SvNumFormatType nFmtCurrencyType = nCurFmtType; sal_uLong nFmtCurrencyIndex = nCurFmtIndex; SvNumFormatType nFmtPercentType = nCurFmtType; @@ -1235,6 +1237,7 @@ void ScInterpreter::CalculateAddSub(bool _bSub) case SvNumFormatType::DATETIME : case SvNumFormatType::DURATION : nFmt2 = nCurFmtType; + bDuration = true; break; case SvNumFormatType::CURRENCY : nFmtCurrencyType = nCurFmtType; @@ -1258,6 +1261,7 @@ void ScInterpreter::CalculateAddSub(bool _bSub) case SvNumFormatType::DATETIME : case SvNumFormatType::DURATION : nFmt1 = nCurFmtType; + bDuration = true; break; case SvNumFormatType::CURRENCY : nFmtCurrencyType = nCurFmtType; @@ -1334,10 +1338,22 @@ void ScInterpreter::CalculateAddSub(bool _bSub) if (nFmtPercentType == SvNumFormatType::PERCENT && nFuncFmtType == SvNumFormatType::NUMBER) nFuncFmtType = SvNumFormatType::PERCENT; } - if ( _bSub ) - PushDouble( ::rtl::math::approxSub( fVal1, fVal2 ) ); + if ((nFuncFmtType == SvNumFormatType::DURATION || bDuration) + && ((_bSub && std::fabs(fVal1 - fVal2) <= SAL_MAX_INT32) + || (!_bSub && std::fabs(fVal1 + fVal2) <= SAL_MAX_INT32))) + { + if (_bSub) + PushDouble( ::tools::Duration( fVal1 - fVal2).GetInDays()); + else + PushDouble( ::tools::Duration( fVal1 + fVal2).GetInDays()); + } else - PushDouble( ::rtl::math::approxAdd( fVal1, fVal2 ) ); + { + if (_bSub) + PushDouble( ::rtl::math::approxSub( fVal1, fVal2 ) ); + else + PushDouble( ::rtl::math::approxAdd( fVal1, fVal2 ) ); + } } } diff --git a/tools/qa/cppunit/test_duration.cxx b/tools/qa/cppunit/test_duration.cxx index 0f5a4e002219..c4032be83a03 100644 --- a/tools/qa/cppunit/test_duration.cxx +++ b/tools/qa/cppunit/test_duration.cxx @@ -104,6 +104,84 @@ void DurationTest::testDuration() const Duration aN = -aD; CPPUNIT_ASSERT_EQUAL(1.5, aN.GetInDays()); } + { + const Duration aD(1, Time(2, 3, 4, 5)); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(1), aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(2), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(3), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(4), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(5), aD.GetTime().GetNanoSec()); + } + { + // 235929599 seconds == SAL_MAX_UINT16 hours + 59 minutes + 59 seconds + const Duration aD(0, Time(0, 0, 235929599, Time::nanoSecPerSec - 1)); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(2730), aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(15), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(59), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(59), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(999999999), aD.GetTime().GetNanoSec()); + } + { + // 235929599 seconds == SAL_MAX_UINT16 hours + 59 minutes + 59 seconds + const Duration aD(0, 0, 0, 235929599, Time::nanoSecPerSec - 1); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(2730), aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(15), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(59), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(59), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(999999999), aD.GetTime().GetNanoSec()); + } + { + const Duration aD(1, 2, 3, 4, 5); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(1), aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(2), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(3), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(4), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(5), aD.GetTime().GetNanoSec()); + } + { + const Duration aD(-1, 2, 3, 4, 5); + CPPUNIT_ASSERT(aD.IsNegative()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(-1), aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(2), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(3), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(4), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(5), aD.GetTime().GetNanoSec()); + } + { + const Duration aD(1, SAL_MAX_UINT32, SAL_MAX_UINT32, SAL_MAX_UINT32, SAL_MAX_UINT64); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(182202802), aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(17), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(48), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(709551615), aD.GetTime().GetNanoSec()); + } + { + const Duration aD(-1, SAL_MAX_UINT32, SAL_MAX_UINT32, SAL_MAX_UINT32, SAL_MAX_UINT64); + CPPUNIT_ASSERT(aD.IsNegative()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(-182202802), aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(17), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(48), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(709551615), aD.GetTime().GetNanoSec()); + } + { // Maximum days with all max possible. + const Duration aD(1965280846, SAL_MAX_UINT32, SAL_MAX_UINT32, SAL_MAX_UINT32, + SAL_MAX_UINT64); + CPPUNIT_ASSERT_EQUAL(SAL_MAX_INT32, aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(17), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(48), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(709551615), aD.GetTime().GetNanoSec()); + } + { // Maximum negative days with all max possible. + const Duration aD(-1965280847, SAL_MAX_UINT32, SAL_MAX_UINT32, SAL_MAX_UINT32, + SAL_MAX_UINT64); + CPPUNIT_ASSERT_EQUAL(SAL_MIN_INT32, aD.GetDays()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), aD.GetTime().GetHour()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(17), aD.GetTime().GetMin()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(48), aD.GetTime().GetSec()); + CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt32>(709551615), aD.GetTime().GetNanoSec()); + } { // Add() const DateTime aS(Date(23, 11, 1999), Time(0, 0, 0)); const DateTime aE(Date(23, 11, 1999), Time(1, 23, 45)); diff --git a/tools/source/datetime/duration.cxx b/tools/source/datetime/duration.cxx index 1199ced39443..a655f016a1bc 100644 --- a/tools/source/datetime/duration.cxx +++ b/tools/source/datetime/duration.cxx @@ -47,8 +47,9 @@ Duration::Duration(const Time& rStart, const Time& rEnd) } } -Duration::Duration(double fTimeInDays) +Duration::Duration(double fTimeInDays, sal_uInt64 nAccuracyEpsilonNanoseconds) { + assert(nAccuracyEpsilonNanoseconds <= Time::nanoSecPerSec - 1); double fInt, fFrac; if (fTimeInDays < 0.0) { @@ -66,19 +67,21 @@ Duration::Duration(double fTimeInDays) fFrac *= Time::nanoSecPerDay; fFrac = ::rtl::math::approxFloor(fFrac); sal_Int64 nNS = static_cast<sal_Int64>(fFrac); - // Round by 1 nanosecond if it's just 1 off to a second, i.e. - // 0999999999 or 0000000001. This could be losened to rounding by 2 or - // such if necessary. const sal_Int64 nN = nNS % Time::nanoSecPerSec; - if (std::abs(nN) == 1) - nNS -= (nNS < 0) ? -1 : 1; - else if (std::abs(nN) == Time::nanoSecPerSec - 1) + if (nN) { - nNS += (nNS < 0) ? -1 : 1; - if (std::abs(nNS) >= Time::nanoSecPerDay) + const sal_uInt64 nA = std::abs(nN); + if (nA <= nAccuracyEpsilonNanoseconds) + nNS -= (nNS < 0) ? -nN : nN; + else if (nA >= Time::nanoSecPerSec - nAccuracyEpsilonNanoseconds) { - mnDays += nNS / Time::nanoSecPerDay; - nNS %= Time::nanoSecPerDay; + const sal_Int64 nD = Time::nanoSecPerSec - nA; + nNS += (nNS < 0) ? -nD : nD; + if (std::abs(nNS) >= Time::nanoSecPerDay) + { + mnDays += nNS / Time::nanoSecPerDay; + nNS %= Time::nanoSecPerDay; + } } } maTime.MakeTimeFromNS(nNS); @@ -86,12 +89,80 @@ Duration::Duration(double fTimeInDays) } } +Duration::Duration(sal_Int32 nDays, const Time& rTime) + : mnDays(nDays) +{ + assert(nDays == 0 || rTime.GetTime() == 0 || (nDays < 0) == (rTime.GetTime() < 0)); + Normalize(rTime.GetHour(), rTime.GetMin(), rTime.GetSec(), rTime.GetNanoSec(), + ((nDays < 0) || (rTime.GetTime() < 0))); +} + +Duration::Duration(sal_Int32 nDays, sal_uInt32 nHours, sal_uInt32 nMinutes, sal_uInt32 nSeconds, + sal_uInt64 nNanoseconds) + : mnDays(nDays) +{ + Normalize(nHours, nMinutes, nSeconds, nNanoseconds, nDays < 0); +} + Duration::Duration(sal_Int32 nDays, sal_Int64 nTime) : maTime(nTime) , mnDays(nDays) { } +void Duration::Normalize(sal_uInt64 nHours, sal_uInt64 nMinutes, sal_uInt64 nSeconds, + sal_uInt64 nNanoseconds, bool bNegative) +{ + if (nNanoseconds >= Time::nanoSecPerSec) + { + nSeconds += nNanoseconds / Time::nanoSecPerSec; + nNanoseconds %= Time::nanoSecPerSec; + } + if (nSeconds >= Time::secondPerMinute) + { + nMinutes += nSeconds / Time::secondPerMinute; + nSeconds %= Time::secondPerMinute; + } + if (nMinutes >= Time::minutePerHour) + { + nHours += nMinutes / Time::minutePerHour; + nMinutes %= Time::minutePerHour; + } + if (nHours >= Time::hourPerDay) + { + sal_Int64 nDiff = nHours / Time::hourPerDay; + nHours %= Time::hourPerDay; + bool bOverflow = false; + if (bNegative) + { + nDiff = -nDiff; + bOverflow = (nDiff < SAL_MIN_INT32); + bOverflow |= o3tl::checked_add(mnDays, static_cast<sal_Int32>(nDiff), mnDays); + if (bOverflow) + mnDays = SAL_MIN_INT32; + } + else + { + bOverflow = (nDiff > SAL_MAX_INT32); + bOverflow |= o3tl::checked_add(mnDays, static_cast<sal_Int32>(nDiff), mnDays); + if (bOverflow) + mnDays = SAL_MAX_INT32; + } + assert(!bOverflow); + if (bOverflow) + { + nHours = Time::hourPerDay - 1; + nMinutes = Time::minutePerHour - 1; + nSeconds = Time::secondPerMinute - 1; + nNanoseconds = Time::nanoSecPerSec - 1; + } + } + maTime = Time(nHours, nMinutes, nSeconds, nNanoseconds); + if (bNegative) + maTime = -maTime; + assert(mnDays == 0 || maTime.GetTime() == 0 || (mnDays < 0) == (maTime.GetTime() < 0)); +} + void Duration::ApplyTime(sal_Int64 nNS) { if (mnDays > 0 && nNS < 0) @@ -105,7 +176,7 @@ void Duration::ApplyTime(sal_Int64 nNS) nNS = -Time::nanoSecPerDay + nNS; } maTime.MakeTimeFromNS(nNS); - assert(mnDays == 0 || maTime.GetTime() == 0 || (mnDays < 0) == (nNS < 0)); + assert(mnDays == 0 || maTime.GetTime() == 0 || (mnDays < 0) == (maTime.GetTime() < 0)); } void Duration::SetTimeDiff(const Time& rStart, const Time& rEnd) @@ -168,7 +239,7 @@ Duration Duration::Mult(sal_Int32 nMult, bool& rbOverflow) const } if (bBadNS) { - // Simple calculation in overall nanoseconds overflew, try with + // Simple calculation in overall nanoseconds overflowed, try with // individual components. const sal_uInt64 nMult64 = (nMult < 0) ? -nMult : nMult; do diff --git a/tools/source/datetime/ttime.cxx b/tools/source/datetime/ttime.cxx index 148bf0cbecc6..fcfa2e080e99 100644 --- a/tools/source/datetime/ttime.cxx +++ b/tools/source/datetime/ttime.cxx @@ -125,6 +125,18 @@ void tools::Time::init( sal_uInt32 nHour, sal_uInt32 nMin, sal_uInt32 nSec, sal_ nHour += nMin / minInHour; nMin %= minInHour; + // 922337 * HOUR_MASK = 9223370000000000000 largest possible value, 922338 + // would be -9223364073709551616. + assert(HOUR_MASK * nHour >= 0 && "use tools::Duration with days instead!"); + if (HOUR_MASK * nHour < 0) + nHour = 922337; + + // But as is, GetHour() retrieves only sal_uInt16. Though retrieving in + // nanoseconds or milliseconds might be possible this is all crap. + assert(nHour <= SAL_MAX_UINT16 && "use tools::Duration with days instead!"); + if (nHour > SAL_MAX_UINT16) + nHour = SAL_MAX_UINT16; + // construct time nTime = nNanoSec + nSec * SEC_MASK +