import { describe, it } from "@effect/vitest" import { assertNone, assertSome, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" import * as Equal from "effect/Equal" import { pipe } from "effect/Function" import * as Option from "effect/Option" import * as Result from "effect/Result" import * as Trie from "effect/Trie" describe("Trie", () => { it("toString renders entries in iteration order", () => { const trie = pipe( Trie.empty(), Trie.insert("a", 0), Trie.insert("b", 1) ) strictEqual( String(trie), `Trie([["a",0],["b",1]])` ) }) it("toJSON renders tagged entries", () => { const trie = pipe( Trie.empty(), Trie.insert("a", 0), Trie.insert("b", 1) ) deepStrictEqual(trie.toJSON(), { _id: "Trie", values: [["a", 0], ["b", 1]] }) }) it("inspect renders the JSON representation", () => { if (typeof window !== "undefined") { return } // oxlint-disable-next-line @typescript-eslint/no-require-imports const { inspect } = require("node:util") const trie = pipe( Trie.empty(), Trie.insert("a", 0), Trie.insert("b", 1) ) deepStrictEqual(inspect(trie), inspect({ _id: "Trie", values: [["a", 0], ["b", 1]] })) }) it("iterates no entries for an empty trie", () => { const trie = Trie.empty() strictEqual(Trie.size(trie), 0) deepStrictEqual(Array.from(trie), []) }) it("insert returns new tries and orders entries by key", () => { const trie1 = Trie.empty().pipe( Trie.insert("call", 0) ) const trie2 = trie1.pipe(Trie.insert("me", 1)) const trie3 = trie2.pipe(Trie.insert("mind", 2)) const trie4 = trie3.pipe(Trie.insert("mid", 3)) deepStrictEqual(Array.from(trie1), [["call", 0]]) deepStrictEqual(Array.from(trie2), [["call", 0], ["me", 1]]) deepStrictEqual(Array.from(trie3), [["call", 0], ["me", 1], ["mind", 2]]) deepStrictEqual(Array.from(trie4), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) }) it("fromIterable preserves an empty iterable", () => { const iterable: Array<[string, number]> = [] const trie = Trie.fromIterable(iterable) deepStrictEqual(Array.from(trie), iterable) }) it("make builds a trie from entry pairs", () => { const trie = Trie.make(["ca", 0], ["me", 1]) deepStrictEqual(Array.from(trie), [["ca", 0], ["me", 1]]) strictEqual(Equal.equals(Trie.fromIterable([["ca", 0], ["me", 1]]), trie), true) }) it("fromIterable builds the same trie as make", () => { const iterable: Array<[string, number]> = [["ca", 0], ["me", 1]] const trie = Trie.fromIterable(iterable) deepStrictEqual(Array.from(trie), iterable) strictEqual(Equal.equals(Trie.make(["ca", 0], ["me", 1]), trie), true) }) it("fromIterable orders entries by key", () => { const iterable: Array = [["call", 0], ["me", 1], ["mind", 2], ["mid", 3]] const trie = Trie.fromIterable(iterable) deepStrictEqual(Array.from(trie), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) }) it("fromIterable preserves already sorted entries", () => { const iterable: Array<[string, number]> = [["a", 0], ["b", 1]] const trie = Trie.fromIterable(iterable) deepStrictEqual(Array.from(trie), iterable) }) it("fromIterable accepts a single entry", () => { const iterable: Array<[string, number]> = [["a", 0]] const trie = Trie.fromIterable(iterable) deepStrictEqual(Array.from(trie), iterable) }) it("fromIterable orders keys when one key is a prefix", () => { const iterable: Array<[string, number]> = [["shells", 0], ["she", 1]] const trie = Trie.fromIterable(iterable) deepStrictEqual(Array.from(trie), [["she", 1], ["shells", 0]]) }) it("size counts stored keys", () => { const trie = Trie.empty().pipe( Trie.insert("a", 0), Trie.insert("b", 1) ) strictEqual(Trie.size(trie), 2) }) it("isEmpty distinguishes empty and populated tries", () => { const trie = Trie.empty() const trie1 = trie.pipe(Trie.insert("ma", 0)) strictEqual(Trie.isEmpty(trie), true) strictEqual(Trie.isEmpty(trie1), false) }) it("get returns Some only for exact keys", () => { const trie = Trie.empty().pipe( Trie.insert("call", 0), Trie.insert("me", 1), Trie.insert("mind", 2), Trie.insert("mid", 3) ) assertSome(Trie.get(trie, "call"), 0) assertSome(Trie.get(trie, "me"), 1) assertSome(Trie.get(trie, "mind"), 2) assertSome(Trie.get(trie, "mid"), 3) assertNone(Trie.get(trie, "cale")) assertNone(Trie.get(trie, "ma")) assertNone(Trie.get(trie, "midn")) assertNone(Trie.get(trie, "mea")) }) it("get distinguishes complete keys from prefixes", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) assertNone(Trie.get(trie, "sell")) assertSome(Trie.get(trie, "sells"), 1) assertNone(Trie.get(trie, "shell")) assertSome(Trie.get(trie, "she"), 2) }) it("has returns true only for exact keys", () => { const trie = Trie.empty().pipe( Trie.insert("call", 0), Trie.insert("me", 1), Trie.insert("mind", 2), Trie.insert("mid", 3) ) strictEqual(Trie.has(trie, "call"), true) strictEqual(Trie.has(trie, "me"), true) strictEqual(Trie.has(trie, "mind"), true) strictEqual(Trie.has(trie, "mid"), true) strictEqual(Trie.has(trie, "cale"), false) strictEqual(Trie.has(trie, "ma"), false) strictEqual(Trie.has(trie, "midn"), false) strictEqual(Trie.has(trie, "mea"), false) }) it("getUnsafe returns the value or throws when the key is missing", () => { const trie = Trie.empty().pipe( Trie.insert("call", 0), Trie.insert("me", 1) ) throws(() => Trie.getUnsafe(trie, "mae")) }) it("remove deletes a key without mutating the original trie", () => { const trie = Trie.empty().pipe( Trie.insert("call", 0), Trie.insert("me", 1), Trie.insert("mind", 2), Trie.insert("mid", 3) ) const trie1 = trie.pipe(Trie.remove("call")) const trie2 = trie1.pipe(Trie.remove("mea")) deepStrictEqual(Trie.get(trie, "call"), Option.some(0)) deepStrictEqual(Trie.get(trie1, "call"), Option.none()) deepStrictEqual(Trie.get(trie2, "call"), Option.none()) deepStrictEqual(Array.from(trie), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) deepStrictEqual(Array.from(trie1), [["me", 1], ["mid", 3], ["mind", 2]]) deepStrictEqual(Array.from(trie2), [["me", 1], ["mid", 3], ["mind", 2]]) }) it("keys returns keys in sorted order", () => { const trie = Trie.empty().pipe( Trie.insert("cab", 0), Trie.insert("abc", 1), Trie.insert("bca", 2) ) const result = Array.from(Trie.keys(trie)) deepStrictEqual(result, ["abc", "bca", "cab"]) }) it("keys orders nested prefixes alphabetically", () => { const trie = Trie.make( ["abc", 0], ["bac", 0], ["b", 0], ["ca", 0], ["cac", 0], ["c", 0], ["abb", 0], ["ba", 0], ["a", 0], ["bca", 0], ["cab", 0], ["dca", 0], ["ab", 0], ["adc", 0] ) const result = Array.from(Trie.keys(trie)) deepStrictEqual(result, [ "a", "ab", "abb", "abc", "adc", "b", "ba", "bac", "bca", "c", "ca", "cab", "cac", "dca" ]) }) it("values follow key iteration order", () => { const trie = Trie.empty().pipe( Trie.insert("call", 0), Trie.insert("me", 1), Trie.insert("and", 2) ) const result = Array.from(Trie.values(trie)) deepStrictEqual(result, [2, 0, 1]) }) it("entries iterates key-value pairs in sorted order", () => { const trie = Trie.empty().pipe( Trie.insert("call", 0), Trie.insert("me", 1) ) const result = Array.from(Trie.entries(trie)) deepStrictEqual(result, [["call", 0], ["me", 1]]) }) it("toEntries returns a sorted entry array", () => { const trie = Trie.empty().pipe( Trie.insert("call", 0), Trie.insert("me", 1) ) const result = Trie.toEntries(trie) deepStrictEqual(result, [["call", 0], ["me", 1]]) }) it("keysWithPrefix returns matching keys in sorted order", () => { const trie = Trie.empty().pipe( Trie.insert("she", 0), Trie.insert("shells", 1), Trie.insert("sea", 2), Trie.insert("sells", 3), Trie.insert("by", 4), Trie.insert("the", 5), Trie.insert("sea", 6), Trie.insert("shore", 7) ) const result = Array.from(Trie.keysWithPrefix(trie, "she")) deepStrictEqual(result, ["she", "shells"]) }) it("valuesWithPrefix returns matching values in key order", () => { const trie = pipe( Trie.empty(), Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("sea", 2), Trie.insert("she", 3) ) const result = Array.from(Trie.valuesWithPrefix(trie, "she")) deepStrictEqual(result, [3, 0]) }) it("entriesWithPrefix returns matching entries in key order", () => { const trie = pipe( Trie.empty(), Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("sea", 2), Trie.insert("she", 3) ) const result = Array.from(Trie.entriesWithPrefix(trie, "she")) deepStrictEqual(result, [["she", 3], ["shells", 0]]) }) it("toEntriesWithPrefix returns matching entries as an array", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("sea", 2), Trie.insert("she", 3) ) const result = Trie.toEntriesWithPrefix(trie, "she") deepStrictEqual(result, [["she", 3], ["shells", 0]]) }) it("longestPrefixOf returns the longest stored key that prefixes the input", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) assertNone(Trie.longestPrefixOf(trie, "sell")) assertSome(Trie.longestPrefixOf(trie, "sells"), ["sells", 1]) assertSome(Trie.longestPrefixOf(trie, "shell"), ["she", 2]) assertSome(Trie.longestPrefixOf(trie, "shellsort"), ["shells", 0]) }) it("map transforms values and can use keys", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) const trieMapV = Trie.empty().pipe( Trie.insert("shells", 1), Trie.insert("sells", 2), Trie.insert("she", 3) ) const trieMapK = Trie.empty().pipe( Trie.insert("shells", 6), Trie.insert("sells", 5), Trie.insert("she", 3) ) strictEqual(Equal.equals(Trie.map(trie, (v) => v + 1), trieMapV), true) strictEqual(Equal.equals(Trie.map(trie, (_, k) => k.length), trieMapK), true) }) it("filter keeps entries by value or key", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) const trieMapV = Trie.empty().pipe( Trie.insert("she", 2) ) const trieMapK = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1) ) strictEqual(Equal.equals(Trie.filter(trie, (v) => v > 1), trieMapV), true) strictEqual(Equal.equals(Trie.filter(trie, (_, k) => k.length > 3), trieMapK), true) }) it("filterMap keeps Result successes and can use keys", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) const trieMapV = Trie.empty().pipe( Trie.insert("she", 2) ) const trieMapK = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1) ) strictEqual(Equal.equals(Trie.filterMap(trie, (v) => v > 1 ? Result.succeed(v) : Result.failVoid), trieMapV), true) strictEqual( Equal.equals(Trie.filterMap(trie, (v, k) => k.length > 3 ? Result.succeed(v) : Result.failVoid), trieMapK), true ) }) it("compact keeps Some values", () => { const trie = Trie.empty>().pipe( Trie.insert("shells", Option.some(0)), Trie.insert("sells", Option.none()), Trie.insert("she", Option.some(2)) ) const trieMapV = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("she", 2) ) strictEqual(Equal.equals(Trie.compact(trie), trieMapV), true) }) it("modify updates an existing key and ignores a missing key", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) deepStrictEqual(trie.pipe(Trie.modify("she", (v) => v + 10), Trie.get("she")), Option.some(12)) strictEqual(Equal.equals(trie.pipe(Trie.modify("me", (v) => v)), trie), true) }) it("removeMany deletes all matching keys", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) strictEqual( Equal.equals(trie.pipe(Trie.removeMany(["she", "sells"])), Trie.empty().pipe(Trie.insert("shells", 0))), true ) }) it("insertMany inserts all entries", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) const trieInsert = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insertMany( [["sells", 1], ["she", 2]] ) ) strictEqual( Equal.equals(trie, trieInsert), true ) }) it("reduce visits entries in key order", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2) ) strictEqual( trie.pipe( Trie.reduce(0, (acc, n) => acc + n) ), 3 ) strictEqual( trie.pipe( Trie.reduce(10, (acc, n) => acc + n) ), 13 ) strictEqual( trie.pipe( Trie.reduce("", (acc, _, key) => acc + key) ), "sellssheshells" ) }) it("forEach", () => { let value = 0 Trie.empty().pipe( Trie.insert("shells", 0), Trie.insert("sells", 1), Trie.insert("she", 2), Trie.forEach((n, key) => { value += n + key.length }) ) strictEqual(value, 17) }) it("Equal compares tries by entries", () => { strictEqual( Equal.equals(Trie.empty(), Trie.empty()), true ) strictEqual( Equal.equals( Trie.make(["call", 0], ["me", 1]), Trie.make(["call", 0], ["me", 1]) ), true ) }) })