1import { describe, it } from "@effect/vitest"
2import { Function as Fun, pipe, Predicate } from "effect"
3import assert from "node:assert/strict"
4
5const isPositive: Predicate.Predicate<number> = (n) => n > 0
6const isNegative: Predicate.Predicate<number> = (n) => n < 0
7const isLessThan2: Predicate.Predicate<number> = (n) => n < 2
8const isString: Predicate.Refinement<unknown, string> = (u: unknown): u is string => typeof u === "string"
9
10interface NonEmptyStringBrand {
11 readonly NonEmptyString: unique symbol
12}
13
14type NonEmptyString = string & NonEmptyStringBrand
15
16const isNonEmptyString: Predicate.Refinement<string, NonEmptyString> = (s): s is NonEmptyString => s.length > 0
17
18describe("compose", () => {
19 it("composes refinements to narrow through both checks", () => {
20 const refinement = pipe(isString, Predicate.compose(isNonEmptyString))
21
22 assert.equal(refinement("a"), true)
23 assert.equal(refinement(null), false)
24 assert.equal(refinement(""), false)
25 })
26
27 it("composes a refinement with a predicate", () => {
28 const isLong = (s: string) => s.length > 2
29 const refinement = Predicate.compose(isString, isLong)
30
31 assert.equal(refinement("abcd"), true)
32 assert.equal(refinement("a"), false)
33 assert.equal(refinement(1), false)
34 })
35})
36
37describe("mapInput", () => {
38 it("maps input before applying the predicate (curried)", () => {
39 type A = { readonly a: number }
40 const predicate = pipe(
41 isPositive,
42 Predicate.mapInput((a: A) => a.a)
43 )
44
45 assert.equal(predicate({ a: -1 }), false)
46 assert.equal(predicate({ a: 0 }), false)
47 assert.equal(predicate({ a: 1 }), true)
48 })
49
50 it("maps input before applying the predicate (uncurried)", () => {
51 type A = { readonly a: number }
52 const predicate = Predicate.mapInput(isPositive, (a: A) => a.a)
53
54 assert.equal(predicate({ a: -1 }), false)
55 assert.equal(predicate({ a: 0 }), false)
56 assert.equal(predicate({ a: 1 }), true)
57 })
58})
59
60describe("Tuple", () => {
61 it("returns true only when all element predicates succeed", () => {
62 const p = Predicate.Tuple([isPositive, isNegative])
63
64 assert.equal(p([1, -1]), true)
65 assert.equal(p([1, 1]), false)
66 assert.equal(p([-1, -1]), false)
67 assert.equal(p([-1, 1]), false)
68 })
69
70 it("stops checking after the first failing predicate", () => {
71 let calls = 0
72 const first = (_: number) => {
73 calls += 1
74 return false
75 }
76 const second = (_: number) => {
77 calls += 1
78 return true
79 }
80
81 const p = Predicate.Tuple([first, second])
82
83 assert.equal(p([1, 2]), false)
84 assert.equal(calls, 1)
85 })
86})
87
88describe("Struct", () => {
89 it("returns true only when all field predicates succeed", () => {
90 const p = Predicate.Struct({ a: isPositive, b: isNegative })
91
92 assert.equal(p({ a: 1, b: -1 }), true)
93 assert.equal(p({ a: 1, b: 1 }), false)
94 assert.equal(p({ a: -1, b: -1 }), false)
95 assert.equal(p({ a: -1, b: 1 }), false)
96 })
97
98 it("ignores extra keys and stops at first failing field", () => {
99 let calls = 0
100 const first = (_: number) => {
101 calls += 1
102 return false
103 }
104 const second = (_: string) => {
105 calls += 1
106 return true
107 }
108
109 const p = Predicate.Struct({ a: first, b: second })
110
111 assert.equal(p({ a: 1, b: "ok", extra: true } as any), false)
112 assert.equal(calls, 1)
113 })
114})
115
116describe("not", () => {
117 it("negates the underlying predicate", () => {
118 const p = Predicate.not(isPositive)
119
120 assert.equal(p(1), false)
121 assert.equal(p(0), true)
122 assert.equal(p(-1), true)
123 })
124})
125
126describe("or", () => {
127 it("returns true when either predicate is true", () => {
128 const p = pipe(isPositive, Predicate.or(isNegative))
129
130 assert.equal(p(-1), true)
131 assert.equal(p(1), true)
132 assert.equal(p(0), false)
133 })
134})
135
136describe("and", () => {
137 it("returns true only when both predicates are true", () => {
138 const p = pipe(isPositive, Predicate.and(isLessThan2))
139
140 assert.equal(p(1), true)
141 assert.equal(p(-1), false)
142 assert.equal(p(3), false)
143 })
144})
145
146describe("xor", () => {
147 it("returns true only when exactly one predicate is true", () => {
148 assert.equal(pipe(Fun.constTrue, Predicate.xor(Fun.constTrue))(null), false)
149 assert.equal(pipe(Fun.constTrue, Predicate.xor(Fun.constFalse))(null), true)
150 assert.equal(pipe(Fun.constFalse, Predicate.xor(Fun.constTrue))(null), true)
151 assert.equal(pipe(Fun.constFalse, Predicate.xor(Fun.constFalse))(null), false)
152 })
153})
154
155describe("eqv", () => {
156 it("returns true when both predicates agree", () => {
157 assert.equal(pipe(Fun.constTrue, Predicate.eqv(Fun.constTrue))(null), true)
158 assert.equal(pipe(Fun.constTrue, Predicate.eqv(Fun.constFalse))(null), false)
159 assert.equal(pipe(Fun.constFalse, Predicate.eqv(Fun.constTrue))(null), false)
160 assert.equal(pipe(Fun.constFalse, Predicate.eqv(Fun.constFalse))(null), true)
161 })
162})
163
164describe("implies", () => {
165 it("returns true when antecedent is false or consequent is true", () => {
166 assert.equal(pipe(Fun.constTrue, Predicate.implies(Fun.constTrue))(null), true)
167 assert.equal(pipe(Fun.constTrue, Predicate.implies(Fun.constFalse))(null), false)
168 assert.equal(pipe(Fun.constFalse, Predicate.implies(Fun.constTrue))(null), true)
169 assert.equal(pipe(Fun.constFalse, Predicate.implies(Fun.constFalse))(null), true)
170 })
171})
172
173describe("nor", () => {
174 it("returns true only when both predicates are false", () => {
175 assert.equal(pipe(Fun.constTrue, Predicate.nor(Fun.constTrue))(null), false)
176 assert.equal(pipe(Fun.constTrue, Predicate.nor(Fun.constFalse))(null), false)
177 assert.equal(pipe(Fun.constFalse, Predicate.nor(Fun.constTrue))(null), false)
178 assert.equal(pipe(Fun.constFalse, Predicate.nor(Fun.constFalse))(null), true)
179 })
180})
181
182describe("nand", () => {
183 it("returns false only when both predicates are true", () => {
184 assert.equal(pipe(Fun.constTrue, Predicate.nand(Fun.constTrue))(null), false)
185 assert.equal(pipe(Fun.constTrue, Predicate.nand(Fun.constFalse))(null), true)
186 assert.equal(pipe(Fun.constFalse, Predicate.nand(Fun.constTrue))(null), true)
187 assert.equal(pipe(Fun.constFalse, Predicate.nand(Fun.constFalse))(null), true)
188 })
189})
190
191describe("every", () => {
192 it("returns true when all predicates pass", () => {
193 const predicate = Predicate.every([isPositive, isLessThan2])
194
195 assert.equal(predicate(1), true)
196 assert.equal(predicate(0), false)
197 assert.equal(predicate(-2), false)
198 })
199
200 it("returns true for empty collections", () => {
201 const predicate = Predicate.every([])
202
203 assert.equal(predicate(0), true)
204 })
205})
206
207describe("some", () => {
208 it("returns true when any predicate passes", () => {
209 const predicate = Predicate.some([isPositive, isNegative])
210
211 assert.equal(predicate(0), false)
212 assert.equal(predicate(-1), true)
213 assert.equal(predicate(1), true)
214 })
215
216 it("returns false for empty collections", () => {
217 const predicate = Predicate.some([])
218
219 assert.equal(predicate(0), false)
220 })
221})
222
223describe("isTruthy", () => {
224 it("uses JavaScript truthiness", () => {
225 assert.equal(Predicate.isTruthy(true), true)
226 assert.equal(Predicate.isTruthy(false), false)
227 assert.equal(Predicate.isTruthy("a"), true)
228 assert.equal(Predicate.isTruthy(""), false)
229 assert.equal(Predicate.isTruthy(1), true)
230 assert.equal(Predicate.isTruthy(0), false)
231 assert.equal(Predicate.isTruthy(1n), true)
232 assert.equal(Predicate.isTruthy(0n), false)
233 })
234})
235
236describe("isSet", () => {
237 it("detects Set instances", () => {
238 assert.equal(Predicate.isSet(new Set([1, 2])), true)
239 assert.equal(Predicate.isSet(new Set()), true)
240 assert.equal(Predicate.isSet({}), false)
241 assert.equal(Predicate.isSet(null), false)
242 assert.equal(Predicate.isSet(undefined), false)
243 })
244})
245
246describe("isMap", () => {
247 it("detects Map instances", () => {
248 assert.equal(Predicate.isMap(new Map()), true)
249 assert.equal(Predicate.isMap({}), false)
250 assert.equal(Predicate.isMap(null), false)
251 assert.equal(Predicate.isMap(undefined), false)
252 })
253})
254
255describe("isString", () => {
256 it("detects string values", () => {
257 assert.equal(Predicate.isString("a"), true)
258 assert.equal(Predicate.isString(1), false)
259 assert.equal(Predicate.isString(new String("a")), false)
260 })
261})
262
263describe("isNumber", () => {
264 it("detects number values", () => {
265 assert.equal(Predicate.isNumber(2), true)
266 assert.equal(Predicate.isNumber(Number.NaN), true)
267 assert.equal(Predicate.isNumber("2"), false)
268 })
269})
270
271describe("isBoolean", () => {
272 it("detects boolean values", () => {
273 assert.equal(Predicate.isBoolean(true), true)
274 assert.equal(Predicate.isBoolean(false), true)
275 assert.equal(Predicate.isBoolean("true"), false)
276 })
277})
278
279describe("isBigInt", () => {
280 it("detects bigint values", () => {
281 assert.equal(Predicate.isBigInt(1n), true)
282 assert.equal(Predicate.isBigInt(1), false)
283 })
284})
285
286describe("isSymbol", () => {
287 it("detects symbol values", () => {
288 assert.equal(Predicate.isSymbol(Symbol.for("a")), true)
289 assert.equal(Predicate.isSymbol("a"), false)
290 })
291})
292
293describe("isPropertyKey", () => {
294 it("accepts strings, numbers, and symbols", () => {
295 assert.equal(Predicate.isPropertyKey("a"), true)
296 assert.equal(Predicate.isPropertyKey(1), true)
297 assert.equal(Predicate.isPropertyKey(Number.NaN), true)
298 assert.equal(Predicate.isPropertyKey(Symbol.for("a")), true)
299 assert.equal(Predicate.isPropertyKey(true), false)
300 })
301})
302
303describe("isFunction", () => {
304 it("detects callable values", () => {
305 assert.equal(Predicate.isFunction(Predicate.isFunction), true)
306 assert.equal(Predicate.isFunction("function"), false)
307 })
308})
309
310describe("isUndefined", () => {
311 it("detects undefined", () => {
312 assert.equal(Predicate.isUndefined(undefined), true)
313 assert.equal(Predicate.isUndefined(null), false)
314 assert.equal(Predicate.isUndefined("undefined"), false)
315 })
316})
317
318describe("isNotUndefined", () => {
319 it("filters out undefined", () => {
320 assert.equal(Predicate.isNotUndefined(undefined), false)
321 assert.equal(Predicate.isNotUndefined(null), true)
322 assert.equal(Predicate.isNotUndefined("undefined"), true)
323 })
324})
325
326describe("isNull", () => {
327 it("detects null", () => {
328 assert.equal(Predicate.isNull(null), true)
329 assert.equal(Predicate.isNull(undefined), false)
330 assert.equal(Predicate.isNull("null"), false)
331 })
332})
333
334describe("isNotNull", () => {
335 it("filters out null", () => {
336 assert.equal(Predicate.isNotNull(null), false)
337 assert.equal(Predicate.isNotNull(undefined), true)
338 assert.equal(Predicate.isNotNull("null"), true)
339 })
340})
341
342describe("isNullish", () => {
343 it("detects null and undefined", () => {
344 assert.equal(Predicate.isNullish(null), true)
345 assert.equal(Predicate.isNullish(undefined), true)
346 assert.equal(Predicate.isNullish({}), false)
347 assert.equal(Predicate.isNullish([]), false)
348 })
349})
350
351describe("isNotNullish", () => {
352 it("filters out null and undefined", () => {
353 assert.equal(Predicate.isNotNullish({}), true)
354 assert.equal(Predicate.isNotNullish([]), true)
355 assert.equal(Predicate.isNotNullish(null), false)
356 assert.equal(Predicate.isNotNullish(undefined), false)
357 })
358})
359
360describe("isNever", () => {
361 it("always returns false", () => {
362 assert.equal(Predicate.isNever(null), false)
363 assert.equal(Predicate.isNever(undefined), false)
364 assert.equal(Predicate.isNever({}), false)
365 assert.equal(Predicate.isNever([]), false)
366 })
367})
368
369describe("isUnknown", () => {
370 it("always returns true", () => {
371 assert.equal(Predicate.isUnknown(null), true)
372 assert.equal(Predicate.isUnknown(undefined), true)
373 assert.equal(Predicate.isUnknown({}), true)
374 assert.equal(Predicate.isUnknown([]), true)
375 })
376})
377
378describe("isObjectOrArray", () => {
379 it("accepts objects and arrays but not null", () => {
380 assert.equal(Predicate.isObjectOrArray({}), true)
381 assert.equal(Predicate.isObjectOrArray([]), true)
382 assert.equal(Predicate.isObjectOrArray(null), false)
383 assert.equal(Predicate.isObjectOrArray(() => {}), false)
384 })
385})
386
387describe("isObject", () => {
388 it("accepts objects but not arrays or functions", () => {
389 assert.equal(Predicate.isObject({}), true)
390 assert.equal(Predicate.isObject({ a: 1 }), true)
391 assert.equal(Predicate.isObject([]), false)
392 assert.equal(Predicate.isObject([1, 2, 3]), false)
393 assert.equal(Predicate.isObject(null), false)
394 assert.equal(Predicate.isObject(undefined), false)
395 assert.equal(Predicate.isObject(() => null), false)
396 })
397})
398
399describe("isReadonlyObject", () => {
400 it("behaves like isObject", () => {
401 assert.equal(Predicate.isReadonlyObject({}), true)
402 assert.equal(Predicate.isReadonlyObject({ a: 1 }), true)
403 assert.equal(Predicate.isReadonlyObject([]), false)
404 assert.equal(Predicate.isReadonlyObject([1, 2, 3]), false)
405 assert.equal(Predicate.isReadonlyObject(null), false)
406 assert.equal(Predicate.isReadonlyObject(undefined), false)
407 })
408})
409
410describe("isObjectKeyword", () => {
411 it("accepts objects, arrays, and functions", () => {
412 assert.equal(Predicate.isObjectKeyword({}), true)
413 assert.equal(Predicate.isObjectKeyword([]), true)
414 assert.equal(Predicate.isObjectKeyword(() => 1), true)
415 assert.equal(Predicate.isObjectKeyword(null), false)
416 assert.equal(Predicate.isObjectKeyword(undefined), false)
417 assert.equal(Predicate.isObjectKeyword("a"), false)
418 assert.equal(Predicate.isObjectKeyword(1), false)
419 assert.equal(Predicate.isObjectKeyword(true), false)
420 assert.equal(Predicate.isObjectKeyword(1n), false)
421 assert.equal(Predicate.isObjectKeyword(Symbol.for("a")), false)
422 })
423})
424
425describe("hasProperty", () => {
426 it("detects properties with string and symbol keys", () => {
427 const a = Symbol.for("effect/test/a")
428
429 assert.equal(Predicate.hasProperty({ a: 1 }, "a"), true)
430 assert.equal(Predicate.hasProperty("a")({ a: 1 }), true)
431 assert.equal(Predicate.hasProperty({ [a]: 1 }, a), true)
432 assert.equal(Predicate.hasProperty(a)({ [a]: 1 }), true)
433 })
434
435 it("returns false when the key is missing or the value is not object-like", () => {
436 const a = Symbol.for("effect/test/a")
437
438 assert.equal(Predicate.hasProperty({}, "a"), false)
439 assert.equal(Predicate.hasProperty(null, "a"), false)
440 assert.equal(Predicate.hasProperty(undefined, "a"), false)
441 assert.equal(Predicate.hasProperty(() => {}, "a"), false)
442
443 assert.equal(Predicate.hasProperty({}, a), false)
444 assert.equal(Predicate.hasProperty(null, a), false)
445 assert.equal(Predicate.hasProperty(undefined, a), false)
446 assert.equal(Predicate.hasProperty(() => {}, a), false)
447 })
448})
449
450describe("isTagged", () => {
451 it("matches objects with the expected _tag", () => {
452 assert.equal(Predicate.isTagged(1, "a"), false)
453 assert.equal(Predicate.isTagged("", "a"), false)
454 assert.equal(Predicate.isTagged({}, "a"), false)
455 assert.equal(Predicate.isTagged("a")({}), false)
456 assert.equal(Predicate.isTagged({ a: "a" }, "a"), false)
457 assert.equal(Predicate.isTagged({ _tag: "a" }, "a"), true)
458 assert.equal(Predicate.isTagged("a")({ _tag: "a" }), true)
459 })
460})
461
462describe("isError", () => {
463 it("detects Error instances", () => {
464 assert.equal(Predicate.isError(new Error()), true)
465 assert.equal(Predicate.isError(null), false)
466 assert.equal(Predicate.isError({}), false)
467 })
468})
469
470describe("isUint8Array", () => {
471 it("detects Uint8Array instances", () => {
472 assert.equal(Predicate.isUint8Array(new Uint8Array()), true)
473 assert.equal(Predicate.isUint8Array(null), false)
474 assert.equal(Predicate.isUint8Array({}), false)
475 })
476})
477
478describe("isDate", () => {
479 it("detects Date instances", () => {
480 assert.equal(Predicate.isDate(new Date()), true)
481 assert.equal(Predicate.isDate(null), false)
482 assert.equal(Predicate.isDate({}), false)
483 })
484})
485
486describe("isIterable", () => {
487 it("detects iterable values including strings", () => {
488 assert.equal(Predicate.isIterable([]), true)
489 assert.equal(Predicate.isIterable(""), true)
490 assert.equal(Predicate.isIterable(new Set()), true)
491 assert.equal(Predicate.isIterable(new Map()), true)
492
493 assert.equal(Predicate.isIterable(null), false)
494 assert.equal(Predicate.isIterable({}), false)
495 })
496})
497
498describe("isPromise", () => {
499 it("detects promise-like values with then and catch", () => {
500 assert.equal(Predicate.isPromise(Promise.resolve("ok")), true)
501 /* oxlint-disable-next-line no-thenable */
502 assert.equal(Predicate.isPromise({ then: () => {}, catch: () => {} }), true)
503 /* oxlint-disable-next-line no-thenable */
504 assert.equal(Predicate.isPromise({ then: () => {} }), false)
505 assert.equal(Predicate.isPromise({}), false)
506 })
507})
508
509describe("isPromiseLike", () => {
510 it("detects thenables", () => {
511 assert.equal(Predicate.isPromiseLike(Promise.resolve("ok")), true)
512 /* oxlint-disable-next-line no-thenable */
513 assert.equal(Predicate.isPromiseLike({ then: () => {} }), true)
514 assert.equal(Predicate.isPromiseLike({}), false)
515 assert.equal(Predicate.isPromiseLike(null), false)
516 })
517})
518
519describe("isRegExp", () => {
520 it("detects RegExp instances", () => {
521 assert.equal(Predicate.isRegExp(/a/), true)
522 assert.equal(Predicate.isRegExp(null), false)
523 assert.equal(Predicate.isRegExp("a"), false)
524 })
525})
526
527describe("isTupleOf", () => {
528 it("checks for exact length", () => {
529 assert.equal(Predicate.isTupleOf([1, 2, 3], 3), true)
530 assert.equal(Predicate.isTupleOf([1, 2, 3], 4), false)
531 assert.equal(Predicate.isTupleOf([1, 2, 3], 2), false)
532 })
533
534 it("supports curried usage", () => {
535 const isPair = Predicate.isTupleOf(2)
536
537 assert.equal(isPair(["a", "b"]), true)
538 assert.equal(isPair(["a"]), false)
539 })
540})
541
542describe("isTupleOfAtLeast", () => {
543 it("checks for minimum length", () => {
544 assert.equal(Predicate.isTupleOfAtLeast([1, 2, 3], 3), true)
545 assert.equal(Predicate.isTupleOfAtLeast([1, 2, 3], 2), true)
546 assert.equal(Predicate.isTupleOfAtLeast([1, 2, 3], 4), false)
547 })
548
549 it("supports curried usage", () => {
550 const hasAtLeast2 = Predicate.isTupleOfAtLeast(2)
551
552 assert.equal(hasAtLeast2(["a", "b", "c"]), true)
553 assert.equal(hasAtLeast2(["a"]), false)
554 })
555})