Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bugfix] spec compliant random-number-generator #3072

Merged
merged 4 commits into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -2,10 +2,15 @@

import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.jcip.annotations.NotThreadSafe;
import org.exist.xquery.*;
import org.exist.xquery.functions.map.MapType;
import org.exist.xquery.value.*;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Optional;
import java.util.Random;

import static org.exist.xquery.FunctionDSL.*;
Expand Down Expand Up @@ -38,40 +43,54 @@ public FnRandomNumberGenerator(final XQueryContext context, final FunctionSignat

@Override
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
final Random random;
if (args.length == 1 && !args[0].isEmpty()) {
random = new Random(args[0].itemAt(0).toJavaObject(long.class));
Optional<Long> seed;
if (args.length < 1) {
seed = Optional.empty();
} else {
random = new Random();
final Sequence seedArg = getArgument(0).eval(contextSequence);
if (seedArg.isEmpty()) {
seed = Optional.empty();
} else {
try {
seed = Optional.of(seedArg.convertTo(Type.LONG).toJavaObject(long.class));
} catch(final XPathException e) {
seed = Optional.empty();
}
}
}

final XORShiftRandom random = seed.map(XORShiftRandom::new).orElseGet(() -> new XORShiftRandom());

return buildResult(context, random);
}

private static MapType buildResult(final XQueryContext context, final Random random) throws XPathException {
private static MapType buildResult(final XQueryContext context, XORShiftRandom random) throws XPathException {
// NOTE: we must create a copy so that `Random#nextDouble` does not interfere with multiple `next()` calls on the same random number generator
random = random.copy();

final MapType result = new MapType(context);
result.add(new StringValue("number"), new DoubleValue(random.nextDouble()));
result.add(new StringValue("next"), nextFunction(context, random));
result.add(new StringValue("permute"), permuteFunction(context, random));
return result;
}

private static FunctionReference nextFunction(final XQueryContext context, final Random random) {
private static FunctionReference nextFunction(final XQueryContext context, final XORShiftRandom random) {
final NextFunction nextFunction = new NextFunction(context, random);
final FunctionCall nextFunctionCall = new FunctionCall(context, nextFunction);
return new FunctionReference(nextFunctionCall);
}

private static FunctionReference permuteFunction(final XQueryContext context, final Random random) {
private static FunctionReference permuteFunction(final XQueryContext context, final XORShiftRandom random) {
final PermuteFunction permuteFunction = new PermuteFunction(context, random);
final FunctionCall permuteFunctionCall = new FunctionCall(context, permuteFunction);
return new FunctionReference(permuteFunctionCall);
}

private static class NextFunction extends UserDefinedFunction {
private final Random random;
private final XORShiftRandom random;

public NextFunction(final XQueryContext context, final Random random) {
public NextFunction(final XQueryContext context, final XORShiftRandom random) {
super(context, functionSignature(
"random-number-generator-next",
"Gets the next random number generator.",
Expand All @@ -94,9 +113,9 @@ public void accept(final ExpressionVisitor visitor) {
}

private static class PermuteFunction extends UserDefinedFunction {
private final Random random;
private final XORShiftRandom random;

public PermuteFunction(final XQueryContext context, final Random random) {
public PermuteFunction(final XQueryContext context, final XORShiftRandom random) {
super(context, functionSignature(
"random-number-generator-permute",
"Takes an arbitrary sequence as its argument, and returns a random permutation of that sequence.",
Expand Down Expand Up @@ -145,4 +164,60 @@ public void accept(final ExpressionVisitor visitor) {
visited = true;
}
}

@NotThreadSafe
private static class XORShiftRandom extends Random implements Cloneable {
private long seed;

public XORShiftRandom() {
this.seed = System.nanoTime();
}

public XORShiftRandom(final long seed) {
this.seed = seed;
}

@Override
protected int next(final int nbits) {
long x = this.seed;
x ^= (x << 21);
x ^= (x >>> 35);
x ^= (x << 4);
this.seed = x;
x &= ((1L << nbits) -1);
return (int) x;
}

@Override
public long nextLong() {
long x = this.seed;
x ^= (x << 21);
x ^= (x >>> 35);
x ^= (x << 4);
this.seed = x;
x &= ((1L << 64) -1);
return x;
}

private void writeObject(final ObjectOutputStream out) throws IOException {
out.writeLong(seed);
}

private void readObject(final ObjectInputStream in) throws IOException {
this.seed = in.readLong();
}

private void readObjectNoData() {
this.seed = System.nanoTime();
}

@Override
protected Object clone() {
return copy();
}

public XORShiftRandom copy() {
return new XORShiftRandom(seed);
}
}
}
96 changes: 96 additions & 0 deletions exist-core/src/test/xquery/xquery3/fnRandomNumberGenerator.xql
@@ -0,0 +1,96 @@
xquery version "3.1";

module namespace fn-rng="http://exist-db.org/xquery/test/fnRandomNumberGenerator";

declare namespace test="http://exist-db.org/xquery/xqsuite";

declare variable $fn-rng:long-seed := 123456789;
declare variable $fn-rng:text-seed := 'sample seed';
declare variable $fn-rng:date-seed := xs:date('1970-01-01');
declare variable $fn-rng:dateTime-seed := xs:dateTime('1970-01-01T00:00:00.000Z');

declare
%test:assertExists
function fn-rng:seed-number () {
fn:random-number-generator($fn-rng:long-seed)
};

declare
%test:assertExists
function fn-rng:seed-text () {
fn:random-number-generator($fn-rng:text-seed)
};

declare
%test:assertExists
function fn-rng:seed-date () {
fn:random-number-generator($fn-rng:date-seed)
};

declare
%test:assertExists
function fn-rng:seed-dateTime () {
fn:random-number-generator($fn-rng:dateTime-seed)
};

declare
%test:assertExists
function fn-rng:seed-current-dateTime () {
fn:random-number-generator(fn:current-dateTime())
};

declare
%test:assertTrue
function fn-rng:deterministic () {
fn:random-number-generator($fn-rng:long-seed)?number eq
fn:random-number-generator($fn-rng:long-seed)?number
};

declare
%test:assertTrue
function fn-rng:deterministic-next () {
fn:random-number-generator($fn-rng:long-seed)?next()?number eq
fn:random-number-generator($fn-rng:long-seed)?next()?number
};

declare
%private
function fn-rng:get-generator-reference () {
fn:random-number-generator($fn-rng:long-seed)
};

declare
%test:assertTrue
function fn-rng:deterministic-reference () {
let $fn := fn-rng:get-generator-reference()
return
$fn?number eq
$fn?number
};

declare
%test:assertTrue
function fn-rng:deterministic-reference-next () {
let $fn := fn-rng:get-generator-reference()
return
$fn?next()?number eq
$fn?next()?number
};

declare variable $fn-rng:generator-reference := fn-rng:get-generator-reference();

declare
%private
function fn-rng:number-from-generator-reference () {
$fn-rng:generator-reference?next()?number
};

declare
%test:assertTrue
function fn-rng:deterministic-side-effect () {
let $call := fn-rng:number-from-generator-reference()

return
fn-rng:number-from-generator-reference() eq
$fn-rng:generator-reference?next()?number
};