Use Dragonbox to implement doubleTo*String*

This header-only library is accurate in decimal representation of
doubles; provides API that allows to create custom representation
- so it's possible to use custom decimal separators and grouping.

This allows to unify all corner cases: integers, numbers close to
DBL_MAX, up-rounding to the next decade.

Note that Dragonbox creates the shortest decimal representation
of the number, that is unambiguously convertible back to the same
number; thus it may hide trailing digits that are unneeded for
such conversion.

The functional changes are minimal, and beneficial:
1. Rounding numbers close to DBL_MAX now takes into account the
bEraseTrailingDecZeros argument, as it should, allowing to have
"1.8E+308" for rounding DBL_MAX to 2 decimals without trailing
zeroes, instead of previous "1.80E+308".
2. Incorrect rounding is fixed in some cases, e.g. 9.9999999999999929
rounded to 10 previously using rtl_math_DecimalPlaces_Max.
3. Representing the number in the shortest way may change display
of some printed numbers. E.g., 5th greatest double is represented
as "1.797693134862315E+308" instead of a bit longer, but giving
the same double on roundtrip, "1.7976931348623149E+308". This would
generally look better for some numbers similar to the famous 0.1,
where users would likely expect more "round" representation where
it's unambiguous (but we still truncate to 15 significant decimals
anyway - so there's no point in pretending to provide exact digits
for actual binary representation).

These are reflected in the unit tests affected by the change.

