Skip to content

Commit ae00eb9

Browse files
committedApr 4, 2016
[FEATURE] Allow expression functions to use named parameters
This commit sets the framework for allowing expression functions to use named parameters. Ie, instead of: clamp(1,2,3) you can use: clamp( min:=1, value:=2, max:=3) This also allows arguments to be switched, eg: clamp( value:=2, max:=3, min:=1) Additionally, it allows for a more structured definition of function parameters to handle optional arguments and default values for parameters. These are currently being done using a hacky infinite argument list. I've utilised the postgres ':=' syntax for specifying named arguments to avoid potential collisions which may arise with the equality test if we re-used just the '=' operator alone. Sponsored by North Road
1 parent 5e54b93 commit ae00eb9

File tree

10 files changed

+551
-75
lines changed

10 files changed

+551
-75
lines changed
 

‎python/core/qgsexpression.sip

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -303,12 +303,47 @@ class QgsExpression
303303
//! @note not available in Python bindings
304304
// static const char* UnaryOperatorText[];
305305

306+
/**
307+
* Represents a single parameter passed to a function.
308+
* \note added in QGIS 2.16
309+
*/
310+
class Parameter
311+
{
312+
public:
313+
314+
/** Constructor for Parameter.
315+
* @param name parameter name, used when named parameter are specified in an expression
316+
* @param optional set to true if parameter should be optional
317+
* @param defaultValue default value to use for optional parameters
318+
*/
319+
Parameter( const QString& name,
320+
bool optional = false,
321+
const QVariant& defaultValue = QVariant() );
322+
323+
//! Returns the name of the parameter.
324+
QString name() const;
325+
326+
//! Returns true if the parameter is optional.
327+
bool optional() const;
328+
329+
//! Returns the default value for the parameter.
330+
QVariant defaultValue() const;
331+
332+
bool operator==( const QgsExpression::Parameter& other ) const;
333+
334+
};
335+
336+
//! List of parameters, used for function definition
337+
typedef QList< QgsExpression::Parameter > ParameterList;
338+
306339
/**
307340
* A abstract base class for defining QgsExpression functions.
308341
*/
309342
class Function
310343
{
311344
public:
345+
346+
//! Constructor for function which uses unnamed parameters
312347
Function( const QString& fnname,
313348
int params,
314349
const QString& group,
@@ -319,14 +354,36 @@ class QgsExpression
319354
bool handlesNull = false,
320355
bool isContextual = false );
321356

357+
/** Constructor for function which uses named parameter list.
358+
* @note added in QGIS 2.16
359+
*/
360+
Function( const QString& fnname,
361+
const QgsExpression::ParameterList& params,
362+
const QString& group,
363+
const QString& helpText = QString(),
364+
bool usesGeometry = false,
365+
const QStringList& referencedColumns = QStringList(),
366+
bool lazyEval = false,
367+
bool handlesNull = false,
368+
bool isContextual = false );
369+
322370
virtual ~Function();
323371

324372
/** The name of the function. */
325-
QString name();
373+
QString name() const;
326374
/** The number of parameters this function takes. */
327-
int params();
375+
int params() const;
376+
377+
/** The mininum number of parameters this function takes. */
378+
int minParams() const;
379+
380+
/** Returns the list of named parameters for the function, if set.
381+
* @note added in QGIS 2.16
382+
*/
383+
const QgsExpression::ParameterList& parameters() const;
384+
328385
/** Does this function use a geometry object. */
329-
bool usesgeometry();
386+
bool usesgeometry() const;
330387

331388
/** Returns a list of possible aliases for the function. These include
332389
* other permissible names for the function, eg deprecated names.
@@ -339,7 +396,7 @@ class QgsExpression
339396
* rather than the node results when called. You can use node->eval(parent, feature) to evaluate the node and return the result
340397
* Functions are non lazy default and will be given the node return value when called
341398
*/
342-
bool lazyEval();
399+
bool lazyEval() const;
343400

344401
virtual QStringList referencedColumns() const;
345402

@@ -349,9 +406,9 @@ class QgsExpression
349406
bool isContextual() const;
350407

351408
/** The group the function belongs to. */
352-
QString group();
409+
QString group() const;
353410
/** The help text for the function. */
354-
const QString helptext();
411+
const QString helptext() const;
355412

356413
//! @deprecated Use QgsExpressionContext variant instead
357414
virtual QVariant func( const QVariantList& values, const QgsFeature* f, QgsExpression* parent ) /Deprecated/;
@@ -562,15 +619,52 @@ class QgsExpression
562619
virtual void accept( QgsExpression::Visitor& v ) const = 0;
563620
};
564621

