From be6bfae4ac11b3c70f3b2478fd75f6a3b1d8d4a1 Mon Sep 17 00:00:00 2001 From: windychen Date: Fri, 7 Aug 2026 10:28:46 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=89=88=E6=9C=AC=200.1.0=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20toTree=20=E6=94=AF=E6=8C=81=E5=B5=8C=E5=A5=97?= =?UTF-8?q?=E5=88=86=E7=BB=84=E4=B8=8E=E5=90=8C=E9=80=BB=E8=BE=91=E5=90=88?= =?UTF-8?q?=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/toTree.js | 70 ++++++++++++++++++++++++++++++++++----- test/parseFilters.test.js | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/src/toTree.js b/src/toTree.js index 58930a7..f73b773 100644 --- a/src/toTree.js +++ b/src/toTree.js @@ -1,15 +1,69 @@ /** + * Normalize an item: unwrap explicit nested group shapes. + * + * Supported input shapes: + * - plain leaf: { field, query, op, value, ... } + * - inline children: { logic, children: [...] } + * - group key: { group: [...], logic? } + * + * Nested children are recursively built into their own tree so inner + * groups honor Q6 (nested and/or groups). + * + * @param {*} item + * @returns {object} + */ +function normalize(item) { + if (Array.isArray(item?.children)) { + return { ...item, children: toTree(item.children) }; + } + if (Array.isArray(item?.group)) { + const { group, ...rest } = item; + return { ...rest, children: toTree(group) }; + } + return item; +} + +/** + * Build a nested condition tree from a flat filter list. + * + * Semantics (aligned with component output, Q7/Q54): + * - the first item carries no `logic`; each following item's `logic` + * expresses how it joins the previous accumulated node + * - adjacent items sharing the same `logic` are merged into one + * `{ logic, children: [...] }` node (Q6: nested groups) + * - mixed logic is left-associative: A AND B OR C -> (A AND B) OR C + * * @param {Array} flat + * @param {{ mergeSameLogic?: boolean }} [options] * @returns {Array} */ -export function toTree(flat) { - if (!flat.length) return []; - if (flat.length === 1) return [flat[0]]; +export function toTree(flat, options = {}) { + const { mergeSameLogic = true } = options; + if (!Array.isArray(flat) || flat.length === 0) return []; - let acc = flat[0]; - for (let i = 1; i < flat.length; i++) { - const logic = flat[i].logic || 'AND'; - acc = { logic, children: [acc, flat[i]] }; + const items = flat.map(normalize); + if (items.length === 1) return items; + + const tree = []; + + for (const item of items) { + if (tree.length === 0) { + tree.push(item); + continue; + } + + const logic = (item.logic || 'AND').toUpperCase(); + const prev = tree[tree.length - 1]; + + if (mergeSameLogic && prev.logic === logic && Array.isArray(prev.children)) { + prev.children.push(item); + } else if (mergeSameLogic && prev.logic === logic) { + tree[tree.length - 1] = { logic, children: [prev, item] }; + } else { + const joined = { logic, children: [tree.pop(), item] }; + tree.push(joined); + } } - return [acc]; + + return tree; } diff --git a/test/parseFilters.test.js b/test/parseFilters.test.js index ff32e6d..81dfc69 100644 --- a/test/parseFilters.test.js +++ b/test/parseFilters.test.js @@ -119,6 +119,73 @@ describe('toTree', () => { logic: 'OR', children: expect.any(Array), }); + expect(tree[0].children).toHaveLength(2); + }); + + it('merges adjacent same-logic leaves into one node', () => { + const flat = [ + { field: 'a', query: 'EQ', op: '=', value: '1' }, + { field: 'b', query: 'EQ', op: '=', value: '2', logic: 'AND' }, + { field: 'c', query: 'EQ', op: '=', value: '3', logic: 'AND' }, + ]; + const tree = toTree(flat); + expect(tree).toHaveLength(1); + expect(tree[0].logic).toBe('AND'); + expect(tree[0].children).toHaveLength(3); + }); + + it('left-associates mixed logic: A AND B OR C -> (A AND B) OR C', () => { + const flat = [ + { field: 'a', query: 'EQ', op: '=', value: '1' }, + { field: 'b', query: 'EQ', op: '=', value: '2', logic: 'AND' }, + { field: 'c', query: 'EQ', op: '=', value: '3', logic: 'OR' }, + ]; + const tree = toTree(flat); + expect(tree).toHaveLength(1); + expect(tree[0].logic).toBe('OR'); + expect(tree[0].children).toHaveLength(2); + expect(tree[0].children[0].logic).toBe('AND'); + expect(tree[0].children[0].children).toHaveLength(2); + }); + + it('supports explicit nested group via children key', () => { + const flat = [ + { field: 'a', query: 'EQ', op: '=', value: '1' }, + { + logic: 'OR', + children: [ + { field: 'b', query: 'EQ', op: '=', value: '2' }, + { field: 'c', query: 'EQ', op: '=', value: '3', logic: 'AND' }, + ], + }, + ]; + const tree = toTree(flat); + // a OR (b AND c) + expect(tree[0].logic).toBe('OR'); + expect(tree[0].children).toHaveLength(2); + const inner = tree[0].children[1].children; + expect(inner).toHaveLength(1); + expect(inner[0].logic).toBe('AND'); + expect(inner[0].children).toHaveLength(2); + }); + + it('supports group key shorthand', () => { + const flat = [ + { field: 'a', query: 'EQ', op: '=', value: '1' }, + { + logic: 'OR', + group: [{ field: 'b', query: 'EQ', op: '=', value: '2' }], + }, + ]; + const tree = toTree(flat); + expect(tree[0].logic).toBe('OR'); + expect(tree[0].children).toHaveLength(2); + expect(tree[0].children[1].children).toHaveLength(1); + }); + + it('returns empty array for empty input', () => { + expect(toTree([])).toEqual([]); + expect(toTree(null)).toEqual([]); }); });