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 `getFirstDifferenceInCommonNodes` API.",
"type": "minor"
}
],
"packageName": "@rushstack/lookup-by-path"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/lookup-by-path",
"comment": "Expose `tree` accessor on `IReadonlyLookupByPath` for a readonly view of the raw tree.",
"type": "minor"
}
],
"packageName": "@rushstack/lookup-by-path"
}
22 changes: 22 additions & 0 deletions common/reviews/api/lookup-by-path.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

```ts

// @beta
export function getFirstDifferenceInCommonNodes<TItem extends {}>(options: IGetFirstDifferenceInCommonNodesOptions<TItem>): string | undefined;

// @beta
export interface IGetFirstDifferenceInCommonNodesOptions<TItem extends {}> {
delimiter?: string;
equals?: (a: TItem, b: TItem) => boolean;
first: IReadonlyPathTrieNode<TItem>;
prefix?: string;
second: IReadonlyPathTrieNode<TItem>;
}

// @beta
export interface IPrefixMatch<TItem extends {}> {
index: number;
Expand All @@ -22,6 +34,14 @@ export interface IReadonlyLookupByPath<TItem extends {}> extends Iterable<[strin
groupByChild<TInfo>(infoByPath: Map<string, TInfo>, delimiter?: string): Map<TItem, Map<string, TInfo>>;
has(query: string, delimiter?: string): boolean;
get size(): number;
// (undocumented)
get tree(): IReadonlyPathTrieNode<TItem>;
}

// @beta
export interface IReadonlyPathTrieNode<TItem extends {}> {
readonly children: ReadonlyMap<string, IReadonlyPathTrieNode<TItem>> | undefined;
readonly value: TItem | undefined;
}

// @beta
Expand All @@ -42,6 +62,8 @@ export class LookupByPath<TItem extends {}> implements IReadonlyLookupByPath<TIt
setItem(serializedPath: string, value: TItem, delimiter?: string): this;
setItemFromSegments(pathSegments: Iterable<string>, value: TItem): this;
get size(): number;
// (undocumented)
get tree(): IReadonlyPathTrieNode<TItem>;
}

```
34 changes: 33 additions & 1 deletion libraries/lookup-by-path/src/LookupByPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ interface IPathTrieNode<TItem extends {}> {
children: Map<string, IPathTrieNode<TItem>> | undefined;
}

/**
* Readonly view of a node in the path trie used in LookupByPath
*
* @remarks
* This interface is used to facilitate parallel traversals for comparing two `LookupByPath` instances.
*
* @beta
*/
export interface IReadonlyPathTrieNode<TItem extends {}> {
/**
* The value that exactly matches the current relative path
*/
readonly value: TItem | undefined;

/**
* Child nodes by subfolder
*/
readonly children: ReadonlyMap<string, IReadonlyPathTrieNode<TItem>> | undefined;
}

interface IPrefixEntry {
/**
* The prefix that was matched
Expand Down Expand Up @@ -123,6 +143,11 @@ export interface IReadonlyLookupByPath<TItem extends {}> extends Iterable<[strin
*/
get size(): number;

/**
* @returns The root node of the trie, corresponding to the path ''
*/
get tree(): IReadonlyPathTrieNode<TItem>;

/**
* Iterates over the entries in this trie.
*
Expand Down Expand Up @@ -263,12 +288,19 @@ export class LookupByPath<TItem extends {}> implements IReadonlyLookupByPath<TIt
}

/**
* {@inheritdoc IReadonlyLookupByPath}
* {@inheritdoc IReadonlyLookupByPath.size}
*/
public get size(): number {
return this._size;
}

/**
* {@inheritdoc IReadonlyLookupByPath.tree}
*/
public get tree(): IReadonlyPathTrieNode<TItem> {
return this._root;
}

/**
* Deletes all entries from this `LookupByPath` instance.
*
Expand Down
118 changes: 118 additions & 0 deletions libraries/lookup-by-path/src/getFirstDifferenceInCommonNodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { IReadonlyPathTrieNode } from './LookupByPath';

/**
* Options for the getFirstDifferenceInCommonNodes function.
* @beta
*/
export interface IGetFirstDifferenceInCommonNodesOptions<TItem extends {}> {
/**
* The first node to compare.
*/
first: IReadonlyPathTrieNode<TItem>;
/**
* The second node to compare.
*/
second: IReadonlyPathTrieNode<TItem>;
/**
* The path prefix to the current node.
* @defaultValue ''
*/
prefix?: string;
/**
* The delimiter used to join path segments.
* @defaultValue '/'
*/
delimiter?: string;
/**
* A function to compare the values of the nodes.
* If not provided, strict equality (===) is used.
*/
equals?: (a: TItem, b: TItem) => boolean;
}

/**
* Recursively compares two path tries to find the first shared node with a different value.
*
* @param options - The options for the comparison
* @returns The path to the first differing node, or undefined if they are identical
*
* @remarks
* Ignores any nodes that are not shared between the two tries.
*
* @beta
*/
export function getFirstDifferenceInCommonNodes<TItem extends {}>(
options: IGetFirstDifferenceInCommonNodesOptions<TItem>
): string | undefined {
const { first, second, prefix = '', delimiter = '/', equals = defaultEquals } = options;

return getFirstDifferenceInCommonNodesInternal({
first,
second,
prefix,
delimiter,
equals
});
}

/**
* Recursively compares two path tries to find the first shared node with a different value.
*
* @param options - The options for the comparison
* @returns The path to the first differing node, or undefined if they are identical
*
* @remarks
* Ignores any nodes that are not shared between the two tries.
* Separated out to avoid redundant parameter defaulting in the recursive calls.
*/
function getFirstDifferenceInCommonNodesInternal<TItem extends {}>(
options: Required<IGetFirstDifferenceInCommonNodesOptions<TItem>>
): string | undefined {
const { first, second, prefix, delimiter, equals } = options;

const firstItem: TItem | undefined = first.value;
const secondItem: TItem | undefined = second.value;

if (firstItem !== undefined && secondItem !== undefined && !equals(firstItem, secondItem)) {
// If this value was present in both tries with different values, return the prefix for this node.
return prefix;
}

const { children: firstChildren } = first;
const { children: secondChildren } = second;

if (firstChildren && secondChildren) {
for (const [key, firstChild] of firstChildren) {
const secondChild: IReadonlyPathTrieNode<TItem> | undefined = secondChildren.get(key);
if (!secondChild) {
continue;
}
const result: string | undefined = getFirstDifferenceInCommonNodesInternal({
first: firstChild,
second: secondChild,
prefix: key,
delimiter,
equals
});

if (result !== undefined) {
return prefix ? `${prefix}${delimiter}${result}` : result;
}
}
}

return;
}

/**
* Default equality function for comparing two items, using strict equality.
* @param a - The first item to compare
* @param b - The second item to compare
* @returns True if the items are reference equal, false otherwise
*/
function defaultEquals<TItem>(a: TItem, b: TItem): boolean {
return a === b;
}
4 changes: 3 additions & 1 deletion libraries/lookup-by-path/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
* @packageDocumentation
*/

export type { IPrefixMatch, IReadonlyLookupByPath } from './LookupByPath';
export type { IPrefixMatch, IReadonlyLookupByPath, IReadonlyPathTrieNode } from './LookupByPath';
export { LookupByPath } from './LookupByPath';
export type { IGetFirstDifferenceInCommonNodesOptions } from './getFirstDifferenceInCommonNodes';
export { getFirstDifferenceInCommonNodes } from './getFirstDifferenceInCommonNodes';
Loading
Loading