622+
//! Named node
623+
//! @note added in QGIS 2.16
624+
class NamedNode
625+
{
626+
public:
627+
628+
/** Constructor for NamedNode
629+
* @param name node name
630+
* @param node node
631+
*/
632+
NamedNode( const QString& name, QgsExpression::Node* node );
633+
634+
//! Node name
635+
QString name;
636+
637+
//! Node
638+
QgsExpression::Node* node;
639+
};
640+
565641
class NodeList
566642
{
567643
public:
568644
NodeList();
569645
~NodeList();
570646
/** Takes ownership of the provided node */
571647
void append( QgsExpression::Node* node /Transfer/ );
572-
int count();
648+
649+
/** Adds a named node. Takes ownership of the provided node.
650+
* @note added in QGIS 2.16
651+
*/
652+
void append( QgsExpression::NamedNode* node /Transfer/ );
653+
654+
/** Returns the number of nodes in the list.
655+
*/
656+
int count() const;
657+
658+
//! Returns true if list contains any named nodes
659+
//! @note added in QGIS 2.16
660+
bool hasNamedNodes() const;
661+
573662
const QList<QgsExpression::Node*>& list();
663+
664+
//! Returns a list of names for nodes. Unnamed nodes will be indicated by an empty string in the list.
665+
//! @note added in QGIS 2.16
666+
QStringList names() const;
667+
574668
/** Creates a deep copy of this list. Ownership is transferred to the caller */
575669
QgsExpression::NodeList* clone() const;
576670

@@ -698,6 +792,9 @@ class QgsExpression
698792
virtual bool needsGeometry() const;
699793
virtual void accept( QgsExpression::Visitor& v ) const;
700794
virtual QgsExpression::Node* clone() const;
795+
796+
//! Tests whether the provided argument list is valid for the matching function
797+
static bool validateParams( int fnIndex, QgsExpression::NodeList* args, QString& error );
701798
};
702799

703800
class NodeLiteral : QgsExpression::Node

‎resources/function_help/json/azimuth

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"name": "azimuth",
33
"type": "function",
4-
"description": "Returns the north-based azimuth as the angle in radians measured clockwise from the vertical on pointA to pointB.",
4+
"description": "Returns the north-based azimuth as the angle in radians measured clockwise from the vertical on point_a to point_b.",
55
"arguments": [
6-
{"arg":"pointA","description":"point geometry"},
7-
{"arg":"pointB","description":"point geometry"}
6+
{"arg":"point_a","description":"point geometry"},
7+
{"arg":"point_b","description":"point geometry"}
88
],
99
"examples": [
1010
{ "expression":"degrees( azimuth( make_point(25, 45), make_point(75, 100) ) )", "returns":"42.273689"},

‎resources/function_help/json/randf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "randf",
33
"type": "function",
44
"description": "Returns a random float within the range specified by the minimum and maximum argument (inclusive).",
5-
"arguments": [ {"arg":"min","description":"an float representing the smallest possible random number desired"},
6-
{"arg":"max","description":"an float representing the largest possible random number desired"}],
5+
"arguments": [ {"arg":"min","optional":true,"default":"0.0","description":"an float representing the smallest possible random number desired"},
6+
{"arg":"max","optional":true,"default":"1.0","description":"an float representing the largest possible random number desired"}],
77
"examples": [ { "expression":"randf(1, 10)", "returns":"4.59258286403147"}]
88
}

‎resources/function_help/json/round

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"type": "function",
44
"description": "Rounds a number to number of decimal places.",
55
"arguments": [
6-
{"arg":"decimal","description":"decimal number to be rounded"},
7-
{"arg":"places","description":"Optional integer representing number of places to round decimals to. Can be negative."}
6+
{"arg":"value","description":"decimal number to be rounded"},
7+
{"arg":"places","optional":true,"default":"0","description":"Optional integer representing number of places to round decimals to. Can be negative."}
88
],
99
"examples": [
1010
{ "expression":"round(1234.567, 2)", "returns":"1234.57"},

‎scripts/process_function_template.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ def quote(v):
9090
a['arg'],
9191
a.get('description', ''),
9292
"true" if a.get('descOnly', False) else "false",
93-
"true" if a.get('syntaxOnly', False) else "false")
93+
"true" if a.get('syntaxOnly', False) else "false",
94+
"true" if a.get('optional', False) else "false",
95+
a.get('default', ''))
9496
)
9597

