basic/qa/basic_coverage/test_format_function.bas | 1 basic/source/sbx/sbxscan.cxx | 281 ++++++++++++----------- 2 files changed, 152 insertions(+), 130 deletions(-)
New commits: commit 33434f16206aaa07f56af8f4c23b05b3c88a2124 Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Thu May 8 13:32:20 2025 +0200 Commit: Mike Kaganski <mike.kagan...@collabora.com> CommitDate: Thu May 8 17:42:21 2025 +0200 tdf#166342: refactor SbxValue::Format again, to handle date/time strings Commit 81e1e0a2a671f19950c1bd3c69f9aa24b0c562e7 changed string-to-number procedure from SvNumberFormatter to only use SbxValue::ScanNumIntnl. But the latter can only handle strings with simple numbers, not with dates, times and the like. This change restores use of SvNumberFormatter to convert input strings to number, when SbxValue::ScanNumIntnl fails. The code is re-structured for performance, to avoid creating SvNumberFormatter twice, and repeated calls to GetOUString. Change-Id: I89a4fc041fe24fe00b099ab4fbee371d1f6b5567 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/185047 Tested-by: Jenkins Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com> diff --git a/basic/qa/basic_coverage/test_format_function.bas b/basic/qa/basic_coverage/test_format_function.bas index 00d7d54a9f3f..d1c51fe791a5 100644 --- a/basic/qa/basic_coverage/test_format_function.bas +++ b/basic/qa/basic_coverage/test_format_function.bas @@ -20,6 +20,7 @@ Sub verify_testFormat Dim d As Date d = "2024-09-16 17:03:30" TestUtil.AssertEqual(Format(d, "YYYY-MM-DD"), "2024-09-16", "Format(d, ""YYYY-MM-DD"")") + TestUtil.AssertEqual(Format("2024-09-16 05:03:30 PM", "hh-mm-ss"), "17-03-30", "Format(""2024-09-16 05:03:30 PM"", ""hh-mm-ss"")") Exit Sub errorHandler: diff --git a/basic/source/sbx/sbxscan.cxx b/basic/source/sbx/sbxscan.cxx index f843d6b44e13..546366af5698 100644 --- a/basic/source/sbx/sbxscan.cxx +++ b/basic/source/sbx/sbxscan.cxx @@ -20,6 +20,7 @@ #include <sal/config.h> #include <cstdlib> +#include <optional> #include <string_view> #include <config_features.h> @@ -457,19 +458,50 @@ void BasicFormatNum(double d, const OUString& rFmt, OUString& rRes) rRes = rAppData.pBasicFormater->BasicFormat(d, rFmt); } -void BasicFormatNum(double d, const OUString* pFmt, SbxDataType eType, OUString& rRes) +std::shared_ptr<SvNumberFormatter> GetFormatter() { - if (pFmt) - BasicFormatNum(d, *pFmt, rRes); + if (auto pInst = GetSbData()->pInst) + { + return pInst->GetNumberFormatter(); + } else - ImpCvtNum(d, eType == SbxSINGLE ? 6 : eType == SbxDOUBLE ? 14 : 0, rRes); + { + return SbiInstance::PrepareNumberFormatter(o3tl::temporary(sal_uInt32()), + o3tl::temporary(sal_uInt32()), + o3tl::temporary(sal_uInt32())); + } +} + +std::optional<double> StrToNumberIntl(const OUString& s, + std::shared_ptr<SvNumberFormatter>& rpFormatter) +{ + double ret; + if (SbxValue::ScanNumIntnl(s, ret) == ERRCODE_NONE) + return ret; + + // We couldn't detect a Basic-formatted number (including type characters & specific exponents). + // Try generic number detection (works also for dates/times). + + rpFormatter = GetFormatter(); + assert(rpFormatter); + LanguageType eLangType = Application::GetSettings().GetLanguageTag().getLanguageType(); + + // Passing an index of a locale switches IsNumberFormat() to use that + // locale in case the formatter wasn't default created with it. + sal_uInt32 nIndex = rpFormatter->GetStandardIndex(eLangType); + + if (rpFormatter->IsNumberFormat(s, nIndex, ret)) + return ret; + + return {}; } -#if HAVE_FEATURE_SCRIPTING // For numeric types, takes the number directly; otherwise, tries to take string and convert it -bool GetNumberIntl(const SbxValue& val, double& ret) +std::optional<double> GetNumberIntl(const SbxValue& val, OUString& rStrVal, + std::shared_ptr<SvNumberFormatter>& rpFormatter, + bool extendedNumberDetection) { - switch (val.GetType()) + switch (SbxDataType type = val.GetType()) { case SbxCHAR: case SbxBYTE: @@ -482,14 +514,19 @@ bool GetNumberIntl(const SbxValue& val, double& ret) case SbxSINGLE: case SbxDOUBLE: case SbxDATE: - ret = val.GetDouble(); - return true; + return val.GetDouble(); + case SbxBOOL: + if (extendedNumberDetection) + return val.GetDouble(); + [[fallthrough]]; case SbxSTRING: default: - return SbxValue::ScanNumIntnl(val.GetOUString(), ret) == ERRCODE_NONE; + rStrVal = val.GetOUString(); + return extendedNumberDetection || type == SbxSTRING + ? StrToNumberIntl(rStrVal, rpFormatter) + : std::nullopt; } } -#endif } // namespace void SbxValue::Format( OUString& rRes, const OUString* pFmt ) const @@ -506,140 +543,124 @@ void SbxValue::Format( OUString& rRes, const OUString* pFmt ) const rRes = SvtSysLocale().GetCharClass().uppercase(GetOUString()); return; } + } + + const SbxDataType eType = GetType(); + if (eType == SbxNULL) + { + rRes = SbxBasicFormater::BasicFormatNull(pFmt ? *pFmt : std::u16string_view{}); + return; + } + + std::shared_ptr<SvNumberFormatter> pFormatter; + std::optional<double> number = GetNumberIntl(*this, rRes, pFormatter, pFmt != nullptr); + + if (!number) + { + if (eType == SbxSTRING && pFmt) + printfmtstr(rRes, rRes, *pFmt); + return; + } + + if (!pFmt) + { + ImpCvtNum(*number, eType == SbxSINGLE ? 6 : eType == SbxDOUBLE ? 14 : 0, rRes); + return; + } + + if (SbxBasicFormater::isBasicFormat(*pFmt)) + { + BasicFormatNum(*number, *pFmt, rRes); + return; + } // pflin, It is better to use SvNumberFormatter to handle the date/time/number format. // the SvNumberFormatter output is mostly compatible with // VBA output besides the OOo-basic output #if HAVE_FEATURE_SCRIPTING - // number format, use SvNumberFormatter to handle it. - if (double nNumber; !SbxBasicFormater::isBasicFormat(*pFmt) && GetNumberIntl(*this, nNumber)) - { - LanguageType eLangType = Application::GetSettings().GetLanguageTag().getLanguageType(); - std::shared_ptr<SvNumberFormatter> pFormatter; - if (GetSbData()->pInst) - { - pFormatter = GetSbData()->pInst->GetNumberFormatter(); - } - else - { - sal_uInt32 n; // Dummy - pFormatter = SbiInstance::PrepareNumberFormatter(n, n, n); - } + // number format, use SvNumberFormatter to handle it. + if (!pFormatter) + pFormatter = GetFormatter(); - sal_uInt32 nIndex; - const Color* pCol; - sal_Int32 nCheckPos = 0; - SvNumFormatType nType; - OUString aFmtStr = *pFmt; - if (const VbaFormatInfo* pInfo = getFormatInfo(aFmtStr)) - { - if( pInfo->meType == VbaFormatType::Offset ) - { - nIndex = pFormatter->GetFormatIndex( pInfo->meOffset, eLangType ); - } - else - { - aFmtStr = pInfo->mpOOoFormat; - pFormatter->PutandConvertEntry( aFmtStr, nCheckPos, nType, nIndex, LANGUAGE_ENGLISH_US, eLangType, true); - } - pFormatter->GetOutputString( nNumber, nIndex, rRes, &pCol ); - } - else if (aFmtStr.equalsIgnoreAsciiCase("General Date") // VBA general date variants - || aFmtStr.equalsIgnoreAsciiCase("c")) - { - OUString dateStr; - if( nNumber <=-1.0 || nNumber >= 1.0 ) - { - // short date - nIndex = pFormatter->GetFormatIndex( NF_DATE_SYSTEM_SHORT, eLangType ); - pFormatter->GetOutputString(nNumber, nIndex, dateStr, &pCol); + LanguageType eLangType = Application::GetSettings().GetLanguageTag().getLanguageType(); - if (floor(nNumber) == nNumber) - { - rRes = dateStr; - return; - } - } - // long time - aFmtStr = u"H:MM:SS AM/PM"_ustr; - pFormatter->PutandConvertEntry(aFmtStr, nCheckPos, nType, nIndex, - LANGUAGE_ENGLISH_US, eLangType, true); - pFormatter->GetOutputString(nNumber, nIndex, rRes, &pCol); - if (!dateStr.isEmpty()) - rRes = dateStr + " " + rRes; - } - else if (aFmtStr.equalsIgnoreAsciiCase("n") // VBA minute variants - || aFmtStr.equalsIgnoreAsciiCase("nn")) - { - sal_Int32 nMin = implGetMinute( nNumber ); - if (nMin < 10 && aFmtStr.equalsIgnoreAsciiCase("nn")) - { - // Minute in two digits - sal_Unicode aBuf[2]; - aBuf[0] = '0'; - aBuf[1] = '0' + nMin; - rRes = OUString(aBuf, std::size(aBuf)); - } - else - { - rRes = OUString::number(nMin); - } - } - else if (aFmtStr.equalsIgnoreAsciiCase("w")) // VBA weekday number - { - rRes = OUString::number(implGetWeekDay(nNumber)); - } - else if (aFmtStr.equalsIgnoreAsciiCase("y")) // VBA year day number - { - sal_Int16 nYear = implGetDateYear( nNumber ); - double dBaseDate; - implDateSerial( nYear, 1, 1, true, SbDateCorrection::None, dBaseDate ); - sal_Int32 nYear32 = 1 + sal_Int32( nNumber - dBaseDate ); - rRes = OUString::number(nYear32); - } - else + sal_uInt32 nIndex; + const Color* pCol; + sal_Int32 nCheckPos = 0; + SvNumFormatType nType; + OUString aFmtStr = *pFmt; + if (const VbaFormatInfo* pInfo = getFormatInfo(aFmtStr)) + { + if( pInfo->meType == VbaFormatType::Offset ) + { + nIndex = pFormatter->GetFormatIndex( pInfo->meOffset, eLangType ); + } + else + { + aFmtStr = pInfo->mpOOoFormat; + pFormatter->PutandConvertEntry( aFmtStr, nCheckPos, nType, nIndex, LANGUAGE_ENGLISH_US, eLangType, true); + } + pFormatter->GetOutputString(*number, nIndex, rRes, &pCol); + } + else if (aFmtStr.equalsIgnoreAsciiCase("General Date") // VBA general date variants + || aFmtStr.equalsIgnoreAsciiCase("c")) + { + OUString dateStr; + if (*number <= -1.0 || *number >= 1.0) + { + // short date + nIndex = pFormatter->GetFormatIndex( NF_DATE_SYSTEM_SHORT, eLangType ); + pFormatter->GetOutputString(*number, nIndex, dateStr, &pCol); + + if (floor(*number) == *number) { - pFormatter->PutandConvertEntry( aFmtStr, nCheckPos, nType, nIndex, LANGUAGE_ENGLISH_US, eLangType, true); - pFormatter->GetOutputString( nNumber, nIndex, rRes, &pCol ); + rRes = dateStr; + return; } - - return; } -#endif + // long time + aFmtStr = u"H:MM:SS AM/PM"_ustr; + pFormatter->PutandConvertEntry(aFmtStr, nCheckPos, nType, nIndex, + LANGUAGE_ENGLISH_US, eLangType, true); + pFormatter->GetOutputString(*number, nIndex, rRes, &pCol); + if (!dateStr.isEmpty()) + rRes = dateStr + " " + rRes; } - - SbxDataType eType = GetType(); - switch( eType ) + else if (aFmtStr.equalsIgnoreAsciiCase("n") // VBA minute variants + || aFmtStr.equalsIgnoreAsciiCase("nn")) { - case SbxNULL: - rRes = SbxBasicFormater::BasicFormatNull(pFmt ? *pFmt : std::u16string_view{}); - break; - case SbxCHAR: - case SbxBYTE: - case SbxINTEGER: - case SbxUSHORT: - case SbxLONG: - case SbxULONG: - case SbxINT: - case SbxUINT: - case SbxSINGLE: - case SbxDOUBLE: - BasicFormatNum(GetDouble(), pFmt, eType, rRes); - break; - case SbxSTRING: - rRes = GetOUString(); - if( pFmt ) + sal_Int32 nMin = implGetMinute(*number); + if (nMin < 10 && aFmtStr.equalsIgnoreAsciiCase("nn")) { - // #45355 converting if numeric - if (double d; ScanNumIntnl(rRes, d) == ERRCODE_NONE) - BasicFormatNum(d, *pFmt, rRes); - else - printfmtstr(rRes, rRes, *pFmt); + // Minute in two digits + sal_Unicode aBuf[2]; + aBuf[0] = '0'; + aBuf[1] = '0' + nMin; + rRes = OUString(aBuf, std::size(aBuf)); } - break; - default: - rRes = GetOUString(); + else + { + rRes = OUString::number(nMin); + } + } + else if (aFmtStr.equalsIgnoreAsciiCase("w")) // VBA weekday number + { + rRes = OUString::number(implGetWeekDay(*number)); + } + else if (aFmtStr.equalsIgnoreAsciiCase("y")) // VBA year day number + { + sal_Int16 nYear = implGetDateYear(*number); + double dBaseDate; + implDateSerial( nYear, 1, 1, true, SbDateCorrection::None, dBaseDate ); + sal_Int32 nYear32 = 1 + sal_Int32(*number - dBaseDate); + rRes = OUString::number(nYear32); } + else + { + pFormatter->PutandConvertEntry( aFmtStr, nCheckPos, nType, nIndex, LANGUAGE_ENGLISH_US, eLangType, true); + pFormatter->GetOutputString(*number, nIndex, rRes, &pCol); + } +#endif }