// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // deno-lint-ignore-file no-explicit-any import { equal } from "./_equal.ts"; export abstract class AsymmetricMatcher { constructor( protected value: T, protected inverse: boolean = false, ) {} abstract equals(other: unknown): boolean; } export class Anything extends AsymmetricMatcher { equals(other: unknown): boolean { return other !== null && other !== undefined; } } export function anything(): Anything { return new Anything(); } export class Any extends AsymmetricMatcher { constructor(value: unknown) { if (value === undefined) { throw new TypeError("Expected a constructor function"); } super(value); } equals(other: unknown): boolean { if (typeof other === "object") { return other instanceof this.value; } else { if (this.value === Number) { return typeof other === "number"; } if (this.value === String) { return typeof other === "string"; } if (this.value === Number) { return typeof other === "number"; } if (this.value === Function) { return typeof other === "function"; } if (this.value === Boolean) { return typeof other === "boolean"; } if (this.value === BigInt) { return typeof other === "bigint"; } if (this.value === Symbol) { return typeof other === "symbol"; } } return false; } } export function any(c: unknown): Any { return new Any(c); } export class ArrayContaining extends AsymmetricMatcher { constructor(arr: any[], inverse = false) { super(arr, inverse); } equals(other: any[]): boolean { const res = Array.isArray(other) && this.value.every((e) => other.includes(e)); return this.inverse ? !res : res; } } export function arrayContaining(c: any[]): ArrayContaining { return new ArrayContaining(c); } export function arrayNotContaining(c: any[]): ArrayContaining { return new ArrayContaining(c, true); } export class CloseTo extends AsymmetricMatcher { readonly #precision: number; constructor(num: number, precision: number = 2) { super(num); this.#precision = precision; } equals(other: number): boolean { if (typeof other !== "number") { return false; } if ( (this.value === Number.POSITIVE_INFINITY && other === Number.POSITIVE_INFINITY) || (this.value === Number.NEGATIVE_INFINITY && other === Number.NEGATIVE_INFINITY) ) { return true; } return Math.abs(this.value - other) < Math.pow(10, -this.#precision) / 2; } } export function closeTo(num: number, numDigits?: number): CloseTo { return new CloseTo(num, numDigits); } export class StringContaining extends AsymmetricMatcher { constructor(str: string, inverse = false) { super(str, inverse); } equals(other: string): boolean { const res = typeof other !== "string" ? false : other.includes(this.value); return this.inverse ? !res : res; } } export function stringContaining(str: string): StringContaining { return new StringContaining(str); } export function stringNotContaining(str: string): StringContaining { return new StringContaining(str, true); } export class StringMatching extends AsymmetricMatcher { constructor(pattern: string | RegExp, inverse = false) { super(new RegExp(pattern), inverse); } equals(other: string): boolean { const res = typeof other !== "string" ? false : this.value.test(other); return this.inverse ? !res : res; } } export function stringMatching(pattern: string | RegExp): StringMatching { return new StringMatching(pattern); } export function stringNotMatching(pattern: string | RegExp): StringMatching { return new StringMatching(pattern, true); } export class ObjectContaining extends AsymmetricMatcher> { constructor(obj: Record, inverse = false) { super(obj, inverse); } equals(other: Record): boolean { const keys = Object.keys(this.value); let res = true; for (const key of keys) { if ( !Object.hasOwn(other, key) || !equal(this.value[key], other[key]) ) { res = false; } } return this.inverse ? !res : res; } } export function objectContaining( obj: Record, ): ObjectContaining { return new ObjectContaining(obj); } export function objectNotContaining( obj: Record, ): ObjectContaining { return new ObjectContaining(obj, true); }