9698
cpp.write(",\n /* variableLenArguments */ {0}".format(

‎src/core/qgsexpression.cpp

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,28 +2860,28 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
28602860
if ( gmFunctions.isEmpty() )
28612861
{
28622862
gmFunctions
2863-
<< new StaticFunction( "sqrt", 1, fcnSqrt, "Math" )
2864-
<< new StaticFunction( "radians", 1, fcnRadians, "Math" )
2865-
<< new StaticFunction( "degrees", 1, fcnDegrees, "Math" )
2866-
<< new StaticFunction( "azimuth", 2, fcnAzimuth, "Math" )
2867-
<< new StaticFunction( "abs", 1, fcnAbs, "Math" )
2868-
<< new StaticFunction( "cos", 1, fcnCos, "Math" )
2869-
<< new StaticFunction( "sin", 1, fcnSin, "Math" )
2870-
<< new StaticFunction( "tan", 1, fcnTan, "Math" )
2871-
<< new StaticFunction( "asin", 1, fcnAsin, "Math" )
2872-
<< new StaticFunction( "acos", 1, fcnAcos, "Math" )
2873-
<< new StaticFunction( "atan", 1, fcnAtan, "Math" )
2874-
<< new StaticFunction( "atan2", 2, fcnAtan2, "Math" )
2875-
<< new StaticFunction( "exp", 1, fcnExp, "Math" )
2876-
<< new StaticFunction( "ln", 1, fcnLn, "Math" )
2877-
<< new StaticFunction( "log10", 1, fcnLog10, "Math" )
2878-
<< new StaticFunction( "log", 2, fcnLog, "Math" )
2879-
<< new StaticFunction( "round", -1, fcnRound, "Math" )
2880-
<< new StaticFunction( "rand", 2, fcnRnd, "Math" )
2881-
<< new StaticFunction( "randf", 2, fcnRndF, "Math" )
2863+
<< new StaticFunction( "sqrt", ParameterList() << Parameter( "value" ), fcnSqrt, "Math" )
2864+
<< new StaticFunction( "radians", ParameterList() << Parameter( "degrees" ), fcnRadians, "Math" )
2865+
<< new StaticFunction( "degrees", ParameterList() << Parameter( "radians" ), fcnDegrees, "Math" )
2866+
<< new StaticFunction( "azimuth", ParameterList() << Parameter( "point_a" ) << Parameter( "point_b" ), fcnAzimuth, "Math" )
2867+
<< new StaticFunction( "abs", ParameterList() << Parameter( "value" ), fcnAbs, "Math" )
2868+
<< new StaticFunction( "cos", ParameterList() << Parameter( "angle" ), fcnCos, "Math" )
2869+
<< new StaticFunction( "sin", ParameterList() << Parameter( "angle" ), fcnSin, "Math" )
2870+
<< new StaticFunction( "tan", ParameterList() << Parameter( "angle" ), fcnTan, "Math" )
2871+
<< new StaticFunction( "asin", ParameterList() << Parameter( "value" ), fcnAsin, "Math" )
2872+
<< new StaticFunction( "acos", ParameterList() << Parameter( "value" ), fcnAcos, "Math" )
2873+
<< new StaticFunction( "atan", ParameterList() << Parameter( "value" ), fcnAtan, "Math" )
2874+
<< new StaticFunction( "atan2", ParameterList() << Parameter( "dx" ) << Parameter( "dy" ), fcnAtan2, "Math" )
2875+
<< new StaticFunction( "exp", ParameterList() << Parameter( "value" ), fcnExp, "Math" )
2876+
<< new StaticFunction( "ln", ParameterList() << Parameter( "value" ), fcnLn, "Math" )
2877+
<< new StaticFunction( "log10", ParameterList() << Parameter( "value" ), fcnLog10, "Math" )
2878+
<< new StaticFunction( "log", ParameterList() << Parameter( "base" ) << Parameter( "value" ), fcnLog, "Math" )
2879+
<< new StaticFunction( "round", ParameterList() << Parameter( "value" ) << Parameter( "places", true, 0 ), fcnRound, "Math" )
2880+
<< new StaticFunction( "rand", ParameterList() << Parameter( "min" ) << Parameter( "max" ), fcnRnd, "Math" )
2881+
<< new StaticFunction( "randf", ParameterList() << Parameter( "min", true, 0.0 ) << Parameter( "max", true, 1.0 ), fcnRndF, "Math" )
28822882
<< new StaticFunction( "max", -1, fcnMax, "Math" )
28832883
<< new StaticFunction( "min", -1, fcnMin, "Math" )
2884-
<< new StaticFunction( "clamp", 3, fcnClamp, "Math" )
2884+
<< new StaticFunction( "clamp", ParameterList() << Parameter( "min" ) << Parameter( "value" ) << Parameter( "max" ), fcnClamp, "Math" )
28852885
<< new StaticFunction( "scale_linear", 5, fcnLinearScale, "Math" )
28862886
<< new StaticFunction( "scale_exp", 6, fcnExpScale, "Math" )
28872887
<< new StaticFunction( "floor", 1, fcnFloor, "Math" )
@@ -2915,7 +2915,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
29152915
<< new StaticFunction( "longest_common_substring", 2, fcnLCS, "Fuzzy Matching" )
29162916
<< new StaticFunction( "hamming_distance", 2, fcnHamming, "Fuzzy Matching" )
29172917
<< new StaticFunction( "soundex", 1, fcnSoundex, "Fuzzy Matching" )
2918-
<< new StaticFunction( "wordwrap", -1, fcnWordwrap, "String" )
2918+
<< new StaticFunction( "wordwrap", ParameterList() << Parameter( "text" ) << Parameter( "length" ) << Parameter( "delimiter", true, " " ), fcnWordwrap, "String" )
29192919
<< new StaticFunction( "length", 1, fcnLength, "String" )
29202920
<< new StaticFunction( "replace", 3, fcnReplace, "String" )
29212921
<< new StaticFunction( "regexp_replace", 3, fcnRegexpReplace, "String" )
@@ -3567,6 +3567,7 @@ QgsExpression::NodeList* QgsExpression::NodeList::clone() const
35673567
{
35683568
nl->mList.append( node->clone() );
35693569
}
3570+
nl->mNameList = mNameList;
35703571

35713572
return nl;
35723573
}
@@ -4450,7 +4451,10 @@ QString QgsExpression::helptext( QString name )
44504451
helpContents += delim;
44514452
delim = ", ";
44524453
if ( !a.mDescOnly )
4453-
helpContents += QString( "<span class=\"argument\">%1</span>" ).arg( a.mArg );
4454+
{
4455+
helpContents += QString( "<span class=\"argument %1\">%2%3</span>" ).arg( a.mOptional ? "optional" : "", a.mArg,
4456+
a.mDefaultVal.isEmpty() ? "" : '=' + a.mDefaultVal );
4457+
}
44544458
}
44554459

44564460
if ( v.mVariableLenArguments )

‎src/core/qgsexpression.h

Lines changed: 290 additions & 17 deletions
Large diffs are not rendered by default.

‎src/core/qgsexpressionlexer.ll

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ static QString stripText(QString text)
7979
return text;
8080
}
8181

