From bf8fd7022971d9e071f947a4d191026db48f63c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3is=C3=ADn=20Grannell?= Date: Sun, 6 Aug 2023 14:33:10 +0100 Subject: [PATCH] adds bigint methods, Array.choose --- src/array/array.test.ts | 48 +++++++++++++++++++++++++++++++++++++++ src/array/array.ts | 26 ++++++++++++++++++++- src/bigint/bigint.test.ts | 27 ++++++++++++++++++++++ src/bigint/bigint.ts | 31 +++++++++++++++++++++++++ src/mod.ts | 1 + src/types.ts | 14 ++++++++++++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/bigint/bigint.test.ts create mode 100644 src/bigint/bigint.ts diff --git a/src/array/array.test.ts b/src/array/array.test.ts index 048093c..8583bcc 100644 --- a/src/array/array.test.ts +++ b/src/array/array.test.ts @@ -16,3 +16,51 @@ Deno.test({ } }, }); + +Deno.test({ + name: "Array.choose | returns a subset of the provided collection", + fn() { + const integers = Peach.Array.from( + Peach.Number.uniform(0, 100), + Peach.Number.uniform(0, 100)); + + for (let idx = 0; idx < 1_000; idx++) { + const sample = integers(); + + const result = Peach.Array.choose(sample, Peach.BigInt.uniform); + if (result.length > 100) { + throw new Error(`out of range 0...100: ${result}`); + } + + for (const elem of sample) { + if (elem < 0 || elem > 100) { + throw new Error(`out of range 0...100: ${elem}`); + } + } + } + } +}); + +Deno.test({ + name: "Array.choose | preserves elements", + fn() { + const integers = Peach.Array.from( + 0, + Peach.Number.uniform(0, 100)); + + for (let idx = 0; idx < 1_000; idx++) { + const sample = integers(); + + const result = Peach.Array.choose(sample, Peach.BigInt.uniform); + if (result.length > 100) { + throw new Error(`out of range 0...100: ${result}`); + } + + for (const elem of sample) { + if (elem !== 0) { + throw new Error(`elem was not zero: ${elem}`); + } + } + } + } +}) \ No newline at end of file diff --git a/src/array/array.ts b/src/array/array.ts index 776cac6..949a501 100644 --- a/src/array/array.ts +++ b/src/array/array.ts @@ -1,4 +1,4 @@ -import type { Thunk, Wrapped } from "../types.ts"; +import type { Thunk, Wrapped, DensityBigInt } from "../types.ts"; import { unwrap } from "../types.ts"; /** @@ -29,3 +29,27 @@ export function concat(...elems: Wrapped[]): Thunk { return elems.map((elem) => unwrap(elem)); }; } + +export function choose(elems: Wrapped, density: DensityBigInt): Thunk { + return () => { + const concreteElems = unwrap(elems); + const subsetCount = BigInt(2) ^ BigInt(concreteElems.length); + const index = unwrap(density(BigInt(0), subsetCount)); + + // bits correspond to a include-or-don't for each element + const bits = index + .toString(2) + .padStart(concreteElems.length, "0") + .split(""); + + const output: T[] = []; + + for (let idx = 0; idx < bits.length; idx++) { + if (bits[idx] === "1") { + output.push(concreteElems[idx]); + } + } + + return output; + } +} \ No newline at end of file diff --git a/src/bigint/bigint.test.ts b/src/bigint/bigint.test.ts new file mode 100644 index 0000000..ade35fd --- /dev/null +++ b/src/bigint/bigint.test.ts @@ -0,0 +1,27 @@ +import { assertEquals } from "https://deno.land/std@0.160.0/testing/asserts.ts"; + +import * as Peach from "../mod.ts"; + +Deno.test({ + name: "Peach.BigInt.Uniform | Choosing 0...0 returns 0", + fn() { + assertEquals(Peach.BigInt.uniform(0n, 0n)(), 0n, "uniform 0...0 must be zero"); + }, +}); + +Deno.test({ + name: "Peach.BigInt.Uniform | Always in range", + fn() { + const upper = Peach.BigInt.uniform(0n, 10n); + + for (let idx = 0; idx < 1_000; idx++) { + const sizeTgt = upper(); + const random = Peach.BigInt.uniform(0n, sizeTgt); + const val = random(); + + if (val < 0 || val > sizeTgt) { + throw new Error(`out of range 0...${sizeTgt}: ${val}`); + } + } + }, +}); diff --git a/src/bigint/bigint.ts b/src/bigint/bigint.ts new file mode 100644 index 0000000..f4e81a8 --- /dev/null +++ b/src/bigint/bigint.ts @@ -0,0 +1,31 @@ +import type { Thunk, Wrapped } from "../types.ts"; +import { unwrap } from "../types.ts"; + +/** + * Return a random integer in the chosen range. The distribution is + * uniform (pseudo RNG willing). + * + * @param from A wrapped integer that represents the lower bound of the range + * @param to A wrapped integer that represents the upper bound of the range + * + * @returns A thunk that returns a random integer in the chosen range + */ +export function uniform( + from: Wrapped, + to: Wrapped, +): Thunk { + return () => { + const lower = unwrap(from); + const upper = unwrap(to); + + const diff = upper - lower; + + if (diff > Number.MAX_SAFE_INTEGER) { + throw new Error( + `Range too large: ${lower}...${upper} (${diff} > ${Number.MAX_SAFE_INTEGER})`, + ) + } + + return BigInt(Math.floor(Math.random() * Number(diff))) + lower; + }; +} diff --git a/src/mod.ts b/src/mod.ts index ea2c4bb..74b85f8 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -13,6 +13,7 @@ export * as Set from "./set/set.ts"; export * as String from "./string/string.ts"; export * as Object from "./object/object.ts"; export * as Boolean from "./boolean/boolean.ts"; +export * as BigInt from "./bigint/bigint.ts"; //export * as StateMachine from "./state_machine/state_machine.ts"; export type { Density, Thunk, Wrapped } from "./types.ts"; diff --git a/src/types.ts b/src/types.ts index 14dcd7e..06e5f1f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,6 +47,20 @@ export type Density = ( to: Wrapped, ) => Wrapped; +/* +* Density functions take a lower and upper bound as wrapped values, and return +* a wrapped value that is a number within that range +* +* @param from A wrapped value that represents the lower bound +* @param to A wrapped value that represents the upper bound +* +* @returns A wrapped value that is a number within the range [from, to] +*/ +export type DensityBigInt = ( + from: Wrapped, + to: Wrapped, +) => Wrapped; + export type StateMachineResult = { state: string; value: T;