Skip to content

Commit

Permalink
Merge dcf8f48 into 8f358da
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Nov 4, 2020
2 parents 8f358da + dcf8f48 commit c513a2d
Show file tree
Hide file tree
Showing 9 changed files with 690 additions and 98 deletions.
3 changes: 2 additions & 1 deletion perf/benchmark.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ const argv = yargs(hideBin(process.argv))
.option('print-confidence', {
type: 'boolean',
default: false,
description: 'Print 95 % confidence range in reports instead of +X% (increase the number of samples to reduce this range)',
description:
'Print 95 % confidence range in reports instead of +X% (increase the number of samples to reduce this range)',
})
.option('verbose', {
alias: 'v',
Expand Down
45 changes: 38 additions & 7 deletions src/distribution/UniformIntDistribution.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
import Distribution from './Distribution';
import RandomGenerator from '../generator/RandomGenerator';
import { uniformIntDistributionInternal } from './internals/UniformIntDistributionInternal';
import { ArrayInt64, fromNumberToArrayInt64, substractArrayInt64, toNumber } from './internals/ArrayInt';
import { uniformArrayIntDistributionInternal } from './internals/UniformArrayIntDistributionInternal';

function uniformIntInternal(from: number, rangeSize: number, rng: RandomGenerator): [number, RandomGenerator] {
const g = uniformIntDistributionInternal(rangeSize, rng);
g[0] += from;
return g;
const sharedA: ArrayInt64 = { sign: 1, data: [0, 0] };
const sharedB: ArrayInt64 = { sign: 1, data: [0, 0] };
const sharedC: ArrayInt64 = { sign: 1, data: [0, 0] };
const sharedD: ArrayInt64 = { sign: 1, data: [0, 0] };

function uniformIntInternal(from: number, to: number, rng: RandomGenerator): [number, RandomGenerator] {
const rangeSize = to - from;
if (rangeSize <= 0xffffffff) {
// Calling uniformIntDistributionInternal can be considered safe
// up-to 2**32 values. Above this range it may miss values.
const g = uniformIntDistributionInternal(rangeSize + 1, rng);
g[0] += from;
return g;
}

const rangeSizeArrayIntValue =
rangeSize <= Number.MAX_SAFE_INTEGER
? fromNumberToArrayInt64(sharedC, rangeSize) // no possible overflow given rangeSize is in a safe range
: substractArrayInt64(sharedC, fromNumberToArrayInt64(sharedA, to), fromNumberToArrayInt64(sharedB, from)); // rangeSize might be incorrect, we compute a safer range

// Adding 1 to the range
if (rangeSizeArrayIntValue.data[1] === 0xffffffff) {
// rangeSizeArrayIntValue.length === 2 by construct
// rangeSize >= 0x00000001_00000000 and rangeSize <= 0x003fffff_fffffffe
// with Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER = 0x003fffff_fffffffe
rangeSizeArrayIntValue.data[0] += 1;
rangeSizeArrayIntValue.data[1] = 0;
} else {
rangeSizeArrayIntValue.data[1] += 1;
}

sharedD.sign = 1;
const g = uniformArrayIntDistributionInternal(sharedD.data, rangeSizeArrayIntValue.data, rng);
return [from + toNumber(sharedD), g[1]];
}

/**
Expand All @@ -28,12 +60,11 @@ function uniformIntDistribution(from: number, to: number): Distribution<number>;
*/
function uniformIntDistribution(from: number, to: number, rng: RandomGenerator): [number, RandomGenerator];
function uniformIntDistribution(from: number, to: number, rng?: RandomGenerator) {
const rangeSize = to - from + 1;
if (rng != null) {
return uniformIntInternal(from, rangeSize, rng);
return uniformIntInternal(from, to, rng);
}
return function (rng: RandomGenerator) {
return uniformIntInternal(from, rangeSize, rng);
return uniformIntInternal(from, to, rng);
};
}

Expand Down
109 changes: 109 additions & 0 deletions src/distribution/internals/ArrayInt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* An ArrayInt represents an integer larger than what can be represented in classical JavaScript.
* The values stored in data must be in the range [0, 0xffffffff].
*
* @example
* ```js
* { data: [ 42 ] } // = 42
* { sign: -1, data: [ 42 ] } // = -42
* { sign: -1, data: [ 5, 42 ] } // = -1 * (5 * 2**32 + 42)
* { sign: -1, data: [ 1, 5, 42 ] } // = -1 * (1 * 2**64 + 5 * 2**32 + 42)
* ```
*/
export type ArrayInt = {
/**
* Sign of the represented number
* @defaultValue 1
*/
sign?: -1 | 1;
/**
* Value of the number, must only contain numbers in the range [0, 0xffffffff]
*/
data: number[];
};

/** @internal */
export function toNumber(arrayInt: ArrayInt): number {
let current = arrayInt.data[0];
const arrayIntLength = arrayInt.data.length;
for (let index = 1; index < arrayIntLength; ++index) {
current *= 0x100000000;
current += arrayInt.data[index];
}
return current * (arrayInt.sign || 1);
}

// Helpers specific to 64 bits versions

/** @internal */
export type ArrayInt64 = ArrayInt & { data: [number, number] };

/**
* We only accept safe integers here
* @internal
*/
export function fromNumberToArrayInt64(out: ArrayInt64, n: number): ArrayInt64 {
if (n < 0) {
const posN = -n;
out.sign = -1;
out.data[0] = ~~(posN / 0x100000000);
out.data[1] = posN >>> 0;
} else {
out.sign = 1;
out.data[0] = ~~(n / 0x100000000);
out.data[1] = n >>> 0;
}
return out;
}

/**
* Substract two ArrayInt of 64 bits on 64 bits.
* With arrayIntA - arrayIntB >= 0
* @internal
*/
export function substractArrayInt64(out: ArrayInt64, arrayIntA: ArrayInt64, arrayIntB: ArrayInt64): ArrayInt64 {
const lowA = arrayIntA.data[1];
const highA = arrayIntA.data[0];
const signA = arrayIntA.sign || 1;
const lowB = arrayIntB.data[1];
const highB = arrayIntB.data[0];
const signB = arrayIntB.sign || 1;

// Requirement: arrayIntA - arrayIntB >= 0
out.sign = 1;

if (signA === 1 && signB === -1) {
// Operation is a simple sum of arrayIntA + abs(arrayIntB)
const low = lowA + lowB;
const high = highA + highB + (low > 0xffffffff ? 1 : 0);
out.data[0] = high >>> 0;
out.data[1] = low >>> 0;
return out;
}
// signA === -1 with signB === 1 is impossible given: arrayIntA - arrayIntB >= 0

// Operation is a substraction
let lowFirst = lowA;
let highFirst = highA;
let lowSecond = lowB;
let highSecond = highB;
if (signA === -1) {
lowFirst = lowB;
highFirst = highB;
lowSecond = lowA;
highSecond = highA;
}
let reminderLow = 0;
let low = lowFirst - lowSecond;
if (low < 0) {
reminderLow = 1;
low = low >>> 0;
}
let high = highFirst - highSecond - reminderLow;
if (high < 0) {
high = high >>> 0;
}
out.data[0] = high;
out.data[1] = low;
return out;
}
44 changes: 44 additions & 0 deletions src/distribution/internals/UniformArrayIntDistributionInternal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import RandomGenerator from '../../generator/RandomGenerator';
import { ArrayInt } from './ArrayInt';
import { uniformIntDistributionInternal } from './UniformIntDistributionInternal';

/**
* Uniformly generate ArrayInt in range [0 ; rangeSize[
*
* @remarks
* In the worst case scenario it may discard half of the randomly generated value.
* Worst case being: most significant number is 1 and remaining part evaluates to 0.
*
* @internal
*/
export function uniformArrayIntDistributionInternal(
out: ArrayInt['data'],
rangeSize: ArrayInt['data'],
rng: RandomGenerator
): [ArrayInt['data'], RandomGenerator] {
const rangeLength = rangeSize.length;
let nrng = rng;

// We iterate until we find a valid value for arrayInt
while (true) {
// We compute a new value for arrayInt
for (let index = 0; index !== rangeLength; ++index) {
const indexRangeSize = index === 0 ? rangeSize[0] + 1 : 0x100000000;
const g = uniformIntDistributionInternal(indexRangeSize, nrng);
out[index] = g[0];
nrng = g[1];
}

// If in the correct range we can return it
for (let index = 0; index !== rangeLength; ++index) {
const current = out[index];
const currentInRange = rangeSize[index];
if (current < currentInRange) {
return [out, nrng]; // arrayInt < rangeSize
} else if (current > currentInRange) {
break; // arrayInt > rangeSize
}
}
// Otherwise we need to try another one
}
}
11 changes: 11 additions & 0 deletions test/unit/distribution/UniformIntDistribution.noreg.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fc from 'fast-check';

import { uniformIntDistribution } from '../../../src/distribution/UniformIntDistribution';
import mersenne from '../../../src/generator/MersenneTwister';

Expand Down Expand Up @@ -37,4 +39,13 @@ describe('uniformIntDistribution [non regression]', () => {
}
expect(values).toMatchSnapshot();
});

it('Should always generate values within the range [from ; to]', () =>
fc.assert(
fc.property(fc.integer().noShrink(), fc.maxSafeInteger(), fc.maxSafeInteger(), (seed, a, b) => {
const [from, to] = a < b ? [a, b] : [b, a];
const [v, _nrng] = uniformIntDistribution(from, to)(mersenne(seed));
return v >= from && v <= to;
})
));
});

0 comments on commit c513a2d

Please sign in to comment.