82+
static QString stripNamedText(QString text)
83+
{
84+
text.remove(":=");
85+
return text.trimmed();
86+
}
87+
8288
static QString stripColumnRef(QString text)
8389
{
8490
// strip double quotes on start,end
@@ -110,6 +116,8 @@ deprecated_function "$"[xXyY]_?[aA][tT]
110116
special_col "$"{column_ref}
111117
variable "@"{column_ref}
112118

119+
named_node {column_ref}{white}*":="{white}*
120+
113121
col_str_char "\"\""|[^\"]
114122
column_ref_quoted "\""{col_str_char}*"\""
115123
@@ -196,6 +204,8 @@ string "'"{str_char}*"'"
196204
197205
{deprecated_function} { TEXT; return FUNCTION; }
198206
207+
{named_node} { TEXT_FILTER(stripNamedText); return NAMED_NODE; }
208+
199209
{special_col} { TEXT; return SPECIAL_COL; }
200210
201211
{variable} { TEXT; return VARIABLE; }

‎src/core/qgsexpressionparser.yy

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ struct expression_parser_context
7777
{
7878
QgsExpression::Node* node;
7979
QgsExpression::NodeList* nodelist;
80+
QgsExpression::NamedNode* namednode;
8081
double numberFloat;
8182
int numberInt;
8283
bool boolVal;
@@ -108,7 +109,7 @@ struct expression_parser_context
108109
// tokens for conditional expressions
109110
%token CASE WHEN THEN ELSE END
110111

111-
%token <text> STRING COLUMN_REF FUNCTION SPECIAL_COL VARIABLE
112+
%token <text> STRING COLUMN_REF FUNCTION SPECIAL_COL VARIABLE NAMED_NODE
112113

113114
%token COMMA
114115

@@ -122,6 +123,7 @@ struct expression_parser_context
122123
%type <nodelist> exp_list
123124
%type <whenthen> when_then_clause
124125
%type <whenthenlist> when_then_clauses
126+
%type <namednode> named_node
125127

126128
// debugging
127129
%error-verbose
@@ -148,6 +150,7 @@ struct expression_parser_context
148150

149151
%destructor { delete $$; } <node>
150152
%destructor { delete $$; } <nodelist>
153+
%destructor { delete $$; } <namednode>
151154
%destructor { delete $$; } <text>
152155
%destructor { delete $$; } <whenthen>
153156
%destructor { delete $$; } <whenthenlist>
@@ -191,10 +194,18 @@ expression:
191194
delete $3;
192195
YYERROR;
193196
}
197+
QString paramError;
198+
if ( !QgsExpression::NodeFunction::validateParams( fnIndex, $3, paramError ) )
199+
{
200+
exp_error( parser_ctx, paramError.toLocal8Bit().constData() );
201+
delete $3;
202+
YYERROR;
203+
}
194204
if ( QgsExpression::Functions()[fnIndex]->params() != -1
195-
&& QgsExpression::Functions()[fnIndex]->params() != $3->count() )
205+
&& !( QgsExpression::Functions()[fnIndex]->params() >= $3->count()
206+
&& QgsExpression::Functions()[fnIndex]->minParams() <= $3->count() ) )
196207
{
197-
exp_error(parser_ctx, "Function is called with wrong number of arguments");
208+
exp_error(parser_ctx, QString( "%1 function is called with wrong number of arguments" ).arg( QgsExpression::Functions()[fnIndex]->name() ).toLocal8Bit().constData() );
198209
delete $3;
199210
YYERROR;
200211
}
@@ -214,7 +225,7 @@ expression:
214225
}
215226
if ( QgsExpression::Functions()[fnIndex]->params() != 0 )
216227
{
217-
exp_error(parser_ctx, "Function is called with wrong number of arguments");
228+
exp_error(parser_ctx, QString( "%1 function is called with wrong number of arguments" ).arg( QgsExpression::Functions()[fnIndex]->name() ).toLocal8Bit().constData() );
218229
YYERROR;
219230
}
220231
$$ = new QgsExpression::NodeFunction(fnIndex, new QgsExpression::NodeList());
@@ -276,9 +287,27 @@ expression:
276287
| NULLVALUE { $$ = new QgsExpression::NodeLiteral( QVariant() ); }
277288
;
278289

