Skip to content

Commit

Permalink
[FEATURE][expressions] allow to seed random functions
Browse files Browse the repository at this point in the history
useful to get deterministic random values
  • Loading branch information
olivierdalang committed Dec 3, 2019
1 parent af1c087 commit 65fed42
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 12 deletions.
5 changes: 3 additions & 2 deletions resources/function_help/json/rand
@@ -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"}]
}
7 changes: 4 additions & 3 deletions resources/function_help/json/randf
@@ -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"} ]
}
60 changes: 53 additions & 7 deletions src/core/expression/qgsexpressionfunction.cpp
Expand Up @@ -368,22 +368,68 @@ static QVariant fcnRndF( const QVariantList &values, const QgsExpressionContext
{
double min = QgsExpressionUtils::getDoubleValue( values.at( 0 ), parent );
double max = QgsExpressionUtils::getDoubleValue( values.at( 1 ), parent );

if ( max < min )
return QVariant();

QRandomGenerator *generator;
if ( QgsExpressionUtils::isNull( values.at( 2 ) ) )
{
generator = QRandomGenerator::global();
}
else
{
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 = &QRandomGenerator::QRandomGenerator( seed );
}

// Return a random double in the range [min, max] (inclusive)
double f = static_cast< double >( qrand() ) / RAND_MAX;
return QVariant( min + f * ( max - min ) );
return QVariant( min + generator->generateDouble() * ( max - min ) );
}
static QVariant fcnRnd( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
qlonglong min = QgsExpressionUtils::getIntValue( values.at( 0 ), parent );
qlonglong max = QgsExpressionUtils::getIntValue( values.at( 1 ), parent );
quint32 min = QgsExpressionUtils::getIntValue( values.at( 0 ), parent );
quint32 max = QgsExpressionUtils::getIntValue( values.at( 1 ), parent );
if ( max < min )
return QVariant();

QRandomGenerator *generator;
if ( QgsExpressionUtils::isNull( values.at( 2 ) ) )
{
generator = QRandomGenerator::global();
}
else
{
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 = &QRandomGenerator::QRandomGenerator( seed );
}

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

static QVariant fcnLinearScale( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
Expand Down Expand Up @@ -5216,11 +5262,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;

Expand Down
56 changes: 56 additions & 0 deletions tests/src/core/testqgsexpression.cpp
Expand Up @@ -2160,6 +2160,34 @@ 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,100,123)" ) );
QVariant v4 = exp4.evaluate();
QCOMPARE( v4.type(), QVariant::Int );
QgsExpression exp5( QStringLiteral( "rand(1,100,1.23)" ) );
QVariant v5 = exp5.evaluate();
QCOMPARE( v5.type(), QVariant::Int );
QgsExpression exp6( QStringLiteral( "rand(1,100,'123')" ) );
QVariant v6 = exp6.evaluate();
QCOMPARE( v6.type(), QVariant::Int );
QgsExpression exp7( QStringLiteral( "rand(1,100,'abc')" ) );
QVariant v7 = exp7.evaluate();
QCOMPARE( v7.type(), QVariant::Int );

// Two calls with the same seed return the same numer
QgsExpression exp8( QStringLiteral( "rand(1,100000000000,1)" ) );
QVariant v8 = exp8.evaluate();
QgsExpression exp9( QStringLiteral( "rand(1,100000000000,1)" ) );
QVariant v9 = exp9.evaluate();
QCOMPARE( v8.toInt() == v9.toInt(), true );

// Two calls with a different seed return a different number
QgsExpression exp10( QStringLiteral( "rand(1,100000000000,1)" ) );
QVariant v10 = exp10.evaluate();
QgsExpression exp11( QStringLiteral( "rand(1,100000000000,2)" ) );
QVariant v11 = exp11.evaluate();
QCOMPARE( v10.toInt() != v11.toInt(), true );
}

void eval_randf()
Expand All @@ -2177,6 +2205,34 @@ class TestQgsExpression: public QObject
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,100,123)" ) );
QVariant v4 = exp4.evaluate();
QCOMPARE( v4.type(), QVariant::Float );
QgsExpression exp5( QStringLiteral( "randf(1,100,1.23)" ) );
QVariant v5 = exp5.evaluate();
QCOMPARE( v5.type(), QVariant::Float );
QgsExpression exp6( QStringLiteral( "randf(1,100,'123')" ) );
QVariant v6 = exp6.evaluate();
QCOMPARE( v6.type(), QVariant::Float );
QgsExpression exp7( QStringLiteral( "randf(1,100,'abc')" ) );
QVariant v7 = exp7.evaluate();
QCOMPARE( v7.type(), QVariant::Float );

// Two calls with the same seed return the same numer
QgsExpression exp8( QStringLiteral( "randf(seed:=1)" ) );
QVariant v8 = exp8.evaluate();
QgsExpression exp9( QStringLiteral( "randf(seed:=1)" ) );
QVariant v9 = exp9.evaluate();
QCOMPARE( v8.toFloat() == v9.toFloat(), true );

// Two calls with a different seed return a different number
QgsExpression exp10( QStringLiteral( "randf(seed:=1)" ) );
QVariant v10 = exp10.evaluate();
QgsExpression exp11( QStringLiteral( "randf(seed:=2)" ) );
QVariant v11 = exp11.evaluate();
QCOMPARE( v10.toFloat() != v11.toFloat(), true );
}

void referenced_columns()
Expand Down

0 comments on commit 65fed42

Please sign in to comment.