Skip to content

Commit

Permalink
[FEATURE] Allow expression functions to use named parameters
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nyalldawson committed Apr 4, 2016
1 parent 5e54b93 commit ae00eb9
Show file tree
Hide file tree
Showing 10 changed files with 551 additions and 75 deletions.
111 changes: 104 additions & 7 deletions python/core/qgsexpression.sip
Expand Up @@ -303,12 +303,47 @@ class QgsExpression
//! @note not available in Python bindings
// static const char* UnaryOperatorText[];

/**
* Represents a single parameter passed to a function.
* \note added in QGIS 2.16
*/
class Parameter
{
public:

/** Constructor for Parameter.
* @param name parameter name, used when named parameter are specified in an expression
* @param optional set to true if parameter should be optional
* @param defaultValue default value to use for optional parameters
*/
Parameter( const QString& name,
bool optional = false,
const QVariant& defaultValue = QVariant() );

//! Returns the name of the parameter.
QString name() const;

//! Returns true if the parameter is optional.
bool optional() const;

//! Returns the default value for the parameter.
QVariant defaultValue() const;

bool operator==( const QgsExpression::Parameter& other ) const;

};

//! List of parameters, used for function definition
typedef QList< QgsExpression::Parameter > ParameterList;

/**
* A abstract base class for defining QgsExpression functions.
*/
class Function
{
public:

//! Constructor for function which uses unnamed parameters
Function( const QString& fnname,
int params,
const QString& group,
Expand All @@ -319,14 +354,36 @@ class QgsExpression
bool handlesNull = false,
bool isContextual = false );

/** Constructor for function which uses named parameter list.
* @note added in QGIS 2.16
*/
Function( const QString& fnname,
const QgsExpression::ParameterList& params,
const QString& group,
const QString& helpText = QString(),
bool usesGeometry = false,
const QStringList& referencedColumns = QStringList(),
bool lazyEval = false,
bool handlesNull = false,
bool isContextual = false );

virtual ~Function();

/** The name of the function. */
QString name();
QString name() const;
/** The number of parameters this function takes. */
int params();
int params() const;

/** The mininum number of parameters this function takes. */
int minParams() const;

/** Returns the list of named parameters for the function, if set.
* @note added in QGIS 2.16
*/
const QgsExpression::ParameterList& parameters() const;

/** Does this function use a geometry object. */
bool usesgeometry();
bool usesgeometry() const;

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

virtual QStringList referencedColumns() const;

Expand All @@ -349,9 +406,9 @@ class QgsExpression
bool isContextual() const;

/** The group the function belongs to. */
QString group();
QString group() const;
/** The help text for the function. */
const QString helptext();
const QString helptext() const;

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

//! Named node
//! @note added in QGIS 2.16
class NamedNode
{
public:

/** Constructor for NamedNode
* @param name node name
* @param node node
*/
NamedNode( const QString& name, QgsExpression::Node* node );

//! Node name
QString name;

//! Node
QgsExpression::Node* node;
};

class NodeList
{
public:
NodeList();
~NodeList();
/** Takes ownership of the provided node */
void append( QgsExpression::Node* node /Transfer/ );
int count();

/** Adds a named node. Takes ownership of the provided node.
* @note added in QGIS 2.16
*/
void append( QgsExpression::NamedNode* node /Transfer/ );

/** Returns the number of nodes in the list.
*/
int count() const;

//! Returns true if list contains any named nodes
//! @note added in QGIS 2.16
bool hasNamedNodes() const;

const QList<QgsExpression::Node*>& list();

//! Returns a list of names for nodes. Unnamed nodes will be indicated by an empty string in the list.
//! @note added in QGIS 2.16
QStringList names() const;

/** Creates a deep copy of this list. Ownership is transferred to the caller */
QgsExpression::NodeList* clone() const;

Expand Down Expand Up @@ -698,6 +792,9 @@ class QgsExpression
virtual bool needsGeometry() const;
virtual void accept( QgsExpression::Visitor& v ) const;
virtual QgsExpression::Node* clone() const;

//! Tests whether the provided argument list is valid for the matching function
static bool validateParams( int fnIndex, QgsExpression::NodeList* args, QString& error );
};

class NodeLiteral : QgsExpression::Node
Expand Down
6 changes: 3 additions & 3 deletions resources/function_help/json/azimuth
@@ -1,10 +1,10 @@
{
"name": "azimuth",
"type": "function",
"description": "Returns the north-based azimuth as the angle in radians measured clockwise from the vertical on pointA to pointB.",
"description": "Returns the north-based azimuth as the angle in radians measured clockwise from the vertical on point_a to point_b.",
"arguments": [
{"arg":"pointA","description":"point geometry"},
{"arg":"pointB","description":"point geometry"}
{"arg":"point_a","description":"point geometry"},
{"arg":"point_b","description":"point geometry"}
],
"examples": [
{ "expression":"degrees( azimuth( make_point(25, 45), make_point(75, 100) ) )", "returns":"42.273689"},
Expand Down
4 changes: 2 additions & 2 deletions resources/function_help/json/randf
Expand Up @@ -2,7 +2,7 @@
"name": "randf",
"type": "function",
"description": "Returns a random float within the range specified by the minimum and maximum argument (inclusive).",
"arguments": [ {"arg":"min","description":"an float representing the smallest possible random number desired"},
{"arg":"max","description":"an float representing the largest possible random number desired"}],
"arguments": [ {"arg":"min","optional":true,"default":"0.0","description":"an float representing the smallest possible random number desired"},
{"arg":"max","optional":true,"default":"1.0","description":"an float representing the largest possible random number desired"}],
"examples": [ { "expression":"randf(1, 10)", "returns":"4.59258286403147"}]
}
4 changes: 2 additions & 2 deletions resources/function_help/json/round
Expand Up @@ -3,8 +3,8 @@
"type": "function",
"description": "Rounds a number to number of decimal places.",
"arguments": [
{"arg":"decimal","description":"decimal number to be rounded"},
{"arg":"places","description":"Optional integer representing number of places to round decimals to. Can be negative."}
{"arg":"value","description":"decimal number to be rounded"},
{"arg":"places","optional":true,"default":"0","description":"Optional integer representing number of places to round decimals to. Can be negative."}
],
"examples": [
{ "expression":"round(1234.567, 2)", "returns":"1234.57"},
Expand Down
4 changes: 3 additions & 1 deletion scripts/process_function_template.py
Expand Up @@ -90,7 +90,9 @@ def quote(v):
a['arg'],
a.get('description', ''),
"true" if a.get('descOnly', False) else "false",
"true" if a.get('syntaxOnly', False) else "false")
"true" if a.get('syntaxOnly', False) else "false",
"true" if a.get('optional', False) else "false",
a.get('default', ''))
)