290+
named_node:
291+
NAMED_NODE expression { $$ = new QgsExpression::NamedNode( *$1, $2 ); delete $1; }
292+
;
293+
279294
exp_list:
280-
exp_list COMMA expression { $$ = $1; $1->append($3); }
295+
exp_list COMMA expression
296+
{
297+
if ( $1->hasNamedNodes() )
298+
{
299+
exp_error(parser_ctx, "All parameters following a named parameter must also be named.");
300+
delete $1;
301+
YYERROR;
302+
}
303+
else
304+
{
305+
$$ = $1; $1->append($3);
306+
}
307+
}
308+
| exp_list COMMA named_node { $$ = $1; $1->append($3); }
281309
| expression { $$ = new QgsExpression::NodeList(); $$->append($1); }
310+
| named_node { $$ = new QgsExpression::NodeList(); $$->append($1); }
282311
;
283312

284313
when_then_clauses:

‎tests/src/core/testqgsexpression.cpp

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,65 @@ class TestQgsExpression: public QObject
228228
QCOMPARE( exp.dump(), dump );
229229
}
230230

231+
void named_parameter_data()
232+
{
233+
//test passing named parameters to functions
234+
QTest::addColumn<QString>( "string" );
235+
QTest::addColumn<bool>( "parserError" );
236+
QTest::addColumn<QString>( "dump" );
237+
QTest::addColumn<QVariant>( "result" );
238+
239+
QTest::newRow( "unsupported" ) << "min( val1:=1, val2:=2, val3:=3 )" << true << "" << QVariant();
240+
QTest::newRow( "named params named" ) << "clamp( min:=1, value:=2, max:=3)" << false << "clamp(1, 2, 3)" << QVariant( 2.0 );
241+
QTest::newRow( "named params unnamed" ) << "clamp(1,2,3)" << false << "clamp(1, 2, 3)" << QVariant( 2.0 );
242+
QTest::newRow( "named params mixed" ) << "clamp( 1, value:=2, max:=3)" << false << "clamp(1, 2, 3)" << QVariant( 2.0 );
243+
QTest::newRow( "named params mixed bad" ) << "clamp( 1, value:=2, 3)" << true << "" << QVariant();
244+
QTest::newRow( "named params mixed 2" ) << "clamp( 1, 2, max:=3)" << false << "clamp(1, 2, 3)" << QVariant( 2.0 );
245+
QTest::newRow( "named params reordered" ) << "clamp( value := 2, max:=3, min:=1)" << false << "clamp(1, 2, 3)" << QVariant( 2.0 );
246+
QTest::newRow( "named params mixed case" ) << "clamp( Min:=1, vAlUe:=2,MAX:=3)" << false << "clamp(1, 2, 3)" << QVariant( 2.0 );
247+
QTest::newRow( "named params expression node" ) << "clamp( min:=1*2, value:=2+2, max:=3+1+2)" << false << "clamp(1 * 2, 2 + 2, 3 + 1 + 2)" << QVariant( 4.0 );
248+
QTest::newRow( "named params bad name" ) << "clamp( min:=1, x:=2, y:=3)" << true << "" << QVariant();
249+
QTest::newRow( "named params dupe implied" ) << "clamp( 1, 2, value:= 3, max:=4)" << true << "" << QVariant();
250+
QTest::newRow( "named params dupe explicit" ) << "clamp( 1, value := 2, value:= 3, max:=4)" << true << "" << QVariant();
251+
QTest::newRow( "named params dupe explicit 2" ) << "clamp( value:=1, value := 2, max:=4)" << true << "" << QVariant();
252+
QTest::newRow( "named params non optional omitted" ) << "clamp( min:=1, max:=2)" << true << "" << QVariant();
253+
QTest::newRow( "optional parameters specified" ) << "wordwrap( 'testxstring', 5, 'x')" << false << "wordwrap('testxstring', 5, 'x')" << QVariant( "test\nstring" );
254+
QTest::newRow( "optional parameters specified named" ) << "wordwrap( text:='testxstring', length:=5, delimiter:='x')" << false << "wordwrap('testxstring', 5, 'x')" << QVariant( "test\nstring" );
255+
QTest::newRow( "optional parameters unspecified" ) << "wordwrap( text:='test string', length:=5 )" << false << "wordwrap('test string', 5, ' ')" << QVariant( "test\nstring" );
256+
QTest::newRow( "named params dupe explicit 3" ) << "wordwrap( 'test string', 5, length:=6 )" << true << "" << QVariant();
257+
QTest::newRow( "named params dupe explicit 4" ) << "wordwrap( text:='test string', length:=5, length:=6 )" << true << "" << QVariant();
258+
}
259+
260+
void named_parameter()
261+
{
262+
QFETCH( QString, string );
263+
QFETCH( bool, parserError );
264+
QFETCH( QString, dump );
265+
QFETCH( QVariant, result );
266+
267+
QgsExpression exp( string );
268+
QCOMPARE( exp.hasParserError(), parserError );
269+
if ( exp.hasParserError() )
270+
{
271+
//parser error, so no point continuing testing
272+
qDebug() << exp.parserErrorString();
273+
return;
274+
}
275+
276+
QgsExpressionContext context;
277+
Q_ASSERT( exp.prepare( &context ) );
278+
279+
QVariant res = exp.evaluate();
280+
if ( exp.hasEvalError() )
281+
qDebug() << exp.evalErrorString();
282+
if ( res.type() != result.type() )
283+
{
284+
qDebug() << "got " << res.typeName() << " instead of " << result.typeName();
285+
}
286+
QCOMPARE( res, result );
287+
QCOMPARE( exp.dump(), dump );
288+
}
289+
231290
void evaluation_data()
232291
{
233292
QTest::addColumn<QString>( "string" );
@@ -354,28 +413,29 @@ class TestQgsExpression: public QObject
354413
// math functions
355414
QTest::newRow( "pi" ) << "pi()" << false << QVariant( M_PI );
356415
QTest::newRow( "sqrt" ) << "sqrt(16)" << false << QVariant( 4. );
416+
QTest::newRow( "sqrt" ) << "sqrt(value:=16)" << false << QVariant( 4. );
357417
QTest::newRow( "abs(0.1)" ) << "abs(0.1)" << false << QVariant( 0.1 );
358418
QTest::newRow( "abs(0)" ) << "abs(0)" << false << QVariant( 0. );
359-
QTest::newRow( "abs(-0.1)" ) << "abs(-0.1)" << false << QVariant( 0.1 );
419+
QTest::newRow( "abs( value:=-0.1)" ) << "abs(value:=-0.1)" << false << QVariant( 0.1 );
360420
QTest::newRow( "invalid sqrt value" ) << "sqrt('a')" << true << QVariant();
361-
QTest::newRow( "degrees to radians" ) << "toint(radians(45)*1000000)" << false << QVariant( 785398 ); // sorry for the nasty hack to work around floating point comparison problems
362-
QTest::newRow( "radians to degrees" ) << "toint(degrees(2)*1000)" << false << QVariant( 114592 );
363-
QTest::newRow( "sin 0" ) << "sin(0)" << false << QVariant( 0. );
364-
QTest::newRow( "cos 0" ) << "cos(0)" << false << QVariant( 1. );
365-
QTest::newRow( "tan 0" ) << "tan(0)" << false << QVariant( 0. );
366-
QTest::newRow( "asin 0" ) << "asin(0)" << false << QVariant( 0. );
367-
QTest::newRow( "acos 1" ) << "acos(1)" << false << QVariant( 0. );
368-
QTest::newRow( "atan 0" ) << "atan(0)" << false << QVariant( 0. );
421+
QTest::newRow( "degrees to radians" ) << "toint(radians(degrees:=45)*1000000)" << false << QVariant( 785398 ); // sorry for the nasty hack to work around floating point comparison problems
422+
QTest::newRow( "radians to degrees" ) << "toint(degrees(radians:=2)*1000)" << false << QVariant( 114592 );
423+
QTest::newRow( "sin 0" ) << "sin(angle:=0)" << false << QVariant( 0. );
424+
QTest::newRow( "cos 0" ) << "cos(angle:=0)" << false << QVariant( 1. );
425+
QTest::newRow( "tan 0" ) << "tan(angle:=0)" << false << QVariant( 0. );
426+
QTest::newRow( "asin 0" ) << "asin(value:=0)" << false << QVariant( 0. );
427+
QTest::newRow( "acos 1" ) << "acos(value:=1)" << false << QVariant( 0. );
428+
QTest::newRow( "atan 0" ) << "atan(value:=0)" << false << QVariant( 0. );
369429
QTest::newRow( "atan2(0,1)" ) << "atan2(0,1)" << false << QVariant( 0. );
370-
QTest::newRow( "atan2(1,0)" ) << "atan2(1,0)" << false << QVariant( M_PI / 2 );
430+
QTest::newRow( "atan2(1,0)" ) << "atan2(dx:=1,dy:=0)" << false << QVariant( M_PI / 2 );
371431
QTest::newRow( "exp(0)" ) << "exp(0)" << false << QVariant( 1. );
372-
QTest::newRow( "exp(1)" ) << "exp(1)" << false << QVariant( exp( 1. ) );
432+
QTest::newRow( "exp(1)" ) << "exp(value:=1)" << false << QVariant( exp( 1. ) );
373433
QTest::newRow( "ln(0)" ) << "ln(0)" << false << QVariant();
374434
QTest::newRow( "log10(-1)" ) << "log10(-1)" << false << QVariant();
375-
QTest::newRow( "ln(1)" ) << "ln(1)" << false << QVariant( log( 1. ) );
435+
QTest::newRow( "ln(1)" ) << "ln(value:=1)" << false << QVariant( log( 1. ) );
376436
QTest::newRow( "log10(100)" ) << "log10(100)" << false << QVariant( 2. );
377437
QTest::newRow( "log(2,32)" ) << "log(2,32)" << false << QVariant( 5. );
378-
QTest::newRow( "log(10,1000)" ) << "log(10,1000)" << false << QVariant( 3. );
438+
QTest::newRow( "log(10,1000)" ) << "log(base:=10,value:=1000)" << false << QVariant( 3. );
379439
QTest::newRow( "log(-2,32)" ) << "log(-2,32)" << false << QVariant();
380440
QTest::newRow( "log(2,-32)" ) << "log(2,-32)" << false << QVariant();
381441
QTest::newRow( "log(0.5,32)" ) << "log(0.5,32)" << false << QVariant( -5. );
@@ -389,6 +449,7 @@ class TestQgsExpression: public QObject
389449
QTest::newRow( "min(-16.6,3.5,-2.1)" ) << "min(-16.6,3.5,-2.1)" << false << QVariant( -16.6 );
390450
QTest::newRow( "min(5,3.5,-2.1)" ) << "min(5,3.5,-2.1)" << false << QVariant( -2.1 );
391451
QTest::newRow( "clamp(-2,1,5)" ) << "clamp(-2,1,5)" << false << QVariant( 1.0 );
452+
QTest::newRow( "clamp(min:=-2,value:=1,max:=5)" ) << "clamp(min:=-2,value:=1,max:=5)" << false << QVariant( 1.0 );
392453
QTest::newRow( "clamp(-2,-10,5)" ) << "clamp(-2,-10,5)" << false << QVariant( -2.0 );
393454
QTest::newRow( "clamp(-2,100,5)" ) << "clamp(-2,100,5)" << false << QVariant( 5.0 );
394455
QTest::newRow( "floor(4.9)" ) << "floor(4.9)" << false << QVariant( 4. );
@@ -582,7 +643,7 @@ class TestQgsExpression: public QObject
582643
QTest::newRow( "relate bad 2" ) << "relate(geom_from_wkt('POINT(110 120)'),geom_from_wkt(''))" << false << QVariant();
583644
QTest::newRow( "relate pattern true" ) << "relate( geom_from_wkt( 'LINESTRING(40 40,120 120)' ), geom_from_wkt( 'LINESTRING(40 40,60 120)' ), '**1F001**' )" << false << QVariant( true );
584645
QTest::newRow( "relate pattern false" ) << "relate( geom_from_wkt( 'LINESTRING(40 40,120 120)' ), geom_from_wkt( 'LINESTRING(40 40,60 120)' ), '**1F002**' )" << false << QVariant( false );
585-
QTest::newRow( "azimuth" ) << "toint(degrees(azimuth( make_point(25, 45), make_point(75, 100)))*1000000)" << false << QVariant( 42273689 );
646+
QTest::newRow( "azimuth" ) << "toint(degrees(azimuth( point_a := make_point(25, 45), point_b := make_point(75, 100)))*1000000)" << false << QVariant( 42273689 );
586647
QTest::newRow( "azimuth" ) << "toint(degrees( azimuth( make_point(75, 100), make_point(25,45) ) )*1000000)" << false << QVariant( 222273689 );
587648
QTest::newRow( "extrude geom" ) << "geom_to_wkt(extrude( geom_from_wkt('LineString( 1 2, 3 2, 4 3)'),1,2))" << false << QVariant( "Polygon ((1 2, 3 2, 4 3, 5 5, 4 4, 2 4, 1 2))" );
588649
QTest::newRow( "extrude not geom" ) << "extrude('g',5,6)" << true << QVariant();
@@ -1075,7 +1136,7 @@ class TestQgsExpression: public QObject
10751136
QCOMPARE( v1.toInt() <= 10, true );
10761137
QCOMPARE( v1.toInt() >= 1, true );
10771138

1078-
QgsExpression exp2( "rand(-5,-5)" );
1139+
QgsExpression exp2( "rand(min:=-5,max:=-5)" );
10791140
QVariant v2 = exp2.evaluate();
10801141
QCOMPARE( v2.toInt(), -5 );
10811142

@@ -1092,7 +1153,7 @@ class TestQgsExpression: public QObject
10921153
QCOMPARE( v1.toDouble() <= 9.5, true );
10931154
QCOMPARE( v1.toDouble() >= 1.5, true );
10941155

1095-
QgsExpression exp2( "randf(-0.0005,-0.0005)" );
1156+
QgsExpression exp2( "randf(min:=-0.0005,max:=-0.0005)" );
10961157
QVariant v2 = exp2.evaluate();
10971158
QCOMPARE( v2.toDouble(), -0.0005 );
10981159

0 commit comments

Comments
 (0)
Please sign in to comment.