Skip to content
Permalink
Browse files

Merge pull request #33204 from olivierdalang/feature_random_seed_squa…

…shed

[FEATURE][expressions] allow to seed random functions
  • Loading branch information
m-kuhn committed Dec 17, 2019
2 parents 30b2919 + 3eeaddd commit 98f8a897044dc758eacdc1d5594edad415ae2bd8
@@ -1,8 +1,9 @@
{
"name": "rand",
"type": "function",
"description": "Returns a random integer within the range specified by the minimum and maximum argument (inclusive).",
"description": "Returns a random integer within the range specified by the minimum and maximum argument (inclusive). If a seed is provided, the returned will always be the same, depending on the seed.",
"arguments": [ {"arg":"min","description":"an integer representing the smallest possible random number desired"},
{"arg":"max","description":"an integer representing the largest possible random number desired"}],
{"arg":"max","description":"an integer representing the largest possible random number desired"},
{"arg":"seed","optional":true,"default":"null","description":"any value to use as seed"}],
"examples": [ { "expression":"rand(1, 10)", "returns":"8"}]
}
@@ -1,8 +1,9 @@
{
"name": "randf",
"type": "function",
"description": "Returns a random float within the range specified by the minimum and maximum argument (inclusive).",
"description": "Returns a random float within the range specified by the minimum and maximum argument (inclusive). If a seed is provided, the returned will always be the same, depending on the seed.",
"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"}]
{"arg":"max","optional":true,"default":"1.0","description":"an float representing the largest possible random number desired"},
{"arg":"seed","optional":true,"default":"null","description":"any value to use as seed"}],
"examples": [ { "expression":"randf(1, 10)", "returns":"4.59258286403147"} ]
}
@@ -13,6 +13,9 @@
* *
***************************************************************************/


#include <random>

#include "qgscoordinateformatter.h"
#include "qgsexpressionfunction.h"
#include "qgsexpressionutils.h"
@@ -371,8 +374,29 @@ static QVariant fcnRndF( const QVariantList &values, const QgsExpressionContext
if ( max < min )
return QVariant();

std::random_device rd;
std::mt19937_64 generator( rd() );

if ( !QgsExpressionUtils::isNull( values.at( 2 ) ) )
{
quint32 seed;
if ( QgsExpressionUtils::isIntSafe( values.at( 2 ) ) )
{
// if seed can be converted to int, we use as is
seed = QgsExpressionUtils::getIntValue( values.at( 2 ), parent );
}
else
{
// if not, we hash string representation to int
QString seedStr = QgsExpressionUtils::getStringValue( values.at( 2 ), parent );
std::hash<std::string> hasher;
seed = hasher( seedStr.toStdString() );
}
generator.seed( seed );
}

// Return a random double in the range [min, max] (inclusive)
double f = static_cast< double >( qrand() ) / RAND_MAX;
double f = static_cast< double >( generator() ) / generator.max();
return QVariant( min + f * ( max - min ) );
}
static QVariant fcnRnd( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
@@ -382,8 +406,29 @@ static QVariant fcnRnd( const QVariantList &values, const QgsExpressionContext *
if ( max < min )
return QVariant();

std::random_device rd;
std::mt19937_64 generator( rd() );

if ( !QgsExpressionUtils::isNull( values.at( 2 ) ) )
{
quint32 seed;
if ( QgsExpressionUtils::isIntSafe( values.at( 2 ) ) )
{
// if seed can be converted to int, we use as is
seed = QgsExpressionUtils::getIntValue( values.at( 2 ), parent );
}
else
{
// if not, we hash string representation to int
QString seedStr = QgsExpressionUtils::getStringValue( values.at( 2 ), parent );
std::hash<std::string> hasher;
seed = hasher( seedStr.toStdString() );
}
generator.seed( seed );
}

// Return a random integer in the range [min, max] (inclusive)
return QVariant( min + ( qrand() % static_cast< qlonglong >( max - min + 1 ) ) );
return QVariant( min + ( generator() % ( max - min + 1 ) ) );
}

static QVariant fcnLinearScale( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
@@ -5234,11 +5279,11 @@ const QList<QgsExpressionFunction *> &QgsExpression::Functions()
<< new QgsStaticExpressionFunction( QStringLiteral( "log" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "base" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ), fcnLog, QStringLiteral( "Math" ) )
<< new QgsStaticExpressionFunction( QStringLiteral( "round" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "value" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "places" ), true, 0 ), fcnRound, QStringLiteral( "Math" ) );

QgsStaticExpressionFunction *randFunc = new QgsStaticExpressionFunction( QStringLiteral( "rand" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "max" ) ), fcnRnd, QStringLiteral( "Math" ) );
QgsStaticExpressionFunction *randFunc = new QgsStaticExpressionFunction( QStringLiteral( "rand" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "min" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "max" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "seed" ), true ), fcnRnd, QStringLiteral( "Math" ) );
randFunc->setIsStatic( false );
functions << randFunc;

