Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/lookup-by-path",
"comment": "Add `deleteSubtree` method.",
"type": "minor"
}
],
"packageName": "@rushstack/lookup-by-path"
}
1 change: 1 addition & 0 deletions common/reviews/api/lookup-by-path.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class LookupByPath<TItem extends {}> implements IReadonlyLookupByPath<TIt
constructor(entries?: Iterable<[string, TItem]>, delimiter?: string);
clear(): this;
deleteItem(query: string, delimeter?: string): boolean;
deleteSubtree(query: string, delimeter?: string): boolean;
readonly delimiter: string;
entries(query?: string, delimiter?: string): IterableIterator<[string, TItem]>;
findChildPath(childPath: string, delimiter?: string): TItem | undefined;
Expand Down
32 changes: 32 additions & 0 deletions libraries/lookup-by-path/src/LookupByPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,38 @@ export class LookupByPath<TItem extends {}> implements IReadonlyLookupByPath<TIt
return false;
}

/**
* Deletes an item and all its children.
* @param query - The path to the item to delete
* @param delimeter - Optional override delimeter for parsing the query
* @returns `true` if any nodes were deleted, `false` otherwise
*/
public deleteSubtree(query: string, delimeter: string = this.delimiter): boolean {
const queryNode: IPathTrieNode<TItem> | undefined = this._findNodeAtPrefix(query, delimeter);
if (!queryNode) {
return false;
}

const queue: IPathTrieNode<TItem>[] = [queryNode];
let removed: number = 0;
while (queue.length > 0) {
const node: IPathTrieNode<TItem> = queue.pop()!;
if (node.value !== undefined) {
node.value = undefined;
removed++;
}
if (node.children) {
for (const child of node.children.values()) {
queue.push(child);
}
node.children.clear();
}
}

this._size -= removed;
return removed > 0;
}

/**
* Associates the value with the specified path.
* If a value is already associated, will overwrite.
Expand Down
75 changes: 75 additions & 0 deletions libraries/lookup-by-path/src/test/LookupByPath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,81 @@ describe(LookupByPath.prototype.deleteItem.name, () => {
});
});

describe(LookupByPath.prototype.deleteSubtree.name, () => {
it('returns false for an empty tree', () => {
expect(new LookupByPath().deleteSubtree('foo')).toEqual(false);
});

it('deletes the matching node in a trivial tree', () => {
const tree = new LookupByPath([['foo', 1]]);
expect(tree.deleteSubtree('foo')).toEqual(true);
expect(tree.size).toEqual(0);
expect(tree.get('foo')).toEqual(undefined);
});

it('returns false for non-matching paths in a single-layer tree', () => {
const tree: LookupByPath<number> = new LookupByPath([
['foo', 1],
['bar', 2],
['baz', 3]
]);

expect(tree.deleteSubtree('buzz')).toEqual(false);
expect(tree.size).toEqual(3);
});

it('deletes the matching node in a single-layer tree', () => {
const tree: LookupByPath<number> = new LookupByPath([
['foo', 1],
['bar', 2],
['baz', 3]
]);

expect(tree.deleteSubtree('bar')).toEqual(true);
expect(tree.size).toEqual(2);
expect(tree.get('bar')).toEqual(undefined);
});

it('deletes the matching subtree in a multi-layer tree', () => {
const tree: LookupByPath<number> = new LookupByPath([
['foo', 1],
['foo/bar', 2],
['foo/bar/baz', 3]
]);

expect(tree.deleteSubtree('foo/bar')).toEqual(true);
expect(tree.size).toEqual(1);
expect(tree.get('foo/bar')).toEqual(undefined);
expect(tree.get('foo/bar/baz')).toEqual(undefined); // child nodes are deleted
});

it('returns false for non-matching paths in a multi-layer tree', () => {
const tree: LookupByPath<number> = new LookupByPath([
['foo', 1],
['foo/bar', 2],
['foo/bar/baz', 3]
]);

expect(tree.deleteSubtree('foo/baz')).toEqual(false);
expect(tree.size).toEqual(3);
});

it('handles custom delimiters', () => {
const tree: LookupByPath<number> = new LookupByPath(
[
['foo,bar', 1],
['foo,bar,baz', 2]
],
','
);

expect(tree.deleteSubtree('foo\0bar', '\0')).toEqual(true);
expect(tree.size).toEqual(0);
expect(tree.get('foo\0bar', '\0')).toEqual(undefined);
expect(tree.get('foo\0bar\0baz', '\0')).toEqual(undefined); // child nodes are deleted
});
});

describe(LookupByPath.prototype.findChildPath.name, () => {
it('returns empty for an empty tree', () => {
expect(new LookupByPath().findChildPath('foo')).toEqual(undefined);
Expand Down
Loading