From 59688e4b077aa4cf991941d1ef076850933a6262 Mon Sep 17 00:00:00 2001 From: jituanlin Date: Mon, 26 Jul 2021 15:06:44 +0800 Subject: [PATCH] add Tree.findDepthFirst and Tree.findBreadthFirst and Tree.filter --- src/Tree.ts | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/Tree.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/src/Tree.ts b/src/Tree.ts index fb4a0308e..18a1173ac 100644 --- a/src/Tree.ts +++ b/src/Tree.ts @@ -32,6 +32,7 @@ import { Predicate } from './Predicate' import * as RA from './ReadonlyArray' import type { Show } from './Show' import type { Traversable1 } from './Traversable' +import * as O from './Option' // ------------------------------------------------------------------------------------- // model @@ -531,6 +532,58 @@ export const exists = (predicate: Predicate) => (ma: Tree): boolean => */ export const elem = (E: Eq) => (a: A): ((fa: Tree) => boolean) => exists(E.equals(a)) +/** + * Deep first search + * The predicate judgment acts on the tree itself, not the value, so that the predicate can access the forest + * @since:3.0.0 + */ +export const findDepthFirst = (predicate: (a: Tree) => boolean) => (tree: Tree): O.Option> => { + if (predicate(tree)) { + return O.some(tree) + } + + const todo: Array> = [...tree.forest] + + while (todo.length > 0) { + const current = todo.shift()! + if (predicate(current)) { + return O.some(current) + } + todo.unshift(...current.forest) + } + + return O.none +} + +/** + * Breadth first search + * The predicate judgment acts on the tree itself, not the value, so that the predicate can access the forest + * @since:3.0.0 + */ +export const findBreadthFirst = (predicate: (a: Tree) => boolean) => (tree: Tree): O.Option> => { + if (predicate(tree)) { + return O.some(tree) + } + + const todo: Array> = [...tree.forest] + + while (todo.length > 0) { + const result = pipe( + todo, + RA.findFirst((tree) => predicate(tree)) + ) + + if (O.isSome(result)) { + return result + } + + const todoCopy: ReadonlyArray> = [...todo].reverse() + todoCopy.forEach((tree) => todo.unshift(...tree.forest)) + } + + return O.none +} + const draw = (indentation: string, forest: Forest): string => { let r = '' const len = forest.length @@ -575,6 +628,27 @@ export const drawForest = (forest: Forest): string => draw('\n', forest) */ export const drawTree = (tree: Tree): string => tree.value + drawForest(tree.forest) +/** + * Filter the tree, if the root does not pass the predicate, it returns none + * @since:3.0.0 + */ +export const filter = (predicate: (a: Tree) => boolean) => (t: Tree): O.Option> => { + if (predicate(t)) { + const forest = pipe(t.forest, RA.map(filter(predicate)), RA.compact) + return O.some(tree(t.value, forest)) + } + return O.none +} + +/** + * Like the `map`, but give the chance to change the forest + * @since:3.0.0 + */ +export const modify = (f: (ta: Tree) => Tree) => (ta: Tree): Tree => { + const tb = f(ta) + return tree(tb.value, tb.forest.map(modify(f))) +} + // ------------------------------------------------------------------------------------- // do notation // ------------------------------------------------------------------------------------- diff --git a/test/Tree.ts b/test/Tree.ts index 22a3f829e..c513744fc 100644 --- a/test/Tree.ts +++ b/test/Tree.ts @@ -1,4 +1,5 @@ import * as Eq from '../src/Eq' +import * as Ra from '../src/ReadonlyArray' import { flow, identity, pipe } from '../src/function' import * as S from '../src/string' import * as O from '../src/Option' @@ -258,4 +259,80 @@ describe('Tree', () => { U.deepStrictEqual(_.exists((user: User) => user.id === 4)(users), true) U.deepStrictEqual(_.exists((user: User) => user.id === 5)(users), false) }) + + it('findDepthFirst', () => { + interface User { + readonly id: number + readonly name?: string + } + const users: _.Tree = _.tree({ id: 1 }, [ + _.tree({ id: 1 }, [ + _.tree({ id: 3, name: '' }, [_.tree({ id: 2, name: 'deep first [id]=2 node' })]), + _.tree({ id: 4 }) + ]), + _.tree({ id: 2 }) + ]) + + U.deepStrictEqual( + _.findDepthFirst((tree: _.Tree) => tree.value.id === 2)(users), + O.some(_.tree({ id: 2, name: 'deep first [id]=2 node' })) + ) + U.deepStrictEqual(_.findDepthFirst((tree: _.Tree) => tree.value.id === 5)(users), O.none) + }) + + it('findBreadthFirst', () => { + interface User { + readonly id: number + readonly name?: string + } + const users: _.Tree = _.tree({ id: 1 }, [ + _.tree({ id: 1 }, [_.tree({ id: 3 }, [_.tree({ id: 2 })]), _.tree({ id: 4 })]), + _.tree({ id: 2, name: 'breadth first [id]=2 node' }) + ]) + + U.deepStrictEqual( + _.findBreadthFirst((tree: _.Tree) => tree.value.id === 2)(users), + O.some(_.tree({ id: 2, name: 'breadth first [id]=2 node' })) + ) + U.deepStrictEqual(_.findDepthFirst((tree: _.Tree) => tree.value.id === 5)(users), O.none) + }) + + it('filter', () => { + interface User { + readonly id: number + readonly name?: string + } + const users: _.Tree = _.tree({ id: 1 }, [ + _.tree({ id: 1 }, [_.tree({ id: 3 }, [_.tree({ id: 2 })]), _.tree({ id: 4 })]), + _.tree({ id: 2, name: 'breadth first [id]=2 node' }) + ]) + + U.deepStrictEqual(_.filter((tree: _.Tree) => tree.value.id < 0)(users), O.none) + U.deepStrictEqual( + _.filter((tree: _.Tree) => tree.value.id !== 2)(users), + O.some(_.tree({ id: 1 }, [_.tree({ id: 1 }, [_.tree({ id: 3 }), _.tree({ id: 4 })])])) + ) + }) + + it('modify', () => { + interface User { + readonly id: number + readonly name?: string + } + const users: _.Tree = _.tree({ id: 1 }, [ + _.tree({ id: 1 }, [_.tree({ id: 3 }, [_.tree({ id: 2 })]), _.tree({ id: 4 })]), + _.tree({ id: 2, name: 'breadth first [id]=2 node' }) + ]) + + U.deepStrictEqual( + _.modify((tree: _.Tree) => ({ + value: tree.value, + forest: Ra.reverse(tree.forest) + }))(users), + _.tree({ id: 1 }, [ + _.tree({ id: 2, name: 'breadth first [id]=2 node' }), + _.tree({ id: 1 }, [_.tree({ id: 4 }), _.tree({ id: 3 }, [_.tree({ id: 2 })])]) + ]) + ) + }) })