cpp.write(",\n /* variableLenArguments */ {0}".format(
Expand Down
48 changes: 26 additions & 22 deletions src/core/qgsexpression.cpp
Expand Up @@ -2860,28 +2860,28 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
if ( gmFunctions.isEmpty() )
{
gmFunctions
<< new StaticFunction( "sqrt", 1, fcnSqrt, "Math" )
<< new StaticFunction( "radians", 1, fcnRadians, "Math" )
<< new StaticFunction( "degrees", 1, fcnDegrees, "Math" )
<< new StaticFunction( "azimuth", 2, fcnAzimuth, "Math" )
<< new StaticFunction( "abs", 1, fcnAbs, "Math" )
<< new StaticFunction( "cos", 1, fcnCos, "Math" )
<< new StaticFunction( "sin", 1, fcnSin, "Math" )
<< new StaticFunction( "tan", 1, fcnTan, "Math" )
<< new StaticFunction( "asin", 1, fcnAsin, "Math" )
<< new StaticFunction( "acos", 1, fcnAcos, "Math" )
<< new StaticFunction( "atan", 1, fcnAtan, "Math" )
<< new StaticFunction( "atan2", 2, fcnAtan2, "Math" )
<< new StaticFunction( "exp", 1, fcnExp, "Math" )
<< new StaticFunction( "ln", 1, fcnLn, "Math" )
<< new StaticFunction( "log10", 1, fcnLog10, "Math" )
<< new StaticFunction( "log", 2, fcnLog, "Math" )
<< new StaticFunction( "round", -1, fcnRound, "Math" )
<< new StaticFunction( "rand", 2, fcnRnd, "Math" )
<< new StaticFunction( "randf", 2, fcnRndF, "Math" )
<< new StaticFunction( "sqrt", ParameterList() << Parameter( "value" ), fcnSqrt, "Math" )
<< new StaticFunction( "radians", ParameterList() << Parameter( "degrees" ), fcnRadians, "Math" )
<< new StaticFunction( "degrees", ParameterList() << Parameter( "radians" ), fcnDegrees, "Math" )
<< new StaticFunction( "azimuth", ParameterList() << Parameter( "point_a" ) << Parameter( "point_b" ), fcnAzimuth, "Math" )
<< new StaticFunction( "abs", ParameterList() << Parameter( "value" ), fcnAbs, "Math" )
<< new StaticFunction( "cos", ParameterList() << Parameter( "angle" ), fcnCos, "Math" )
<< new StaticFunction( "sin", ParameterList() << Parameter( "angle" ), fcnSin, "Math" )
<< new StaticFunction( "tan", ParameterList() << Parameter( "angle" ), fcnTan, "Math" )
<< new StaticFunction( "asin", ParameterList() << Parameter( "value" ), fcnAsin, "Math" )
<< new StaticFunction( "acos", ParameterList() << Parameter( "value" ), fcnAcos, "Math" )
<< new StaticFunction( "atan", ParameterList() << Parameter( "value" ), fcnAtan, "Math" )
<< new StaticFunction( "atan2", ParameterList() << Parameter( "dx" ) << Parameter( "dy" ), fcnAtan2, "Math" )
<< new StaticFunction( "exp", ParameterList() << Parameter( "value" ), fcnExp, "Math" )
<< new StaticFunction( "ln", ParameterList() << Parameter( "value" ), fcnLn, "Math" )
<< new StaticFunction( "log10", ParameterList() << Parameter( "value" ), fcnLog10, "Math" )
<< new StaticFunction( "log", ParameterList() << Parameter( "base" ) << Parameter( "value" ), fcnLog, "Math" )
<< new StaticFunction( "round", ParameterList() << Parameter( "value" ) << Parameter( "places", true, 0 ), fcnRound, "Math" )
<< new StaticFunction( "rand", ParameterList() << Parameter( "min" ) << Parameter( "max" ), fcnRnd, "Math" )
<< new StaticFunction( "randf", ParameterList() << Parameter( "min", true, 0.0 ) << Parameter( "max", true, 1.0 ), fcnRndF, "Math" )
<< new StaticFunction( "max", -1, fcnMax, "Math" )
<< new StaticFunction( "min", -1, fcnMin, "Math" )
<< new StaticFunction( "clamp", 3, fcnClamp, "Math" )
<< new StaticFunction( "clamp", ParameterList() << Parameter( "min" ) << Parameter( "value" ) << Parameter( "max" ), fcnClamp, "Math" )
<< new StaticFunction( "scale_linear", 5, fcnLinearScale, "Math" )
<< new StaticFunction( "scale_exp", 6, fcnExpScale, "Math" )
<< new StaticFunction( "floor", 1, fcnFloor, "Math" )
Expand Down Expand Up @@ -2915,7 +2915,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
<< new StaticFunction( "longest_common_substring", 2, fcnLCS, "Fuzzy Matching" )
<< new StaticFunction( "hamming_distance", 2, fcnHamming, "Fuzzy Matching" )
<< new StaticFunction( "soundex", 1, fcnSoundex, "Fuzzy Matching" )
<< new StaticFunction( "wordwrap", -1, fcnWordwrap, "String" )
<< new StaticFunction( "wordwrap", ParameterList() << Parameter( "text" ) << Parameter( "length" ) << Parameter( "delimiter", true, " " ), fcnWordwrap, "String" )
<< new StaticFunction( "length", 1, fcnLength, "String" )
<< new StaticFunction( "replace", 3, fcnReplace, "String" )
<< new StaticFunction( "regexp_replace", 3, fcnRegexpReplace, "String" )
Expand Down Expand Up @@ -3567,6 +3567,7 @@ QgsExpression::NodeList* QgsExpression::NodeList::clone() const
{
nl->mList.append( node->clone() );
}
nl->mNameList = mNameList;

return nl;
}
Expand Down Expand Up @@ -4450,7 +4451,10 @@ QString QgsExpression::helptext( QString name )
helpContents += delim;
delim = ", ";
if ( !a.mDescOnly )
helpContents += QString( "<span class=\"argument\">%1</span>" ).arg( a.mArg );
{
helpContents += QString( "<span class=\"argument %1\">%2%3</span>" ).arg( a.mOptional ? "optional" : "", a.mArg,
a.mDefaultVal.isEmpty() ? "" : '=' + a.mDefaultVal );
}
}

if ( v.mVariableLenArguments )
Expand Down

0 comments on commit ae00eb9

Please sign in to comment.