1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Equal, Hash, HashSet, TxHashSet } from "effect"
3
4class TestValue implements Equal.Equal {
5 constructor(readonly value: string) {}
6
7 [Equal.symbol](other: unknown): boolean {
8 return other instanceof TestValue && this.value === other.value
9 }
10
11 [Hash.symbol](): number {
12 return Hash.string(this.value)
13 }
14
15 toString(): string {
16 return `TestValue(${this.value})`
17 }
18}
19
20describe("TxHashSet", () => {
21 describe("constructors", () => {
22 it.effect("empty creates an empty TxHashSet", () =>
23 Effect.tx(Effect.gen(function*() {
24 const txSet = yield* TxHashSet.empty<string>()
25 const size = yield* TxHashSet.size(txSet)
26 const isEmpty = yield* TxHashSet.isEmpty(txSet)
27
28 assert.strictEqual(size, 0)
29 assert.strictEqual(isEmpty, true)
30 })))
31
32 it.effect("make creates TxHashSet from values", () =>
33 Effect.tx(Effect.gen(function*() {
34 const txSet = yield* TxHashSet.make("a", "b", "c")
35 const size = yield* TxHashSet.size(txSet)
36
37 assert.strictEqual(size, 3)
38 assert.strictEqual(yield* TxHashSet.has(txSet, "a"), true)
39 assert.strictEqual(yield* TxHashSet.has(txSet, "b"), true)
40 assert.strictEqual(yield* TxHashSet.has(txSet, "c"), true)
41 assert.strictEqual(yield* TxHashSet.has(txSet, "d"), false)
42 })))
43
44 it.effect("make removes duplicates", () =>
45 Effect.tx(Effect.gen(function*() {
46 const txSet = yield* TxHashSet.make("a", "b", "a", "c", "b")
47 const size = yield* TxHashSet.size(txSet)
48
49 assert.strictEqual(size, 3)
50 })))
51
52 it.effect("fromIterable creates TxHashSet from iterable", () =>
53 Effect.tx(Effect.gen(function*() {
54 const txSet = yield* TxHashSet.fromIterable(["x", "y", "z", "x", "y"])
55 const size = yield* TxHashSet.size(txSet)
56
57 assert.strictEqual(size, 3)
58 assert.strictEqual(yield* TxHashSet.has(txSet, "x"), true)
59 assert.strictEqual(yield* TxHashSet.has(txSet, "y"), true)
60 assert.strictEqual(yield* TxHashSet.has(txSet, "z"), true)
61 })))
62
63 it.effect("fromHashSet creates TxHashSet from HashSet", () =>
64 Effect.tx(Effect.gen(function*() {
65 const hashSet = HashSet.make("foo", "bar", "baz")
66 const txSet = yield* TxHashSet.fromHashSet(hashSet)
67 const size = yield* TxHashSet.size(txSet)
68
69 assert.strictEqual(size, 3)
70 assert.strictEqual(yield* TxHashSet.has(txSet, "foo"), true)
71 assert.strictEqual(yield* TxHashSet.has(txSet, "bar"), true)
72 assert.strictEqual(yield* TxHashSet.has(txSet, "baz"), true)
73 })))
74 })
75
76 describe("type guards", () => {
77 it.effect("isTxHashSet identifies TxHashSet instances", () =>
78 Effect.tx(Effect.gen(function*() {
79 const txSet = yield* TxHashSet.make(1, 2, 3)
80 const hashSet = HashSet.make(1, 2, 3)
81 const array = [1, 2, 3]
82
83 assert.strictEqual(TxHashSet.isTxHashSet(txSet), true)
84 assert.strictEqual(TxHashSet.isTxHashSet(hashSet), false)
85 assert.strictEqual(TxHashSet.isTxHashSet(array), false)
86 assert.strictEqual(TxHashSet.isTxHashSet(null), false)
87 assert.strictEqual(TxHashSet.isTxHashSet(undefined), false)
88 })))
89 })
90
91 describe("basic operations", () => {
92 it.effect("add adds values to TxHashSet", () =>
93 Effect.tx(Effect.gen(function*() {
94 const txSet = yield* TxHashSet.make("a", "b")
95
96 yield* TxHashSet.add(txSet, "c")
97 assert.strictEqual(yield* TxHashSet.size(txSet), 3)
98 assert.strictEqual(yield* TxHashSet.has(txSet, "c"), true)
99
100 // Adding existing value has no effect
101 yield* TxHashSet.add(txSet, "a")
102 assert.strictEqual(yield* TxHashSet.size(txSet), 3)
103 })))
104
105 it.effect("remove removes values from TxHashSet", () =>
106 Effect.tx(Effect.gen(function*() {
107 const txSet = yield* TxHashSet.make("a", "b", "c")
108
109 const removed = yield* TxHashSet.remove(txSet, "b")
110 assert.strictEqual(removed, true)
111 assert.strictEqual(yield* TxHashSet.size(txSet), 2)
112 assert.strictEqual(yield* TxHashSet.has(txSet, "b"), false)
113
114 // Removing non-existent value returns false
115 const notRemoved = yield* TxHashSet.remove(txSet, "d")
116 assert.strictEqual(notRemoved, false)
117 assert.strictEqual(yield* TxHashSet.size(txSet), 2)
118 })))
119
120 it.effect("has checks for value existence", () =>
121 Effect.tx(Effect.gen(function*() {
122 const txSet = yield* TxHashSet.make("apple", "banana", "cherry")
123
124 assert.strictEqual(yield* TxHashSet.has(txSet, "apple"), true)
125 assert.strictEqual(yield* TxHashSet.has(txSet, "grape"), false)
126 })))
127
128 it.effect("size returns correct count", () =>
129 Effect.tx(Effect.gen(function*() {
130 const empty = yield* TxHashSet.empty<string>()
131 assert.strictEqual(yield* TxHashSet.size(empty), 0)
132
133 const small = yield* TxHashSet.make("a", "b")
134 assert.strictEqual(yield* TxHashSet.size(small), 2)
135
136 const fromIterable = yield* TxHashSet.fromIterable(["x", "y", "z", "x", "y"])
137 assert.strictEqual(yield* TxHashSet.size(fromIterable), 3)
138 })))
139
140 it.effect("isEmpty and isNonEmpty check the set", () =>
141 Effect.tx(Effect.gen(function*() {
142 const empty = yield* TxHashSet.empty<string>()
143 assert.strictEqual(yield* TxHashSet.isEmpty(empty), true)
144 assert.strictEqual(yield* TxHashSet.isNonEmpty(empty), false)
145
146 const nonEmpty = yield* TxHashSet.make("a")
147 assert.strictEqual(yield* TxHashSet.isEmpty(nonEmpty), false)
148 assert.strictEqual(yield* TxHashSet.isNonEmpty(nonEmpty), true)
149 })))
150
151 it.effect("clear removes all values", () =>
152 Effect.tx(Effect.gen(function*() {
153 const txSet = yield* TxHashSet.make("a", "b", "c")
154 assert.strictEqual(yield* TxHashSet.size(txSet), 3)
155
156 yield* TxHashSet.clear(txSet)
157 assert.strictEqual(yield* TxHashSet.size(txSet), 0)
158 assert.strictEqual(yield* TxHashSet.isEmpty(txSet), true)
159 })))
160 })
161
162 describe("set operations", () => {
163 it.effect("union combines two sets", () =>
164 Effect.tx(Effect.gen(function*() {
165 const set1 = yield* TxHashSet.make("a", "b")
166 const set2 = yield* TxHashSet.make("b", "c")
167 const combined = yield* TxHashSet.union(set1, set2)
168
169 const values = yield* TxHashSet.toHashSet(combined)
170 const sortedValues = Array.from(values).sort()
171 assert.deepStrictEqual(sortedValues, ["a", "b", "c"])
172 assert.strictEqual(yield* TxHashSet.size(combined), 3)
173 })))
174
175 it.effect("intersection finds common elements", () =>
176 Effect.tx(Effect.gen(function*() {
177 const set1 = yield* TxHashSet.make("a", "b", "c")
178 const set2 = yield* TxHashSet.make("b", "c", "d")
179 const common = yield* TxHashSet.intersection(set1, set2)
180
181 const values = yield* TxHashSet.toHashSet(common)
182 const sortedValues = Array.from(values).sort()
183 assert.deepStrictEqual(sortedValues, ["b", "c"])
184 assert.strictEqual(yield* TxHashSet.size(common), 2)
185 })))
186
187 it.effect("difference finds elements only in first set", () =>
188 Effect.tx(Effect.gen(function*() {
189 const set1 = yield* TxHashSet.make("a", "b", "c")
190 const set2 = yield* TxHashSet.make("b", "d")
191 const diff = yield* TxHashSet.difference(set1, set2)
192
193 const values = yield* TxHashSet.toHashSet(diff)
194 const sortedValues = Array.from(values).sort()
195 assert.deepStrictEqual(sortedValues, ["a", "c"])
196 assert.strictEqual(yield* TxHashSet.size(diff), 2)
197 })))
198
199 it.effect("isSubset checks subset relationship", () =>
200 Effect.tx(Effect.gen(function*() {
201 const small = yield* TxHashSet.make("a", "b")
202 const large = yield* TxHashSet.make("a", "b", "c", "d")
203 const other = yield* TxHashSet.make("x", "y")
204
205 assert.strictEqual(yield* TxHashSet.isSubset(small, large), true)
206 assert.strictEqual(yield* TxHashSet.isSubset(large, small), false)
207 assert.strictEqual(yield* TxHashSet.isSubset(small, other), false)
208 assert.strictEqual(yield* TxHashSet.isSubset(small, small), true)
209 })))
210 })
211
212 describe("query operations", () => {
213 it.effect("some tests if any element satisfies predicate", () =>
214 Effect.tx(Effect.gen(function*() {
215 const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5)
216
217 assert.strictEqual(yield* TxHashSet.some(numbers, (n) => n > 3), true)
218 assert.strictEqual(yield* TxHashSet.some(numbers, (n) => n > 10), false)
219
220 const empty = yield* TxHashSet.empty<number>()
221 assert.strictEqual(yield* TxHashSet.some(empty, (n) => n > 0), false)
222 })))
223
224 it.effect("every tests if all elements satisfy predicate", () =>
225 Effect.tx(Effect.gen(function*() {
226 const numbers = yield* TxHashSet.make(2, 4, 6, 8)
227
228 assert.strictEqual(yield* TxHashSet.every(numbers, (n) => n % 2 === 0), true)
229 assert.strictEqual(yield* TxHashSet.every(numbers, (n) => n > 5), false)
230
231 const empty = yield* TxHashSet.empty<number>()
232 assert.strictEqual(yield* TxHashSet.every(empty, (n) => n > 0), true) // vacuously true
233 })))
234 })
235
236 describe("functional operations", () => {
237 it.effect("map transforms values", () =>
238 Effect.tx(Effect.gen(function*() {
239 const numbers = yield* TxHashSet.make(1, 2, 3)
240 const doubled = yield* TxHashSet.map(numbers, (n) => n * 2)
241
242 const values = yield* TxHashSet.toHashSet(doubled)
243 const sortedValues = Array.from(values).sort()
244 assert.deepStrictEqual(sortedValues, [2, 4, 6])
245 assert.strictEqual(yield* TxHashSet.size(doubled), 3)
246 })))
247
248 it.effect("map can reduce size with duplicate results", () =>
249 Effect.tx(Effect.gen(function*() {
250 const strings = yield* TxHashSet.make("apple", "banana", "cherry")
251 const lengths = yield* TxHashSet.map(strings, (s) => s.length)
252
253 const values = yield* TxHashSet.toHashSet(lengths)
254 const sortedValues = Array.from(values).sort()
255 assert.deepStrictEqual(sortedValues, [5, 6]) // apple=5, banana=6, cherry=6
256 })))
257
258 it.effect("filter keeps only matching values", () =>
259 Effect.tx(Effect.gen(function*() {
260 const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5, 6)
261 const evens = yield* TxHashSet.filter(numbers, (n) => n % 2 === 0)
262
263 const values = yield* TxHashSet.toHashSet(evens)
264 const sortedValues = Array.from(values).sort()
265 assert.deepStrictEqual(sortedValues, [2, 4, 6])
266 assert.strictEqual(yield* TxHashSet.size(evens), 3)
267 })))
268
269 it.effect("reduce accumulates values", () =>
270 Effect.tx(Effect.gen(function*() {
271 const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5)
272 const sum = yield* TxHashSet.reduce(numbers, 0, (acc, n) => acc + n)
273
274 assert.strictEqual(sum, 15)
275
276 const strings = yield* TxHashSet.make("a", "b", "c")
277 const concatenated = yield* TxHashSet.reduce(strings, "", (acc, s) => acc + s)
278
279 // Order may vary, but all letters should be present
280 assert.strictEqual(concatenated.length, 3)
281 assert.strictEqual(concatenated.includes("a"), true)
282 assert.strictEqual(concatenated.includes("b"), true)
283 assert.strictEqual(concatenated.includes("c"), true)
284 })))
285 })
286
287 describe("conversions", () => {
288 it.effect("toHashSet creates immutable snapshot", () =>
289 Effect.tx(Effect.gen(function*() {
290 const txSet = yield* TxHashSet.make("x", "y", "z")
291 const hashSet = yield* TxHashSet.toHashSet(txSet)
292
293 assert.strictEqual(HashSet.size(hashSet), 3)
294 assert.strictEqual(HashSet.has(hashSet, "y"), true)
295
296 // hashSet is a snapshot - modifications to txSet don't affect it
297 yield* TxHashSet.add(txSet, "w")
298 assert.strictEqual(HashSet.size(hashSet), 3) // unchanged
299 assert.strictEqual(yield* TxHashSet.size(txSet), 4)
300 })))
301 })
302
303 describe("custom Equal objects", () => {
304 it.effect("works with custom Equal implementations", () =>
305 Effect.tx(Effect.gen(function*() {
306 const txSet = yield* TxHashSet.make(
307 new TestValue("alice"),
308 new TestValue("bob"),
309 new TestValue("charlie")
310 )
311
312 assert.strictEqual(yield* TxHashSet.size(txSet), 3)
313 assert.strictEqual(yield* TxHashSet.has(txSet, new TestValue("alice")), true)
314 assert.strictEqual(yield* TxHashSet.has(txSet, new TestValue("dave")), false)
315
316 // Adding duplicate based on Equal should have no effect
317 yield* TxHashSet.add(txSet, new TestValue("alice"))
318 assert.strictEqual(yield* TxHashSet.size(txSet), 3)
319
320 // Removing should work with Equal
321 const removed = yield* TxHashSet.remove(txSet, new TestValue("bob"))
322 assert.strictEqual(removed, true)
323 assert.strictEqual(yield* TxHashSet.size(txSet), 2)
324 assert.strictEqual(yield* TxHashSet.has(txSet, new TestValue("bob")), false)
325 })))
326 })
327
328 describe("transactional behavior", () => {
329 it.effect("operations are atomic within transactions", () =>
330 Effect.tx(Effect.gen(function*() {
331 const txSet = yield* TxHashSet.make("a", "b", "c")
332
333 const hasCherry = yield* TxHashSet.has(txSet, "c")
334 if (hasCherry) {
335 yield* TxHashSet.remove(txSet, "c")
336 yield* TxHashSet.add(txSet, "orange")
337 }
338 yield* TxHashSet.add(txSet, "grape")
339
340 assert.strictEqual(yield* TxHashSet.size(txSet), 4)
341 assert.strictEqual(yield* TxHashSet.has(txSet, "c"), false)
342 assert.strictEqual(yield* TxHashSet.has(txSet, "orange"), true)
343 assert.strictEqual(yield* TxHashSet.has(txSet, "grape"), true)
344 })))
345
346 it.effect("multiple set operations work together", () =>
347 Effect.tx(Effect.gen(function*() {
348 const set1 = yield* TxHashSet.make("a", "b", "c")
349 const set2 = yield* TxHashSet.make("c", "d", "e")
350
351 yield* TxHashSet.add(set1, "f")
352 yield* TxHashSet.remove(set2, "d")
353
354 const intersection = yield* TxHashSet.intersection(set1, set2)
355 const commonSize = yield* TxHashSet.size(intersection)
356
357 if (commonSize > 0) {
358 yield* TxHashSet.add(set1, "shared")
359 yield* TxHashSet.add(set2, "shared")
360 }
361
362 assert.strictEqual(yield* TxHashSet.has(set1, "f"), true)
363 assert.strictEqual(yield* TxHashSet.has(set1, "shared"), true)
364 assert.strictEqual(yield* TxHashSet.has(set2, "d"), false)
365 assert.strictEqual(yield* TxHashSet.has(set2, "shared"), true)
366 })))
367 })
368
369 describe("stress testing", () => {
370 it.effect("handles large number of operations", () =>
371 Effect.tx(Effect.gen(function*() {
372 const txSet = yield* TxHashSet.empty<number>()
373
374 // Add 1000 numbers
375 for (let i = 0; i < 1000; i++) {
376 yield* TxHashSet.add(txSet, i)
377 }
378
379 assert.strictEqual(yield* TxHashSet.size(txSet), 1000)
380
381 // Check random elements
382 assert.strictEqual(yield* TxHashSet.has(txSet, 500), true)
383 assert.strictEqual(yield* TxHashSet.has(txSet, 999), true)
384 assert.strictEqual(yield* TxHashSet.has(txSet, 1000), false)
385
386 // Remove half the elements
387 for (let i = 0; i < 500; i++) {
388 yield* TxHashSet.remove(txSet, i)
389 }
390
391 assert.strictEqual(yield* TxHashSet.size(txSet), 500)
392 assert.strictEqual(yield* TxHashSet.has(txSet, 250), false)
393 assert.strictEqual(yield* TxHashSet.has(txSet, 750), true)
394 })))
395
396 it.effect("functional operations work with large sets", () =>
397 Effect.tx(Effect.gen(function*() {
398 const numbers = yield* TxHashSet.fromIterable(Array.from({ length: 100 }, (_, i) => i))
399
400 const evens = yield* TxHashSet.filter(numbers, (n) => n % 2 === 0)
401 assert.strictEqual(yield* TxHashSet.size(evens), 50)
402
403 const doubled = yield* TxHashSet.map(evens, (n) => n * 2)
404 assert.strictEqual(yield* TxHashSet.size(doubled), 50)
405
406 const sum = yield* TxHashSet.reduce(evens, 0, (acc, n) => acc + n)
407 assert.strictEqual(sum, 2450) // sum of even numbers 0 to 98
408 })))
409 })
410
411 describe("edge cases", () => {
412 it.effect("handles empty set operations correctly", () =>
413 Effect.tx(Effect.gen(function*() {
414 const empty1 = yield* TxHashSet.empty<string>()
415 const empty2 = yield* TxHashSet.empty<string>()
416 const nonEmpty = yield* TxHashSet.make("a", "b")
417
418 const emptyUnion = yield* TxHashSet.union(empty1, empty2)
419 assert.strictEqual(yield* TxHashSet.size(emptyUnion), 0)
420
421 const nonEmptyUnion = yield* TxHashSet.union(empty1, nonEmpty)
422 assert.strictEqual(yield* TxHashSet.size(nonEmptyUnion), 2)
423
424 const emptyIntersection = yield* TxHashSet.intersection(empty1, nonEmpty)
425 assert.strictEqual(yield* TxHashSet.size(emptyIntersection), 0)
426
427 assert.strictEqual(yield* TxHashSet.isSubset(empty1, nonEmpty), true)
428 assert.strictEqual(yield* TxHashSet.isSubset(nonEmpty, empty1), false)
429 })))
430
431 it.effect("handles single element sets", () =>
432 Effect.tx(Effect.gen(function*() {
433 const single = yield* TxHashSet.make("only")
434
435 assert.strictEqual(yield* TxHashSet.size(single), 1)
436 assert.strictEqual(yield* TxHashSet.has(single, "only"), true)
437 assert.strictEqual(yield* TxHashSet.isEmpty(single), false)
438
439 yield* TxHashSet.clear(single)
440 assert.strictEqual(yield* TxHashSet.size(single), 0)
441 assert.strictEqual(yield* TxHashSet.isEmpty(single), true)
442 })))
443 })
444})