QgsStaticExpressionFunction *randfFunc = new QgsStaticExpressionFunction( QStringLiteral( "randf" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "min" ), true, 0.0 ) << QgsExpressionFunction::Parameter( QStringLiteral( "max" ), true, 1.0 ), fcnRndF, QStringLiteral( "Math" ) );
QgsStaticExpressionFunction *randfFunc = new QgsStaticExpressionFunction( QStringLiteral( "randf" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "min" ), true, 0.0 ) << QgsExpressionFunction::Parameter( QStringLiteral( "max" ), true, 1.0 ) << QgsExpressionFunction::Parameter( QStringLiteral( "seed" ), true ), fcnRndF, QStringLiteral( "Math" ) );
randfFunc->setIsStatic( false );
functions << randfFunc;

@@ -2161,8 +2161,8 @@ class TestQgsExpression: public QObject
{
QgsExpression exp1( QStringLiteral( "rand(1,10)" ) );
QVariant v1 = exp1.evaluate();
QCOMPARE( v1.toInt() <= 10, true );
QCOMPARE( v1.toInt() >= 1, true );
QVERIFY2( v1.toInt() <= 10, QString( "Calculated: %1 Expected <= %2" ).arg( QString::number( v1.toInt() ), QString::number( 10 ) ).toUtf8().constData() );
QVERIFY2( v1.toInt() >= 1, QString( "Calculated: %1 Expected >= %2" ).arg( QString::number( v1.toInt() ), QString::number( 1 ) ).toUtf8().constData() );

QgsExpression exp2( QStringLiteral( "rand(min:=-5,max:=-5)" ) );
QVariant v2 = exp2.evaluate();
@@ -2172,23 +2172,83 @@ class TestQgsExpression: public QObject
QgsExpression exp3( QStringLiteral( "rand(10,1)" ) );
QVariant v3 = exp3.evaluate();
QCOMPARE( v3.type(), QVariant::Invalid );

// Supports multiple type of seeds
QgsExpression exp4( QStringLiteral( "rand(1,10,123)" ) );
QVariant v4 = exp4.evaluate();
QVERIFY2( v4.toInt() <= 10, QString( "Calculated: %1 > Expected %2" ).arg( QString::number( v4.toInt() ), QString::number( 10 ) ).toUtf8().constData() );
QVERIFY2( v4.toInt() >= 1, QString( "Calculated: %1 < Expected %2" ).arg( QString::number( v4.toInt() ), QString::number( 1 ) ).toUtf8().constData() );
QgsExpression exp5( QStringLiteral( "rand(1,10,1.23)" ) );
QVariant v5 = exp5.evaluate();
QVERIFY2( v5.toInt() <= 10, QString( "Calculated: %1 > Expected %2" ).arg( QString::number( v5.toInt() ), QString::number( 10 ) ).toUtf8().constData() );
QVERIFY2( v5.toInt() >= 1, QString( "Calculated: %1 < Expected %2" ).arg( QString::number( v5.toInt() ), QString::number( 1 ) ).toUtf8().constData() );
QgsExpression exp6( QStringLiteral( "rand(1,10,'123')" ) );
QVariant v6 = exp6.evaluate();
QVERIFY2( v6.toInt() <= 10, QString( "Calculated: %1 > Expected %2" ).arg( QString::number( v6.toInt() ), QString::number( 10 ) ).toUtf8().constData() );
QVERIFY2( v6.toInt() >= 1, QString( "Calculated: %1 < Expected %2" ).arg( QString::number( v6.toInt() ), QString::number( 1 ) ).toUtf8().constData() );
QgsExpression exp7( QStringLiteral( "rand(1,10,'abc')" ) );
QVariant v7 = exp7.evaluate();
QVERIFY2( v7.toInt() <= 10, QString( "Calculated: %1 > Expected %2" ).arg( QString::number( v7.toInt() ), QString::number( 10 ) ).toUtf8().constData() );
QVERIFY2( v7.toInt() >= 1, QString( "Calculated: %1 < Expected %2" ).arg( QString::number( v7.toInt() ), QString::number( 1 ) ).toUtf8().constData() );

// Two calls with the same seed always return the same number
QgsExpression exp8( QStringLiteral( "rand(1,1000000000,1)" ) );
QVariant v8 = exp8.evaluate();
QCOMPARE( v8.toInt(), 546311529 );

// Two calls with a different seed return a different number
QgsExpression exp9( QStringLiteral( "rand(1,100000000000,1)" ) );
QVariant v9 = exp9.evaluate();
QgsExpression exp10( QStringLiteral( "rand(1,100000000000,2)" ) );
QVariant v10 = exp10.evaluate();
QVERIFY2( v9.toInt() != v10.toInt(), QStringLiteral( "Calculated: %1 Expected != %2" ).arg( QString::number( v9.toInt() ), QString::number( v10.toInt() ) ).toUtf8().constData() );
}

