diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9b05bd65c4a..5caf791bd85 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -53,6 +53,7 @@ SET(QGIS_CORE_SRCS qgsdiagram.cpp qgsdiagramrendererv2.cpp qgsdistancearea.cpp + qgsexpression.cpp qgsfeature.cpp qgsfield.cpp qgsgeometry.cpp @@ -216,9 +217,9 @@ IF (WITH_INTERNAL_SPATIALITE) INCLUDE_DIRECTORIES(BEFORE spatialite/headers/spatialite) ENDIF (WITH_INTERNAL_SPATIALITE) -ADD_FLEX_FILES(QGIS_CORE_SRCS qgssearchstringlexer.ll) +ADD_FLEX_FILES(QGIS_CORE_SRCS qgssearchstringlexer.ll qgsexpressionlexer.ll) -ADD_BISON_FILES(QGIS_CORE_SRCS qgssearchstringparser.yy) +ADD_BISON_FILES(QGIS_CORE_SRCS qgssearchstringparser.yy qgsexpressionparser.yy) SET(QGIS_CORE_MOC_HDRS qgsapplication.h @@ -286,6 +287,7 @@ SET(QGIS_CORE_HDRS qgsdistancearea.h qgscsexception.h qgsexception.h + qgsexpression.h qgsfeature.h qgsfield.h qgsgeometry.h diff --git a/src/core/qgsexpression.cpp b/src/core/qgsexpression.cpp new file mode 100644 index 00000000000..34b77775ab2 --- /dev/null +++ b/src/core/qgsexpression.cpp @@ -0,0 +1,904 @@ +/*************************************************************************** + qgsexpression.cpp + ------------------- + begin : August 2011 + copyright : (C) 2011 Martin Dobias + email : wonder.sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsexpression.h" + +#include +#include +#include + +#include "qgsdistancearea.h" +#include "qgsfeature.h" +#include "qgsgeometry.h" + +// from parser +extern QgsExpression::Node* parseExpression( const QString& str, QString& parserErrorMsg ); + + +/////////////////////////////////////////////// +// three-value logic + +class TVL +{ + public: + enum Value + { + False, + True, + Unknown + }; + + static TVL::Value AND[3][3]; + static TVL::Value OR[3][3]; + static TVL::Value NOT[3]; + + TVL() : val( Unknown ) {} + TVL( bool v ) : val( v ? True : False ) {} + TVL( Value v ) : val( v ) {} + + TVL operator!() { return NOT[val]; } + TVL operator&( const TVL& other ) { return AND[val][other.val]; } + TVL operator|( const TVL& other ) { return OR[val][other.val]; } + + // conversion for result + QVariant toVariant() + { + if ( val == Unknown ) + return QVariant(); + else + return ( val == True ? 1 : 0 ); + } + + Value val; +}; + +// false true unknown +TVL::Value TVL::AND[3][3] = { { TVL::False, TVL::False, TVL::False }, // false + { TVL::False, TVL::True, TVL::Unknown }, // true + { TVL::False, TVL::Unknown, TVL::Unknown } +};// unknown + +TVL::Value TVL::OR[3][3] = { { TVL::False, TVL::True, TVL::Unknown }, // false + { TVL::True, TVL::True, TVL::True }, // true + { TVL::Unknown, TVL::True, TVL::Unknown } +};// unknown + +TVL::Value TVL::NOT[3] = { TVL::True, TVL::False, TVL::Unknown }; + +Q_DECLARE_METATYPE( TVL ) + +#define TVL_True QVariant::fromValue(TVL(true)) +#define TVL_False QVariant::fromValue(TVL(false)) +#define TVL_Unknown QVariant::fromValue(TVL()) + +/////////////////////////////////////////////// +// QVariant checks and conversions + +inline bool isInt( const QVariant& v ) { return v.type() == QVariant::Int; } +inline bool isDouble( const QVariant& v ) { return v.type() == QVariant::Double; } +inline bool isNumeric( const QVariant& v ) { return v.type() == QVariant::Int || v.type() == QVariant::Double; } +inline bool isString( const QVariant& v ) { return v.type() == QVariant::String; } +inline bool isNull( const QVariant& v ) { return v.type() == QVariant::Invalid; } +inline bool isLogic( const QVariant& v ) { return v.canConvert(); } +inline bool isNumericOrNull( const QVariant& v ) { return v.type() == QVariant::Int || v.type() == QVariant::Double; } + +inline int qvInt( const QVariant& v ) { return v.toInt(); } +inline double qvDouble( const QVariant& v ) { return v.toDouble(); } +inline QString qvString( const QVariant& v ) { return v.toString(); } +inline TVL qvLogic( const QVariant& v ) { return v.value(); } + +/////////////////////////////////////////////// +// evaluation error macros + +#define ENSURE_NO_EVAL_ERROR { if (!parent->mEvalErrorString.isNull()) return QVariant(); } +#define SET_EVAL_ERROR(x) { parent->mEvalErrorString = x; return QVariant(); } + +/////////////////////////////////////////////// +// operators + +const char* QgsExpression::BinaryOperatorText[] = +{ + "OR", "AND", + "=", "<>", "<=", ">=", "<", ">", "~", "LIKE", "ILIKE", "IS", "IS NOT", + "+", "-", "*", "/", "%", "^", + "||" +}; + +const char* QgsExpression::UnaryOperatorText[] = +{ + "NOT", "-" +}; + +/////////////////////////////////////////////// +// functions + +static int getIntArg( int i, const QVariantList& values, QgsExpression* parent ) +{ + QVariant v = values.at( i ); + if ( !isInt( v ) ) + { + parent->setEvalErrorString( "Function needs integer value" ); + return 0; + } + return v.toInt(); +} + +static double getNumericArg( int i, const QVariantList& values, QgsExpression* parent ) +{ + QVariant v = values.at( i ); + if ( !isNumeric( v ) ) + { + parent->setEvalErrorString( "Function needs numeric value" ); + return NAN; + } + return v.toDouble(); +} + +static QString getStringArg( int i, const QVariantList& values, QgsExpression* parent ) +{ + QVariant v = values.at( i ); + if ( !isString( v ) ) + { + parent->setEvalErrorString( "Function needs string value" ); + return QString(); + } + return v.toString(); +} + +QVariant fcnSqrt( const QVariantList& values, QgsFeature* /*f*/, QgsExpression* parent ) +{ + double x = getNumericArg( 0, values, parent ); + return isnan( x ) ? QVariant() : QVariant( sqrt( x ) ); +} +QVariant fcnSin( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + double x = getNumericArg( 0, values, parent ); + return isnan( x ) ? QVariant() : QVariant( sin( x ) ); +} +QVariant fcnCos( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + double x = getNumericArg( 0, values, parent ); + return isnan( x ) ? QVariant() : QVariant( cos( x ) ); +} +QVariant fcnTan( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + double x = getNumericArg( 0, values, parent ); + return isnan( x ) ? QVariant() : QVariant( tan( x ) ); +} +QVariant fcnAsin( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + double x = getNumericArg( 0, values, parent ); + return isnan( x ) ? QVariant() : QVariant( asin( x ) ); +} +QVariant fcnAcos( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + double x = getNumericArg( 0, values, parent ); + return isnan( x ) ? QVariant() : QVariant( acos( x ) ); +} +QVariant fcnAtan( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + double x = getNumericArg( 0, values, parent ); + return isnan( x ) ? QVariant() : QVariant( atan( x ) ); +} +QVariant fcnAtan2( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + double y = getNumericArg( 0, values, parent ); + double x = getNumericArg( 1, values, parent ); + if ( isnan( y ) || isnan( x ) ) return QVariant(); + return QVariant( atan2( y, x ) ); +} +QVariant fcnToInt( const QVariantList& values, QgsFeature* , QgsExpression* /*parent*/ ) +{ + QVariant v = values.at( 0 ); + if ( v.type() == QVariant::Invalid ) return QVariant(); + return QVariant( v.toInt() ); +} +QVariant fcnToReal( const QVariantList& values, QgsFeature* , QgsExpression* /*parent*/ ) +{ + QVariant v = values.at( 0 ); + if ( v.type() == QVariant::Invalid ) return QVariant(); + return QVariant( v.toDouble() ); +} +QVariant fcnToString( const QVariantList& values, QgsFeature* , QgsExpression* /*parent*/ ) +{ + QVariant v = values.at( 0 ); + if ( v.type() == QVariant::Invalid ) return QVariant(); + return QVariant( v.toString() ); +} +QVariant fcnLower( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + QString str = getStringArg( 0, values, parent ); + if ( str.isNull() ) return QVariant(); // error or null string + return QVariant( str.toLower() ); +} +QVariant fcnUpper( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + QString str = getStringArg( 0, values, parent ); + if ( str.isNull() ) return QVariant(); // error or null string + return QVariant( str.toUpper() ); +} +QVariant fcnLength( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + QString str = getStringArg( 0, values, parent ); + if ( str.isNull() ) return QVariant(); // error or null string + return QVariant( str.length() ); +} +QVariant fcnReplace( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + QString str = getStringArg( 0, values, parent ); + QString before = getStringArg( 1, values, parent ); + QString after = getStringArg( 2, values, parent ); + if ( str.isNull() || before.isNull() || after.isNull() ) return QVariant(); // error or null string + return QVariant( str.replace( before, after ) ); +} +QVariant fcnRegexpReplace( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + QString str = getStringArg( 0, values, parent ); + QString regexp = getStringArg( 1, values, parent ); + QString after = getStringArg( 2, values, parent ); + if ( str.isNull() || regexp.isNull() || after.isNull() ) return QVariant(); // error or null string + + QRegExp re( regexp ); + if ( !re.isValid() ) + { + parent->setEvalErrorString( QString( "Invalid regular expression '%1': %2" ).arg( regexp ).arg( re.errorString() ) ); + return QVariant(); + } + return QVariant( str.replace( re, after ) ); +} +QVariant fcnSubstr( const QVariantList& values, QgsFeature* , QgsExpression* parent ) +{ + QString str = getStringArg( 0, values, parent ); + if ( str.isNull() ) return QVariant(); // error or null string + int from = getIntArg( 1, values, parent ); + if ( parent->hasEvalError() ) return QVariant(); + int len = getIntArg( 2, values, parent ); + if ( parent->hasEvalError() ) return QVariant(); + return QVariant( str.mid( from -1, len ) ); +} + +QVariant fcnRowNumber( const QVariantList& , QgsFeature* , QgsExpression* parent ) +{ + return QVariant( parent->currentRowNumber() ); +} + +QVariant fcnFeatureId( const QVariantList& , QgsFeature* f, QgsExpression* ) +{ + // TODO: handling of 64-bit feature ids? + return f ? QVariant(( int )f->id() ) : QVariant(); +} + +#define ENSURE_GEOM_TYPE(f, g, geomtype) if (!f) return QVariant(); \ + QgsGeometry* g = f->geometry(); \ + if (!g || g->type() != geomtype) return QVariant(); + + +QVariant fcnX( const QVariantList& , QgsFeature* f, QgsExpression* ) +{ + ENSURE_GEOM_TYPE( f, g, QGis::Point ); + return g->asPoint().x(); +} +QVariant fcnY( const QVariantList& , QgsFeature* f, QgsExpression* ) +{ + ENSURE_GEOM_TYPE( f, g, QGis::Point ); + return g->asPoint().y(); +} + +static QVariant pointAt( const QVariantList& values, QgsFeature* f, QgsExpression* parent ) // helper function +{ + int idx = getIntArg( 0, values, parent ); + ENSURE_GEOM_TYPE( f, g, QGis::Line ); + QgsPolyline polyline = g->asPolyline(); + if ( idx < 0 ) + idx += polyline.count(); + + if ( idx < 0 || idx >= polyline.count() ) + { + parent->setEvalErrorString( "Index is out of range" ); + return QVariant(); + } + return QVariant( QPointF( polyline[idx].x(), polyline[idx].y() ) ); +} + +QVariant fcnXat( const QVariantList& values, QgsFeature* f, QgsExpression* parent ) +{ + QVariant v = pointAt( values, f, parent ); + if ( v.type() == QVariant::PointF ) + return QVariant( v.toPointF().x() ); + else + return QVariant(); +} +QVariant fcnYat( const QVariantList& values, QgsFeature* f, QgsExpression* parent ) +{ + QVariant v = pointAt( values, f, parent ); + if ( v.type() == QVariant::PointF ) + return QVariant( v.toPointF().y() ); + else + return QVariant(); +} + +QVariant fcnGeomArea( const QVariantList& , QgsFeature* f, QgsExpression* parent ) +{ + ENSURE_GEOM_TYPE( f, g, QGis::Polygon ); + QgsDistanceArea* calc = parent->geomCalculator(); + return QVariant( calc->measure( f->geometry() ) ); +} +QVariant fcnGeomLength( const QVariantList& , QgsFeature* f, QgsExpression* parent ) +{ + ENSURE_GEOM_TYPE( f, g, QGis::Line ); + QgsDistanceArea* calc = parent->geomCalculator(); + return QVariant( calc->measure( f->geometry() ) ); +} +QVariant fcnGeomPerimeter( const QVariantList& , QgsFeature* f, QgsExpression* parent ) +{ + ENSURE_GEOM_TYPE( f, g, QGis::Polygon ); + QgsDistanceArea* calc = parent->geomCalculator(); + return QVariant( calc->measurePerimeter( f->geometry() ) ); +} + +typedef QgsExpression::FunctionDef FnDef; + +FnDef QgsExpression::BuiltinFunctions[] = +{ + // math + FnDef( "sqrt", 1, fcnSqrt ), + FnDef( "sin", 1, fcnSin ), + FnDef( "cos", 1, fcnCos ), + FnDef( "tan", 1, fcnTan ), + FnDef( "asin", 1, fcnAsin ), + FnDef( "acos", 1, fcnAcos ), + FnDef( "atan", 1, fcnAtan ), + FnDef( "atan2", 2, fcnAtan2 ), + // casts + FnDef( "toint", 1, fcnToInt ), + FnDef( "toreal", 1, fcnToReal ), + FnDef( "tostring", 1, fcnToString ), + // string manipulation + FnDef( "lower", 1, fcnLower ), + FnDef( "upper", 1, fcnUpper ), + FnDef( "length", 1, fcnLength ), + FnDef( "replace", 3, fcnReplace ), + FnDef( "regexp_replace", 3, fcnRegexpReplace ), + FnDef( "substr", 3, fcnSubstr ), + // geometry accessors + FnDef( "xat", 1, fcnXat, true ), + FnDef( "yat", 1, fcnYat, true ), + // special columns + FnDef( "$rownum", 0, fcnRowNumber ), + FnDef( "$area", 0, fcnGeomArea, true ), + FnDef( "$length", 0, fcnGeomLength, true ), + FnDef( "$perimeter", 0, fcnGeomPerimeter, true ), + FnDef( "$x", 0, fcnX, true ), + FnDef( "$y", 0, fcnY, true ), + FnDef( "$id", 0, fcnFeatureId ), +}; + + +bool QgsExpression::isFunctionName( QString name ) +{ + return functionIndex( name ) != -1; +} + +int QgsExpression::functionIndex( QString name ) +{ + int count = sizeof( BuiltinFunctions ) / sizeof( FunctionDef ); + for ( int i = 0; i < count; i++ ) + { + if ( QString::compare( name, BuiltinFunctions[i].mName, Qt::CaseInsensitive ) == 0 ) + return i; + } + return -1; +} + + +QgsExpression::QgsExpression( const QString& expr ) + : mExpression( expr ), mRowNumber( 0 ), mCalc( NULL ) +{ + mRootNode = ::parseExpression( mExpression, mParserErrorString ); + + if ( mParserErrorString.isNull() ) + { + Q_ASSERT( mRootNode != NULL ); + } +} + +QgsExpression::~QgsExpression() +{ + delete mRootNode; + delete mCalc; +} + +QStringList QgsExpression::referencedColumns() +{ + if ( !mRootNode ) + return QStringList(); + QStringList columns = mRootNode->referencedColumns(); + + // filter out duplicates + for ( int i = 0; i < columns.count(); i++ ) + { + QString col = columns.at( i ); + for ( int j = i + 1; j < columns.count(); j++ ) + { + if ( QString::compare( col, columns[j], Qt::CaseInsensitive ) == 0 ) + { + // this column is repeated: remove it! + columns.removeAt( j-- ); + } + } + } + + return columns; +} + +bool QgsExpression::needsGeometry() +{ + if ( !mRootNode ) + return false; + return mRootNode->needsGeometry(); +} + +void QgsExpression::initGeomCalculator() +{ + mCalc = new QgsDistanceArea; + mCalc->setProjectionsEnabled( false ); + QSettings settings; + QString ellipsoid = settings.value( "/qgis/measure/ellipsoid", "WGS84" ).toString(); + mCalc->setEllipsoid( ellipsoid ); +} + +bool QgsExpression::prepare( const QgsFieldMap& fields ) +{ + mEvalErrorString = QString(); + if ( !mRootNode ) + { + mEvalErrorString = "No root node! Parsing failed?"; + return false; + } + + return mRootNode->prepare( this, fields ); +} + +QVariant QgsExpression::evaluate( QgsFeature* f ) +{ + mEvalErrorString = QString(); + if ( !mRootNode ) + { + mEvalErrorString = "No root node! Parsing failed?"; + return QVariant(); + } + + QVariant res = mRootNode->eval( this, f ); + if ( res.canConvert() ) + { + // convert 3-value logic to int (0/1) or null + return res.value().toVariant(); + } + return res; +} + +QVariant QgsExpression::evaluate( QgsFeature* f, const QgsFieldMap& fields ) +{ + // first prepare + bool res = prepare( fields ); + if ( !res ) + return QVariant(); + + // then evaluate + return evaluate( f ); +} + +QString QgsExpression::dump() const +{ + if ( !mRootNode ) return "(no root)"; + + return mRootNode->dump(); +} + + +/////////////////////////////////////////////// +// nodes + +QString QgsExpression::NodeList::dump() const +{ + QString msg; bool first = true; + foreach( Node* n, mList ) + { + if ( !first ) msg += ", "; else first = false; + msg += n->dump(); + } + return msg; +} + +// + +QVariant QgsExpression::NodeUnaryOperator::eval( QgsExpression* parent, QgsFeature* f ) +{ + QVariant val = mOperand->eval( parent, f ); + ENSURE_NO_EVAL_ERROR; + + switch ( mOp ) + { + case uoNot: + if ( isLogic( val ) ) + val.setValue( ! qvLogic( val ) ); + else + SET_EVAL_ERROR( "NOT applicable only on boolean" ); + break; + + case uoMinus: + if ( isInt( val ) ) + val.setValue( - qvInt( val ) ); + else if ( isDouble( val ) ) + val.setValue( - qvDouble( val ) ); + else + SET_EVAL_ERROR( "Unary minus only for numeric values." ); + break; + default: + Q_ASSERT( 0 && "unknown unary operation" ); + } + + return val; +} + +bool QgsExpression::NodeUnaryOperator::prepare( QgsExpression* parent, const QgsFieldMap& fields ) +{ + return mOperand->prepare( parent, fields ); +} + +QString QgsExpression::NodeUnaryOperator::dump() const +{ + return QString( "%1 %2" ).arg( UnaryOperatorText[mOp] ).arg( mOperand->dump() ); +} + +// + +QVariant QgsExpression::NodeBinaryOperator::eval( QgsExpression* parent, QgsFeature* f ) +{ + QVariant vL = mOpLeft->eval( parent, f ); + ENSURE_NO_EVAL_ERROR; + QVariant vR = mOpRight->eval( parent, f ); + ENSURE_NO_EVAL_ERROR; + + switch ( mOp ) + { + case boPlus: + case boMinus: + case boMul: + case boDiv: + case boMod: + if ( isNull( vL ) || isNull( vR ) ) + return QVariant(); + else if ( isNumeric( vL ) && isNumeric( vR ) ) + { + if ( isInt( vL ) && isInt( vR ) ) + return QVariant( computeInt( qvInt( vL ), qvInt( vR ) ) ); + else + return QVariant( computeDouble( qvDouble( vL ), qvDouble( vR ) ) ); + } + else + SET_EVAL_ERROR( "Arithmetic possible only with numeric values" ); + + case boPow: + if ( isNull( vL ) || isNull( vR ) ) + return QVariant(); + else if ( isNumeric( vL ) && isNumeric( vR ) ) + return QVariant( pow( qvDouble( vL ), qvDouble( vR ) ) ); + else + SET_EVAL_ERROR( "Arithmetic possible only with numeric values" ); + + case boAnd: + if ( isLogic( vL ) && isLogic( vR ) ) + return QVariant::fromValue( qvLogic( vL ) & qvLogic( vR ) ); + else + SET_EVAL_ERROR( "AND applicable only on boolean" ); + + case boOr: + if ( isLogic( vL ) && isLogic( vR ) ) + return QVariant::fromValue( qvLogic( vL ) | qvLogic( vR ) ); + else + SET_EVAL_ERROR( "OR applicable only on boolean" ); + + case boEQ: + case boNE: + case boLT: + case boGT: + case boLE: + case boGE: + if ( isNull( vL ) || isNull( vR ) ) + { + return TVL_Unknown; + } + else if ( isNumeric( vL ) && isNumeric( vR ) ) + { + double diff = qvDouble( vL ) - qvDouble( vR ); + return compare( diff ) ? TVL_True : TVL_False; + } + else if ( isString( vL ) && isString( vR ) ) + { + int diff = QString::compare( qvString( vL ), qvString( vR ) ); + return compare( diff ) ? TVL_True : TVL_False; + } + else + SET_EVAL_ERROR( "Invalid arguments for comparison" ); + + case boIs: + case boIsNot: + if ( isNull( vL ) && isNull( vR ) ) // both operators null + return ( mOp == boIs ? TVL_True : TVL_False ); + else if ( isNull( vL ) || isNull( vR ) ) // one operator null + return ( mOp == boIs ? TVL_False : TVL_True ); + else // both operators non-null + { + bool equal = false; + if ( isNumeric( vL ) && isNumeric( vR ) ) + equal = qvDouble( vL ) == qvDouble( vR ); + else if ( isString( vL ) && isString( vR ) ) + equal = QString::compare( qvString( vL ), qvString( vR ) ) == 0; + else + SET_EVAL_ERROR( "Invalid arguments for comparison" ); + if ( equal ) + return mOp == boIs ? TVL_True : TVL_False; + else + return mOp == boIs ? TVL_False : TVL_True; + } + + case boRegexp: + case boLike: + case boILike: + if ( isNull( vL ) || isNull( vR ) ) + return TVL_Unknown; + else if ( isString( vL ) && isString( vR ) ) + { + QString str = qvString( vL ), regexp = qvString( vR ); + // TODO: cache QRegExp in case that regexp is a literal string (i.e. it will stay constant) + bool matches; + if ( mOp == boLike || mOp == boILike ) // change from LIKE syntax to regexp + { + // XXX escape % and _ ??? + regexp.replace( "%", ".*" ); + regexp.replace( "_", "." ); + matches = QRegExp( regexp, mOp == boLike ? Qt::CaseSensitive : Qt::CaseInsensitive ).exactMatch( str ); + } + else + { + matches = QRegExp( regexp ).indexIn( str ) != -1; + } + return matches ? TVL_True : TVL_False; + } + else + SET_EVAL_ERROR( "Invalid arguments for regexp" ); + + case boConcat: + if ( isNull( vL ) || isNull( vR ) ) + return QVariant(); + else if ( isString( vL ) || isString( vR ) ) + { + return QVariant( qvString( vL ) + qvString( vR ) ); + } + else + SET_EVAL_ERROR( "Invalid arguments for concatenation" ); + + default: break; + } + Q_ASSERT( false ); + return QVariant(); +} + +bool QgsExpression::NodeBinaryOperator::compare( double diff ) +{ + switch ( mOp ) + { + case boEQ: return diff == 0; + case boNE: return diff != 0; + case boLT: return diff < 0; + case boGT: return diff > 0; + case boLE: return diff <= 0; + case boGE: return diff >= 0; + default: Q_ASSERT( false ); return false; + } +} + +int QgsExpression::NodeBinaryOperator::computeInt( int x, int y ) +{ + switch ( mOp ) + { + case boPlus: return x+y; + case boMinus: return x-y; + case boMul: return x*y; + case boDiv: return x/y; + case boMod: return x%y; + default: Q_ASSERT( false ); return 0; + } +} + +double QgsExpression::NodeBinaryOperator::computeDouble( double x, double y ) +{ + switch ( mOp ) + { + case boPlus: return x+y; + case boMinus: return x-y; + case boMul: return x*y; + case boDiv: return x/y; + case boMod: return fmod( x,y ); + default: Q_ASSERT( false ); return 0; + } +} + + +bool QgsExpression::NodeBinaryOperator::prepare( QgsExpression* parent, const QgsFieldMap& fields ) +{ + bool resL = mOpLeft->prepare( parent, fields ); + bool resR = mOpRight->prepare( parent, fields ); + return resL && resR; +} + +QString QgsExpression::NodeBinaryOperator::dump() const +{ + return QString( "%1 %2 %3" ).arg( mOpLeft->dump() ).arg( BinaryOperatorText[mOp] ).arg( mOpRight->dump() ); +} + +// + +QVariant QgsExpression::NodeInOperator::eval( QgsExpression* parent, QgsFeature* f ) +{ + if ( mList->count() == 0 ) + return mNotIn ? TVL_True : TVL_False; + QVariant v1 = mNode->eval( parent, f ); + ENSURE_NO_EVAL_ERROR; + if ( isNull( v1 ) ) + return TVL_Unknown; + + bool listHasNull = false; + + foreach( Node* n, mList->list() ) + { + QVariant v2 = n->eval( parent, f ); + ENSURE_NO_EVAL_ERROR; + if ( isNull( v2 ) ) + listHasNull = true; + else + { + bool equal = false; + // check whether they are equal + if ( isNumeric( v1 ) && isNumeric( v2 ) ) + equal = ( qvDouble( v1 ) == qvDouble( v2 ) ); + else if ( isString( v1 ) && isString( v2 ) ) + equal = ( QString::compare( qvString( v1 ), qvString( v2 ) ) == 0 ); + else + SET_EVAL_ERROR( "Invalid arguments for comparison (IN operator)" ); + + if ( equal ) // we know the result + return mNotIn ? TVL_False : TVL_True; + } + } + + // item not found + if ( listHasNull ) + return TVL_Unknown; + else + return mNotIn ? TVL_True : TVL_False; +} + +bool QgsExpression::NodeInOperator::prepare( QgsExpression* parent, const QgsFieldMap& fields ) +{ + bool res = true; + foreach( Node* n, mList->list() ) + { + res = res && n->prepare( parent, fields ); + } + return res; +} + +QString QgsExpression::NodeInOperator::dump() const +{ + return QString( "%1 IN (%2)" ).arg( mNode->dump() ).arg( mList->dump() ); +} + +// + +QVariant QgsExpression::NodeFunction::eval( QgsExpression* parent, QgsFeature* f ) +{ + const FunctionDef& fd = BuiltinFunctions[mFnIndex]; + + // evaluate arguments + QVariantList argValues; + if ( mArgs ) + { + foreach( Node* n, mArgs->list() ) + { + argValues.append( n->eval( parent, f ) ); + ENSURE_NO_EVAL_ERROR; + } + } + + // run the function + QVariant res = fd.mFcn( argValues, f, parent ); + ENSURE_NO_EVAL_ERROR; + + // everything went fine + return res; +} + +bool QgsExpression::NodeFunction::prepare( QgsExpression* parent, const QgsFieldMap& fields ) +{ + bool res = true; + foreach( Node* n, mArgs->list() ) + { + res = res && n->prepare( parent, fields ); + } + return res; +} + +QString QgsExpression::NodeFunction::dump() const +{ + const FnDef& fd = BuiltinFunctions[mFnIndex]; + if ( fd.mParams == 0 ) + return fd.mName; // special column + else + return QString( "%1(%2)" ).arg( fd.mName ).arg( mArgs->dump() ); // function +} + +// + +QVariant QgsExpression::NodeLiteral::eval( QgsExpression* , QgsFeature* ) +{ + return mValue; +} + +bool QgsExpression::NodeLiteral::prepare( QgsExpression* /*parent*/, const QgsFieldMap& /*fields*/ ) +{ + return true; +} + + +QString QgsExpression::NodeLiteral::dump() const +{ + switch ( mValue.type() ) + { + case QVariant::Invalid: return "NULL"; + case QVariant::Int: return QString::number( mValue.toInt() ); + case QVariant::Double: return QString::number( mValue.toDouble() ); + case QVariant::String: return QString( "'%1'" ).arg( mValue.toString() ); + default: return "[unsupported value]"; + } +} + +// + +QVariant QgsExpression::NodeColumnRef::eval( QgsExpression* /*parent*/, QgsFeature* f ) +{ + return f->attributeMap()[mIndex]; +} + +bool QgsExpression::NodeColumnRef::prepare( QgsExpression* parent, const QgsFieldMap& fields ) +{ + foreach( int i, fields.keys() ) + { + if ( QString::compare( fields[i].name(), mName, Qt::CaseInsensitive ) == 0 ) + { + mIndex = i; + return true; + } + } + parent->mEvalErrorString = QString( "Column %1 not found" ).arg( mName ); + mIndex = -1; + return false; +} + +QString QgsExpression::NodeColumnRef::dump() const +{ + return mName; +} diff --git a/src/core/qgsexpression.h b/src/core/qgsexpression.h new file mode 100644 index 00000000000..8f574288bf2 --- /dev/null +++ b/src/core/qgsexpression.h @@ -0,0 +1,318 @@ +/*************************************************************************** + qgsexpression.h + ------------------- + begin : August 2011 + copyright : (C) 2011 Martin Dobias + email : wonder.sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSEXPRESSION_H +#define QGSEXPRESSION_H + +#include +#include + +#include "qgsfield.h" + +class QgsDistanceArea; +class QgsFeature; + +/** +Class for parsing and evaluation of expressions (formerly called "search strings") + +Usage: + + QgsExpression exp("gid*2 > 10 and type not in ('D','F'); + if (exp.hasParserError()) + { + // show error message with parserErrorString() and exit + } + QVariant result = exp.evaluate(feature, fields); + if (exp.hasEvalError()) + { + // show error message with evalErrorString() + } + else + { + // examine the result + } + +Possible QVariant value types: +- invalid (null) +- int +- double +- string + +For better performance with many evaluations you may first call prepare(fields) function +to find out indices of columns and then repeatedly call evaluate(feature). + +@note added in 2.0 +*/ +class QgsExpression +{ + public: + QgsExpression( const QString& expr ); + ~QgsExpression(); + + //! Returns true if an error occurred when parsing the input expression + bool hasParserError() const { return !mParserErrorString.isNull(); } + //! Returns parser error + QString parserErrorString() const { return mParserErrorString; } + + //! Get the expression ready for evaluation - find out column indexes. + bool prepare( const QgsFieldMap& fields ); + + //! Get list of columns referenced by the expression + QStringList referencedColumns(); + //! Returns true if the expression uses feature geometry for some computation + bool needsGeometry(); + + // evaluation + + //! Evaluate the feature and return the result + //! @note prepare() should be called before calling this method + QVariant evaluate( QgsFeature* f = NULL ); + + //! Evaluate the feature and return the result + //! @note this method does not expect that prepare() has been called on this instance + QVariant evaluate( QgsFeature* f, const QgsFieldMap& fields ); + + //! Returns true if an error occurred when evaluating last input + bool hasEvalError() const { return !mEvalErrorString.isNull(); } + //! Returns evaluation error + QString evalErrorString() const { return mEvalErrorString; } + //! Set evaluation error (used internally by evaluation functions) + void setEvalErrorString( QString str ) { mEvalErrorString = str; } + + //! Set the number for $rownum special column + void setCurrentRowNumber( int rowNumber ) { mRowNumber = rowNumber; } + //! Return the number used for $rownum special column + int currentRowNumber() { return mRowNumber; } + + //! Return the parsed expression as a string - useful for debugging + QString dump() const; + + //! Return calculator used for distance and area calculations + //! (used by internal functions) + QgsDistanceArea* geomCalculator() { if ( mCalc == NULL ) initGeomCalculator(); return mCalc; } + + // + + enum UnaryOperator + { + uoNot, + uoMinus, + }; + enum BinaryOperator + { + // logical + boOr, + boAnd, + + // comparison + boEQ, // = + boNE, // <> + boLE, // <= + boGE, // >= + boLT, // < + boGT, // > + boRegexp, + boLike, + boILike, + boIs, + boIsNot, + + // math + boPlus, + boMinus, + boMul, + boDiv, + boMod, + boPow, + + // strings + boConcat, + }; + + static const char* BinaryOperatorText[]; + static const char* UnaryOperatorText[]; + + + typedef QVariant( *FcnEval )( const QVariantList& values, QgsFeature* f, QgsExpression* parent ); + + struct FunctionDef + { + FunctionDef( QString fnname, int params, FcnEval fcn, bool usesGeometry = false ) + : mName( fnname ), mParams( params ), mFcn( fcn ), mUsesGeometry( usesGeometry ) {} + QString mName; + int mParams; + FcnEval mFcn; + bool mUsesGeometry; + }; + + static FunctionDef BuiltinFunctions[]; + + // tells whether the identifier is a name of existing function + static bool isFunctionName( QString name ); + + // return index of the function in BuiltinFunctions array + static int functionIndex( QString name ); + + ////// + + class Node + { + public: + virtual ~Node() {} + // abstract virtual eval function + // errors are reported to the parent + virtual QVariant eval( QgsExpression* parent, QgsFeature* f ) = 0; + + // abstract virtual preparation function + // errors are reported to the parent + virtual bool prepare( QgsExpression* parent, const QgsFieldMap& fields ) = 0; + + virtual QString dump() const = 0; + + virtual QStringList referencedColumns() const = 0; + virtual bool needsGeometry() const = 0; + }; + + class NodeList + { + public: + NodeList() {} + ~NodeList() { foreach( Node* n, mList ) delete n; } + void append( Node* node ) { mList.append( node ); } + int count() { return mList.count(); } + QList list() { return mList; } + + virtual QString dump() const; + protected: + QList mList; + }; + + class NodeUnaryOperator : public Node + { + public: + NodeUnaryOperator( UnaryOperator op, Node* operand ) : mOp( op ), mOperand( operand ) {} + ~NodeUnaryOperator() { delete mOperand; } + + virtual bool prepare( QgsExpression* parent, const QgsFieldMap& fields ); + virtual QVariant eval( QgsExpression* parent, QgsFeature* f ); + virtual QString dump() const; + virtual QStringList referencedColumns() const { return mOperand->referencedColumns(); } + virtual bool needsGeometry() const { return mOperand->needsGeometry(); } + protected: + UnaryOperator mOp; + Node* mOperand; + }; + + class NodeBinaryOperator : public Node + { + public: + NodeBinaryOperator( BinaryOperator op, Node* opLeft, Node* opRight ) : mOp( op ), mOpLeft( opLeft ), mOpRight( opRight ) {} + ~NodeBinaryOperator() { delete mOpLeft; delete mOpRight; } + + virtual bool prepare( QgsExpression* parent, const QgsFieldMap& fields ); + virtual QVariant eval( QgsExpression* parent, QgsFeature* f ); + virtual QString dump() const; + virtual QStringList referencedColumns() const { return mOpLeft->referencedColumns() + mOpRight->referencedColumns(); } + virtual bool needsGeometry() const { return mOpLeft->needsGeometry() || mOpRight->needsGeometry(); } + protected: + bool compare( double diff ); + int computeInt( int x, int y ); + double computeDouble( double x, double y ); + + BinaryOperator mOp; + Node* mOpLeft; + Node* mOpRight; + }; + + class NodeInOperator : public Node + { + public: + NodeInOperator( Node* node, NodeList* list, bool notin = false ) : mNode( node ), mList( list ), mNotIn( notin ) {} + ~NodeInOperator() { delete mNode; delete mList; } + + virtual bool prepare( QgsExpression* parent, const QgsFieldMap& fields ); + virtual QVariant eval( QgsExpression* parent, QgsFeature* f ); + virtual QString dump() const; + virtual QStringList referencedColumns() const { QStringList lst( mNode->referencedColumns() ); foreach( Node* n, mList->list() ) lst.append( n->referencedColumns() ); return lst; } + virtual bool needsGeometry() const { bool needs = false; foreach( Node* n, mList->list() ) needs |= n->needsGeometry(); return needs; } + protected: + Node* mNode; + NodeList* mList; + bool mNotIn; + }; + + class NodeFunction : public Node + { + public: + NodeFunction( int fnIndex, NodeList* args ): mFnIndex( fnIndex ), mArgs( args ) {} + //NodeFunction( QString name, NodeList* args ) : mName(name), mArgs(args) {} + ~NodeFunction() { delete mArgs; } + + virtual bool prepare( QgsExpression* parent, const QgsFieldMap& fields ); + virtual QVariant eval( QgsExpression* parent, QgsFeature* f ); + virtual QString dump() const; + virtual QStringList referencedColumns() const { QStringList lst; if ( !mArgs ) return lst; foreach( Node* n, mArgs->list() ) lst.append( n->referencedColumns() ); return lst; } + virtual bool needsGeometry() const { return BuiltinFunctions[mFnIndex].mUsesGeometry; } + protected: + //QString mName; + int mFnIndex; + NodeList* mArgs; + }; + + class NodeLiteral : public Node + { + public: + NodeLiteral( QVariant value ) : mValue( value ) {} + + virtual bool prepare( QgsExpression* parent, const QgsFieldMap& fields ); + virtual QVariant eval( QgsExpression* parent, QgsFeature* f ); + virtual QString dump() const; + virtual QStringList referencedColumns() const { return QStringList(); } + virtual bool needsGeometry() const { return false; } + protected: + QVariant mValue; + }; + + class NodeColumnRef : public Node + { + public: + NodeColumnRef( QString name ) : mName( name ), mIndex( -1 ) {} + + virtual bool prepare( QgsExpression* parent, const QgsFieldMap& fields ); + virtual QVariant eval( QgsExpression* parent, QgsFeature* f ); + virtual QString dump() const; + virtual QStringList referencedColumns() const { return QStringList( mName ); } + virtual bool needsGeometry() const { return false; } + protected: + QString mName; + int mIndex; + }; + + + protected: + + QString mExpression; + Node* mRootNode; + + QString mParserErrorString; + QString mEvalErrorString; + + int mRowNumber; + + void initGeomCalculator(); + QgsDistanceArea* mCalc; +}; + +#endif // QGSEXPRESSION_H diff --git a/src/core/qgsexpressionlexer.ll b/src/core/qgsexpressionlexer.ll new file mode 100644 index 00000000000..f4a3789dcf0 --- /dev/null +++ b/src/core/qgsexpressionlexer.ll @@ -0,0 +1,171 @@ +/*************************************************************************** + qgsexpressionlexer.ll + -------------------- + begin : August 2011 + copyright : (C) 2011 by Martin Dobias + email : wonder.sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +%option noyywrap +%option case-insensitive +%option never-interactive +%option nounput +%option prefix="exp_" + + // ensure that lexer will be 8-bit (and not just 7-bit) +%option 8bit + +%{ + +#include // atof() + +#include "qgsexpression.h" +#include "qgsexpressionparser.hpp" +#include + + +// if not defined, searches for isatty() +// which doesn't in MSVC compiler +#define YY_NEVER_INTERACTIVE 1 + +#ifndef YY_NO_UNPUT +#define YY_NO_UNPUT // unused +#endif + +#ifdef _MSC_VER +#define YY_NO_UNISTD_H +#endif + +#define B_OP(x) exp_lval.b_op = QgsExpression::x +#define U_OP(x) exp_lval.u_op = QgsExpression::x +//#define TEXT exp_lval.text = new QString(); *exp_lval.text = QString::fromUtf8(yytext); +#define TEXT_FILTER(filter_fn) exp_lval.text = new QString(); *exp_lval.text = filter_fn( QString::fromUtf8(yytext) ); +#define TEXT TEXT_FILTER() + + +static QString stripText(QString text) +{ + // strip single quotes on start,end + text = text.mid( 1, text.length() - 2 ); + + // make single "single quotes" from double "single quotes" + text.replace( QRegExp( "''" ), "'" ); + + // strip \n \' etc. + int index = 0; + while (( index = text.indexOf( '\\', index ) ) != -1 ) + { + text.remove( index, 1 ); // delete backslash + QChar chr; + switch ( text[index].toLatin1() ) // evaluate backslashed character + { + case 'n': chr = '\n'; break; + case 't': chr = '\t'; break; + case '\\': chr = '\\'; break; + case '\'': chr = '\''; break; + default: chr = '?'; break; + } + text[index++] = chr; // set new character and push index +1 + } + return text; +} + +static QString stripColumnRef(QString text) +{ + // strip double quotes on start,end + text = text.mid( 1, text.length() - 2 ); + + // make single "double quotes" from double "double quotes" + text.replace( QRegExp( "\"\"" ), "\"" ); + return text; +} + + +%} + +white [ \t\r\n]+ + +non_ascii [\x80-\xFF] + +col_first [A-Za-z_]|{non_ascii} +col_next [A-Za-z0-9_]|{non_ascii} +column_ref {col_first}{col_next}* + +special_col "$"{column_ref} + +col_str_char "\"\""|[^\"] +column_ref_quoted "\""{col_str_char}*"\"" + +dig [0-9] +num_int {dig}+ +num_float {dig}*\.{dig}+([eE][-+]?{dig}+)? + +str_char ('')|(\\.)|[^'\\] +string "'"{str_char}*"'" + +%% + +"NOT" { U_OP(uoNot); return NOT; } +"AND" { B_OP(boAnd); return AND; } +"OR" { B_OP(boOr); return OR; } + +"=" { B_OP(boEQ); return EQ; } +"!=" { B_OP(boNE); return NE; } +"<=" { B_OP(boLE); return LE; } +">=" { B_OP(boGE); return GE; } +"<>" { B_OP(boNE); return NE; } +"<" { B_OP(boLT); return LT; } +">" { B_OP(boGT); return GT; } + +"~" { B_OP(boRegexp); return REGEXP; } +"LIKE" { B_OP(boLike); return LIKE; } +"ILIKE" { B_OP(boILike); return ILIKE; } +"IS" { B_OP(boIs); return IS; } +"IS NOT" { B_OP(boIsNot); return ISNOT; } +"||" { B_OP(boConcat); return CONCAT; } + +"+" { B_OP(boPlus); return PLUS; } +"-" { B_OP(boMinus); return MINUS; } +"*" { B_OP(boMul); return MUL; } +"/" { B_OP(boDiv); return DIV; } +"%" { B_OP(boMod); return MOD; } +"^" { B_OP(boPow); return POW; } + +"IN" { return IN; } + +"NULL" { return NULLVALUE; } + + +[()] { return yytext[0]; } + +"," { return COMMA; } + +{num_float} { exp_lval.numberFloat = atof(yytext); return NUMBER_FLOAT; } +{num_int} { exp_lval.numberInt = atoi(yytext); return NUMBER_INT; } + +{string} { TEXT_FILTER(stripText); return STRING; } + +{special_col} { TEXT; return SPECIAL_COL; } + +{column_ref} { TEXT; return QgsExpression::isFunctionName(*exp_lval.text) ? FUNCTION : COLUMN_REF; } + +{column_ref_quoted} { TEXT_FILTER(stripColumnRef); return COLUMN_REF; } + +{white} /* skip blanks and tabs */ + +. { return Unknown_CHARACTER; } + +%% + +void exp_set_input_buffer(const char* buffer) +{ + exp__scan_string(buffer); +} + diff --git a/src/core/qgsexpressionparser.yy b/src/core/qgsexpressionparser.yy new file mode 100644 index 00000000000..c88205d1b13 --- /dev/null +++ b/src/core/qgsexpressionparser.yy @@ -0,0 +1,232 @@ +/*************************************************************************** + qgsexpressionparser.yy + -------------------- + begin : August 2011 + copyright : (C) 2011 by Martin Dobias + email : wonder.sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +%{ +#include +#include +#include +#include "qgsexpression.h" + +#ifdef _MSC_VER +# pragma warning( disable: 4065 ) // switch statement contains 'default' but no 'case' labels +# pragma warning( disable: 4702 ) // unreachable code +#endif + +// don't redeclare malloc/free +#define YYINCLUDED_STDLIB_H 1 + +//! from lexer +extern int exp_lex(); +extern void exp_set_input_buffer(const char* buffer); + +/** returns parsed tree, otherwise returns NULL and sets parserErrorMsg + (interface function to be called from QgsExpression) + */ +QgsExpression::Node* parseExpression(const QString& str, QString& parserErrorMsg); + +/** error handler for bison */ +void exp_error(const char* msg); + +//! varible where the parser error will be stored +QString gExpParserErrorMsg; +QgsExpression::Node* gExpParserRootNode; + + +// we want verbose error messages +#define YYERROR_VERBOSE 1 + +#define BINOP(x, y, z) new QgsExpression::NodeBinaryOperator(x, y, z) + +%} + +%name-prefix "exp_" + +%union +{ + QgsExpression::Node* node; + QgsExpression::NodeList* nodelist; + double numberFloat; + int numberInt; + QString* text; + QgsExpression::BinaryOperator b_op; + QgsExpression::UnaryOperator u_op; +} + +%start root + + +// +// token definitions +// + +// operator tokens +%token OR AND EQ NE LE GE LT GT REGEXP LIKE ILIKE IS ISNOT PLUS MINUS MUL DIV MOD CONCAT POW +%token NOT +%token IN + +// literals +%token NUMBER_FLOAT +%token NUMBER_INT +%token NULLVALUE + +%token STRING COLUMN_REF FUNCTION SPECIAL_COL + +%token COMMA + +%token Unknown_CHARACTER + +// +// definition of non-terminal types +// + +%type expression +%type exp_list + +// debugging +%error-verbose + +// +// operator precedence +// + +// left associativity means that 1+2+3 translates to (1+2)+3 +// the order of operators here determines their precedence + +%left OR +%left AND +%right NOT +%left EQ NE LE GE LT GT REGEXP LIKE ILIKE IS ISNOT IN +%left PLUS MINUS +%left MUL DIV MOD +%right POW +%left CONCAT + +%right UMINUS // fictitious symbol (for unary minus) + +%left COMMA + +%destructor { delete $$; } +%destructor { delete $$; } +%destructor { delete $$; } + +%% + +root: expression { gExpParserRootNode = $1; } + ; + +expression: + expression AND expression { $$ = BINOP($2, $1, $3); } + | expression OR expression { $$ = BINOP($2, $1, $3); } + | expression EQ expression { $$ = BINOP($2, $1, $3); } + | expression NE expression { $$ = BINOP($2, $1, $3); } + | expression LE expression { $$ = BINOP($2, $1, $3); } + | expression GE expression { $$ = BINOP($2, $1, $3); } + | expression LT expression { $$ = BINOP($2, $1, $3); } + | expression GT expression { $$ = BINOP($2, $1, $3); } + | expression REGEXP expression { $$ = BINOP($2, $1, $3); } + | expression LIKE expression { $$ = BINOP($2, $1, $3); } + | expression ILIKE expression { $$ = BINOP($2, $1, $3); } + | expression IS expression { $$ = BINOP($2, $1, $3); } + | expression ISNOT expression { $$ = BINOP($2, $1, $3); } + | expression PLUS expression { $$ = BINOP($2, $1, $3); } + | expression MINUS expression { $$ = BINOP($2, $1, $3); } + | expression MUL expression { $$ = BINOP($2, $1, $3); } + | expression DIV expression { $$ = BINOP($2, $1, $3); } + | expression MOD expression { $$ = BINOP($2, $1, $3); } + | expression POW expression { $$ = BINOP($2, $1, $3); } + | expression CONCAT expression { $$ = BINOP($2, $1, $3); } + | NOT expression { $$ = new QgsExpression::NodeUnaryOperator($1, $2); } + | '(' expression ')' { $$ = $2; } + + | FUNCTION '(' exp_list ')' + { + int fnIndex = QgsExpression::functionIndex(*$1); + if (fnIndex == -1) + { + // this should not actually happen because already in lexer we check whether an identifier is a known function + // (if the name is not known the token is parsed as a column) + exp_error("Function is not known"); + YYERROR; + } + if (QgsExpression::BuiltinFunctions[fnIndex].mParams != $3->count()) + { + exp_error("Function is called with wrong number of arguments"); + YYERROR; + } + $$ = new QgsExpression::NodeFunction(fnIndex, $3); + delete $1; + } + + | expression IN '(' exp_list ')' { $$ = new QgsExpression::NodeInOperator($1, $4, false); } + | expression NOT IN '(' exp_list ')' { $$ = new QgsExpression::NodeInOperator($1, $5, true); } + + | PLUS expression %prec UMINUS { $$ = $2; } + | MINUS expression %prec UMINUS { $$ = new QgsExpression::NodeUnaryOperator( QgsExpression::uoMinus, $2); } + + // columns + | COLUMN_REF { $$ = new QgsExpression::NodeColumnRef( *$1 ); delete $1; } + + // special columns (actually functions with no arguments) + | SPECIAL_COL + { + int fnIndex = QgsExpression::functionIndex(*$1); + if (fnIndex == -1) + { + exp_error("Special column is not known"); + YYERROR; + } + $$ = new QgsExpression::NodeFunction( fnIndex, NULL ); + delete $1; + } + + // literals + | NUMBER_FLOAT { $$ = new QgsExpression::NodeLiteral( QVariant($1) ); } + | NUMBER_INT { $$ = new QgsExpression::NodeLiteral( QVariant($1) ); } + | STRING { $$ = new QgsExpression::NodeLiteral( QVariant(*$1) ); delete $1; } + | NULLVALUE { $$ = new QgsExpression::NodeLiteral( QVariant() ); } +; + +exp_list: + exp_list COMMA expression { $$ = $1; $1->append($3); } + | expression { $$ = new QgsExpression::NodeList(); $$->append($1); } + ; + +%% + +// returns parsed tree, otherwise returns NULL and sets parserErrorMsg +QgsExpression::Node* parseExpression(const QString& str, QString& parserErrorMsg) +{ + gExpParserRootNode = NULL; + exp_set_input_buffer(str.toUtf8().constData()); + int res = exp_parse(); + + // list should be empty when parsing was OK + if (res == 0) // success? + { + return gExpParserRootNode; + } + else // error? + { + parserErrorMsg = gExpParserErrorMsg; + return NULL; + } +} + + +void exp_error(const char* msg) +{ + gExpParserErrorMsg = msg; +} + diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index 42f2d466a69..ac5d1c9e0af 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -67,6 +67,7 @@ ENDMACRO (ADD_QGIS_TEST) # Tests: ADD_QGIS_TEST(applicationtest testqgsapplication.cpp) +ADD_QGIS_TEST(expressiontest testqgsexpression.cpp) ADD_QGIS_TEST(filewritertest testqgsvectorfilewriter.cpp) ADD_QGIS_TEST(regression992 regression992.cpp) ADD_QGIS_TEST(regression1141 regression1141.cpp) diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp new file mode 100644 index 00000000000..f744909bfee --- /dev/null +++ b/tests/src/core/testqgsexpression.cpp @@ -0,0 +1,395 @@ +/*************************************************************************** + test_template.cpp + -------------------------------------- + Date : Sun Sep 16 12:22:23 AKDT 2007 + Copyright : (C) 2007 by Gary E. Sherman + Email : sherman at mrcc dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include +#include +#include +#include +//header for class being tested +#include +#include +#include + +class TestQgsExpression: public QObject +{ + Q_OBJECT; + private slots: + + void parsing_data() + { + QTest::addColumn( "string" ); + QTest::addColumn( "valid" ); + + // invalid strings + QTest::newRow( "empty string" ) << "" << false; + QTest::newRow( "invalid character" ) << "@" << false; + QTest::newRow( "invalid column reference" ) << "my col" << false; + QTest::newRow( "invalid binary operator" ) << "1+" << false; + QTest::newRow( "invalid function no params" ) << "cos" << false; + QTest::newRow( "invalid function not known" ) << "coz(1)" << false; + QTest::newRow( "invalid operator IN" ) << "x in y" << false; + QTest::newRow( "empty node list" ) << "1 in ()" << false; + QTest::newRow( "invalid sqrt params" ) << "sqrt(2,4)" << false; + QTest::newRow( "special column as function" ) << "$id()" << false; + QTest::newRow( "unknown special column" ) << "$idx" << false; + + // valid strings + QTest::newRow( "null" ) << "NULL" << true; + QTest::newRow( "int literal" ) << "1" << true; + QTest::newRow( "float literal" ) << "1.23" << true; + QTest::newRow( "string literal" ) << "'test'" << true; + QTest::newRow( "column reference" ) << "my_col" << true; + QTest::newRow( "quoted column" ) << "\"my col\"" << true; + QTest::newRow( "unary minus" ) << "--3" << true; + QTest::newRow( "function" ) << "cos(0)" << true; + QTest::newRow( "function2" ) << "atan2(0,1)" << true; + QTest::newRow( "operator IN" ) << "x in (a,b)" << true; + QTest::newRow( "pow" ) << "2 ^ 8" << true; + QTest::newRow( "$id" ) << "$id + 1" << true; + + QTest::newRow( "arithmetics" ) << "1+2*3" << true; + QTest::newRow( "logic" ) << "be or not be" << true; + + } + void parsing() + { + QFETCH( QString, string ); + QFETCH( bool, valid ); + + QgsExpression exp( string ); + + if ( exp.hasParserError() ) + qDebug() << "Parser error: " << exp.parserErrorString(); + else + qDebug() << "Parsed string: " << exp.dump(); + + QCOMPARE( !exp.hasParserError(), valid ); + } + + void evaluation_data() + { + QTest::addColumn( "string" ); + QTest::addColumn( "evalError" ); + QTest::addColumn( "result" ); + + // literal evaluation + QTest::newRow( "literal null" ) << "NULL" << false << QVariant(); + QTest::newRow( "literal int" ) << "123" << false << QVariant( 123 ); + QTest::newRow( "literal double" ) << "1.2" << false << QVariant( 1.2 ); + QTest::newRow( "literal text" ) << "'hello'" << false << QVariant( "hello" ); + + // unary minus + QTest::newRow( "unary minus double" ) << "-1.3" << false << QVariant( -1.3 ); + QTest::newRow( "unary minus int" ) << "-1" << false << QVariant( -1 ); + QTest::newRow( "unary minus text" ) << "-'hello'" << true << QVariant(); + QTest::newRow( "unary minus null" ) << "-null" << true << QVariant(); + + // arithmetics + QTest::newRow( "plus int" ) << "1+3" << false << QVariant( 4 ); + QTest::newRow( "plus double" ) << "1+1.3" << false << QVariant( 2.3 ); + QTest::newRow( "plus with null" ) << "null+3" << false << QVariant(); + QTest::newRow( "plus invalid" ) << "1+'foo'" << true << QVariant(); + QTest::newRow( "minus int" ) << "1-3" << false << QVariant( -2 ); + QTest::newRow( "mul int" ) << "8*7" << false << QVariant( 56 ); + QTest::newRow( "div int" ) << "20/6" << false << QVariant( 3 ); + QTest::newRow( "mod int" ) << "20%6" << false << QVariant( 2 ); + QTest::newRow( "minus double" ) << "5.2-3.1" << false << QVariant( 2.1 ); + QTest::newRow( "mul double" ) << "2.1*5" << false << QVariant( 10.5 ); + QTest::newRow( "div double" ) << "11.0/2" << false << QVariant( 5.5 ); + QTest::newRow( "mod double" ) << "6.1 % 2.5" << false << QVariant( 1.1 ); + QTest::newRow( "pow" ) << "2^8" << false << QVariant( 256. ); + + // comparison + QTest::newRow( "eq int" ) << "1+1 = 2" << false << QVariant( 1 ); + QTest::newRow( "eq double" ) << "3.2 = 2.2+1" << false << QVariant( 1 ); + QTest::newRow( "eq string" ) << "'a' = 'b'" << false << QVariant( 0 ); + QTest::newRow( "eq null" ) << "2 = null" << false << QVariant(); + QTest::newRow( "eq invalid" ) << "'a' = 1" << true << QVariant(); + QTest::newRow( "ne int 1" ) << "3 != 4" << false << QVariant( 1 ); + QTest::newRow( "ne int 2" ) << "3 != 3" << false << QVariant( 0 ); + QTest::newRow( "lt int 1" ) << "3 < 4" << false << QVariant( 1 ); + QTest::newRow( "lt int 2" ) << "3 < 3" << false << QVariant( 0 ); + QTest::newRow( "gt int 1" ) << "3 > 4" << false << QVariant( 0 ); + QTest::newRow( "gt int 2" ) << "3 > 3" << false << QVariant( 0 ); + QTest::newRow( "le int 1" ) << "3 <= 4" << false << QVariant( 1 ); + QTest::newRow( "le int 2" ) << "3 <= 3" << false << QVariant( 1 ); + QTest::newRow( "ge int 1" ) << "3 >= 4" << false << QVariant( 0 ); + QTest::newRow( "ge int 2" ) << "3 >= 3" << false << QVariant( 1 ); + QTest::newRow( "lt text 1" ) << "'bar' < 'foo'" << false << QVariant( 1 ); + QTest::newRow( "lt text 2" ) << "'foo' < 'bar'" << false << QVariant( 0 ); + + // is, is not + QTest::newRow( "is null,null" ) << "null is null" << false << QVariant( 1 ); + QTest::newRow( "is not null,null" ) << "null is not null" << false << QVariant( 0 ); + QTest::newRow( "is null" ) << "1 is null" << false << QVariant( 0 ); + QTest::newRow( "is not null" ) << "1 is not null" << false << QVariant( 1 ); + QTest::newRow( "is int" ) << "1 is 1" << false << QVariant( 1 ); + QTest::newRow( "is not int" ) << "1 is not 1" << false << QVariant( 0 ); + QTest::newRow( "is text" ) << "'x' is 'y'" << false << QVariant( 0 ); + QTest::newRow( "is not text" ) << "'x' is not 'y'" << false << QVariant( 1 ); + + // logical + QTest::newRow( "T or F" ) << "1=1 or 2=3" << false << QVariant( 1 ); + QTest::newRow( "F or F" ) << "1=2 or 2=3" << false << QVariant( 0 ); + QTest::newRow( "T and F" ) << "1=1 and 2=3" << false << QVariant( 0 ); + QTest::newRow( "T and T" ) << "1=1 and 2=2" << false << QVariant( 1 ); + QTest::newRow( "not T" ) << "not 1=1" << false << QVariant( 0 ); + QTest::newRow( "not F" ) << "not 2=3" << false << QVariant( 1 ); + QTest::newRow( "U or F" ) << "null=1 or 2=3" << false << QVariant(); + QTest::newRow( "U and F" ) << "null=1 and 2=3" << false << QVariant( 0 ); + QTest::newRow( "invalid and" ) << "'foo' and 2=3" << true << QVariant(); + QTest::newRow( "invalid or" ) << "'foo' or 2=3" << true << QVariant(); + QTest::newRow( "invalid not" ) << "not 'foo'" << true << QVariant(); + + // in, not in + QTest::newRow( "in 1" ) << "1 in (1,2,3)" << false << QVariant( 1 ); + QTest::newRow( "in 2" ) << "1 in (1,null,3)" << false << QVariant( 1 ); + QTest::newRow( "in 3" ) << "1 in (null,2,3)" << false << QVariant(); + QTest::newRow( "in 4" ) << "null in (1,2,3)" << false << QVariant(); + QTest::newRow( "not in 1" ) << "1 not in (1,2,3)" << false << QVariant( 0 ); + QTest::newRow( "not in 2" ) << "1 not in (1,null,3)" << false << QVariant( 0 ); + QTest::newRow( "not in 3" ) << "1 not in (null,2,3)" << false << QVariant(); + QTest::newRow( "not in 4" ) << "null not in (1,2,3)" << false << QVariant(); + + // regexp, like + QTest::newRow( "like 1" ) << "'hello' like '%ll_'" << false << QVariant( 1 ); + QTest::newRow( "like 2" ) << "'hello' like 'lo'" << false << QVariant( 0 ); + QTest::newRow( "like 3" ) << "'hello' like '%LO'" << false << QVariant( 0 ); + QTest::newRow( "ilike" ) << "'hello' ilike '%LO'" << false << QVariant( 1 ); + QTest::newRow( "regexp 1" ) << "'hello' ~ 'll'" << false << QVariant( 1 ); + QTest::newRow( "regexp 2" ) << "'hello' ~ '^ll'" << false << QVariant( 0 ); + QTest::newRow( "regexp 3" ) << "'hello' ~ 'llo$'" << false << QVariant( 1 ); + + // concatenation + QTest::newRow( "concat" ) << "'a' || 'b'" << false << QVariant( "ab" ); + QTest::newRow( "concat with int" ) << "'a' || 1" << false << QVariant( "a1" ); + QTest::newRow( "concat with int" ) << "2 || 'b'" << false << QVariant( "2b" ); + QTest::newRow( "concat with null" ) << "'a' || null" << false << QVariant(); + QTest::newRow( "invalid concat" ) << "1 || 2" << true << QVariant(); + + // math functions + QTest::newRow( "sqrt" ) << "sqrt(16)" << false << QVariant( 4. ); + QTest::newRow( "invalid sqrt value" ) << "sqrt('a')" << true << QVariant(); + QTest::newRow( "sin 0" ) << "sin(0)" << false << QVariant( 0. ); + QTest::newRow( "cos 0" ) << "cos(0)" << false << QVariant( 1. ); + QTest::newRow( "tan 0" ) << "tan(0)" << false << QVariant( 0. ); + QTest::newRow( "asin 0" ) << "asin(0)" << false << QVariant( 0. ); + QTest::newRow( "acos 1" ) << "acos(1)" << false << QVariant( 0. ); + QTest::newRow( "atan 0" ) << "atan(0)" << false << QVariant( 0. ); + QTest::newRow( "atan2(0,1)" ) << "atan2(0,1)" << false << QVariant( 0. ); + QTest::newRow( "atan2(1,0)" ) << "atan2(1,0)" << false << QVariant( M_PI / 2 ); + + // cast functions + QTest::newRow( "double to int" ) << "toint(3.2)" << false << QVariant( 3 ); + QTest::newRow( "text to int" ) << "toint('53')" << false << QVariant( 53 ); + QTest::newRow( "null to int" ) << "toint(null)" << false << QVariant(); + QTest::newRow( "int to double" ) << "toreal(3)" << false << QVariant( 3. ); + QTest::newRow( "text to double" ) << "toreal('53.1')" << false << QVariant( 53.1 ); + QTest::newRow( "null to double" ) << "toreal(null)" << false << QVariant(); + QTest::newRow( "int to text" ) << "tostring(6)" << false << QVariant( "6" ); + QTest::newRow( "double to text" ) << "tostring(1.23)" << false << QVariant( "1.23" ); + QTest::newRow( "null to text" ) << "tostring(null)" << false << QVariant(); + + // string functions + QTest::newRow( "lower" ) << "lower('HeLLo')" << false << QVariant( "hello" ); + QTest::newRow( "upper" ) << "upper('HeLLo')" << false << QVariant( "HELLO" ); + QTest::newRow( "length" ) << "length('HeLLo')" << false << QVariant( 5 ); + QTest::newRow( "replace" ) << "replace('HeLLo', 'LL', 'xx')" << false << QVariant( "Hexxo" ); + QTest::newRow( "regexp_replace" ) << "regexp_replace('HeLLo','[eL]+', '-')" << false << QVariant( "H-o" ); + QTest::newRow( "regexp_replace invalid" ) << "regexp_replace('HeLLo','[[[', '-')" << true << QVariant(); + QTest::newRow( "substr" ) << "substr('HeLLo', 3,2)" << false << QVariant( "LL" ); + QTest::newRow( "substr outside" ) << "substr('HeLLo', -5,2)" << false << QVariant( "" ); + } + + void evaluation() + { + QFETCH( QString, string ); + QFETCH( bool, evalError ); + QFETCH( QVariant, result ); + + QgsExpression exp( string ); + QCOMPARE( exp.hasParserError(), false ); + + QVariant res = exp.evaluate(); + if ( exp.hasEvalError() ) + qDebug() << exp.evalErrorString(); + //qDebug() << res.type() << " " << result.type(); + //qDebug() << "type " << res.typeName(); + QCOMPARE( exp.hasEvalError(), evalError ); + + QCOMPARE( res.type(), result.type() ); + switch ( res.type() ) + { + case QVariant::Invalid: break; // nothing more to check + case QVariant::Int: + QCOMPARE( res.toInt(), result.toInt() ); + break; + case QVariant::Double: + QCOMPARE( res.toDouble(), result.toDouble() ); + break; + case QVariant::String: + QCOMPARE( res.toString(), result.toString() ); + break; + default: + Q_ASSERT( false ); // should never happen + } + } + + void eval_columns() + { + QgsFieldMap fields; + fields[4] = QgsField( "foo", QVariant::Int ); + + QgsFeature f; + f.addAttribute( 4, QVariant( 20 ) ); + + // good exp + QgsExpression exp( "foo + 1" ); + bool prepareRes = exp.prepare( fields ); + QCOMPARE( prepareRes, true ); + QCOMPARE( exp.hasEvalError(), false ); + QVariant res = exp.evaluate( &f ); + QCOMPARE( res.type(), QVariant::Int ); + QCOMPARE( res.toInt(), 21 ); + + // bad exp + QgsExpression exp2( "bar + 1" ); + bool prepareRes2 = exp2.prepare( fields ); + QCOMPARE( prepareRes2, false ); + QCOMPARE( exp2.hasEvalError(), true ); + QVariant res2 = exp2.evaluate( &f ); + QCOMPARE( res2.type(), QVariant::Invalid ); + } + + void eval_rownum() + { + QgsExpression exp( "$rownum + 1" ); + QVariant v1 = exp.evaluate(); + QCOMPARE( v1.toInt(), 1 ); + + exp.setCurrentRowNumber( 100 ); + QVariant v2 = exp.evaluate(); + QCOMPARE( v2.toInt(), 101 ); + } + + void eval_feature_id() + { + QgsFeature f( 100 ); + QgsExpression exp( "$id * 2" ); + QVariant v = exp.evaluate( &f ); + QCOMPARE( v.toInt(), 200 ); + } + + void referenced_columns() + { + QSet expectedCols; + expectedCols << "foo" << "bar"; + QgsExpression exp( "length(Bar || FOO) = 4 or foo + sqrt(bar) > 0" ); + QCOMPARE( exp.hasParserError(), false ); + QStringList refCols = exp.referencedColumns(); + // make sure we have lower case + QSet refColsSet; + foreach( QString col, refCols ) + refColsSet.insert( col.toLower() ); + + QCOMPARE( refColsSet, expectedCols ); + } + + void needs_geometry_data() + { + QTest::addColumn( "string" ); + QTest::addColumn( "needsGeom" ); + + // literal evaluation + QTest::newRow( "geom 1" ) << "x > 0" << false; + QTest::newRow( "geom 2" ) << "$x > 0" << true; + QTest::newRow( "geom 2" ) << "xat(0) > 0" << true; + } + + void needs_geometry() + { + QFETCH( QString, string ); + QFETCH( bool, needsGeom ); + + QgsExpression exp( string ); + QCOMPARE( exp.hasParserError(), false ); + QCOMPARE( exp.needsGeometry(), needsGeom ); + } + + void eval_geometry_data() + { + QTest::addColumn( "string" ); + QTest::addColumn( "geomptr" ); + QTest::addColumn( "evalError" ); + QTest::addColumn( "result" ); + + QgsPoint point( 123, 456 ); + QgsPolyline line; + line << QgsPoint( 1, 1 ) << QgsPoint( 4, 2 ) << QgsPoint( 3, 1 ); + + QTest::newRow( "geom x" ) << "$x" << ( void* ) QgsGeometry::fromPoint( point ) << false << 123.; + QTest::newRow( "geom y" ) << "$y" << ( void* ) QgsGeometry::fromPoint( point ) << false << 456.; + QTest::newRow( "geom xat" ) << "xat(-1)" << ( void* ) QgsGeometry::fromPolyline( line ) << false << 3.; + QTest::newRow( "geom yat" ) << "yat(1)" << ( void* ) QgsGeometry::fromPolyline( line ) << false << 2.; + } + + void eval_geometry() + { + QFETCH( QString, string ); + QFETCH( void*, geomptr ); + QFETCH( bool, evalError ); + QFETCH( double, result ); + + QgsGeometry* geom = ( QgsGeometry* ) geomptr; + + QgsFeature f; + f.setGeometry( geom ); + + QgsExpression exp( string ); + QCOMPARE( exp.hasParserError(), false ); + QCOMPARE( exp.needsGeometry(), true ); + QVariant out = exp.evaluate( &f ); + QCOMPARE( exp.hasEvalError(), evalError ); + QCOMPARE( out.toDouble(), result ); + } + + void eval_geometry_calc() + { + QgsPolyline polyline, polygon_ring; + polyline << QgsPoint( 0, 0 ) << QgsPoint( 10, 0 ); + polygon_ring << QgsPoint( 1, 1 ) << QgsPoint( 6, 1 ) << QgsPoint( 6, 6 ) << QgsPoint( 1, 6 ) << QgsPoint( 1, 1 ); + QgsPolygon polygon; + polygon << polygon_ring; + QgsFeature fPolygon, fPolyline; + fPolyline.setGeometry( QgsGeometry::fromPolyline( polyline ) ); + fPolygon.setGeometry( QgsGeometry::fromPolygon( polygon ) ); + + QgsExpression exp1( "$area" ); + QVariant vArea = exp1.evaluate( &fPolygon ); + QCOMPARE( vArea.toDouble(), 25. ); + + QgsExpression exp2( "$length" ); + QVariant vLength = exp2.evaluate( &fPolyline ); + QCOMPARE( vLength.toDouble(), 10. ); + + QgsExpression exp3( "$perimeter" ); + QVariant vPerimeter = exp3.evaluate( &fPolygon ); + QCOMPARE( vPerimeter.toDouble(), 20. ); + } +}; + +QTEST_MAIN( TestQgsExpression ) + +#include "moc_testqgsexpression.cxx" +