Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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
  • 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.