Add expression functions for handling date/time:

- year,month,day,hour,minute,second extraction
    - new QgsInterval objects - handles time amounts
    - age() function returns the difference between two  datetimes
    - $now returns the current date and time
    - support for datetime - interval = new datetime
    - tointerval, todate, totime functions
    - tests ;)
This commit is contained in:
Nathan Woodrow 2012-06-30 19:29:07 +10:00
parent 4b81e3601c
commit c467e79f3c
5 changed files with 413 additions and 3 deletions

View File

@ -1,4 +1,3 @@
class QgsExpression
{
%TypeHeaderCode
@ -304,7 +303,24 @@ public:
virtual void visit( QgsExpression::NodeCondition* n ) = 0;
};
class Interval
{
public:
Interval(int seconds);
~Interval();
int years();
int weeks();
int days();
int hours();
int minutes();
int seconds();
bool isValid();
void setValid(bool valid);
bool operator==(const QgsExpression::Interval& other) const;
static QgsExpression::Interval invalidInterVal();
static QgsExpression::Interval fromString(QString string);
};
/** entry function for the visitor pattern */
void acceptVisitor( QgsExpression::Visitor& v );
};

View File

@ -18,6 +18,8 @@
#include <QtDebug>
#include <QDomDocument>
#include <QSettings>
#include <QDate>
#include <QRegExp>
#include <math.h>
#include <limits>
@ -30,6 +32,79 @@
// from parser
extern QgsExpression::Node* parseExpression( const QString& str, QString& parserErrorMsg );
QgsExpression::Interval::~Interval() {}
QgsExpression::Interval QgsExpression::Interval::invalidInterVal()
{
QgsExpression::Interval inter = QgsExpression::Interval();
inter.setValid( false );
return inter;
}
QgsExpression::Interval QgsExpression::Interval::fromString( QString string )
{
int seconds = 0;
QRegExp rx( "(\\d?\\.?\\d+\\s+[a-z]+)", Qt::CaseInsensitive );
QStringList list;
int pos = 0;
while (( pos = rx.indexIn( string, pos ) ) != -1 )
{
list << rx.cap( 1 );
pos += rx.matchedLength();
}
foreach( QString match, list )
{
QStringList split = match.split( QRegExp( "\\s+" ) );
bool ok;
int value = split.at( 0 ).toInt( &ok );
if ( !ok )
{
continue;
}
if ( match.contains( "day", Qt::CaseInsensitive ) ||
match.contains( QObject::tr("day", "Note: Word is part matched in code"), Qt::CaseInsensitive )||
match.contains( QObject::tr("days", "Note: Word is part matched in code"), Qt::CaseInsensitive) )
seconds += value * QgsExpression::Interval::DAY;
if ( match.contains( "week", Qt::CaseInsensitive ) ||
match.contains( QObject::tr("week", "Note: Word is part matched in code"), Qt::CaseInsensitive ) ||
match.contains( QObject::tr("weeks", "Note: Word is part matched in code"), Qt::CaseInsensitive ) )
seconds += value * QgsExpression::Interval::WEEKS;
if ( match.contains( "month", Qt::CaseInsensitive ) ||
match.contains( QObject::tr("month", "Note: Word is part matched in code"), Qt::CaseInsensitive ) ||
match.contains( QObject::tr("months", "Note: Word is part matched in code"), Qt::CaseInsensitive ) )
seconds += value * QgsExpression::Interval::MONTHS;
if ( match.contains( "year", Qt::CaseInsensitive ) ||
match.contains( QObject::tr("year", "Note: Word is part matched in code"), Qt::CaseInsensitive ) ||
match.contains( QObject::tr("years", "Note: Word is part matched in code"), Qt::CaseInsensitive ) )
seconds += value * QgsExpression::Interval::YEARS;
if ( match.contains( "second", Qt::CaseInsensitive ) ||
match.contains( QObject::tr("second", "Note: Word is part matched in code"), Qt::CaseInsensitive ) ||
match.contains( QObject::tr("seconds", "Note: Word is part matched in code"), Qt::CaseInsensitive ) )
seconds += value;
if ( match.contains( "minute", Qt::CaseInsensitive ) ||
match.contains( QObject::tr("minute", "Note: Word is part matched in code"), Qt::CaseInsensitive ) ||
match.contains( QObject::tr("minutes", "Note: Word is part matched in code"), Qt::CaseInsensitive ) )
seconds += value * QgsExpression::Interval::MINUTE;
if ( match.contains( "hour", Qt::CaseInsensitive ) ||
match.contains( QObject::tr("hour", "Note: Word is part matched in code"), Qt::CaseInsensitive ) ||
match.contains( QObject::tr("hours", "Note: Word is part matched in code"), Qt::CaseInsensitive ) )
seconds += value * QgsExpression::Interval::HOUR;
}
// If we can't parse the string at all then we just return invalid
if ( seconds == 0 )
return QgsExpression::Interval::invalidInterVal();
return QgsExpression::Interval( seconds );
}
bool QgsExpression::Interval::operator==( const QgsExpression::Interval& other ) const
{
return ( mSeconds == other.mSeconds );
}
///////////////////////////////////////////////
// three-value logic
@ -91,6 +166,26 @@ inline bool isDoubleSafe( const QVariant& v )
return false;
}
inline bool isDateTimeSafe( const QVariant& v )
{
return v.type() == QVariant::DateTime || v.type() == QVariant::Date ||
v.type() == QVariant::Time;
}
inline bool isIntervalSafe( const QVariant& v )
{
if ( v.canConvert<QgsExpression::Interval>() )
{
return true;
}
if ( v.type() == QVariant::String )
{
return QgsExpression::Interval::fromString( v.toString() ).isValid();
}
return false;
}
inline bool isNull( const QVariant& v ) { return v.isNull(); }
///////////////////////////////////////////////
@ -167,6 +262,65 @@ static int getIntValue( const QVariant& value, QgsExpression* parent )
}
}
static QDateTime getDateTimeValue( const QVariant& value, QgsExpression* parent )
{
QDateTime d = value.toDateTime();
if ( d.isValid() )
{
return d;
}
else
{
parent->setEvalErrorString( QObject::tr( "Cannot convert '%1' to DateTime" ).arg( value.toString() ) );
return QDateTime();
}
}
static QDate getDateValue( const QVariant& value, QgsExpression* parent )
{
QDate d = value.toDate();
if ( d.isValid() )
{
return d;
}
else
{
parent->setEvalErrorString( QObject::tr( "Cannot convert '%1' to Date" ).arg( value.toString() ) );
return QDate();
}
}
static QTime getTimeValue( const QVariant& value, QgsExpression* parent )
{
QTime t = value.toTime();
if ( t.isValid() )
{
return t;
}
else
{
parent->setEvalErrorString( QObject::tr( "Cannot convert '%1' to Time" ).arg( value.toString() ) );
return QTime();
}
}
static QgsExpression::Interval getInterval( const QVariant& value, QgsExpression* parent, bool report_error = false )
{
if ( value.canConvert<QgsExpression::Interval>() )
return value.value<QgsExpression::Interval>();
QgsExpression::Interval inter = QgsExpression::Interval::fromString( value.toString() );
if ( inter.isValid() )
{
return inter;
}
// If we get here then we can't convert so we just error and return invalid.
if ( report_error )
parent->setEvalErrorString( QObject::tr( "Cannot convert '%1' to Interval" ).arg( value.toString() ) );
return QgsExpression::Interval::invalidInterVal();
}
// this handles also NULL values
static TVL getTVLValue( const QVariant& value, QgsExpression* parent )
@ -270,6 +424,12 @@ static QVariant fcnToString( const QVariantList& values, QgsFeature* , QgsExpres
{
return QVariant( getStringValue( values.at( 0 ), parent ) );
}
static QVariant fcnToDateTime( const QVariantList& values, QgsFeature* , QgsExpression* parent )
{
return QVariant( getDateTimeValue( values.at( 0 ), parent ) );
}
static QVariant fcnCoalesce( const QVariantList& values, QgsFeature* , QgsExpression* )
{
foreach( const QVariant &value, values )
@ -344,6 +504,140 @@ static QVariant fcnConcat( const QVariantList& values, QgsFeature* , QgsExpressi
return concat;
}
static QVariant fcnNow( const QVariantList&, QgsFeature* , QgsExpression * )
{
return QVariant( QDateTime::currentDateTime() );
}
static QVariant fcnToDate( const QVariantList& values, QgsFeature* , QgsExpression * parent )
{
return QVariant( getDateValue( values.at( 0 ), parent ) );
}
static QVariant fcnToTime( const QVariantList& values, QgsFeature* , QgsExpression * parent )
{
return QVariant( getTimeValue( values.at( 0 ), parent ) );
}
static QVariant fcnToInterval( const QVariantList& values, QgsFeature* , QgsExpression * parent )
{
return QVariant::fromValue( getInterval( values.at( 0 ), parent ) );
}
static QVariant fcnAge( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QDateTime d1 = getDateTimeValue( values.at( 0 ), parent );
QDateTime d2 = getDateTimeValue( values.at( 1 ), parent );
int seconds = d2.secsTo( d1 );
return QVariant::fromValue( QgsExpression::Interval( seconds ) );
}
static QVariant fcnDay( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QVariant value = values.at( 0 );
QgsExpression::Interval inter = getInterval( value, parent, false );
if ( inter.isValid() )
{
return QVariant( inter.days() );
}
else
{
QDateTime d1 = getDateTimeValue( value, parent );
return QVariant( d1.date().day() );
}
}
static QVariant fcnYear( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QVariant value = values.at( 0 );
QgsExpression::Interval inter = getInterval( value, parent, false );
if ( inter.isValid() )
{
return QVariant( inter.years() );
}
else
{
QDateTime d1 = getDateTimeValue( value, parent );
return QVariant( d1.date().year() );
}
}
static QVariant fcnMonth( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QVariant value = values.at( 0 );
QgsExpression::Interval inter = getInterval( value, parent, false );
if ( inter.isValid() )
{
return QVariant( inter.months() );
}
else
{
QDateTime d1 = getDateTimeValue( value, parent );
return QVariant( d1.date().month() );
}
}
static QVariant fcnWeek( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QVariant value = values.at( 0 );
QgsExpression::Interval inter = getInterval( value, parent, false );
if ( inter.isValid() )
{
return QVariant( inter.weeks() );
}
else
{
QDateTime d1 = getDateTimeValue( value, parent );
return QVariant( d1.date().weekNumber() );
}
}
static QVariant fcnHour( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QVariant value = values.at( 0 );
QgsExpression::Interval inter = getInterval( value, parent, false );
if ( inter.isValid() )
{
return QVariant( inter.hours() );
}
else
{
QDateTime d1 = getDateTimeValue( value, parent );
return QVariant( d1.time().hour() );
}
}
static QVariant fcnMinute( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QVariant value = values.at( 0 );
QgsExpression::Interval inter = getInterval( value, parent, false );
if ( inter.isValid() )
{
return QVariant( inter.minutes() );
}
else
{
QDateTime d1 = getDateTimeValue( value, parent );
return QVariant( d1.time().minute() );
}
}
static QVariant fcnSeconds( const QVariantList& values, QgsFeature* , QgsExpression *parent )
{
QVariant value = values.at( 0 );
QgsExpression::Interval inter = getInterval( value, parent, false );
if ( inter.isValid() )
{
return QVariant( inter.seconds() );
}
else
{
QDateTime d1 = getDateTimeValue( value, parent );
return QVariant( d1.time().second() );
}
}
#define ENSURE_GEOM_TYPE(f, g, geomtype) if (!f) return QVariant(); \
QgsGeometry* g = f->geometry(); \
if (!g || g->type() != geomtype) return QVariant();
@ -450,7 +744,21 @@ const QList<QgsExpression::FunctionDef> &QgsExpression::BuiltinFunctions()
<< FunctionDef( "toint", 1, fcnToInt, QObject::tr( "Conversions" ) )
<< FunctionDef( "toreal", 1, fcnToReal, QObject::tr( "Conversions" ) )
<< FunctionDef( "tostring", 1, fcnToString, QObject::tr( "Conversions" ) )
<< FunctionDef( "todatetime", 1, fcnToDateTime, QObject::tr( "Conversions" ) )
<< FunctionDef( "todate", 1, fcnToDate, QObject::tr( "Conversions" ) )
<< FunctionDef( "totime", 1, fcnToTime, QObject::tr( "Conversions" ) )
<< FunctionDef( "tointerval", 1, fcnToInterval, QObject::tr( "Conversions" ) )
<< FunctionDef( "coalesce", -1, fcnCoalesce, QObject::tr( "Conversions" ) )
// date/time
<< FunctionDef( "$now", 0, fcnNow, QObject::tr( "Date/Time" ) )
<< FunctionDef( "age", 2, fcnAge, QObject::tr( "Date/Time" ) )
<< FunctionDef( "year", 1, fcnYear, QObject::tr( "Date/Time" ) )
<< FunctionDef( "month", 1, fcnMonth, QObject::tr( "Date/Time" ) )
<< FunctionDef( "week", 1, fcnWeek, QObject::tr( "Date/Time" ) )
<< FunctionDef( "day", 1, fcnDay, QObject::tr( "Date/Time" ) )
<< FunctionDef( "hour", 1, fcnHour, QObject::tr( "Date/Time" ) )
<< FunctionDef( "minute", 1, fcnMinute, QObject::tr( "Date/Time" ) )
<< FunctionDef( "second", 1, fcnSeconds, QObject::tr( "Date/Time" ) )
// string manipulation
<< FunctionDef( "lower", 1, fcnLower, QObject::tr( "String" ) )
<< FunctionDef( "upper", 1, fcnUpper, QObject::tr( "String" ) )
@ -905,6 +1213,17 @@ QVariant QgsExpression::NodeBinaryOperator::eval( QgsExpression* parent, QgsFeat
if ( mOp == boDiv && iR == 0 ) return QVariant(); // silently handle division by zero and return NULL
return QVariant( computeInt( iL, iR ) );
}
else if ( isDateTimeSafe( vL ) && isIntervalSafe( vR ) )
{
QDateTime dL = getDateTimeValue( vL, parent ); ENSURE_NO_EVAL_ERROR;
QgsExpression::Interval iL = getInterval( vR, parent ); ENSURE_NO_EVAL_ERROR;
if ( mOp == boDiv || mOp == boMul || mOp == boMod )
{
parent->setEvalErrorString( QObject::tr("Can't preform /, *, or % on DateTime and Interval") );
return QVariant();
}
return QVariant( computeDateTimeFromInterval( dL, &iL ) );
}
else
{
// general floating point arithmetic
@ -1068,6 +1387,16 @@ int QgsExpression::NodeBinaryOperator::computeInt( int x, int y )
}
}
QDateTime QgsExpression::NodeBinaryOperator::computeDateTimeFromInterval( QDateTime d, QgsExpression::Interval *i )
{
switch ( mOp )
{
case boPlus: return d.addSecs( i->seconds() );
case boMinus: return d.addSecs( -i->seconds() );
default: Q_ASSERT( false ); return QDateTime();
}
}
double QgsExpression::NodeBinaryOperator::computeDouble( double x, double y )
{
switch ( mOp )

View File

@ -16,6 +16,7 @@
#ifndef QGSEXPRESSION_H
#define QGSEXPRESSION_H
#include <QMetaType>
#include <QStringList>
#include <QVariant>
#include <QList>
@ -256,6 +257,36 @@ class CORE_EXPORT QgsExpression
QList<Node*> mList;
};
class CORE_EXPORT Interval
{
// YEAR const value taken from postgres query
// SELECT EXTRACT(EPOCH FROM interval '1 year')
static const double YEARS = ( 31557600 );
static const double MONTHS = ( 60 * 60 * 24 * 30 );
static const double WEEKS = ( 60 * 60 * 24 * 7 );
static const double DAY = ( 60 * 60 * 24 );
static const double HOUR = ( 60 * 60 );
static const double MINUTE = ( 60 );
public:
Interval( double seconds = 0 ) { mSeconds = seconds; }
~Interval();
double years() { return mSeconds / YEARS;}
double months() { return mSeconds / MONTHS; }
double weeks() { return mSeconds / WEEKS;}
double days() { return mSeconds / DAY;}
double hours() { return mSeconds / HOUR;}
double minutes() { return mSeconds / MINUTE;}
bool isValid() { return mValid; }
void setValid( bool valid ) { mValid = valid; }
double seconds() { return mSeconds; }
bool operator==( const QgsExpression::Interval& other ) const;
static QgsExpression::Interval invalidInterVal();
static QgsExpression::Interval fromString( QString string );
private:
double mSeconds;
bool mValid;
};
class CORE_EXPORT NodeUnaryOperator : public Node
{
public:
@ -306,6 +337,7 @@ class CORE_EXPORT QgsExpression
bool compare( double diff );
int computeInt( int x, int y );
double computeDouble( double x, double y );
QDateTime computeDateTimeFromInterval( QDateTime d, QgsExpression::Interval *i );
BinaryOperator mOp;
Node* mOpLeft;
@ -483,4 +515,6 @@ class CORE_EXPORT QgsExpression
QgsDistanceArea* mCalc;
};
Q_DECLARE_METATYPE( QgsExpression::Interval )
#endif // QGSEXPRESSION_H

View File

@ -165,7 +165,8 @@ expression:
exp_error("Function is not known");
YYERROR;
}
if ( QgsExpression::BuiltinFunctions()[fnIndex].mParams != -1 && QgsExpression::BuiltinFunctions()[fnIndex].mParams != $3->count() )
if ( QgsExpression::BuiltinFunctions()[fnIndex].mParams != -1
&& QgsExpression::BuiltinFunctions()[fnIndex].mParams != $3->count() )
{
exp_error("Function is called with wrong number of arguments");
YYERROR;

View File

@ -270,6 +270,18 @@ class TestQgsExpression: public QObject
QTest::newRow( "condition else" ) << "case when 1=0 then 'bad' else 678 end" << false << QVariant( 678 );
QTest::newRow( "condition null" ) << "case when length(123)=0 then 111 end" << false << QVariant();
QTest::newRow( "condition 2 when" ) << "case when 2>3 then 23 when 3>2 then 32 else 0 end" << false << QVariant( 32 );
// Datetime functions
QTest::newRow( "to date" ) << "todate('2012-06-28')" << false << QVariant( QDate( 2012, 06, 28 ) );
QTest::newRow( "to interval" ) << "tointerval('1 Year 1 Month 1 Week 1 Hour 1 Minute')" << false << QVariant::fromValue( QgsExpression::Interval( 34758060 ) );
QTest::newRow( "day with date" ) << "day('2012-06-28')" << false << QVariant( 28 );
QTest::newRow( "day with interval" ) << "day(tointerval('28 days'))" << false << QVariant( 28.0 );
QTest::newRow( "month with date" ) << "month('2012-06-28')" << false << QVariant( 6 );
QTest::newRow( "month with interval" ) << "month(tointerval('2 months'))" << false << QVariant( 2.0 );
QTest::newRow( "year with date" ) << "year('2012-06-28')" << false << QVariant( 2012 );
QTest::newRow( "year with interval" ) << "year(tointerval('2 years'))" << false << QVariant( 2.0 );
QTest::newRow( "age" ) << "age('2012-06-30','2012-06-28')" << false << QVariant::fromValue( QgsExpression::Interval( 172800 ) );
QTest::newRow( "negative age" ) << "age('2012-06-28','2012-06-30')" << false << QVariant::fromValue( QgsExpression::Interval( -172800 ) );
}
void evaluation()
@ -280,6 +292,8 @@ class TestQgsExpression: public QObject
QgsExpression exp( string );
QCOMPARE( exp.hasParserError(), false );
if ( exp.hasParserError() )
qDebug() << exp.parserErrorString();
QVariant res = exp.evaluate();
if ( exp.hasEvalError() )
@ -305,6 +319,22 @@ class TestQgsExpression: public QObject
case QVariant::String:
QCOMPARE( res.toString(), result.toString() );
break;
case QVariant::Date:
QCOMPARE( res.toDate(), result.toDate() );
break;
case QVariant::DateTime:
QCOMPARE( res.toDateTime(), result.toDateTime() );
break;
case QVariant::Time:
QCOMPARE( res.toTime(), result.toTime() );
break;
case QVariant::UserType:
{
QgsExpression::Interval inter = res.value<QgsExpression::Interval>();
QgsExpression::Interval gotinter = result.value<QgsExpression::Interval>();
QCOMPARE( inter.seconds(), gotinter.seconds() );
break;
}
default:
Q_ASSERT( false ); // should never happen
}