Created
July 27, 2022 01:22
-
-
Save dfee/f4a89ccdd7c4fb42bbe7fecf5652984a to your computer and use it in GitHub Desktop.
building and traversing a tree using fp-ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Lineage, | |
Node, | |
getSubtreeByLineage, | |
treeFromLineageArray, | |
walkTreeDepthFirst, | |
} from "../node"; | |
describe("Node", () => { | |
const LINEAGE_ARRAY_FIXTURE: ReadonlyArray<Lineage> = [ | |
["a"], | |
["b", "b1"], | |
["b", "b2"], | |
["c", "c1", "c1a"], | |
["c", "c1", "c1b"], | |
]; | |
const TREE_FIXTURE: Node = { | |
a: null, | |
b: { b1: null, b2: null }, | |
c: { c1: { c1a: null, c1b: null } }, | |
}; | |
const DFS_FIXTURE = [ | |
["a", getSubtreeByLineage(TREE_FIXTURE, "a")], | |
["b1", getSubtreeByLineage(TREE_FIXTURE, "b", "b1")], | |
["b2", getSubtreeByLineage(TREE_FIXTURE, "b", "b2")], | |
["b", getSubtreeByLineage(TREE_FIXTURE, "b")], | |
["c1a", getSubtreeByLineage(TREE_FIXTURE, "c", "c1", "c1a")], | |
["c1b", getSubtreeByLineage(TREE_FIXTURE, "c", "c1", "c1b")], | |
["c1", getSubtreeByLineage(TREE_FIXTURE, "c", "c1")], | |
["c", getSubtreeByLineage(TREE_FIXTURE, "c")], | |
]; | |
describe("treeFromLineageArray", () => { | |
it("should build complex tree", () => { | |
expect(treeFromLineageArray(LINEAGE_ARRAY_FIXTURE)).toEqual(TREE_FIXTURE); | |
}); | |
it("should return empty", () => { | |
const input: ReadonlyArray<Lineage> = []; | |
const expected: Node = {}; | |
expect(treeFromLineageArray(input)).toEqual(expected); | |
}); | |
}); | |
describe("getSubtreeByLineage", () => { | |
it("should get undefined", () => { | |
expect(getSubtreeByLineage(TREE_FIXTURE, "z")).toBeUndefined(); | |
}); | |
it("should get null", () => { | |
expect(getSubtreeByLineage(TREE_FIXTURE, "a")).toBeNull(); | |
}); | |
it("should get b's tree", () => { | |
expect(getSubtreeByLineage(TREE_FIXTURE, "b")).toEqual(TREE_FIXTURE["b"]); | |
}); | |
}); | |
describe("depth first ", () => { | |
it("should walk depth first", () => { | |
expect(walkTreeDepthFirst(TREE_FIXTURE)).toEqual(DFS_FIXTURE); | |
}); | |
it("should walk nowhere", () => { | |
expect(walkTreeDepthFirst({})).toEqual([]); | |
}); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
either, | |
function as func, | |
option, | |
ord, | |
readonlyArray, | |
readonlyNonEmptyArray, | |
record, | |
string, | |
} from "fp-ts"; | |
export type Defined<T> = T extends undefined ? never : T; | |
export type Node = { | |
[K in string]?: Node | null; | |
}; | |
export type NodeEntry = Defined<Node[keyof Node]>; | |
export type Lineage = readonlyNonEmptyArray.ReadonlyNonEmptyArray<string>; | |
export const enhanceTreeWithLineage = ( | |
tree: Node, | |
[first, ...remainder]: Lineage, | |
): Node => { | |
return { | |
...tree, | |
[first]: func.pipe( | |
remainder, | |
readonlyNonEmptyArray.fromArray, | |
option.map((nonEmptyRemainder) => | |
enhanceTreeWithLineage( | |
func.pipe( | |
option.fromNullable(tree[first]), | |
option.getOrElse(() => ({})), | |
), | |
nonEmptyRemainder, | |
), | |
), | |
option.getOrElse<NodeEntry>(func.constNull), | |
), | |
}; | |
}; | |
export const treeFromLineageArray = ( | |
lineages: ReadonlyArray<readonlyNonEmptyArray.ReadonlyNonEmptyArray<string>>, | |
): Node => | |
func.pipe( | |
lineages, | |
readonlyArray.reduce({}, (tree, lineage) => | |
enhanceTreeWithLineage(tree, lineage), | |
), | |
); | |
export const walkTreeDepthFirst = ( | |
tree: Node, | |
): ReadonlyArray<[string, NodeEntry]> => | |
func.pipe( | |
tree, | |
record.toEntries, | |
readonlyArray.filter( | |
(entry): entry is [string, NodeEntry] => entry[1] !== undefined, | |
), | |
readonlyArray.sort( | |
func.pipe( | |
string.Ord, | |
ord.contramap<string, [string, NodeEntry]>(([key]) => key), | |
), | |
), | |
readonlyArray.map(([key, subtree]) => | |
func.pipe( | |
subtree, | |
option.fromNullable, | |
option.map((nonNullSubtree) => walkTreeDepthFirst(nonNullSubtree)), | |
option.map((results) => | |
func.pipe(results, readonlyArray.append(func.tuple(key, subtree))), | |
), | |
option.getOrElse<ReadonlyArray<[string, NodeEntry]>>(() => | |
readonlyArray.of(func.tuple(key, subtree)), | |
), | |
), | |
), | |
readonlyArray.flatten, | |
); | |
export const getSubtreeByLineage = ( | |
tree: Node, | |
...lineage: Lineage | |
): Node | null | undefined => { | |
return func.pipe( | |
lineage, | |
readonlyNonEmptyArray.reduce( | |
either.right<undefined, NodeEntry>(tree), | |
(subtree, path) => | |
func.pipe( | |
subtree, | |
either.chain( | |
func.flow( | |
option.fromNullable, | |
either.fromOption(func.constUndefined), | |
either.map((value) => value[path]), | |
either.filterOrElse( | |
(value): value is NodeEntry => value !== undefined, | |
func.constUndefined, | |
), | |
), | |
), | |
), | |
), | |
either.fold(func.identity, func.identity), | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment