Skip to content

Commit

Permalink
choice randomization: better approximation of JR behaviour, fixes get…
Browse files Browse the repository at this point in the history
  • Loading branch information
brontolosone committed Oct 14, 2024
1 parent 6d86e7a commit 71be1b8
Showing 1 changed file with 35 additions and 5 deletions.
40 changes: 35 additions & 5 deletions packages/xpath/src/lib/collections/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class UnseededPseudoRandomNumberGenerator implements PseudoRandomNumberGenerator
}

class SeededPseudoRandomNumberGenerator implements PseudoRandomNumberGenerator {
// Park-Miller PRNG with 32 bits state space.
protected seed: number;

constructor(seed: Int) {
Expand All @@ -38,17 +39,46 @@ class SeededPseudoRandomNumberGenerator implements PseudoRandomNumberGenerator {
}
}

const isInt = (value: number): value is Int => value % 1 === 0;

export const seededRandomize = <T>(values: readonly T[], seed?: number): T[] => {
let generator: PseudoRandomNumberGenerator;

if (seed == null) {
generator = new UnseededPseudoRandomNumberGenerator();
} else if (!isInt(seed)) {
throw 'todo not an int';
} else {
generator = new SeededPseudoRandomNumberGenerator(seed);
let finalSeed: number;
// issue #49: make this "bug-or-feature-compatible" with JavaRosa's implementation.
// org.javarosa.core.model.ItemsetBinding.resolveRandomSeed takes the .longValue() of
// the double produced by randomSeedPathExpr.eval().
// That results in a 0L when the double is NaN, which happens (for instance) when there
// is a string that does not look like a number (which is a problem in itself, as any non-numeric
// looking string will then result in the same seed of 0 -).
// We'll emulate Java's Double -> Long conversion here (for NaN and some other double values)
// so that we produce the same randomization as JR.
if (Number.isNaN(seed)) {
finalSeed = 0;
} else if (seed === Infinity) {
// In Java's .longValue(), this converts to 2**63 -1.
// But that's larger than the JS Number.MAX_SAFE_INTEGER, and thus we cannot guarantee the same
// outcomes as OpenRosa.
// However. When Park-Miller is initialized, it takes the modulus of the seed and 2**31 -1 as
// the first step. This means that for Park-Miller we can use 2**31 (which is smaller than Number.MAX_SAFE_INTEGER)
// as a surrogate equivalent seed for Infinity, since
// ((2**63 -1) % (2**31 -1)) = ((2**31) % (2**31 -1))
// (because of JS Number imprecision (the problem to start with) don't use JS to convince of the above equality,
// or rewrite to use BigInt).
finalSeed = 2 ** 31;
} else if (seed === -Infinity) {
// Analogous with the above conversion for Infinity
finalSeed = -(2 ** 31 + 1);
} else if (!Number.isInteger(seed)) {
// We're not out of the woods yet — see issue: https:/getodk/web-forms/issues/240.
// But one thing we know is that JR converts the double to a long, and thus drops the fractional part.
// We'll do the same here.
finalSeed = Math.floor(seed);
} else {
finalSeed = seed;
}
generator = new SeededPseudoRandomNumberGenerator(finalSeed);
}

const { length } = values;
Expand Down

0 comments on commit 71be1b8

Please sign in to comment.