tdf#153993 Extend ODF: lowercase for exponent char

In scientific format, user may want to have clearer format such as:
0.000" "000" "e+" "0

This change
- adds "exponent-lowercase" boolean attribute to scientific-number format to preserve "e" or "E" char
- includes all positions up to exponent for embedded text of scientific number

Add QA test

Change-Id: Ie263f4ecf30a1a8dcd8046e1e048767020e54dc2
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/153824
Tested-by: Jenkins
Reviewed-by: Laurent Balland <laurent.balland@mailo.fr>
diff --git a/include/xmloff/xmlnumfe.hxx b/include/xmloff/xmlnumfe.hxx
index 64a66a2..8421c5f 100644
--- a/include/xmloff/xmlnumfe.hxx
+++ b/include/xmloff/xmlnumfe.hxx
@@ -71,7 +71,7 @@ private:
                                        bool bGrouping, sal_Int32 nTrailingThousands,
                                        const SvXMLEmbeddedTextEntryArr& rEmbeddedEntries );
    SAL_DLLPRIVATE void WriteScientificElement_Impl( sal_Int32 nDecimals, sal_Int32 nMinDecimals, sal_Int32 nInteger, sal_Int32 nBlankInteger,
                                        bool bGrouping, sal_Int32 nExp, sal_Int32 nExpInterval, bool bExpSign,
                                        bool bGrouping, sal_Int32 nExp, sal_Int32 nExpInterval, bool bExpSign, bool bExponentLowercase,
                                        const SvXMLEmbeddedTextEntryArr& rEmbeddedEntries );
    SAL_DLLPRIVATE void WriteFractionElement_Impl( sal_Int32 nInteger, sal_Int32 nBlankInteger, bool bGrouping,
                                                   const SvNumberformat& rFormat, sal_uInt16 nPart );
diff --git a/include/xmloff/xmltoken.hxx b/include/xmloff/xmltoken.hxx
index 066a559..62f3ebc 100644
--- a/include/xmloff/xmltoken.hxx
+++ b/include/xmloff/xmltoken.hxx
@@ -3467,6 +3467,7 @@ namespace xmloff::token {
        XML_EXTERNALDATA,

        XML_EXPONENT_INTERVAL,
        XML_EXPONENT_LOWERCASE,
        XML_FORCED_EXPONENT_SIGN,
        XML_MIN_DECIMAL_PLACES,
        XML_MAX_DENOMINATOR_VALUE,
diff --git a/sc/qa/unit/data/ods/tdf153993-Exponent-lower-case.ods b/sc/qa/unit/data/ods/tdf153993-Exponent-lower-case.ods
new file mode 100644
index 0000000..aaa07ca
--- /dev/null
+++ b/sc/qa/unit/data/ods/tdf153993-Exponent-lower-case.ods
Binary files differ
diff --git a/sc/qa/unit/subsequent_export_test4.cxx b/sc/qa/unit/subsequent_export_test4.cxx
index b611ceb..2b414142 100644
--- a/sc/qa/unit/subsequent_export_test4.cxx
+++ b/sc/qa/unit/subsequent_export_test4.cxx
@@ -1503,6 +1503,20 @@ CPPUNIT_TEST_FIXTURE(ScExportTest4, testEmbeddedTextInDecimal)
    lcl_TestNumberFormat(*getScDoc(), "#,##0.000\" \"###\" \"###");
}

CPPUNIT_TEST_FIXTURE(ScExportTest4, testLowercaseExponent)
{
    createScDoc("ods/tdf153993-Exponent-lower-case.ods");

    // save to ODS and reload
    saveAndReload("calc8");
    lcl_TestNumberFormat(*getScDoc(), "0.000\" \"000\" \"e+\" \"0");

    // save to XLSX and reload
    // lower case not preserve in XLSX
    saveAndReload("Calc Office Open XML");
    lcl_TestNumberFormat(*getScDoc(), "0.000 000 E+ 0");
}

