Skip to content

Commit

Permalink
feat: add tree.find
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangshichun committed Jul 9, 2023
1 parent 1427e51 commit ba92172
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 3 deletions.
48 changes: 47 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
> 像使用 `lodash.js` 一样方便地操控树结构
## Install (使用
## Install (安装

```shell
yarn add tree-lodash
Expand Down Expand Up @@ -180,6 +180,52 @@ foreach(tree, (t) => t.key < 2)
*/
```

### find

```js
find(tree, predicate, [options])
```

遍历把 "树" 或者 "森林",找到第一个返回非空值的节点。

**添加版本**:v0.1.0

参数:

1. `tree`: 典型树结构,或者由多个树结构构成的数组;
2. `predicate`: 每次迭代调用的函数,返回非真值时,该节点会从树上剔除。
3. `[options]`: 配置项,支持 `strategy``childrenKey`

示例:

```js
const tree = {
key: 1,
children: [
{
key: 2,
children: [
{
key: 3
}
]
}
]
}
find(tree, (t) => t.key === 2)
/**
* 会保留其本来的结构
* {
* key: 2,
* children: [
* {
* key: 3
* }
* ]
* }
*/
```

### toArray

```js
Expand Down
127 changes: 127 additions & 0 deletions src/find.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { ChildrenKey, Tree, BaseOptions, BaseCallbackMeta } from "./types";

export type FindOptions = BaseOptions
export type FindCallbackMeta<T extends ChildrenKey> = BaseCallbackMeta<T>
export type FindCallback<T extends ChildrenKey> = (treeItem: Tree<T>, meta: FindCallbackMeta<T>) => boolean | undefined

type FindInnerOption<T extends ChildrenKey> = {
childrenKey: ChildrenKey
parents: Tree<T>[],
depth: number
}

type FindImpl<T extends ChildrenKey> = (treeItem: Tree<T>, callback: FindCallback<T>, options: FindInnerOption<T>) => Tree<T>|undefined


// 前置深度优先遍历
const preImpl: FindImpl<ChildrenKey> = (treeItem, callback, options) => {
const callbackResult = callback(treeItem, options)
if (callbackResult) {
return treeItem
}
const children = treeItem[options.childrenKey]
if (!children || !Array.isArray(children)) {
return undefined
}
return children.find((childItem) => {
return preImpl(childItem, callback, {
...options,
parents: [...options.parents, treeItem],
depth: options.depth + 1
})
})
}

// 后置深度优先遍历
const postImpl: FindImpl<ChildrenKey> = (treeItem, callback, options) => {
const children = treeItem[options.childrenKey]

if (children && Array.isArray(children)) {
const findOne = children.find((childItem) => {
return postImpl(childItem, callback, {
...options,
parents: [...options.parents, treeItem],
depth: options.depth + 1
})
})
if (findOne) {
return findOne
}
}
const callbackResult = callback(treeItem, options)
if (callbackResult) {
return treeItem
}
return undefined
}

type QueueItem = {
tree: Tree<ChildrenKey>,
options: FindInnerOption<ChildrenKey>
}

// 广度优先遍历
const breadthImpl: FindImpl<ChildrenKey> = (treeItem, callback, options) => {
const queue: QueueItem[] = [
{
tree: treeItem,
options
}
]
const runQueue = () => {
if (queue.length === 0) {
return undefined
}
const queueItem = queue.shift() as QueueItem
const treeItem = queueItem.tree
if (treeItem[options.childrenKey] && Array.isArray(treeItem[options.childrenKey])) {
const subQueueItems = treeItem[options.childrenKey].map((subTree: Tree) => (
{
tree: subTree,
options: {
...queueItem.options,
parents: [...queueItem.options.parents, treeItem],
depth: queueItem.options.depth + 1
}
}
))
queue.push(...subQueueItems)
}
const callbackResult = callback(treeItem, queueItem.options)
if (callbackResult) {
return treeItem;
}
return runQueue()
}
return runQueue()
}

const strategies = {
'pre': preImpl,
'post': postImpl,
'breadth': breadthImpl
}

function find<T extends ChildrenKey>(tree: Tree<T> | Tree<T>[], callback , options?: FindOptions): Tree<T> | undefined {
const childrenKey = options?.childrenKey ?? 'children'
const strategy = options?.strategy ?? 'pre'
const method = strategies[strategy]
const innerOptions = {
childrenKey,
depth: 0,
parents: [] as Tree[]
}
if (Array.isArray(tree)) {
for(let i = 0, count = tree.length; i < count; i++) {
const treeItem = tree[i]
const result = method(treeItem, callback, innerOptions)
if (result) {
return result
}
}
return undefined
}
return method(tree, callback, innerOptions)
}

export default find;
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import foreach from "./foreach";
import map from './map'
import filter from "./filter";
import toArray from "./toArray";
import find from './find';

export default {
foreach,
map,
filter,
toArray
toArray,
find
};

export {
foreach,
map,
filter,
toArray
toArray,
find
};
182 changes: 182 additions & 0 deletions test/find.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/* global describe, it, beforeEach */

import 'mocha'
import { expect } from 'chai'
import { find } from '../src/index'
import type { Tree } from '../src/types'

describe('[find]', function () {
const tree: Tree = {
key: 1,
children: [
{
key: 11,
children: [
{
key: 111
}
]
},
{
key: 12,
children: [
{
key: 112,
children: [
{
key: 1111
}
]
}
]
}
]
}

const treeSubItems: Tree<'subItems'> = {
key: 1,
subItems: [
{
key: 11,
subItems: [
{
key: 111
}
]
},
{
key: 12,
subItems: [
{
key: 112,
subItems: [
{
key: 1111
}
]
}
]
}
]
}

it('by default strategy (pre)', function () {
const res: any[] = []
const newTree: Tree<'children'> | undefined = find(tree, ((t) => {
res.push(t.key)
return t.key <= 100 && t.key > 1
}))
expect(newTree?.key).to.be.equal(11);
expect(newTree?.children?.[0]?.key).to.be.equal(111);
expect(res.length).to.be.equal(2);
expect(res[0]).to.be.equal(1);
expect(res[1]).to.be.equal(11);
})
it('by post strategy ', function () {
const res: any[] = []
const newTree: Tree | undefined = find(tree, ((t) => {
res.push(t.key)
return t.key <= 100 && t.key > 1
}), { strategy: 'post' })
expect(newTree?.key).to.be.equal(11);
expect(newTree?.children?.[0]?.key).to.be.equal(111);
expect(res.length).to.be.equal(2);
expect(res[0]).to.be.equal(111);
expect(res[1]).to.be.equal(11);
})
it('by breadth strategy ', function () {
const res: any[] = []
const newTree: Tree<'children'> | undefined = find(tree, ((t) => {
res.push(t.key)
return t.key === 12
}), { strategy: 'breadth' })
expect(newTree?.key).to.be.equal(12);
expect(newTree?.children?.[0]?.key).to.be.equal(112);
expect(res.length).to.be.equal(3);
expect(res[0]).to.be.equal(1);
expect(res[1]).to.be.equal(11);
expect(res[2]).to.be.equal(12);
})
it('by tree childrenKey is "subItems"', function() {
const res: any[] = []
const newTree: Tree<'subItems'> | undefined = find(treeSubItems, ((t) => {
res.push(t.key)
return t.key <= 100 && t.key > 1
}), { childrenKey: 'subItems' })
expect(newTree?.key).to.be.equal(11);
expect(newTree?.subItems?.[0]?.key).to.be.equal(111);
expect(res.length).to.be.equal(2);
expect(res[0]).to.be.equal(1);
expect(res[1]).to.be.equal(11);
})
it('for forest by default strategy (pre)', function () {
const res: any[] = []
const newTree: Tree<'children'> | undefined = find([tree], ((t) => {
res.push(t.key)
return t.key <= 100 && t.key > 1
}))
console.log("🔨🔪@zsc:: ~ file: find.test.ts:118 ~ constnewTree:Tree<'children'>|undefined=find ~ newTree:", newTree)
expect(newTree?.key).to.be.equal(11);
expect(newTree?.children?.[0]?.key).to.be.equal(111);
expect(res.length).to.be.equal(2);
expect(res[0]).to.be.equal(1);
expect(res[1]).to.be.equal(11);
})
// it('for forest by post strategy', function () {
// const res: any[] = []
// const newTree: Tree<'children'> | undefined = find(tree, ((t) => {
// res.push(t.key)
// return t.key <= 100 && t.key > 1
// }), { strategy: 'post' })
// expect(newTree?.key).to.be.equal(11);
// expect(newTree?.children?.[0]?.key).to.be.equal(111);
// expect(res.length).to.be.equal(2);
// expect(res[0]).to.be.equal(111);
// expect(res[1]).to.be.equal(11);
// })
// it('for forest by breadth strategy', function () {
// const res: any[] = []
// const newForest: Tree<'children'>[] | [] = find([tree], ((t) => {
// res.push(t.key)
// return t.key <= 100
// }), { strategy: 'breadth' })
// expect(newForest[0]?.key).to.be.equal(1);
// expect(newForest[0]?.children?.[0]?.key).to.be.equal(11);
// expect(newForest[0]?.children?.[0]?.children?.[0]?.key).to.be.equal(undefined);
// expect(res.length).to.be.equal(5);
// expect(res[0]).to.be.equal(1);
// expect(res[1]).to.be.equal(11);
// expect(res[2]).to.be.equal(12);
// expect(res[3]).to.be.equal(111);
// })

// it('for forest no-matched item by default strategy (pre)', function () {
// const res: any[] = []
// const newForest: Tree<'children'>[] | [] = find([tree], ((t) => {
// res.push(t.key)
// return t.key <= 0
// }))
// expect(newForest.length).to.be.equal(0);
// expect(res.length).to.be.equal(1);
// })

// it('for forest no-matched item by post strategy', function () {
// const res: any[] = []
// const newForest: Tree<'children'>[] | [] = find([tree], ((t) => {
// res.push(t.key)
// return t.key <= 0
// }), { strategy: 'post' })
// expect(newForest.length).to.be.equal(0);
// expect(res.length).to.be.equal(6);
// })

// it('for forest no-matched item by breadth strategy', function () {
// const res: any[] = []
// const newForest: Tree<'children'>[] | [] = find([tree], ((t) => {
// res.push(t.key)
// return t.key <= 0
// }), { strategy: 'breadth' })
// expect(newForest.length).to.be.equal(0);
// expect(res.length).to.be.equal(1);
// })
})

0 comments on commit ba92172

Please sign in to comment.