From ea5bca499f0336405fa132fc7957006b5de6fb30 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Fri, 6 Mar 2026 00:12:01 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20`uniformFloat32`=20distributi?= =?UTF-8?q?on=20(#906)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 +-------------- package.json | 4 +++ .../uniformFloat32.noreg.spec.ts.snap | 16 +++++++++++ src/distribution/distribution.bench.ts | 13 +++++++++ src/distribution/uniformFloat32.noreg.spec.ts | 23 ++++++++++++++++ src/distribution/uniformFloat32.spec.ts | 27 +++++++++++++++++++ src/distribution/uniformFloat32.ts | 19 +++++++++++++ 7 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 src/distribution/__snapshots__/uniformFloat32.noreg.spec.ts.snap create mode 100644 src/distribution/uniformFloat32.noreg.spec.ts create mode 100644 src/distribution/uniformFloat32.spec.ts create mode 100644 src/distribution/uniformFloat32.ts diff --git a/README.md b/README.md index e4f6049..377640b 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ pure-rand provides 3 built-in functions for uniform distributions of values: - `uniformInt(rng, min, max)` - `uniformBigInt(rng, min, max)` - with `min` and `max` being `bigint` +- `uniformFloat32(rng)` - to generate value between 0 (included) and 1 (excluded) Each of these distributions come with its own import: `pure-rand/distribution/`. @@ -167,27 +168,6 @@ For detailed benchmark results and methodology, see the [full comparison](./COMP ## Advanced patterns -### Generate 32-bit floating point numbers - -The following snippet is responsible for generating 32-bit floating point numbers that spread uniformly between 0 (included) and 1 (excluded). - -```js -import { uniformInt } from 'pure-rand/distribution/uniformInt'; -import { xoroshiro128plus } from 'pure-rand/generator/xoroshiro128plus'; - -function generateFloat32(rng) { - const g1 = uniformInt(rng, 0, (1 << 24) - 1); - const value = g1 / (1 << 24); - return value; -} - -const seed = 42; -const rng = xoroshiro128plus(seed); - -const float32Bits1 = generateFloat32(rng); -const float32Bits2 = generateFloat32(rng); -``` - ### Generate 64-bit floating point numbers The following snippet is responsible for generating 64-bit floating point numbers that spread uniformly between 0 (included) and 1 (excluded). diff --git a/package.json b/package.json index ee2fdc5..ef52067 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,10 @@ "require": "./lib/distribution/uniformInt.js", "import": "./lib/esm/distribution/uniformInt.js" }, + "./distribution/uniformFloat32": { + "require": "./lib/distribution/uniformFloat32.js", + "import": "./lib/esm/distribution/uniformFloat32.js" + }, "./generator/congruential32": { "require": "./lib/generator/congruential32.js", "import": "./lib/esm/generator/congruential32.js" diff --git a/src/distribution/__snapshots__/uniformFloat32.noreg.spec.ts.snap b/src/distribution/__snapshots__/uniformFloat32.noreg.spec.ts.snap new file mode 100644 index 0000000..a6323ab --- /dev/null +++ b/src/distribution/__snapshots__/uniformFloat32.noreg.spec.ts.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`uniformFloat32 [non regression] > Should not change its output except for major bumps 1`] = ` +[ + 0.49625658988952637, + 0.7682217955589294, + 0.08847743272781372, + 0.13203048706054688, + 0.30742281675338745, + 0.6340786814689636, + 0.4900934100151062, + 0.8964447379112244, + 0.455627977848053, + 0.6323062777519226, +] +`; diff --git a/src/distribution/distribution.bench.ts b/src/distribution/distribution.bench.ts index 3eecd4e..da8580d 100644 --- a/src/distribution/distribution.bench.ts +++ b/src/distribution/distribution.bench.ts @@ -3,6 +3,7 @@ import { xorshift128plus } from '../generator/xorshift128plus'; import type { RandomGenerator } from '../../src/types/RandomGenerator'; import { uniformInt } from './uniformInt'; import { uniformBigInt } from './uniformBigInt'; +import { uniformFloat32 } from './uniformFloat32'; describe('distribution', () => { const rng = xorshift128plus(0); @@ -62,6 +63,14 @@ describe('distribution', () => { }); describe('various ranges', () => { + // no specific range + bench(`native Math.random()`, () => { + nativeMathRandom(); + }); + bench(`uniformFloat32`, () => { + uniformFloat32(rng); + }); + // range < 2 ** 8 const smallRangeLabel = `{{S range}} [0, 48]`; bench(`dummyFastInt @@ ${smallRangeLabel}`, () => { @@ -116,6 +125,10 @@ describe('distribution', () => { }); }); +function nativeMathRandom() { + return Math.random(); +} + function dummyFastInt(rng: RandomGenerator, from: number, to: number) { const out = rng.next() >>> 0; return from + (out % (to - from + 1)); diff --git a/src/distribution/uniformFloat32.noreg.spec.ts b/src/distribution/uniformFloat32.noreg.spec.ts new file mode 100644 index 0000000..7c0702a --- /dev/null +++ b/src/distribution/uniformFloat32.noreg.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { uniformFloat32 } from './uniformFloat32'; +import { mersenne } from '../generator/mersenne'; +import type { RandomGenerator } from '../types/RandomGenerator'; + +describe('uniformFloat32 [non regression]', () => { + it('Should not change its output except for major bumps', () => { + // Remark: + // ======================== + // This test is purely there to ensure that we do not introduce any regression + // during a commit without noticing it. + // The values we expect in the output are just a snapshot taken at a certain time + // in the past. They might be wrong values with bugs. + + const rng: RandomGenerator = mersenne(0); + const values: number[] = []; + for (let idx = 0; idx !== 10; ++idx) { + const v = uniformFloat32(rng); + values.push(v); + } + expect(values).toMatchSnapshot(); + }); +}); diff --git a/src/distribution/uniformFloat32.spec.ts b/src/distribution/uniformFloat32.spec.ts new file mode 100644 index 0000000..e92d49d --- /dev/null +++ b/src/distribution/uniformFloat32.spec.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import fc from 'fast-check'; +import { uniformFloat32 } from './uniformFloat32'; +import { mersenne } from '../generator/mersenne'; + +describe('uniformFloat32', () => { + it('Should always generate values in [0, 1)', () => + fc.assert( + fc.property(fc.noShrink(fc.integer()), (seed) => { + const rng = mersenne(seed); + const v = uniformFloat32(rng); + expect(v).toBeGreaterThanOrEqual(0); + expect(v).toBeLessThan(1); + }), + )); + + it('Should always generate values representable as 32-bit floats', () => + fc.assert( + fc.property(fc.noShrink(fc.integer()), (seed) => { + const rng = mersenne(seed); + const v = uniformFloat32(rng); + const buf = new Float32Array(1); + buf[0] = v; + expect(buf[0]).toBe(v); + }), + )); +}); diff --git a/src/distribution/uniformFloat32.ts b/src/distribution/uniformFloat32.ts new file mode 100644 index 0000000..b73abb0 --- /dev/null +++ b/src/distribution/uniformFloat32.ts @@ -0,0 +1,19 @@ +import type { RandomGenerator } from '../types/RandomGenerator'; + +const divisor = 1 << 24; +const scale = 1 / divisor; +const mask = divisor - 1; + +/** + * Uniformly generate random 32-bit floating point values between 0 (included) and 1 (excluded) + * + * @remarks Generated values are multiples of 2**-24, providing 24 bits of randomness. + * + * @param rng - Instance of RandomGenerator to extract random values from + * + * @public + */ +export function uniformFloat32(rng: RandomGenerator): number { + const value = rng.next() & mask; + return value * scale; +} -- 2.51.2