CPPUNIT_TEST_FIXTURE(ScExportTest4, testTotalsRowFunction)
{
    createScDoc("xlsx/totalsRowFunction.xlsx");
diff --git a/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng b/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng
index b9fbcfc..c999376 100644
--- a/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng
+++ b/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng
@@ -2845,6 +2845,15 @@ xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.
    </rng:interleave>
  </rng:define>

  <!-- TODO no proposal,  -->
  <rng:define name="number-scientific-number-attlist" combine="interleave">
    <rng:optional>
      <rng:attribute name="loext:exponent-lowercase">
        <rng:ref name="boolean"/>
      </rng:attribute>
    </rng:optional>
  </rng:define>

  <!-- TODO no proposal -->
  <rng:define name="table-data-pilot-level-attlist" combine="interleave">
    <rng:optional>
diff --git a/xmloff/source/core/xmltoken.cxx b/xmloff/source/core/xmltoken.cxx
index fa439f9..00234904 100644
--- a/xmloff/source/core/xmltoken.cxx
+++ b/xmloff/source/core/xmltoken.cxx
@@ -3472,6 +3472,7 @@ namespace xmloff::token {
        TOKEN( "external-data", XML_EXTERNALDATA),

        TOKEN( "exponent-interval",               XML_EXPONENT_INTERVAL ),
        TOKEN( "exponent-lowercase",              XML_EXPONENT_LOWERCASE ),
        TOKEN( "forced-exponent-sign",            XML_FORCED_EXPONENT_SIGN ),
        TOKEN( "min-decimal-places",              XML_MIN_DECIMAL_PLACES ),
        TOKEN( "max-denominator-value",           XML_MAX_DENOMINATOR_VALUE ),
diff --git a/xmloff/source/style/xmlnumfe.cxx b/xmloff/source/style/xmlnumfe.cxx
index 67675cf..ee09dd0 100644
--- a/xmloff/source/style/xmlnumfe.cxx
+++ b/xmloff/source/style/xmlnumfe.cxx
@@ -695,7 +695,7 @@ void SvXMLNumFmtExport::WriteNumberElement_Impl(

void SvXMLNumFmtExport::WriteScientificElement_Impl(
                            sal_Int32 nDecimals, sal_Int32 nMinDecimals, sal_Int32 nInteger, sal_Int32 nBlankInteger,
                            bool bGrouping, sal_Int32 nExp, sal_Int32 nExpInterval, bool bExpSign,
                            bool bGrouping, sal_Int32 nExp, sal_Int32 nExpInterval, bool bExpSign, bool bExponentLowercase,
                            const SvXMLEmbeddedTextEntryArr& rEmbeddedEntries )
{
    FinishTextElement_Impl();
@@ -753,6 +753,13 @@ void SvXMLNumFmtExport::WriteScientificElement_Impl(
                             XML_FORCED_EXPONENT_SIGN,
                             bExpSign? XML_TRUE : XML_FALSE );
    }
    //  exponent string
    // Export only for 1.x with extensions
    if (eVersion & SvtSaveOptions::ODFSVER_EXTENDED)
    {
        if (bExponentLowercase)
            m_rExport.AddAttribute( XML_NAMESPACE_LO_EXT, XML_EXPONENT_LOWERCASE, XML_TRUE );
    }

    SvXMLElementExport aElem( m_rExport,
                              XML_NAMESPACE_NUMBER, XML_SCIENTIFIC_NUMBER,
@@ -1351,6 +1358,7 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt
        bool bCurrFound  = false;
        bool bInInteger  = true;
        bool bExpSign = true;
        bool bExponentLowercase = false;        // 'e' or 'E' for scientific notation
        bool bDecAlign   = false;               // decimal alignment with "?"
        sal_Int32 nExpDigits = 0;
        sal_Int32 nIntegerSymbols = 0;          // for embedded-text, including "#"
@@ -1421,6 +1429,8 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt
                    if ( pElemStr && ( pElemStr->getLength() == 1
                                  || ( pElemStr->getLength() == 2 && (*pElemStr)[1] == '-' ) ) )
                        bExpSign = false;       // for 0.00E0 or 0.00E-00
                    if ( pElemStr && (*pElemStr)[0] == 'e' )
                        bExponentLowercase = true;   // for 0.00e+00
                    break;
                case NF_SYMBOLTYPE_CURRENCY:
                    bCurrFound = true;
@@ -1459,8 +1469,12 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt
            // Enable embedded text in decimal part only if there's a decimal part
            if ( nPrecision )
                nEmbeddedPositionsMax += nPrecision + 1;
            // Enable embedded text in exponent in scientific number
            if ( nFmtType == SvNumFormatType::SCIENTIFIC )
                nEmbeddedPositionsMax += 1 + nExpDigits;
            nPos = 0;
            bEnd = false;
            bExpFound = false;
            while (!bEnd)
            {
                short nElemType = rFormat.GetNumForType( nPart, nPos );
@@ -1475,6 +1489,9 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt
                        if ( pElemStr )
                            nDigitsPassed += pElemStr->getLength();
                        break;
                    case NF_SYMBOLTYPE_EXP:
                        bExpFound = true;
                        [[fallthrough]];
                    case NF_SYMBOLTYPE_DECSEP:
                        nDigitsPassed++;
                        break;
@@ -1505,6 +1522,9 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt

                            aEmbeddedEntries.push_back(
                                SvXMLEmbeddedTextEntry( nPos, nEmbedPos, aEmbeddedStr, bSaveBlankWidthSymbol ));
                            // exponent sign is required with embedded text in exponent
                            if ( bExpFound && !bExpSign )
                                bExpSign = true;
                        }
                        break;
                }
@@ -1662,7 +1682,7 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt
                                // as integer digits: use nIntegerSymbols instead of nLeading
                                // nIntegerSymbols represents exponent interval (for engineering notation)
                                WriteScientificElement_Impl( nPrecision, nMinDecimals, nLeading, nBlankInteger, bThousand, nExpDigits, nIntegerSymbols, bExpSign,
                                    aEmbeddedEntries );
                                    bExponentLowercase, aEmbeddedEntries );
                                bAnyContent = true;
                                break;
                            case SvNumFormatType::FRACTION:
diff --git a/xmloff/source/style/xmlnumfi.cxx b/xmloff/source/style/xmlnumfi.cxx
index f9f9bce..d729149 100644
--- a/xmloff/source/style/xmlnumfi.cxx
+++ b/xmloff/source/style/xmlnumfi.cxx
@@ -103,6 +103,7 @@ struct SvXMLNumberInfo
    bool        bGrouping           = false;
    bool        bDecReplace         = false;
    bool        bExpSign            = true;
    bool        bExponentLowercase  = false;     /// Exponent is 'e' instead of 'E'
    bool        bDecAlign           = false;
    double      fDisplayFactor      = 1.0;
    OUString    aIntegerFractionDelimiter;
@@ -722,6 +723,11 @@ SvXMLNumFmtElementContext::SvXMLNumFmtElementContext( SvXMLImport& rImport,
                if (::sax::Converter::convertBool( bAttrBool, aIter.toView() ))
                    aNumInfo.bExpSign = bAttrBool;
                break;
            case XML_ELEMENT(NUMBER, XML_EXPONENT_LOWERCASE):
            case XML_ELEMENT(LO_EXT, XML_EXPONENT_LOWERCASE):
                if (::sax::Converter::convertBool( bAttrBool, aIter.toView() ))
                    aNumInfo.bExponentLowercase = bAttrBool;
                break;
            case XML_ELEMENT(NUMBER, XML_MIN_NUMERATOR_DIGITS):
                if (::sax::Converter::convertNumber( nAttrVal, aIter.toView(), 0 ))
                    aNumInfo.nMinNumerDigits = nAttrVal;
@@ -1173,16 +1179,7 @@ void SvXMLNumFmtElementContext::endFastElement(sal_Int32 )
                        rParent.AddToCode( '#' );
                    }
                }
                rParent.AddNumber( aNumInfo );      // simple number

                if ( aNumInfo.bExpSign )
                    rParent.AddToCode( u"E+" );
                else
                    rParent.AddToCode( u"E" );
                for (sal_Int32 i=0; i<aNumInfo.nExpDigits; i++)
                {
                    rParent.AddToCode( '0' );
                }
                rParent.AddNumber( aNumInfo );      //  number and exponent
            }
            break;

