diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index cab8d3b6e58..4c6a31d4a32 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -182,6 +182,16 @@ Returns a string representation of a double :param precision: number of decimal places to retain %End +bool qgsNanCompatibleEquals( double a, double b ); +%Docstring +Compare two doubles, treating nan values as equal + +:param a: first double +:param b: second double + +.. versionadded:: 3.20 +%End + bool qgsDoubleNear( double a, double b, double epsilon = 4 * DBL_EPSILON ); %Docstring Compare two doubles (but allow some difference) diff --git a/src/core/proj/qgscoordinatereferencesystem.cpp b/src/core/proj/qgscoordinatereferencesystem.cpp index 5c4c71eb513..f70775167bb 100644 --- a/src/core/proj/qgscoordinatereferencesystem.cpp +++ b/src/core/proj/qgscoordinatereferencesystem.cpp @@ -1678,9 +1678,7 @@ bool QgsCoordinateReferenceSystem::operator==( const QgsCoordinateReferenceSyste if ( !d->mIsValid || !srs.d->mIsValid ) return false; - if ( std::isnan( d->mCoordinateEpoch ) != std::isnan( srs.d->mCoordinateEpoch ) ) - return false; - else if ( !std::isnan( d->mCoordinateEpoch ) && d->mCoordinateEpoch != srs.d->mCoordinateEpoch ) + if ( !qgsNanCompatibleEquals( d->mCoordinateEpoch, srs.d->mCoordinateEpoch ) ) return false; const bool isUser = d->mSrsId >= USER_CRS_START_ID; diff --git a/src/core/proj/qgscoordinatetransform.cpp b/src/core/proj/qgscoordinatetransform.cpp index 6f46580dca3..bbe3d867dd4 100644 --- a/src/core/proj/qgscoordinatetransform.cpp +++ b/src/core/proj/qgscoordinatetransform.cpp @@ -901,13 +901,8 @@ bool QgsCoordinateTransform::setFromCache( const QgsCoordinateReferenceSystem &s { if ( ( *valIt ).coordinateOperation() == coordinateOperationProj && ( *valIt ).allowFallbackTransforms() == allowFallback - - // careful here, nan != nan, so we need to explicitly handle the case when both crses have nan coordinateEpoch - && ( std::isnan( ( *valIt ).sourceCrs().coordinateEpoch() ) == std::isnan( src.coordinateEpoch() ) - && ( std::isnan( src.coordinateEpoch() ) || src.coordinateEpoch() == ( *valIt ).sourceCrs().coordinateEpoch() ) ) - - && ( std::isnan( ( *valIt ).destinationCrs().coordinateEpoch() ) == std::isnan( dest.coordinateEpoch() ) - && ( std::isnan( dest.coordinateEpoch() ) || dest.coordinateEpoch() == ( *valIt ).destinationCrs().coordinateEpoch() ) ) + && qgsNanCompatibleEquals( src.coordinateEpoch(), ( *valIt ).sourceCrs().coordinateEpoch() ) + && qgsNanCompatibleEquals( dest.coordinateEpoch(), ( *valIt ).destinationCrs().coordinateEpoch() ) ) { // need to save, and then restore the context... we don't want this to be cached or to use the values from the cache diff --git a/src/core/proj/qgscoordinatetransform_p.cpp b/src/core/proj/qgscoordinatetransform_p.cpp index e4f7fa4151f..ce0c28d86b4 100644 --- a/src/core/proj/qgscoordinatetransform_p.cpp +++ b/src/core/proj/qgscoordinatetransform_p.cpp @@ -173,8 +173,7 @@ bool QgsCoordinateTransformPrivate::initialize() : ( mDestIsDynamic && !std::isnan( mDestCoordinateEpoch ) && !mSourceIsDynamic ) ? mDestCoordinateEpoch : std::numeric_limits< double >::quiet_NaN(); - if ( mSourceIsDynamic && mDestIsDynamic - && !std::isnan( mSourceCoordinateEpoch ) && mSourceCoordinateEpoch != mDestCoordinateEpoch ) + if ( mSourceIsDynamic && mDestIsDynamic && !qgsNanCompatibleEquals( mSourceCoordinateEpoch, mDestCoordinateEpoch ) ) { // transforms from dynamic crs to dynamic crs with different coordinate epochs are not yet supported by PROJ if ( sDynamicCrsToDynamicCrsWarningHandler ) diff --git a/src/core/qgis.h b/src/core/qgis.h index fff77cef103..c9216e8faa2 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -341,6 +341,22 @@ inline QString qgsDoubleToString( double a, int precision = 17 ) return str; } +/** + * Compare two doubles, treating nan values as equal + * \param a first double + * \param b second double + * \since QGIS 3.20 + */ +inline bool qgsNanCompatibleEquals( double a, double b ) +{ + const bool aIsNan = std::isnan( a ); + const bool bIsNan = std::isnan( b ); + if ( aIsNan || bIsNan ) + return aIsNan && bIsNan; + + return a == b; +} + /** * Compare two doubles (but allow some difference) * \param a first double @@ -349,8 +365,10 @@ inline QString qgsDoubleToString( double a, int precision = 17 ) */ inline bool qgsDoubleNear( double a, double b, double epsilon = 4 * std::numeric_limits::epsilon() ) { - if ( std::isnan( a ) || std::isnan( b ) ) - return std::isnan( a ) && std::isnan( b ) ; + const bool aIsNan = std::isnan( a ); + const bool bIsNan = std::isnan( b ); + if ( aIsNan || bIsNan ) + return aIsNan && bIsNan; const double diff = a - b; return diff > -epsilon && diff <= epsilon; @@ -364,8 +382,10 @@ inline bool qgsDoubleNear( double a, double b, double epsilon = 4 * std::numeric */ inline bool qgsFloatNear( float a, float b, float epsilon = 4 * FLT_EPSILON ) { - if ( std::isnan( a ) || std::isnan( b ) ) - return std::isnan( a ) && std::isnan( b ) ; + const bool aIsNan = std::isnan( a ); + const bool bIsNan = std::isnan( b ); + if ( aIsNan || bIsNan ) + return aIsNan && bIsNan; const float diff = a - b; return diff > -epsilon && diff <= epsilon; @@ -374,8 +394,10 @@ inline bool qgsFloatNear( float a, float b, float epsilon = 4 * FLT_EPSILON ) //! Compare two doubles using specified number of significant digits inline bool qgsDoubleNearSig( double a, double b, int significantDigits = 10 ) { - if ( std::isnan( a ) || std::isnan( b ) ) - return std::isnan( a ) && std::isnan( b ) ; + const bool aIsNan = std::isnan( a ); + const bool bIsNan = std::isnan( b ); + if ( aIsNan || bIsNan ) + return aIsNan && bIsNan; // The most simple would be to print numbers as %.xe and compare as strings // but that is probably too costly diff --git a/tests/src/core/testqgis.cpp b/tests/src/core/testqgis.cpp index e2ebd5a3747..23a874a8887 100644 --- a/tests/src/core/testqgis.cpp +++ b/tests/src/core/testqgis.cpp @@ -47,6 +47,8 @@ class TestQgis : public QObject void signalBlocker(); void qVariantCompare_data(); void qVariantCompare(); + void testNanCompatibleEquals_data(); + void testNanCompatibleEquals(); void testQgsAsConst(); void testQgsRound(); void testQgsVariantEqual(); @@ -320,6 +322,29 @@ void TestQgis::qVariantCompare() QCOMPARE( qgsVariantGreaterThan( lhs, rhs ), greaterThan ); } +void TestQgis::testNanCompatibleEquals_data() +{ + QTest::addColumn( "lhs" ); + QTest::addColumn( "rhs" ); + QTest::addColumn( "expected" ); + + QTest::newRow( "both nan" ) << std::numeric_limits< double >::quiet_NaN() << std::numeric_limits< double >::quiet_NaN() << true; + QTest::newRow( "first is nan" ) << std::numeric_limits< double >::quiet_NaN() << 5.0 << false; + QTest::newRow( "second is nan" ) << 5.0 << std::numeric_limits< double >::quiet_NaN() << false; + QTest::newRow( "two numbers, not equal" ) << 5.0 << 6.0 << false; + QTest::newRow( "two numbers, equal" ) << 5.0 << 5.0 << true; +} + +void TestQgis::testNanCompatibleEquals() +{ + QFETCH( double, lhs ); + QFETCH( double, rhs ); + QFETCH( bool, expected ); + + QCOMPARE( qgsNanCompatibleEquals( lhs, rhs ), expected ); + QCOMPARE( qgsNanCompatibleEquals( rhs, lhs ), expected ); +} + class ConstTester { public: