From 59d6850aac86b646861fdcf1b4e193c54d851c20 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 4 Apr 2025 11:17:22 +1000 Subject: [PATCH] [expressions] Add 'reverse' variant for reversing a string Allows using a string argument for the reverse function, which returns a reversed version of the string --- resources/function_help/json/reverse | 33 +++++++--- src/core/expression/qgsexpressionfunction.cpp | 62 +++++++++++-------- tests/src/core/testqgsexpression.cpp | 3 +- 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/resources/function_help/json/reverse b/resources/function_help/json/reverse index eb03e7f8813..d606dc6487a 100644 --- a/resources/function_help/json/reverse +++ b/resources/function_help/json/reverse @@ -1,15 +1,30 @@ { "name": "reverse", "type": "function", - "groups": ["GeometryGroup"], - "description": "Reverses the direction of a line string by reversing the order of its vertices.", - "arguments": [{ - "arg": "geometry", - "description": "a geometry" - }], - "examples": [{ - "expression": "geom_to_wkt(reverse(geom_from_wkt('LINESTRING(0 0, 1 1, 2 2)')))", - "returns": "'LINESTRING(2 2, 1 1, 0 0)'" + "groups": ["String", "GeometryGroup"], + "description": "Reverses the direction of a line string or reverses a string of text.", + "variants": [{ + "variant": "String variant", + "variant_description": "Reverses the order of characters in a string.", + "arguments": [{ + "arg": "string", + "description": "string to reverse" + }], + "examples": [{ + "expression": "reverse('hello')", + "returns": "'olleh'" + }] + }, { + "variant": "Geometry variant", + "variant_description": "Reverses the direction of a line string by reversing the order of its vertices.", + "arguments": [{ + "arg": "geometry", + "description": "a geometry" + }], + "examples": [{ + "expression": "geom_to_wkt(reverse(geom_from_wkt('LINESTRING(0 0, 1 1, 2 2)')))", + "returns": "'LINESTRING(2 2, 1 1, 0 0)'" + }] }], "tags": ["direction", "order", "vertices", "reverses", "line", "reversing"] } diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 38d43dd978c..6db32b7511d 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -5252,38 +5252,48 @@ static QVariant fcnDifference( const QVariantList &values, const QgsExpressionCo static QVariant fcnReverse( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) { - QgsGeometry fGeom = QgsExpressionUtils::getGeometry( values.at( 0 ), parent ); - if ( fGeom.isNull() ) + if ( QgsVariantUtils::isNull( values.at( 0 ) ) ) return QVariant(); - QVariant result; - if ( !fGeom.isMultipart() ) - { - const QgsCurve *curve = qgsgeometry_cast( fGeom.constGet() ); - if ( !curve ) - return QVariant(); + // two variants, one for geometry, one for string - QgsCurve *reversed = curve->reversed(); - result = reversed ? QVariant::fromValue( QgsGeometry( reversed ) ) : QVariant(); - } - else + QgsGeometry fGeom = QgsExpressionUtils::getGeometry( values.at( 0 ), parent, true ); + if ( !fGeom.isNull() ) { - const QgsGeometryCollection *collection = qgsgeometry_cast< const QgsGeometryCollection *>( fGeom.constGet() ); - std::unique_ptr< QgsGeometryCollection > reversed( collection->createEmptyWithSameType() ); - for ( int i = 0; i < collection->numGeometries(); ++i ) + QVariant result; + if ( !fGeom.isMultipart() ) { - if ( const QgsCurve *curve = qgsgeometry_cast( collection->geometryN( i ) ) ) - { - reversed->addGeometry( curve->reversed() ); - } - else - { - reversed->addGeometry( collection->geometryN( i )->clone() ); - } + const QgsCurve *curve = qgsgeometry_cast( fGeom.constGet() ); + if ( !curve ) + return QVariant(); + + QgsCurve *reversed = curve->reversed(); + result = reversed ? QVariant::fromValue( QgsGeometry( reversed ) ) : QVariant(); } - result = reversed ? QVariant::fromValue( QgsGeometry( std::move( reversed ) ) ) : QVariant(); + else + { + const QgsGeometryCollection *collection = qgsgeometry_cast< const QgsGeometryCollection *>( fGeom.constGet() ); + std::unique_ptr< QgsGeometryCollection > reversed( collection->createEmptyWithSameType() ); + for ( int i = 0; i < collection->numGeometries(); ++i ) + { + if ( const QgsCurve *curve = qgsgeometry_cast( collection->geometryN( i ) ) ) + { + reversed->addGeometry( curve->reversed() ); + } + else + { + reversed->addGeometry( collection->geometryN( i )->clone() ); + } + } + result = reversed ? QVariant::fromValue( QgsGeometry( std::move( reversed ) ) ) : QVariant(); + } + return result; } - return result; + + //fall back to string variant + QString string = QgsExpressionUtils::getStringValue( values.at( 0 ), parent ); + std::reverse( string.begin(), string.end() ); + return string; } static QVariant fcnExteriorRing( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) @@ -9056,7 +9066,7 @@ const QList &QgsExpression::Functions() << new QgsStaticExpressionFunction( QStringLiteral( "point_on_surface" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ), fcnPointOnSurface, QStringLiteral( "GeometryGroup" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "pole_of_inaccessibility" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "tolerance" ) ), fcnPoleOfInaccessibility, QStringLiteral( "GeometryGroup" ) ) - << new QgsStaticExpressionFunction( QStringLiteral( "reverse" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ), fcnReverse, QStringLiteral( "GeometryGroup" ) ) + << new QgsStaticExpressionFunction( QStringLiteral( "reverse" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ), fcnReverse, { QStringLiteral( "String" ), QStringLiteral( "GeometryGroup" ) } ) << new QgsStaticExpressionFunction( QStringLiteral( "exterior_ring" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ), fcnExteriorRing, QStringLiteral( "GeometryGroup" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "interior_ring_n" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "index" ) ), diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index b2e5642ca4b..a480005657d 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1242,7 +1242,6 @@ class TestQgsExpression : public QObject QTest::newRow( "end_point multipoint" ) << "geom_to_wkt(end_point(geom_from_wkt('MULTIPOINT((3 3), (1 1), (2 2))')))" << false << QVariant( "Point (2 2)" ); QTest::newRow( "end_point line" ) << "geom_to_wkt(end_point(geom_from_wkt('LINESTRING(4 1, 1 1, 2 2)')))" << false << QVariant( "Point (2 2)" ); QTest::newRow( "end_point polygon" ) << "geom_to_wkt(end_point(geom_from_wkt('POLYGON((-1 -1, 4 0, 4 2, 0 2, -1 -1))')))" << false << QVariant( "Point (-1 -1)" ); - QTest::newRow( "reverse not geom" ) << "reverse('g')" << true << QVariant(); QTest::newRow( "reverse null" ) << "reverse(NULL)" << false << QVariant(); QTest::newRow( "reverse point" ) << "reverse(geom_from_wkt('POINT(1 2)'))" << false << QVariant(); QTest::newRow( "reverse polygon" ) << "reverse(geom_from_wkt('POLYGON((-1 -1, 4 0, 4 2, 0 2, -1 -1))'))" << false << QVariant(); @@ -1696,6 +1695,8 @@ class TestQgsExpression : public QObject QTest::newRow( "regexp_replace non greedy" ) << "regexp_replace('HeLLo','(?<=H).*?L', '-')" << false << QVariant( "H-Lo" ); QTest::newRow( "regexp_replace cap group" ) << "regexp_replace('HeLLo','(eL)', 'x\\\\1x')" << false << QVariant( "HxeLxLo" ); QTest::newRow( "regexp_replace invalid" ) << "regexp_replace('HeLLo','[[[', '-')" << true << QVariant(); + QTest::newRow( "reverse string" ) << "reverse('HeLLo')" << false << QVariant( "oLLeH" ); + QTest::newRow( "reverse empty string" ) << "reverse('')" << false << QVariant( "" ); QTest::newRow( "substr" ) << "substr('HeLLo', 3,2)" << false << QVariant( "LL" ); QTest::newRow( "substr named parameters" ) << "substr(string:='HeLLo',start:=3,length:=2)" << false << QVariant( "LL" ); QTest::newRow( "substr negative start" ) << "substr('HeLLo', -4)" << false << QVariant( "eLLo" );