@@ -1868,6 +1865,23 @@ void SvXMLNumFormatContext::AddNumber( const SvXMLNumberInfo& rInfo )
            aNumStr.append( cAdd );
    }

    // Scientific number
    sal_Int32 nExpPos = -1;
    if ( rInfo.nExpDigits > 0 )
    {
        nExpPos = aNumStr.getLength();
        aNumStr.append( rInfo.bExponentLowercase ? u"e" : u"E" );
                                // exponent sign is required with embedded text in exponent
        if ( rInfo.bExpSign || ( nEmbeddedCount && ( rInfo.nDecimals + 1 < -rInfo.m_EmbeddedElements.begin()->first ) ) )
        {
            aNumStr.append( u"+" );
        }
        for (sal_Int32 i=0; i<rInfo.nExpDigits; i++)
        {
            aNumStr.append( '0' );
        }
    }

    if ( nEmbeddedCount )
    {
        //  insert embedded strings into number string
@@ -1893,6 +1907,8 @@ void SvXMLNumFormatContext::AddNumber( const SvXMLNumberInfo& rInfo )
                aNumStr.insert(0, '#');
            }
            nZeroPos = nZeroPos + nAddCount;
            if ( nExpPos > 0 )
                nExpPos = nExpPos + nAddCount;
        }

        // m_EmbeddedElements is sorted with ascending positions - loop is from right to left
@@ -1900,7 +1916,9 @@ void SvXMLNumFormatContext::AddNumber( const SvXMLNumberInfo& rInfo )
        {
            sal_Int32 const nFormatPos = it.first;
            sal_Int32 nInsertPos = nZeroPos - nFormatPos;
            if ( nInsertPos >= 0 )
            if ( nExpPos > 0 && nInsertPos > nExpPos )
                nInsertPos ++;
            if ( 0 <= nInsertPos && nInsertPos <= aNumStr.getLength() )
            {
                aNumStr.insert( nInsertPos, it.second );
            }
diff --git a/xmloff/source/token/tokens.txt b/xmloff/source/token/tokens.txt
index 85d9473..cdd3877 100644
--- a/xmloff/source/token/tokens.txt
+++ b/xmloff/source/token/tokens.txt
@@ -3228,6 +3228,7 @@ display-units
display-units-built-in-unit
external-data
exponent-interval
exponent-lowercase
forced-exponent-sign
min-decimal-places
max-denominator-value