1import { assert, describe, it } from "@effect/vitest"
2import * as Cause from "effect/Cause"
3import * as Effect from "effect/Effect"
4import * as Random from "effect/Random"
5
6describe("Random", () => {
7 describe("next", () => {
8 it.effect("generates a number between 0 inclusive and 1 exclusive", () =>
9 Effect.gen(function*() {
10 const value = yield* Random.next
11
12 assert.isAtLeast(value, 0)
13 assert.isBelow(value, 1)
14 }))
15 })
16
17 describe("nextBoolean", () => {
18 it.effect("generates a boolean", () =>
19 Effect.gen(function*() {
20 const value = yield* Random.nextBoolean
21
22 assert.isTrue(value === true || value === false)
23 }))
24
25 it.effect("is deterministic with the same seed", () =>
26 Effect.gen(function*() {
27 const program = Effect.all([Random.nextBoolean, Random.nextBoolean, Random.nextBoolean])
28
29 const result1 = yield* program.pipe(Random.withSeed("next-boolean-seed"))
30 const result2 = yield* program.pipe(Random.withSeed("next-boolean-seed"))
31
32 assert.deepStrictEqual(result1, result2)
33 }))
34
35 it.effect("uses a strict greater-than threshold", () =>
36 Effect.gen(function*() {
37 const values = [0.5, 0.500001, 0.1]
38 let index = 0
39
40 const result = yield* Effect.all([Random.nextBoolean, Random.nextBoolean, Random.nextBoolean]).pipe(
41 Effect.provideService(Random.Random, {
42 nextIntUnsafe: () => 0,
43 nextDoubleUnsafe: () => values[index++] ?? 0
44 })
45 )
46
47 assert.deepStrictEqual(result, [false, true, false])
48 }))
49 })
50
51 describe("nextInt", () => {
52 it.effect("generates a safe integer", () =>
53 Effect.gen(function*() {
54 const value = yield* Random.nextInt
55
56 assert.isTrue(Number.isSafeInteger(value))
57 assert.isAtLeast(value, Number.MIN_SAFE_INTEGER)
58 assert.isAtMost(value, Number.MAX_SAFE_INTEGER)
59 }))
60 })
61
62 describe("nextBetween", () => {
63 it.effect.each(
64 [
65 ["positive", 10, 20, 1 - Number.EPSILON, 20 - 2 ** -48],
66 ["adjacent", 1e16, 1e16 + 2, 0.75, 1e16],
67 ["negative", -1e16 - 2, -1e16, 0.75, -1e16 - 2],
68 ["zero", -Number.MIN_VALUE, 0, 0.75, -Number.MIN_VALUE]
69 ] as const
70 )("excludes a rounded upper bound: %s", ([_name, min, max, draw, expected]) =>
71 Effect.gen(function*() {
72 const value = yield* Random.nextBetween(min, max).pipe(
73 Effect.provideService(Random.Random, {
74 nextIntUnsafe: () => 0,
75 nextDoubleUnsafe: () => draw
76 })
77 )
78
79 assert.isAtLeast(value, min)
80 assert.isBelow(value, max)
81 assert.strictEqual(value, expected)
82 }))
83
84 it.effect("preserves degenerate bounds", () =>
85 Effect.gen(function*() {
86 const value = yield* Random.nextBetween(10, 10).pipe(
87 Effect.provideService(Random.Random, {
88 nextIntUnsafe: () => 0,
89 nextDoubleUnsafe: () => 0.75
90 })
91 )
92
93 assert.strictEqual(value, 10)
94 }))
95
96 it.effect("documents current behavior for unsupported bounds", () =>
97 Effect.gen(function*() {
98 const value = yield* Random.nextBetween(10, Infinity).pipe(
99 Effect.provideService(Random.Random, {
100 nextIntUnsafe: () => 0,
101 nextDoubleUnsafe: () => 0.75
102 })
103 )
104
105 assert.strictEqual(value, Infinity)
106 }))
107
108 it.effect("generates number in half-open range", () =>
109 Effect.gen(function*() {
110 for (let i = 0; i < 100; i++) {
111 const value = yield* Random.nextBetween(10, 20)
112 assert.isAtLeast(value, 10)
113 assert.isBelow(value, 20)
114 }
115 }))
116
117 it.effect("does not round the bounds", () =>
118 Effect.gen(function*() {
119 const value = yield* Random.nextBetween(10.5, 20.5).pipe(
120 Effect.provideService(Random.Random, {
121 nextIntUnsafe: () => 0,
122 nextDoubleUnsafe: () => 0.75
123 })
124 )
125
126 assert.strictEqual(value, 18)
127 }))
128
129 it.effect("handles negative ranges", () =>
130 Effect.gen(function*() {
131 const value = yield* Random.nextBetween(-10, 10)
132
133 assert.isAtLeast(value, -10)
134 assert.isBelow(value, 10)
135 }))
136 })
137
138 describe("nextIntBetween", () => {
139 it.effect("generates integer in closed range", () =>
140 Effect.gen(function*() {
141 for (let i = 0; i < 100; i++) {
142 const value = yield* Random.nextIntBetween(1, 6)
143 assert.isTrue(Number.isInteger(value))
144 assert.isAtLeast(value, 1)
145 assert.isAtMost(value, 6)
146 }
147 }))
148
149 it.effect("generates integer in half-open range", () =>
150 Effect.gen(function*() {
151 for (let i = 0; i < 100; i++) {
152 const value = yield* Random.nextIntBetween(1, 6, {
153 halfOpen: true
154 })
155 assert.isTrue(Number.isInteger(value))
156 assert.isAtLeast(value, 1)
157 assert.isBelow(value, 6)
158 }
159 }))
160
161 it.effect("excludes the upper bound in half-open ranges", () =>
162 Effect.gen(function*() {
163 const value = yield* Random.nextIntBetween(1, 6, { halfOpen: true }).pipe(
164 Effect.provideService(Random.Random, {
165 nextIntUnsafe: () => 0,
166 nextDoubleUnsafe: () => 1 - Number.EPSILON
167 })
168 )
169
170 assert.strictEqual(value, 5)
171 }))
172 })
173
174 describe("shuffle", () => {
175 it.effect("returns a shuffled copy of all values", () =>
176 Effect.gen(function*() {
177 const input = [1, 2, 3, 4, 5]
178 const output = yield* Random.shuffle(input)
179
180 assert.notStrictEqual(output, input)
181 assert.deepStrictEqual(input, [1, 2, 3, 4, 5])
182 assert.deepStrictEqual(Array.from(output).sort((a, b) => a - b), [1, 2, 3, 4, 5])
183 }))
184
185 it.effect("is deterministic with the same seed", () =>
186 Effect.gen(function*() {
187 const program = Random.shuffle([1, 2, 3, 4, 5])
188
189 const result1 = yield* program.pipe(Random.withSeed("shuffle-seed"))
190 const result2 = yield* program.pipe(Random.withSeed("shuffle-seed"))
191
192 assert.deepStrictEqual(result1, result2)
193 }))
194 })
195
196 describe("choice", () => {
197 it.effect("selects a random element from an iterable", () =>
198 Effect.gen(function*() {
199 const value = yield* Random.choice(["a", "b", "c"]).pipe(
200 Effect.provideService(Random.Random, {
201 nextIntUnsafe: () => 0,
202 nextDoubleUnsafe: () => 0.75
203 })
204 )
205
206 assert.strictEqual(value, "c")
207 }))
208
209 it.effect("fails with NoSuchElementError for an empty iterable", () =>
210 Effect.gen(function*() {
211 const error = yield* Random.choice([]).pipe(Effect.flip)
212
213 assert.isTrue(Cause.isNoSuchElementError(error))
214 assert.strictEqual(error.message, "Cannot select a random element from an empty array")
215 }))
216
217 it.effect("is deterministic with the same seed", () =>
218 Effect.gen(function*() {
219 const program = Effect.all([
220 Random.choice([1, 2, 3, 4, 5]),
221 Random.choice([1, 2, 3, 4, 5]),
222 Random.choice([1, 2, 3, 4, 5])
223 ])
224
225 const result1 = yield* program.pipe(Random.withSeed("choice-seed"))
226 const result2 = yield* program.pipe(Random.withSeed("choice-seed"))
227
228 assert.deepStrictEqual(result1, result2)
229 }))
230 })
231
232 describe("withSeed", () => {
233 it.effect("produces deterministic sequence with same seed", () =>
234 Effect.gen(function*() {
235 const program = Effect.gen(function*() {
236 const v1 = yield* Random.next
237 const v2 = yield* Random.nextInt
238 return [v1, v2]
239 })
240
241 const result1 = yield* program.pipe(Random.withSeed("test-seed"))
242 const result2 = yield* program.pipe(Random.withSeed("test-seed"))
243
244 assert.deepStrictEqual(result1, result2)
245 }))
246
247 it.effect("produces different sequences with different seeds", () =>
248 Effect.gen(function*() {
249 const program = Effect.gen(function*() {
250 const v1 = yield* Random.next
251 const v2 = yield* Random.nextInt
252 return [v1, v2]
253 })
254
255 const result1 = yield* program.pipe(Random.withSeed(12345))
256 const result2 = yield* program.pipe(Random.withSeed(67890))
257
258 assert.notDeepEqual(result1, result2)
259 }))
260
261 it.effect("distinguishes one-character string seeds", () =>
262 Effect.gen(function*() {
263 const program = Effect.all([Random.next, Random.next, Random.next, Random.next, Random.next])
264
265 const empty = yield* program.pipe(Random.withSeed(""))
266 const result1 = yield* program.pipe(Random.withSeed("a"))
267 const result2 = yield* program.pipe(Random.withSeed("b"))
268
269 assert.notDeepEqual(result1, empty)
270 assert.notDeepEqual(result2, empty)
271 assert.notDeepEqual(result1, result2)
272 }))
273
274 it.effect("distinguishes trailing partial UTF-8 words in string seeds", () =>
275 Effect.gen(function*() {
276 const program = Effect.all([Random.next, Random.next, Random.next, Random.next, Random.next])
277 const pairs = [
278 ["1234a", "1234b"],
279 ["1234ab", "1234ac"],
280 ["1234abc", "1234abd"]
281 ] as const
282
283 for (const [seed1, seed2] of pairs) {
284 const result1 = yield* program.pipe(Random.withSeed(seed1))
285 const result2 = yield* program.pipe(Random.withSeed(seed2))
286
287 assert.notDeepEqual(result1, result2)
288 }
289 }))
290
291 it.effect("distinguishes astral character string seeds", () =>
292 Effect.gen(function*() {
293 const program = Effect.all([Random.next, Random.next, Random.next, Random.next, Random.next])
294
295 const empty = yield* program.pipe(Random.withSeed(""))
296 const result1 = yield* program.pipe(Random.withSeed("😀"))
297 const result2 = yield* program.pipe(Random.withSeed("😁"))
298 const result3 = yield* program.pipe(Random.withSeed("🐀"))
299
300 assert.notDeepEqual(result1, empty)
301 assert.notDeepEqual(result2, empty)
302 assert.notDeepEqual(result1, result2)
303 assert.notDeepEqual(result1, result3)
304 }))
305
306 it.effect("works with numeric seeds", () =>
307 Effect.gen(function*() {
308 const program = Effect.gen(function*() {
309 const v1 = yield* Random.next
310 const v2 = yield* Random.nextInt
311 return [v1, v2]
312 })
313
314 const result1 = yield* program.pipe(Random.withSeed(12345))
315 const result2 = yield* program.pipe(Random.withSeed(12345))
316
317 assert.deepStrictEqual(result1, result2)
318 }))
319 })
320})