@fuzdev/fuz_util 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bytes.d.ts +14 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +22 -0
- package/dist/hash.d.ts +5 -5
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +8 -24
- package/dist/hash_blake3.d.ts +15 -0
- package/dist/hash_blake3.d.ts.map +1 -0
- package/dist/hash_blake3.js +17 -0
- package/dist/hex.d.ts +13 -0
- package/dist/hex.d.ts.map +1 -0
- package/dist/hex.js +30 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +7 -1
- package/dist/random.d.ts +9 -0
- package/dist/random.d.ts.map +1 -1
- package/dist/random.js +9 -0
- package/dist/random_alea.d.ts +33 -10
- package/dist/random_alea.d.ts.map +1 -1
- package/dist/random_alea.js +33 -32
- package/dist/random_xoshiro.d.ts +51 -0
- package/dist/random_xoshiro.d.ts.map +1 -0
- package/dist/random_xoshiro.js +92 -0
- package/dist/zod.d.ts +76 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +198 -0
- package/package.json +15 -8
- package/src/lib/bytes.ts +21 -0
- package/src/lib/hash.ts +9 -28
- package/src/lib/hash_blake3.ts +20 -0
- package/src/lib/hex.ts +32 -0
- package/src/lib/log.ts +6 -1
- package/src/lib/random.ts +10 -0
- package/src/lib/random_alea.ts +37 -12
- package/src/lib/random_xoshiro.ts +124 -0
- package/src/lib/zod.ts +218 -0
package/dist/zod.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema introspection utilities.
|
|
3
|
+
*
|
|
4
|
+
* Generic helpers for extracting metadata from Zod schemas.
|
|
5
|
+
* Designed for CLI argument parsing but applicable elsewhere.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
//
|
|
10
|
+
// Schema Introspection
|
|
11
|
+
//
|
|
12
|
+
/**
|
|
13
|
+
* Unwrap nested schema types (optional, default, nullable, etc).
|
|
14
|
+
*
|
|
15
|
+
* @param def - Zod type definition to unwrap.
|
|
16
|
+
* @returns Inner schema if wrapped, undefined otherwise.
|
|
17
|
+
*/
|
|
18
|
+
export const zod_to_subschema = (def) => {
|
|
19
|
+
if ('innerType' in def) {
|
|
20
|
+
return def.innerType;
|
|
21
|
+
}
|
|
22
|
+
else if ('in' in def) {
|
|
23
|
+
return def.in;
|
|
24
|
+
}
|
|
25
|
+
else if ('schema' in def) {
|
|
26
|
+
return def.schema;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get the description from a schema's metadata, unwrapping if needed.
|
|
32
|
+
*
|
|
33
|
+
* @param schema - Zod schema to extract description from.
|
|
34
|
+
* @returns Description string or null if not found.
|
|
35
|
+
*/
|
|
36
|
+
export const zod_to_schema_description = (schema) => {
|
|
37
|
+
const meta = schema.meta();
|
|
38
|
+
if (meta?.description) {
|
|
39
|
+
return meta.description;
|
|
40
|
+
}
|
|
41
|
+
const subschema = zod_to_subschema(schema.def);
|
|
42
|
+
if (subschema) {
|
|
43
|
+
return zod_to_schema_description(subschema);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Get the default value from a schema, unwrapping if needed.
|
|
49
|
+
*
|
|
50
|
+
* @param schema - Zod schema to extract default from.
|
|
51
|
+
* @returns Default value or undefined.
|
|
52
|
+
*/
|
|
53
|
+
export const zod_to_schema_default = (schema) => {
|
|
54
|
+
const { def } = schema._zod;
|
|
55
|
+
if ('defaultValue' in def) {
|
|
56
|
+
return def.defaultValue;
|
|
57
|
+
}
|
|
58
|
+
const subschema = zod_to_subschema(def);
|
|
59
|
+
if (subschema) {
|
|
60
|
+
return zod_to_schema_default(subschema);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Get aliases from a schema's metadata, unwrapping if needed.
|
|
66
|
+
*
|
|
67
|
+
* @param schema - Zod schema to extract aliases from.
|
|
68
|
+
* @returns Array of alias strings.
|
|
69
|
+
*/
|
|
70
|
+
export const zod_to_schema_aliases = (schema) => {
|
|
71
|
+
const meta = schema.meta();
|
|
72
|
+
if (meta?.aliases) {
|
|
73
|
+
return meta.aliases;
|
|
74
|
+
}
|
|
75
|
+
const subschema = zod_to_subschema(schema.def);
|
|
76
|
+
if (subschema) {
|
|
77
|
+
return zod_to_schema_aliases(subschema);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Get the type string for a schema, suitable for display.
|
|
83
|
+
*
|
|
84
|
+
* @param schema - Zod schema to get type string for.
|
|
85
|
+
* @returns Human-readable type string.
|
|
86
|
+
*/
|
|
87
|
+
export const zod_to_schema_type_string = (schema) => {
|
|
88
|
+
const { def } = schema._zod;
|
|
89
|
+
switch (def.type) {
|
|
90
|
+
case 'string':
|
|
91
|
+
return 'string';
|
|
92
|
+
case 'number':
|
|
93
|
+
return 'number';
|
|
94
|
+
case 'int':
|
|
95
|
+
return 'int';
|
|
96
|
+
case 'boolean':
|
|
97
|
+
return 'boolean';
|
|
98
|
+
case 'bigint':
|
|
99
|
+
return 'bigint';
|
|
100
|
+
case 'null':
|
|
101
|
+
return 'null';
|
|
102
|
+
case 'undefined':
|
|
103
|
+
return 'undefined';
|
|
104
|
+
case 'any':
|
|
105
|
+
return 'any';
|
|
106
|
+
case 'unknown':
|
|
107
|
+
return 'unknown';
|
|
108
|
+
case 'array':
|
|
109
|
+
return 'Array<string>';
|
|
110
|
+
case 'enum':
|
|
111
|
+
return schema.options
|
|
112
|
+
.map((v) => `'${v}'`)
|
|
113
|
+
.join(' | ');
|
|
114
|
+
case 'literal':
|
|
115
|
+
return def.values
|
|
116
|
+
.map((v) => zod_format_value(v))
|
|
117
|
+
.join(' | ');
|
|
118
|
+
case 'nullable': {
|
|
119
|
+
const subschema = zod_to_subschema(def);
|
|
120
|
+
return subschema ? zod_to_schema_type_string(subschema) + ' | null' : 'nullable';
|
|
121
|
+
}
|
|
122
|
+
case 'optional': {
|
|
123
|
+
const subschema = zod_to_subschema(def);
|
|
124
|
+
return subschema ? zod_to_schema_type_string(subschema) + ' | undefined' : 'optional';
|
|
125
|
+
}
|
|
126
|
+
default: {
|
|
127
|
+
const subschema = zod_to_subschema(def);
|
|
128
|
+
return subschema ? zod_to_schema_type_string(subschema) : def.type;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Format a value for display in help text.
|
|
134
|
+
*
|
|
135
|
+
* @param value - Value to format.
|
|
136
|
+
* @returns Formatted string representation.
|
|
137
|
+
*/
|
|
138
|
+
export const zod_format_value = (value) => {
|
|
139
|
+
if (value === undefined)
|
|
140
|
+
return '';
|
|
141
|
+
if (value === null)
|
|
142
|
+
return 'null';
|
|
143
|
+
if (typeof value === 'string')
|
|
144
|
+
return `'${value}'`;
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return '[]';
|
|
147
|
+
if (typeof value === 'object')
|
|
148
|
+
return JSON.stringify(value);
|
|
149
|
+
if (typeof value === 'boolean' || typeof value === 'number')
|
|
150
|
+
return String(value);
|
|
151
|
+
return '';
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Extract properties from a Zod object schema.
|
|
155
|
+
*
|
|
156
|
+
* @param schema - Zod object schema to extract from.
|
|
157
|
+
* @returns Array of property definitions.
|
|
158
|
+
*/
|
|
159
|
+
export const zod_to_schema_properties = (schema) => {
|
|
160
|
+
const { def } = schema;
|
|
161
|
+
if (!('shape' in def)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const shape = def.shape;
|
|
165
|
+
const properties = [];
|
|
166
|
+
for (const name in shape) {
|
|
167
|
+
// Skip no- prefixed fields (used for boolean negation)
|
|
168
|
+
if ('no-' + name in shape)
|
|
169
|
+
continue;
|
|
170
|
+
const field = shape[name];
|
|
171
|
+
properties.push({
|
|
172
|
+
name,
|
|
173
|
+
type: zod_to_schema_type_string(field),
|
|
174
|
+
description: zod_to_schema_description(field) ?? '',
|
|
175
|
+
default: zod_to_schema_default(field),
|
|
176
|
+
aliases: zod_to_schema_aliases(field),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return properties;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Get all property names and their aliases from an object schema.
|
|
183
|
+
*
|
|
184
|
+
* @param schema - Zod object schema.
|
|
185
|
+
* @returns Set of all names and aliases.
|
|
186
|
+
*/
|
|
187
|
+
export const zod_to_schema_names_with_aliases = (schema) => {
|
|
188
|
+
const names = new Set();
|
|
189
|
+
for (const prop of zod_to_schema_properties(schema)) {
|
|
190
|
+
if (prop.name !== '_') {
|
|
191
|
+
names.add(prop.name);
|
|
192
|
+
for (const alias of prop.aliases) {
|
|
193
|
+
names.add(alias);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return names;
|
|
198
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_util",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "utility belt for JS",
|
|
5
5
|
"glyph": "🦕",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"benchmark:save": "gro run src/benchmarks/run.ts --save",
|
|
32
32
|
"benchmark_slugify": "gro run src/benchmarks/slugify.benchmark.ts",
|
|
33
33
|
"benchmark_deep_equal": "gro run src/benchmarks/deep_equal.benchmark.ts",
|
|
34
|
-
"benchmark_deep_equal_comparison": "gro run src/benchmarks/deep_equal_comparison.benchmark.ts"
|
|
34
|
+
"benchmark_deep_equal_comparison": "gro run src/benchmarks/deep_equal_comparison.benchmark.ts",
|
|
35
|
+
"benchmark_random": "gro run src/benchmarks/random.benchmark.ts",
|
|
36
|
+
"benchmark_random_quality": "gro run src/benchmarks/random_quality.ts"
|
|
35
37
|
},
|
|
36
38
|
"type": "module",
|
|
37
39
|
"engines": {
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
"web"
|
|
45
47
|
],
|
|
46
48
|
"peerDependencies": {
|
|
49
|
+
"@fuzdev/blake3_wasm": "^0.1.0",
|
|
47
50
|
"@types/estree": "^1",
|
|
48
51
|
"@types/node": "^24",
|
|
49
52
|
"esm-env": "^1.2.2",
|
|
@@ -51,13 +54,16 @@
|
|
|
51
54
|
"zod": "^4.0.14"
|
|
52
55
|
},
|
|
53
56
|
"peerDependenciesMeta": {
|
|
54
|
-
"@
|
|
57
|
+
"@fuzdev/blake3_wasm": {
|
|
55
58
|
"optional": true
|
|
56
59
|
},
|
|
57
|
-
"
|
|
60
|
+
"@types/estree": {
|
|
58
61
|
"optional": true
|
|
59
62
|
},
|
|
60
|
-
"@types/
|
|
63
|
+
"@types/node": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"esm-env": {
|
|
61
67
|
"optional": true
|
|
62
68
|
},
|
|
63
69
|
"svelte": {
|
|
@@ -69,10 +75,11 @@
|
|
|
69
75
|
},
|
|
70
76
|
"devDependencies": {
|
|
71
77
|
"@changesets/changelog-git": "^0.2.1",
|
|
78
|
+
"@fuzdev/blake3_wasm": "^0.1.0",
|
|
72
79
|
"@fuzdev/fuz_code": "^0.45.1",
|
|
73
|
-
"@fuzdev/fuz_css": "^0.
|
|
74
|
-
"@fuzdev/fuz_ui": "^0.
|
|
75
|
-
"@fuzdev/gro": "^0.
|
|
80
|
+
"@fuzdev/fuz_css": "^0.54.0",
|
|
81
|
+
"@fuzdev/fuz_ui": "^0.185.2",
|
|
82
|
+
"@fuzdev/gro": "^0.196.0",
|
|
76
83
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
77
84
|
"@ryanatkn/eslint-config": "^0.9.0",
|
|
78
85
|
"@sveltejs/adapter-static": "^3.0.10",
|
package/src/lib/bytes.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary data conversion helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts string or binary data to a `Uint8Array`.
|
|
11
|
+
* Strings are UTF-8 encoded. `Uint8Array` inputs are returned as-is.
|
|
12
|
+
*
|
|
13
|
+
* @param data - String or `BufferSource` to convert.
|
|
14
|
+
* @returns `Uint8Array` view of the data.
|
|
15
|
+
*/
|
|
16
|
+
export const to_bytes = (data: BufferSource | string): Uint8Array => {
|
|
17
|
+
if (typeof data === 'string') return encoder.encode(data);
|
|
18
|
+
if (data instanceof Uint8Array) return data;
|
|
19
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
20
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
21
|
+
};
|
package/src/lib/hash.ts
CHANGED
|
@@ -1,45 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hash utilities for content comparison and cache invalidation.
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
4
|
+
* Provides `hash_sha256` (Web Crypto, async) and `hash_insecure` (DJB2, fast non-cryptographic).
|
|
5
|
+
* For BLAKE3, see `hash_blake3` in `hash_blake3.ts`.
|
|
5
6
|
*
|
|
6
7
|
* @module
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
import {to_hex} from './hex.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
let byte_to_hex: Array<string> | undefined;
|
|
13
|
-
const get_byte_to_hex = (): Array<string> => {
|
|
14
|
-
if (byte_to_hex === undefined) {
|
|
15
|
-
byte_to_hex = new Array(256); // 256 possible byte values (0x00-0xff)
|
|
16
|
-
for (let i = 0; i < 256; i++) {
|
|
17
|
-
byte_to_hex[i] = i.toString(16).padStart(2, '0');
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return byte_to_hex;
|
|
21
|
-
};
|
|
12
|
+
const encoder = new TextEncoder();
|
|
22
13
|
|
|
23
14
|
/**
|
|
24
|
-
* Computes a
|
|
15
|
+
* Computes a SHA-256 hash using Web Crypto API.
|
|
25
16
|
*
|
|
26
17
|
* @param data - String or binary data to hash. Strings are UTF-8 encoded.
|
|
27
|
-
* @
|
|
28
|
-
* @returns Hexadecimal hash string.
|
|
18
|
+
* @returns 64-character hexadecimal hash string.
|
|
29
19
|
*/
|
|
30
|
-
export const
|
|
31
|
-
data: BufferSource | string,
|
|
32
|
-
algorithm: 'SHA-256' | 'SHA-384' | 'SHA-512' = 'SHA-256',
|
|
33
|
-
): Promise<string> => {
|
|
20
|
+
export const hash_sha256 = async (data: BufferSource | string): Promise<string> => {
|
|
34
21
|
const buffer = typeof data === 'string' ? encoder.encode(data) : data;
|
|
35
|
-
const digested = await crypto.subtle.digest(
|
|
36
|
-
|
|
37
|
-
const lookup = get_byte_to_hex();
|
|
38
|
-
let hex = '';
|
|
39
|
-
for (const byte of bytes) {
|
|
40
|
-
hex += lookup[byte];
|
|
41
|
-
}
|
|
42
|
-
return hex;
|
|
22
|
+
const digested = await crypto.subtle.digest('SHA-256', buffer);
|
|
23
|
+
return to_hex(new Uint8Array(digested));
|
|
43
24
|
};
|
|
44
25
|
|
|
45
26
|
/**
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLAKE3 cryptographic hashing via `@fuzdev/blake3_wasm`.
|
|
3
|
+
*
|
|
4
|
+
* Synchronous and fast. Returns hex-encoded 256-bit (32-byte) digests.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {hash} from '@fuzdev/blake3_wasm';
|
|
10
|
+
|
|
11
|
+
import {to_hex} from './hex.js';
|
|
12
|
+
import {to_bytes} from './bytes.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Computes a BLAKE3 hash synchronously.
|
|
16
|
+
*
|
|
17
|
+
* @param data - String or binary data to hash. Strings are UTF-8 encoded.
|
|
18
|
+
* @returns 64-character hexadecimal hash string (32 bytes).
|
|
19
|
+
*/
|
|
20
|
+
export const hash_blake3 = (data: BufferSource | string): string => to_hex(hash(to_bytes(data)));
|
package/src/lib/hex.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hex encoding helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts a `Uint8Array` to a lowercase hex string.
|
|
9
|
+
*
|
|
10
|
+
* @param bytes - Binary data to encode.
|
|
11
|
+
* @returns Hex string with two characters per byte.
|
|
12
|
+
*/
|
|
13
|
+
export const to_hex = (bytes: Uint8Array): string => {
|
|
14
|
+
const lookup = get_byte_to_hex();
|
|
15
|
+
let hex = '';
|
|
16
|
+
for (const byte of bytes) {
|
|
17
|
+
hex += lookup[byte];
|
|
18
|
+
}
|
|
19
|
+
return hex;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Lazily computed lookup table for byte to hex conversion
|
|
23
|
+
let byte_to_hex: Array<string> | undefined;
|
|
24
|
+
const get_byte_to_hex = (): Array<string> => {
|
|
25
|
+
if (byte_to_hex === undefined) {
|
|
26
|
+
byte_to_hex = new Array(256); // 256 possible byte values (0x00-0xff)
|
|
27
|
+
for (let i = 0; i < 256; i++) {
|
|
28
|
+
byte_to_hex[i] = i.toString(16).padStart(2, '0');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return byte_to_hex;
|
|
32
|
+
};
|
package/src/lib/log.ts
CHANGED
|
@@ -415,7 +415,12 @@ export class Logger {
|
|
|
415
415
|
*/
|
|
416
416
|
info(...args: Array<unknown>): void {
|
|
417
417
|
if (this.#get_cached_level() < LOG_LEVEL_VALUES.info) return;
|
|
418
|
-
this
|
|
418
|
+
const prefix = this.#get_info_prefix();
|
|
419
|
+
if (prefix) {
|
|
420
|
+
this.console.log(prefix, ...args);
|
|
421
|
+
} else {
|
|
422
|
+
this.console.log(...args);
|
|
423
|
+
}
|
|
419
424
|
}
|
|
420
425
|
|
|
421
426
|
/**
|
package/src/lib/random.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random helpers that accept an optional `random` parameter,
|
|
3
|
+
* defaulting to `Math.random`. Pass a seeded PRNG for reproducible results:
|
|
4
|
+
*
|
|
5
|
+
* - {@link create_random_xoshiro} — fast, high-quality numeric seeding (recommended)
|
|
6
|
+
* - {@link create_random_alea} — supports string and variadic seeds
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
import type {ArrayElement} from './types.js';
|
|
2
12
|
|
|
3
13
|
/**
|
package/src/lib/random_alea.ts
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Alea: a seedable pseudo-random number generator by Johannes Baagøe.
|
|
3
|
+
* Supports variadic and string seeds (`create_random_alea('my', 3, 'seeds')`).
|
|
4
|
+
* For numeric seeds, prefer `create_random_xoshiro` which is faster with equal quality.
|
|
5
|
+
*
|
|
6
|
+
* DO NOT USE when security matters — use the Web Crypto API (`crypto.getRandomValues`) instead.
|
|
7
|
+
*
|
|
8
|
+
* Alea passes all 11 distribution quality tests at 10M samples,
|
|
9
|
+
* performing on par with `Math.random` (V8's xorshift128+):
|
|
10
|
+
*
|
|
11
|
+
* - mean, variance, chi-squared uniformity, Kolmogorov-Smirnov
|
|
12
|
+
* - lag-1 through lag-8 autocorrelation
|
|
13
|
+
* - runs test, gap test, permutation test (triples)
|
|
14
|
+
* - bit-level frequency (bits 0-7)
|
|
15
|
+
* - 2D serial pairs (25x25 through 200x200 grids)
|
|
16
|
+
* - birthday spacings (Marsaglia parameters)
|
|
17
|
+
*
|
|
18
|
+
* Speed is ~19% slower than `Math.random` (~12.2M ops/sec vs ~15.0M ops/sec).
|
|
19
|
+
*
|
|
20
|
+
* To reproduce:
|
|
21
|
+
*
|
|
22
|
+
* ```bash
|
|
23
|
+
* npm run benchmark_random_quality # distribution tests (N=1M)
|
|
24
|
+
* npm run benchmark_random_quality -- --deep # thorough (N=10M, multi-trial)
|
|
25
|
+
* npm run benchmark_random # speed comparison
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @see https://github.com/nquinlan/better-random-numbers-for-javascript-mirror
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
5
32
|
|
|
6
|
-
|
|
7
|
-
via https://github.com/nquinlan/better-random-numbers-for-javascript-mirror
|
|
33
|
+
/*
|
|
8
34
|
|
|
9
35
|
Copyright (C) 2010 by Johannes Baagøe <baagoe@baagoe.org>
|
|
10
36
|
|
|
@@ -28,7 +54,7 @@ THE SOFTWARE.
|
|
|
28
54
|
|
|
29
55
|
*/
|
|
30
56
|
|
|
31
|
-
export interface
|
|
57
|
+
export interface RandomAlea {
|
|
32
58
|
(): number;
|
|
33
59
|
uint32: () => number;
|
|
34
60
|
fract53: () => number;
|
|
@@ -40,10 +66,9 @@ export interface Alea {
|
|
|
40
66
|
* Seeded pseudo-random number generator.
|
|
41
67
|
* DO NOT USE when security matters, use webcrypto APIs instead.
|
|
42
68
|
*
|
|
43
|
-
* @see http://baagoe.com/en/RandomMusings/javascript/
|
|
44
69
|
* @see https://github.com/nquinlan/better-random-numbers-for-javascript-mirror
|
|
45
70
|
*/
|
|
46
|
-
export const create_random_alea = (...seed: Array<unknown>):
|
|
71
|
+
export const create_random_alea = (...seed: Array<unknown>): RandomAlea => {
|
|
47
72
|
let s0 = 0;
|
|
48
73
|
let s1 = 0;
|
|
49
74
|
let s2 = 0;
|
|
@@ -65,7 +90,7 @@ export const create_random_alea = (...seed: Array<unknown>): Alea => {
|
|
|
65
90
|
}
|
|
66
91
|
mash = null;
|
|
67
92
|
|
|
68
|
-
const random:
|
|
93
|
+
const random: RandomAlea = (): number => {
|
|
69
94
|
const t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
|
|
70
95
|
s0 = s1;
|
|
71
96
|
s1 = s2;
|
|
@@ -85,10 +110,10 @@ export const create_random_alea = (...seed: Array<unknown>): Alea => {
|
|
|
85
110
|
type Mash = (data: any) => number;
|
|
86
111
|
|
|
87
112
|
/**
|
|
88
|
-
* @source
|
|
113
|
+
* @source https://github.com/nquinlan/better-random-numbers-for-javascript-mirror
|
|
89
114
|
* @copyright Johannes Baagøe <baagoe@baagoe.com>, 2010
|
|
90
115
|
*/
|
|
91
|
-
|
|
116
|
+
const masher = (): Mash => {
|
|
92
117
|
let n = 0xefc8249d;
|
|
93
118
|
return (data) => {
|
|
94
119
|
const d = data + '';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xoshiro128**: a seedable pseudo-random number generator by Blackman & Vigna (2018).
|
|
3
|
+
* Recommended for reproducible randomness (testing, simulations, procedural generation).
|
|
4
|
+
*
|
|
5
|
+
* DO NOT USE when security matters — use the Web Crypto API (`crypto.getRandomValues`) instead.
|
|
6
|
+
*
|
|
7
|
+
* Xoshiro128** has a 2^128-1 period and passes BigCrush.
|
|
8
|
+
* It uses pure 32-bit arithmetic (shifts, rotates, XOR, multiply)
|
|
9
|
+
* making it very fast in JavaScript via `Math.imul`.
|
|
10
|
+
*
|
|
11
|
+
* Xoshiro128** passes all 11 distribution quality tests at 10M samples,
|
|
12
|
+
* performing on par with `Math.random` (V8's xorshift128+):
|
|
13
|
+
*
|
|
14
|
+
* - mean, variance, chi-squared uniformity, Kolmogorov-Smirnov
|
|
15
|
+
* - lag-1 through lag-8 autocorrelation
|
|
16
|
+
* - runs test, gap test, permutation test (triples)
|
|
17
|
+
* - bit-level frequency (bits 0-7)
|
|
18
|
+
* - 2D serial pairs (25x25 through 200x200 grids)
|
|
19
|
+
* - birthday spacings (Marsaglia parameters)
|
|
20
|
+
*
|
|
21
|
+
* Speed is near-native (~4% slower than `Math.random`) and ~18% faster than Alea
|
|
22
|
+
* (~14.4M ops/sec vs ~15.0M and ~12.2M respectively).
|
|
23
|
+
*
|
|
24
|
+
* To reproduce:
|
|
25
|
+
*
|
|
26
|
+
* ```bash
|
|
27
|
+
* npm run benchmark_random_quality # distribution tests (N=1M)
|
|
28
|
+
* npm run benchmark_random_quality -- --deep # thorough (N=10M, multi-trial)
|
|
29
|
+
* npm run benchmark_random # speed comparison
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @see https://prng.di.unimi.it/
|
|
33
|
+
* @see https://vigna.di.unimi.it/ftp/papers/ScrambledLinear.pdf
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
|
|
40
|
+
Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org)
|
|
41
|
+
|
|
42
|
+
To the extent possible under law, the author has dedicated all copyright
|
|
43
|
+
and related and neighboring rights to this software to the public domain
|
|
44
|
+
worldwide. This software is distributed without any warranty.
|
|
45
|
+
|
|
46
|
+
See <https://creativecommons.org/publicdomain/zero/1.0/>.
|
|
47
|
+
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
export interface RandomXoshiro {
|
|
51
|
+
(): number;
|
|
52
|
+
uint32: () => number;
|
|
53
|
+
fract53: () => number;
|
|
54
|
+
version: string;
|
|
55
|
+
seed: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Seeded pseudo-random number generator using the Xoshiro128** algorithm.
|
|
60
|
+
* DO NOT USE when security matters, use webcrypto APIs instead.
|
|
61
|
+
*
|
|
62
|
+
* @see https://prng.di.unimi.it/
|
|
63
|
+
*/
|
|
64
|
+
export const create_random_xoshiro = (seed?: number): RandomXoshiro => {
|
|
65
|
+
const actual_seed = seed ?? Date.now();
|
|
66
|
+
let sm_state = actual_seed | 0;
|
|
67
|
+
|
|
68
|
+
// expand single seed into 4 state words via SplitMix32
|
|
69
|
+
let r = splitmix32_next(sm_state);
|
|
70
|
+
let s0 = r.value;
|
|
71
|
+
sm_state = r.state;
|
|
72
|
+
r = splitmix32_next(sm_state);
|
|
73
|
+
let s1 = r.value;
|
|
74
|
+
sm_state = r.state;
|
|
75
|
+
r = splitmix32_next(sm_state);
|
|
76
|
+
let s2 = r.value;
|
|
77
|
+
sm_state = r.state;
|
|
78
|
+
r = splitmix32_next(sm_state);
|
|
79
|
+
let s3 = r.value;
|
|
80
|
+
|
|
81
|
+
// guard against all-zero state (absorbing fixed point)
|
|
82
|
+
if ((s0 | s1 | s2 | s3) === 0) s0 = 1;
|
|
83
|
+
|
|
84
|
+
const next_uint32 = (): number => {
|
|
85
|
+
const result = Math.imul(rotl(Math.imul(s1, 5), 7), 9);
|
|
86
|
+
const t = s1 << 9;
|
|
87
|
+
|
|
88
|
+
s2 ^= s0;
|
|
89
|
+
s3 ^= s1;
|
|
90
|
+
s1 ^= s2;
|
|
91
|
+
s0 ^= s3;
|
|
92
|
+
s2 ^= t;
|
|
93
|
+
s3 = rotl(s3, 11);
|
|
94
|
+
|
|
95
|
+
return result >>> 0;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const random: RandomXoshiro = (): number => next_uint32() / 0x100000000; // 2^32
|
|
99
|
+
|
|
100
|
+
random.uint32 = next_uint32;
|
|
101
|
+
|
|
102
|
+
random.fract53 = (): number => {
|
|
103
|
+
return random() + ((random() * 0x200000) | 0) * 1.1102230246251565e-16; // 2^-53
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
random.version = 'Xoshiro128** 1.0';
|
|
107
|
+
random.seed = actual_seed;
|
|
108
|
+
|
|
109
|
+
return random;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Expands a 32-bit state by one step using the SplitMix32 algorithm,
|
|
114
|
+
* producing the four state words needed by Xoshiro128**.
|
|
115
|
+
*/
|
|
116
|
+
const splitmix32_next = (prev_state: number): {value: number; state: number} => {
|
|
117
|
+
const next_state = (prev_state + 0x9e3779b9) | 0;
|
|
118
|
+
let z = next_state;
|
|
119
|
+
z = Math.imul(z ^ (z >>> 16), 0x85ebca6b);
|
|
120
|
+
z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35);
|
|
121
|
+
return {value: (z ^ (z >>> 16)) >>> 0, state: next_state};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const rotl = (x: number, k: number): number => (x << k) | (x >>> (32 - k));
|