void eval_randf()
{
QgsExpression exp1( QStringLiteral( "randf(1.5,9.5)" ) );
QVariant v1 = exp1.evaluate();
QCOMPARE( v1.toDouble() <= 9.5, true );
QCOMPARE( v1.toDouble() >= 1.5, true );
QVERIFY2( v1.toDouble() <= 9.5, QStringLiteral( "Calculated: %1 Expected <= %2" ).arg( QString::number( v1.toDouble() ), QString::number( 9.5 ) ).toUtf8().constData() );
QVERIFY2( v1.toDouble() >= 1.5, QStringLiteral( "Calculated: %1 Expected >= %2" ).arg( QString::number( v1.toDouble() ), QString::number( 1.5 ) ).toUtf8().constData() );

QgsExpression exp2( QStringLiteral( "randf(min:=-0.0005,max:=-0.0005)" ) );
QVariant v2 = exp2.evaluate();
QCOMPARE( v2.toDouble(), -0.0005 );
QVERIFY2( qgsDoubleNear( v2.toDouble(), -0.0005 ), QStringLiteral( "Calculated: %1 != Expected %2" ).arg( QString::number( v2.toDouble() ), QString::number( -0.0005 ) ).toUtf8().constData() );

// Invalid expression since max<min
QgsExpression exp3( QStringLiteral( "randf(9.3333,1.784)" ) );
QVariant v3 = exp3.evaluate();
QCOMPARE( v3.type(), QVariant::Invalid );

// Supports multiple type of seeds
QgsExpression exp4( QStringLiteral( "randf(1.5,9.5,123)" ) );
QVariant v4 = exp4.evaluate();
QVERIFY2( v4.toDouble() <= 9.5, QStringLiteral( "Calculated: %1 > Expected %2" ).arg( QString::number( v4.toDouble() ), QString::number( 9.5 ) ).toUtf8().constData() );
QVERIFY2( v4.toDouble() >= 1.5, QStringLiteral( "Calculated: %1 < Expected %2" ).arg( QString::number( v4.toDouble() ), QString::number( 1.5 ) ).toUtf8().constData() );
QgsExpression exp5( QStringLiteral( "randf(1.5,9.5,1.23)" ) );
QVariant v5 = exp5.evaluate();
QVERIFY2( v5.toDouble() <= 9.5, QStringLiteral( "Calculated: %1 > Expected %2" ).arg( QString::number( v5.toDouble() ), QString::number( 9.5 ) ).toUtf8().constData() );
QVERIFY2( v5.toDouble() >= 1.5, QStringLiteral( "Calculated: %1 < Expected %2" ).arg( QString::number( v5.toDouble() ), QString::number( 1.5 ) ).toUtf8().constData() );
QgsExpression exp6( QStringLiteral( "randf(1.5,9.5,'123')" ) );
QVariant v6 = exp6.evaluate();
QVERIFY2( v6.toDouble() <= 9.5, QStringLiteral( "Calculated: %1 > Expected %2" ).arg( QString::number( v6.toDouble() ), QString::number( 9.5 ) ).toUtf8().constData() );
QVERIFY2( v6.toDouble() >= 1.5, QStringLiteral( "Calculated: %1 < Expected %2" ).arg( QString::number( v6.toDouble() ), QString::number( 1.5 ) ).toUtf8().constData() );
QgsExpression exp7( QStringLiteral( "randf(1.5,9.5,'abc')" ) );
QVariant v7 = exp7.evaluate();
QVERIFY2( v7.toDouble() <= 9.5, QStringLiteral( "Calculated: %1 > Expected %2" ).arg( QString::number( v7.toDouble() ), QString::number( 9.5 ) ).toUtf8().constData() );
QVERIFY2( v7.toDouble() >= 1.5, QStringLiteral( "Calculated: %1 < Expected %2" ).arg( QString::number( v7.toDouble() ), QString::number( 1.5 ) ).toUtf8().constData() );

// Two calls with the same seed always return the same number
QgsExpression exp8( QStringLiteral( "randf(seed:=1)" ) );
QVariant v8 = exp8.evaluate();
QVERIFY2( qgsDoubleNear( v8.toDouble(), 0.13387664401253274 ), QStringLiteral( "Calculated: %1 != Expected %2" ).arg( QString::number( v8.toDouble() ), QString::number( 0.13387664401253274 ) ).toUtf8().constData() );

// Two calls with a different seed return a different number
QgsExpression exp9( QStringLiteral( "randf(seed:=1)" ) );
QVariant v9 = exp9.evaluate();
QgsExpression exp10( QStringLiteral( "randf(seed:=2)" ) );
QVariant v10 = exp10.evaluate();
QVERIFY2( ! qgsDoubleNear( v9.toDouble(), v10.toDouble() ), QStringLiteral( "Calculated: %1 == Expected %2" ).arg( QString::number( v9.toDouble() ), QString::number( v10.toDouble() ) ).toUtf8().constData() );
}

void referenced_columns()

0 comments on commit 98f8a89

Please sign in to comment.
You can’t perform that action at this time.