diff --git a/python/core/qgsaggregatecalculator.sip b/python/core/qgsaggregatecalculator.sip index c2cb557226e..72f2c7e1140 100644 --- a/python/core/qgsaggregatecalculator.sip +++ b/python/core/qgsaggregatecalculator.sip @@ -47,7 +47,8 @@ class QgsAggregateCalculator StringMinimumLength, StringMaximumLength, StringConcatenate, - GeometryCollect + GeometryCollect, + ArrayAggregate }; struct AggregateParameters diff --git a/resources/function_help/json/array_agg b/resources/function_help/json/array_agg new file mode 100644 index 00000000000..3d34e0a5035 --- /dev/null +++ b/resources/function_help/json/array_agg @@ -0,0 +1,13 @@ +{ + "name": "array_agg", + "type": "function", + "description": "Returns an array of aggregated values from a field or expression.", + "arguments": [ + {"arg": "expression", "description": "sub expression of field to aggregate"}, + {"arg": "group_by", "optional": true, "description": "optional expression to use to group aggregate calculations"}, + {"arg": "filter", "optional": true, "description": "optional expression to use to filter features used to calculate aggregate"} + ], + "examples": [ + { "expression": "array_agg(\"name\",group_by:=\"state\")", "returns":"list of name values, grouped by state field"} + ] +} diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 5693e8f4ed8..9ec8306a328 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -830,6 +830,11 @@ static QVariant fcnAggregateStringConcat( const QVariantList &values, const QgsE return fcnAggregateGeneric( QgsAggregateCalculator::StringConcatenate, values, parameters, context, parent ); } +static QVariant fcnAggregateArray( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent ) +{ + return fcnAggregateGeneric( QgsAggregateCalculator::ArrayAggregate, values, QgsAggregateCalculator::AggregateParameters(), context, parent ); +} + static QVariant fcnClamp( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent ) { double minValue = QgsExpressionUtils::getDoubleValue( values.at( 0 ), parent ); @@ -3918,6 +3923,7 @@ const QList &QgsExpression::Functions() << new QgsStaticExpressionFunction( QStringLiteral( "max_length" ), aggParams, fcnAggregateMaxLength, QStringLiteral( "Aggregates" ), QString(), false, QSet(), true ) << new QgsStaticExpressionFunction( QStringLiteral( "collect" ), aggParams, fcnAggregateCollectGeometry, QStringLiteral( "Aggregates" ), QString(), false, QSet(), true ) << new QgsStaticExpressionFunction( QStringLiteral( "concatenate" ), aggParams << QgsExpressionFunction::Parameter( QStringLiteral( "concatenator" ), true ), fcnAggregateStringConcat, QStringLiteral( "Aggregates" ), QString(), false, QSet(), true ) + << new QgsStaticExpressionFunction( QStringLiteral( "array_agg" ), aggParams, fcnAggregateArray, QStringLiteral( "Aggregates" ), QString(), false, QSet(), true ) << new QgsStaticExpressionFunction( QStringLiteral( "regexp_match" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "string" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "regex" ) ), fcnRegexpMatch, QStringList() << QStringLiteral( "Conditionals" ) << QStringLiteral( "String" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "regexp_matches" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "string" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "regex" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "emptyvalue" ), true, "" ), fcnRegexpMatches, QStringLiteral( "Arrays" ) ) diff --git a/src/core/qgsaggregatecalculator.cpp b/src/core/qgsaggregatecalculator.cpp index 98ebb830f8c..3aab5a37346 100644 --- a/src/core/qgsaggregatecalculator.cpp +++ b/src/core/qgsaggregatecalculator.cpp @@ -165,6 +165,8 @@ QgsAggregateCalculator::Aggregate QgsAggregateCalculator::stringToAggregate( con return StringConcatenate; else if ( normalized == QLatin1String( "collect" ) ) return GeometryCollect; + else if ( normalized == QLatin1String( "array_agg" ) ) + return ArrayAggregate; if ( ok ) *ok = false; @@ -178,6 +180,13 @@ QVariant QgsAggregateCalculator::calculate( QgsAggregateCalculator::Aggregate ag if ( ok ) *ok = false; + if ( aggregate == QgsAggregateCalculator::ArrayAggregate ) + { + if ( ok ) + *ok = true; + return calculateArrayAggregate( fit, attr, expression, context ); + } + switch ( resultType ) { case QVariant::Int: @@ -293,6 +302,7 @@ QgsStatisticalSummary::Statistic QgsAggregateCalculator::numericStatFromAggregat case StringMaximumLength: case StringConcatenate: case GeometryCollect: + case ArrayAggregate: { if ( ok ) *ok = false; @@ -340,6 +350,7 @@ QgsStringStatisticalSummary::Statistic QgsAggregateCalculator::stringStatFromAgg case InterQuartileRange: case StringConcatenate: case GeometryCollect: + case ArrayAggregate: { if ( ok ) *ok = false; @@ -386,6 +397,7 @@ QgsDateTimeStatisticalSummary::Statistic QgsAggregateCalculator::dateTimeStatFro case StringMaximumLength: case StringConcatenate: case GeometryCollect: + case ArrayAggregate: { if ( ok ) *ok = false; @@ -512,6 +524,9 @@ QVariant QgsAggregateCalculator::defaultValue( QgsAggregateCalculator::Aggregate case StringConcatenate: return ""; // zero length string - not null! + case ArrayAggregate: + return QVariantList(); // empty list + // undefined - nothing makes sense here case Sum: case Min: @@ -559,3 +574,29 @@ QVariant QgsAggregateCalculator::calculateDateTimeAggregate( QgsFeatureIterator s.finalize(); return s.statistic( stat ); } + +QVariant QgsAggregateCalculator::calculateArrayAggregate( QgsFeatureIterator &fit, int attr, QgsExpression *expression, + QgsExpressionContext *context ) +{ + Q_ASSERT( expression || attr >= 0 ); + + QgsFeature f; + + QVariantList array; + + while ( fit.nextFeature( f ) ) + { + if ( expression ) + { + Q_ASSERT( context ); + context->setFeature( f ); + QVariant v = expression->evaluate( context ); + array.append( v ); + } + else + { + array.append( f.attribute( attr ) ); + } + } + return array; +} diff --git a/src/core/qgsaggregatecalculator.h b/src/core/qgsaggregatecalculator.h index 528d7d87850..52a96ac238f 100644 --- a/src/core/qgsaggregatecalculator.h +++ b/src/core/qgsaggregatecalculator.h @@ -65,7 +65,8 @@ class CORE_EXPORT QgsAggregateCalculator StringMinimumLength, //!< Minimum length of string (string fields only) StringMaximumLength, //!< Maximum length of string (string fields only) StringConcatenate, //! Concatenate values with a joining string (string fields only). Specify the delimiter using setDelimiter(). - GeometryCollect //! Create a multipart geometry from aggregated geometries + GeometryCollect, //! Create a multipart geometry from aggregated geometries + ArrayAggregate //! Create an array of values }; //! A bundle of parameters controlling aggregate calculation @@ -165,6 +166,9 @@ class CORE_EXPORT QgsAggregateCalculator QgsExpressionContext *context, QgsDateTimeStatisticalSummary::Statistic stat ); static QVariant calculateGeometryAggregate( QgsFeatureIterator &fit, QgsExpression *expression, QgsExpressionContext *context ); + static QVariant calculateArrayAggregate( QgsFeatureIterator &fit, int attr, QgsExpression *expression, + QgsExpressionContext *context ); + static QVariant calculate( Aggregate aggregate, QgsFeatureIterator &fit, QVariant::Type resultType, int attr, QgsExpression *expression, const QString &delimiter, diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 206ca37e8c4..87c33809718 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1375,6 +1375,10 @@ class TestQgsExpression: public QObject QTest::newRow( "geometry collect" ) << "geom_to_wkt(aggregate('aggregate_layer','collect',$geometry))" << false << QVariant( QStringLiteral( "MultiPoint ((0 0),(1 0),(2 0),(3 0),(5 0))" ) ); + QVariantList array; + array << "test" << QVariant( QVariant::String ) << "test333" << "test4" << QVariant( QVariant::String ) << "test4"; + QTest::newRow( "array aggregate" ) << "aggregate('aggregate_layer','array_agg',\"col2\")" << false << QVariant( array ); + QTest::newRow( "sub expression" ) << "aggregate('test','sum',\"col1\" * 2)" << false << QVariant( 65 * 2 ); QTest::newRow( "bad sub expression" ) << "aggregate('test','sum',\"xcvxcv\" * 2)" << true << QVariant(); diff --git a/tests/src/python/test_qgsaggregatecalculator.py b/tests/src/python/test_qgsaggregatecalculator.py index f1b71c44d4f..db0dc0e9931 100644 --- a/tests/src/python/test_qgsaggregatecalculator.py +++ b/tests/src/python/test_qgsaggregatecalculator.py @@ -130,13 +130,15 @@ class TestQgsAggregateCalculator(unittest.TestCase): [QgsAggregateCalculator.ThirdQuartile, 'flddbl', 7.5], [QgsAggregateCalculator.InterQuartileRange, 'fldint', 3.0], [QgsAggregateCalculator.InterQuartileRange, 'flddbl', 2.5], + [QgsAggregateCalculator.ArrayAggregate, 'fldint', int_values], + [QgsAggregateCalculator.ArrayAggregate, 'flddbl', dbl_values], ] agg = QgsAggregateCalculator(layer) for t in tests: val, ok = agg.calculate(t[0], t[1]) self.assertTrue(ok) - if isinstance(t[2], int): + if isinstance(t[2], (int, list)): self.assertEqual(val, t[2]) else: self.assertAlmostEqual(val, t[2], 3) @@ -171,6 +173,7 @@ class TestQgsAggregateCalculator(unittest.TestCase): [QgsAggregateCalculator.Max, 'fldstring', 'eeee'], [QgsAggregateCalculator.StringMinimumLength, 'fldstring', 0], [QgsAggregateCalculator.StringMaximumLength, 'fldstring', 8], + [QgsAggregateCalculator.ArrayAggregate, 'fldstring', values], ] agg = QgsAggregateCalculator(layer) @@ -251,6 +254,8 @@ class TestQgsAggregateCalculator(unittest.TestCase): [QgsAggregateCalculator.Range, 'flddatetime', QgsInterval(693871147)], [QgsAggregateCalculator.Range, 'flddate', QgsInterval(693792000)], + [QgsAggregateCalculator.ArrayAggregate, 'flddatetime', [None if v.isNull() else v for v in datetime_values]], + [QgsAggregateCalculator.ArrayAggregate, 'flddate', [None if v.isNull() else v for v in date_values]], ] agg = QgsAggregateCalculator(layer) @@ -425,6 +430,12 @@ class TestQgsAggregateCalculator(unittest.TestCase): self.assertTrue(ok) self.assertEqual(val, None) + # array_agg + agg = QgsAggregateCalculator(layer) + val, ok = agg.calculate(QgsAggregateCalculator.ArrayAggregate, 'fldint * 2') + self.assertTrue(ok) + self.assertEqual(val, []) + def testStringToAggregate(self): """ test converting strings to aggregate types """