Change-Id: I05e20274a30eec499593ee3e9ec070e1269232a2
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/129948
Tested-by: Jenkins
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
This commit is contained in:
Mike Kaganski
2022-02-17 21:46:58 +01:00
parent ee373f34ae
commit 9eb9083ff2
12 changed files with 146 additions and 226 deletions
+1
View File
@@ -110,6 +110,7 @@ $(WORKDIR)/download: $(BUILDDIR)/config_$(gb_Side).mk $(SRCDIR)/download.lst $(S
$(call fetch_Optional,CAIRO,PIXMAN_TARBALL) \
$(call fetch_Optional,CDR,CDR_TARBALL) \
$(call fetch_Optional,CLUCENE,CLUCENE_TARBALL) \
DRAGONBOX_TARBALL \
DTOA_TARBALL \
$(call fetch_Optional,LIBCMIS,LIBCMIS_TARBALL) \
$(call fetch_Optional,COINMP,COINMP_TARBALL) \
+9
View File
@@ -4132,6 +4132,15 @@ $(eval $(call gb_Helper_register_libraries_for_install,OOOLIBS,ooo,\
))
endif
define gb_LinkTarget__use_dragonbox
$(call gb_LinkTarget_use_unpacked,$(1),dragonbox)
$(call gb_LinkTarget_set_include,$(1),\
-I$(call gb_UnpackedTarball_get_dir,dragonbox/include/)\
$$(INCLUDE) \
)
endef
define gb_LinkTarget__use_dtoa
$(call gb_LinkTarget_use_unpacked,$(1),dtoa)
$(call gb_LinkTarget_set_include,$(1),\
+2
View File
@@ -23,6 +23,8 @@ export CDR_SHA256SUM := 5666249d613466b9aa1e987ea4109c04365866e9277d80f6cd9663e8
export CDR_TARBALL := libcdr-0.1.7.tar.xz
export CLUCENE_SHA256SUM := ddfdc433dd8ad31b5c5819cc4404a8d2127472a3b720d3e744e8c51d79732eab
export CLUCENE_TARBALL := 48d647fbd8ef8889e5a7f422c1bfda94-clucene-core-2.3.3.4.tar.gz
export DRAGONBOX_SHA256SUM := 293247ccba995ec47ae3abb52c3e83904a7d71efb7093d4c0d2c6367c1cc1e20
export DRAGONBOX_TARBALL := dragonbox-1.1.0.tar.gz
export DTOA_SHA256SUM := 0082d0684f7db6f62361b76c4b7faba19e0c7ce5cb8e36c4b65fea8281e711b4
export DTOA_TARBALL := dtoa-20180411.tgz
export LIBCMIS_SHA256SUM := d7b18d9602190e10d437f8a964a32e983afd57e2db316a07d87477a79f5000a2
+1
View File
@@ -30,6 +30,7 @@ $(eval $(call gb_Module_add_moduledirs,external,\
$(call gb_Helper_optional,CPPUNIT,cppunit) \
$(call gb_Helper_optional,CT2N,ct2n) \
$(call gb_Helper_optional,CURL,curl) \
dragonbox \
dtoa \
$(call gb_Helper_optional,EBOOK,libebook) \
$(call gb_Helper_optional,EPM,epm) \
+16
View File
@@ -0,0 +1,16 @@
# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t; fill-column: 100 -*-
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
$(eval $(call gb_Module_Module,dragonbox))
$(eval $(call gb_Module_add_targets,dragonbox,\
UnpackedTarball_dragonbox \
))
# vim: set noet sw=4 ts=4:
+4
View File
@@ -0,0 +1,4 @@
Dragonbox is available from [ https://github.com/jk-jeon/dragonbox ].
Used to convert a double to a decimal string, providing breakout of significand and exponent
as integers, allowing to create custom number representation (e.g., locale-specific).
+14
View File
@@ -0,0 +1,14 @@
# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t; fill-column: 100 -*-
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
$(eval $(call gb_UnpackedTarball_UnpackedTarball,dragonbox))
$(eval $(call gb_UnpackedTarball_set_tarball,dragonbox,$(DRAGONBOX_TARBALL)))
# vim: set noet sw=4 ts=4:
+21
View File
@@ -1844,6 +1844,27 @@
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
</div>
<h2>Dragonbox</h2>
<p>The following software may be included in this product: Dragonbox.</p>
<p>Dragonbox code is covered by the following license:</p>
<p><a href="#a__Apache_License_version_2_0">Jump to Apache License Version 2.0</a></p>
<blockquote>
<p>--- LLVM Exceptions to the Apache 2.0 License ----</p>
<p>As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.</p>
<p>In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.</p>
</blockquote>
<h2>dtoa</h2>
<p>The following software may be included in this product: dtoa.</p>
<p>dtoa code is covered by the MIT license:</p>
+1
View File
@@ -43,6 +43,7 @@ $(eval $(call gb_Library_use_libraries,sal,\
))
$(eval $(call gb_Library_use_externals,sal,\
dragonbox \
dtoa \
valgrind \
zlib \
+5 -8
View File
@@ -432,7 +432,8 @@ public:
fVal = std::nextafter( fVal, 0);
aRes = rtl::math::doubleToUString( fVal, rtl_math_StringFormat_Automatic,
rtl_math_DecimalPlaces_Max, '.', true);
CPPUNIT_ASSERT_EQUAL( OUString("1.7976931348623149E+308"), aRes);
CPPUNIT_ASSERT_EQUAL( OUString("1.797693134862315E+308"), aRes);
CPPUNIT_ASSERT_EQUAL(fVal, rtl::math::stringToDouble(aRes, '.', ',')); // Test roundtrip
// DBL_MAX and 4 nextafters rounded to 15 decimals
fVal = DBL_MAX;
@@ -463,24 +464,20 @@ public:
// DBL_MAX rounded to 2 decimals
fVal = DBL_MAX;
aRes = rtl::math::doubleToUString( fVal, rtl_math_StringFormat_Automatic, 2, '.', true);
CPPUNIT_ASSERT_EQUAL( OUString("1.80E+308"), aRes);
CPPUNIT_ASSERT_EQUAL( OUString("1.8E+308"), aRes);
// Crashed after commit eae24a9488814e77254d175c11fc4a138c1dbd30
fVal = 123456.789;
aRes = rtl::math::doubleToUString(fVal, rtl_math_StringFormat_E, 2, '.', false);
CPPUNIT_ASSERT_EQUAL(OUString("1.23E+005"), aRes);
// Testing "after-treatment of up-rounding to the next decade" branch
// See void doubleToString in sal/rtl/math.cxx
// 1. Yet empty buffer
fVal = 9.9999999999999929;
aRes = rtl::math::doubleToUString(fVal, rtl_math_StringFormat_Automatic, rtl_math_DecimalPlaces_Max, '.', true);
CPPUNIT_ASSERT_EQUAL(OUString("10"), aRes);
CPPUNIT_ASSERT_EQUAL(OUString("9.99999999999999"), aRes);
// 2. Buffer with some content
fVal = 0.99999999999999933;
aRes = rtl::math::doubleToUString(fVal, rtl_math_StringFormat_F, rtl_math_DecimalPlaces_Max, '.', true);
CPPUNIT_ASSERT_EQUAL(OUString("1"), aRes);
CPPUNIT_ASSERT_EQUAL(OUString("0.999999999999999"), aRes);
}
void test_approx() {
+71 -217
View File
@@ -19,6 +19,7 @@
#include <rtl/math.h>
#include <o3tl/safeint.hxx>
#include <osl/diagnose.h>
#include <rtl/character.hxx>
#include <rtl/math.hxx>
@@ -34,6 +35,7 @@
#include <memory>
#include <stdlib.h>
#include <dragonbox/dragonbox.h>
#include <dtoa.h>
constexpr int minExp = -323, maxExp = 308;
@@ -104,11 +106,6 @@ static double getN10Exp(int nExp)
namespace {
double const nCorrVal[] = {
0, 9e-1, 9e-2, 9e-3, 9e-4, 9e-5, 9e-6, 9e-7, 9e-8,
9e-9, 9e-10, 9e-11, 9e-12, 9e-13, 9e-14, 9e-15
};
struct StringTraits
{
typedef char Char;
@@ -245,6 +242,38 @@ int getBitsInFracPart(double fAbsValue)
return std::max(nBitsInFracPart, 0);
}
constexpr sal_uInt64 eX[] = { 10ull,
100ull,
1000ull,
10000ull,
100000ull,
1000000ull,
10000000ull,
100000000ull,
1000000000ull,
10000000000ull,
100000000000ull,
1000000000000ull,
10000000000000ull,
100000000000000ull,
1000000000000000ull,
10000000000000000ull,
100000000000000000ull,
1000000000000000000ull,
10000000000000000000ull };
int decimalDigits(sal_uInt64 n)
{
return std::distance(std::begin(eX), std::upper_bound(std::begin(eX), std::end(eX), n)) + 1;
}
sal_uInt64 roundToPow10(sal_uInt64 n, int e)
{
assert(e > 0 && o3tl::make_unsigned(e) <= std::size(eX));
const sal_uInt64 d = eX[e - 1];
return (n + d / 2) / d * d;
}
template< typename T >
void doubleToString(typename T::String ** pResult,
sal_Int32 * pResultCapacity, sal_Int32 nResultOffset,
@@ -254,18 +283,6 @@ void doubleToString(typename T::String ** pResult,
typename T::Char cGroupSeparator,
bool bEraseTrailingDecZeros)
{
static double const nRoundVal[] = {
5.0e+0, 0.5e+0, 0.5e-1, 0.5e-2, 0.5e-3, 0.5e-4, 0.5e-5, 0.5e-6,
0.5e-7, 0.5e-8, 0.5e-9, 0.5e-10,0.5e-11,0.5e-12,0.5e-13,0.5e-14
};
// sign adjustment, instead of testing for fValue<0.0 this will also fetch
// -0.0
bool bSign = std::signbit(fValue);
if (bSign)
fValue = -fValue;
if (std::isnan(fValue))
{
// #i112652# XMLSchema-2
@@ -283,8 +300,7 @@ void doubleToString(typename T::String ** pResult,
return;
}
bool bHuge = fValue == HUGE_VAL; // g++ 3.0.1 requires it this way...
if (bHuge || std::isinf(fValue))
if (std::isinf(fValue))
{
// #i112652# XMLSchema-2
sal_Int32 nCapacity = RTL_CONSTASCII_LENGTH("-INF");
@@ -295,7 +311,7 @@ void doubleToString(typename T::String ** pResult,
nResultOffset = 0;
}
if ( bSign )
if (std::signbit(fValue))
T::appendAscii(pResult, pResultCapacity, &nResultOffset,
RTL_CONSTASCII_STRINGPARAM("-"));
@@ -305,6 +321,18 @@ void doubleToString(typename T::String ** pResult,
return;
}
decltype(jkj::dragonbox::to_decimal(fValue)) aParts{};
if (fValue) // to_decimal is documented to only handle non-zero finite numbers
aParts = jkj::dragonbox::to_decimal(fValue);
else
aParts.is_negative = std::signbit(fValue); // Handle -0.0
int nOrigDigits = decimalDigits(aParts.significand);
int nExp = nOrigDigits + aParts.exponent - 1;
int nRoundDigits = 15;
if (aParts.is_negative)
fValue = -fValue;
// Unfortunately the old rounding below writes 1.79769313486232e+308 for
// DBL_MAX and 4 subsequent nextafter(...,0).
static const double fB1 = std::nextafter( DBL_MAX, 0);
@@ -318,77 +346,17 @@ void doubleToString(typename T::String ** pResult,
// they exceed range they should not be written to exchange strings or
// file formats.
// Writing pDig up to decimals(-1,-2) then appending one digit from
// pRou xor one or two digits from pSlot[].
constexpr char pDig[] = "7976931348623157";
constexpr char pRou[] = "8087931359623267"; // the only up-carry is 80
static_assert(SAL_N_ELEMENTS(pDig) == SAL_N_ELEMENTS(pRou), "digit count mismatch");
constexpr sal_Int32 nDig2 = RTL_CONSTASCII_LENGTH(pRou) - 2;
sal_Int32 nCapacity = RTL_CONSTASCII_LENGTH(pRou) + 8; // + "-1.E+308"
const char pSlot[5][2][3] =
{ // rounded, not
"67", "57", // DBL_MAX
"65", "55",
"53", "53",
"51", "51",
"59", "49",
};
if (!pResultCapacity)
{
pResultCapacity = &nCapacity;
T::createBuffer(pResult, pResultCapacity);
nResultOffset = 0;
}
if (bSign)
T::appendAscii(pResult, pResultCapacity, &nResultOffset,
RTL_CONSTASCII_STRINGPARAM("-"));
nDecPlaces = std::clamp<sal_Int32>( nDecPlaces, 0, RTL_CONSTASCII_LENGTH(pRou));
if (nDecPlaces == 0)
{
T::appendAscii(pResult, pResultCapacity, &nResultOffset,
RTL_CONSTASCII_STRINGPARAM("2"));
}
else
{
T::appendAscii(pResult, pResultCapacity, &nResultOffset,
RTL_CONSTASCII_STRINGPARAM("1"));
T::appendChars(pResult, pResultCapacity, &nResultOffset, &cDecSeparator, 1);
if (nDecPlaces <= 2)
{
T::appendAscii(pResult, pResultCapacity, &nResultOffset, pRou, nDecPlaces);
}
else if (nDecPlaces <= nDig2)
{
T::appendAscii(pResult, pResultCapacity, &nResultOffset, pDig, nDecPlaces - 1);
T::appendAscii(pResult, pResultCapacity, &nResultOffset, pRou + nDecPlaces - 1, 1);
}
else
{
const sal_Int32 nDec = nDecPlaces - nDig2;
nDecPlaces -= nDec;
// nDec-1 is also offset into slot, rounded(1-1=0) or not(2-1=1)
const size_t nSlot = ((fValue < fB3) ? 4 : ((fValue < fB2) ? 3
: ((fValue < fB1) ? 2 : ((fValue < DBL_MAX) ? 1 : 0))));
T::appendAscii(pResult, pResultCapacity, &nResultOffset, pDig, nDecPlaces);
T::appendAscii(pResult, pResultCapacity, &nResultOffset, pSlot[nSlot][nDec-1], nDec);
}
}
T::appendAscii(pResult, pResultCapacity, &nResultOffset,
RTL_CONSTASCII_STRINGPARAM("E+308"));
return;
eFormat = rtl_math_StringFormat_E;
nDecPlaces = std::clamp<sal_Int32>( nDecPlaces, 0, 16);
nRoundDigits = 17;
}
// Use integer representation for integer values that fit into the
// mantissa (1.((2^53)-1)) with a precision of 1 for highest accuracy.
if ((eFormat == rtl_math_StringFormat_Automatic ||
eFormat == rtl_math_StringFormat_F) && isRepresentableInteger(fValue))
eFormat == rtl_math_StringFormat_F) && aParts.exponent >= 0 && fValue < 0x1p53)
{
sal_Int64 nInt = static_cast< sal_Int64 >(fValue);
eFormat = rtl_math_StringFormat_F;
if (nDecPlaces == rtl_math_DecimalPlaces_Max)
nDecPlaces = 0;
else
@@ -397,68 +365,7 @@ void doubleToString(typename T::String ** pResult,
if (bEraseTrailingDecZeros && nDecPlaces > 0)
nDecPlaces = 0;
// Round before decimal position.
if (nDecPlaces < 0)
{
sal_Int64 nRounding = static_cast< sal_Int64 >(getN10Exp(-nDecPlaces - 1));
const sal_Int64 nTemp = (nInt / nRounding + 5) / 10;
nInt = nTemp * 10 * nRounding;
}
// Max 1 sign, 16 integer digits, 15 group separators, 1 decimal
// separator, 15 decimals digits.
typename T::Char aBuf[64];
typename T::Char* pEnd = aBuf + 40;
typename T::Char* pStart = pEnd;
// Backward fill.
sal_Int32 nGrouping = cGroupSeparator && pGroups ? *pGroups : 0;
sal_Int32 nGroupDigits = 0;
do
{
typename T::Char nDigit = nInt % 10;
nInt /= 10;
*--pStart = nDigit + '0';
if (nGrouping && nGrouping == ++nGroupDigits && nInt)
{
*--pStart = cGroupSeparator;
if (*(pGroups + 1))
nGrouping = *++pGroups;
nGroupDigits = 0;
}
}
while (nInt);
if (bSign)
*--pStart = '-';
// Append decimals.
if (nDecPlaces > 0)
{
*pEnd++ = cDecSeparator;
pEnd = std::fill_n(pEnd, nDecPlaces, '0');
}
if (!pResultCapacity)
T::createString(pResult, pStart, pEnd - pStart);
else
T::appendChars(pResult, pResultCapacity, &nResultOffset, pStart, pEnd - pStart);
return;
}
// find the exponent
int nExp = 0;
if ( fValue > 0.0 )
{
// Cap nExp at a small value beyond which "fValue /= N10Exp" would lose precision (or N10Exp
// might even be zero); that will produce output with the decimal point in a non-normalized
// position, but the current quality of output for such small values is probably abysmal,
// anyway:
nExp = std::max(
static_cast< int >(floor(log10(fValue))), std::numeric_limits<double>::min_exponent10);
double const N10Exp = getN10Exp(nExp);
assert(N10Exp != 0);
fValue /= N10Exp;
nRoundDigits = nOrigDigits; // no rounding
}
switch (eFormat)
@@ -531,12 +438,14 @@ void doubleToString(typename T::String ** pResult,
nDigits += nExp;
// Round the number
if(nDigits >= 0)
nRoundDigits = std::min<int>(nDigits, nRoundDigits);
if(nDigits >= 0 && nOrigDigits > nRoundDigits)
{
fValue += nRoundVal[std::min<sal_Int32>(nDigits, 15)];
if (fValue >= 10)
aParts.significand = roundToPow10(aParts.significand, nOrigDigits - nRoundDigits);
assert(aParts.significand <= eX[nOrigDigits - 1]);
if (aParts.significand == eX[nOrigDigits - 1]) // up-rounding to the next decade
{
fValue = 1.0;
nOrigDigits++;
nExp++;
if (eFormat == rtl_math_StringFormat_F)
@@ -552,7 +461,7 @@ void doubleToString(typename T::String ** pResult,
assert(nBuf <= 1024);
typename T::Char* pBuf = static_cast<typename T::Char*>(alloca(nBuf * sizeof(typename T::Char)));
typename T::Char * p = pBuf;
if ( bSign )
if (aParts.is_negative)
*p++ = '-';
bool bHasDec = false;
@@ -612,77 +521,22 @@ void doubleToString(typename T::String ** pResult,
// print the number
if (nDigits > 0)
{
for (int i = 0; ; i++)
for (int nCurExp = nOrigDigits - 1;;)
{
if (i < 15) // was 16 in ancient versions, which leads to inaccuracies
int nDigit;
if (aParts.significand > 0 && nCurExp > 0)
{
int nDigit;
if (nDigits-1 == 0 && i > 0 && i < 14)
nDigit = floor( fValue + nCorrVal[15-i]);
else
nDigit = fValue + 1E-15;
if (nDigit >= 10)
{ // after-treatment of up-rounding to the next decade
typename T::Char* p1 = pBuf;
// Assert that no one changed the logic we rely on.
assert(!bSign || *p1 == '-');
// Do not touch leading minus sign put earlier.
if (bSign)
++p1;
assert(p1 <= p);
if (p1 == p)
{
*p++ = '1';
if (eFormat != rtl_math_StringFormat_F)
{
*p++ = cDecSeparator;
nExp++;
bHasDec = true;
}
*p++ = '0';
}
else
{
for (typename T::Char* p2 = p - 1; p2 >= p1; --p2)
{
typename T::Char cS = *p2;
if (cS == cDecSeparator)
continue;
if (cS != '9')
{
++*p2;
break;
}
*p2 = '0';
if (p2 == p1) // The number consisted of all 9s replaced to all 0s
{
if (eFormat == rtl_math_StringFormat_F)
{ // move everything to the right before inserting '1'
std::memmove(p2 + 1, p2, (p++ - p2) * sizeof(*p));
}
else
{
nExp++;
}
*p2 = '1';
}
}
*p++ = '0';
}
fValue = 0.0;
}
else
{
*p++ = nDigit + '0';
fValue = (fValue - nDigit) * 10.0;
}
--nCurExp;
nDigit = aParts.significand / eX[nCurExp];
aParts.significand %= eX[nCurExp];
}
else
{
*p++ = '0';
nDigit = aParts.significand;
aParts.significand = 0;
}
assert(nDigit >= 0 && nDigit < 10);
*p++ = nDigit + '0';
if (!--nDigits)
break; // for
+1 -1
View File
@@ -1632,7 +1632,7 @@ void SdOOXMLExportTest2::testTdf118825()
xmlDocUniquePtr pXmlDocContent = parseExport(tempFile, "ppt/slides/slide1.xml");
CPPUNIT_ASSERT_MOTIONPATH("M 0.0449285714285714 0.00368253968253968 C 0.0575714285714285 -0.00095238095238096 0.0704264795523803 -0.00370117418637049 0.0831071428571428 -0.00819047619047622 C 0.0953550597998766 -0.0125265741339082 0.107821870086751 -0.010397536991717 0.120321428571429 -0.0115555555555556 C 0.133179018681433 -0.0127467438724762 0.151318627483861 -0.0158700272533852 0.1585 0.00539682539682542 C 0.16478291361998 0.0240029898688431 0.15828642886492 0.0483806254341085 0.161392857142857 0.0698412698412698 C 0.165179286017685 0.0959996731216037 0.17453898927982 0.119735912694626 0.187142857142857 0.132634920634921 C 0.199788991845377 0.145577185161529 0.215607110490848 0.142889773028431 0.230107142857143 0.142857142857143 C 0.243821417584191 0.142826280916829 0.257716514999779 0.142685979556724 0.271142857142857 0.137777777777778 C 0.286895094567923 0.132019309914514 0.302318190711873 0.122962218306185 0.317928571428571 0.11568253968254 C 0.333496771884547 0.108422531222479 0.348787823719556 0.0990570571890929 0.363714285714286 0.0885079365079364 C 0.374930683062651 0.080580865157908 0.385357142857143 0.0693333333333332 0.396178571428571 0.0596825396825396 L 0.404785714285714 0.0410158730158729 L 0.401892857142857 0.0342222222222221 E", getXPath(pXmlDocContent, "(//p:animMotion)[1]", "path"));
CPPUNIT_ASSERT_MOTIONPATH("M 0.0449285714285714 0.00368253968253968 C 0.0575714285714285 -0.00095238095238096 0.0704264795523803 -0.00370117418637049 0.0831071428571428 -0.00819047619047622 C 0.0953550597998766 -0.0125265741339082 0.107821870086751 -0.010397536991717 0.120321428571429 -0.0115555555555556 C 0.133179018681433 -0.0127467438724762 0.151318627483861 -0.0158700272533852 0.1585 0.00539682539682542 C 0.16478291361998 0.0240029898688431 0.15828642886492 0.0483806254341085 0.161392857142857 0.0698412698412698 C 0.165179286017685 0.0959996731216037 0.17453898927982 0.119735912694626 0.187142857142857 0.132634920634921 C 0.199788991845377 0.145577185161529 0.215607110490848 0.142889773028431 0.230107142857143 0.142857142857143 C 0.243821417584191 0.142826280916829 0.257716514999779 0.142685979556724 0.271142857142857 0.137777777777778 C 0.286895094567923 0.132019309914514 0.302318190711873 0.122962218306185 0.317928571428571 0.11568253968254 C 0.333496771884548 0.108422531222479 0.348787823719556 0.0990570571890929 0.363714285714286 0.0885079365079364 C 0.374930683062651 0.080580865157908 0.385357142857143 0.0693333333333332 0.396178571428571 0.0596825396825396 L 0.404785714285714 0.0410158730158729 L 0.401892857142857 0.0342222222222221 E", getXPath(pXmlDocContent, "(//p:animMotion)[1]", "path"));
CPPUNIT_ASSERT_MOTIONPATH("M 0.025 0.0571428571428571 L 0.0821428571428571 0.184126984126984 L -0.175 0.234920634920635 L -0.246428571428571 -0.0190476190476191 L -0.0821428571428573 -0.133333333333333 E", getXPath(pXmlDocContent, "(//p:animMotion)[2]", "path"));
CPPUNIT_ASSERT_MOTIONPATH("M -0.0107142857142857 0.00634920634920635 C -0.110714285714286 0.501587301587301 -0.153571428571429 -0.00634920634920635 -0.246428571428572 0.184126984126984 C -0.339285714285715 0.374603174603175 -0.296428571428572 0.514285714285714 -0.267857142857143 0.603174603174603 C -0.239285714285715 0.692063492063492 0.0607142857142858 0.590476190476191 0.0607142857142858 0.590476190476191 E", getXPath(pXmlDocContent, "(//p:animMotion)[3]", "path"));
CPPUNIT_ASSERT_MOTIONPATH("M 0.0535714285714286 -0.0444444444444444 L 0.132142857142857 -0.0444444444444444 L 0.132142857142857 -0.146031746031746 L 0.0964285714285715 -0.146031746031746 E", getXPath(pXmlDocContent, "(//p:animMotion)[4]", "path"));