diff --git a/lib/common/path-set.ts b/lib/common/path-set.ts index 4edad4c..10bb564 100644 --- a/lib/common/path-set.ts +++ b/lib/common/path-set.ts @@ -22,10 +22,12 @@ import Path from "./path"; export default class PathSet { private lengthIndex: Set[]; private fragmentIndex: Map>[]; + private stringIndex: Map; public constructor() { this.lengthIndex = []; this.fragmentIndex = []; + this.stringIndex = new Map(); } public get depth(): number { @@ -38,9 +40,12 @@ export default class PathSet { if (p) return p; + this.stringIndex.set(path.toString(), path); + while (this.lengthIndex.length <= path.length) { this.lengthIndex.push(new Set()); - this.fragmentIndex.push(new Map()); + // fragmentIndex is one less than lengthIndex + if (this.lengthIndex.length > 1) this.fragmentIndex.push(new Map()); } const lengthIndex = this.lengthIndex[path.length]; @@ -63,41 +68,7 @@ export default class PathSet { } public get(path: Path): Path { - if (path.alias) throw new Error("PathSet does not support aliased paths"); - const lengthIndex = this.lengthIndex[path.length]; - if (!lengthIndex || !lengthIndex.size) return null; - - if (lengthIndex.has(path)) return path; - - const sets = [lengthIndex]; - - for (let i = 0; i < path.length; ++i) { - const fragmentIndex = this.fragmentIndex[i]; - if (!fragmentIndex || !fragmentIndex.size) return null; - - const fragment = path.segments[i] as string; - - const fragmentIndexSet = fragmentIndex.get(fragment); - if (!fragmentIndexSet) return null; - - sets.push(fragmentIndexSet); - } - - sets.sort((a, b) => a.size - b.size); - - const smallestSet = sets.shift(); - for (let p of smallestSet) { - for (const s of sets) { - if (!s.has(p)) { - p = null; - break; - } - } - - if (p) return p; - } - - return null; + return this.stringIndex.get(path.toString()) || null; } public find( @@ -107,61 +78,53 @@ export default class PathSet { depth: number = path.length ): Path[] { if (path.alias) throw new Error("PathSet does not support aliased paths"); - const res = []; - const groups = [[]]; - for (let i = path.length; i <= depth; ++i) - if (this.lengthIndex[i]) groups[0].push(this.lengthIndex[i]); + const len = path.length; - if (!groups[0].length) return res; + if (!superset && depth === len && (!subset || !path.wildcard)) { + const p = this.get(path); + return p ? [p] : []; + } - for (let i = 0; i < path.length; ++i) { + const lengthIndex = this.lengthIndex.slice(len, depth + 1); + if (!lengthIndex.length) return []; + + let res; + for (let i = len - 1; i >= 0; --i) { + let fragmentIndexSet2; const fragmentIndex = this.fragmentIndex[i]; - if (!fragmentIndex) return res; + + if ((path.wildcard >> i) & 1) { + if (subset) continue; + } else if (superset) { + fragmentIndexSet2 = fragmentIndex.get("*"); + } const fragment = path.segments[i] as string; + const fragmentIndexSet1 = fragmentIndex.get(fragment); - if (fragment === "*" && subset) continue; - - const g = []; - - const s = fragmentIndex.get(fragment); - if (s) g.push(s); - - if (fragment !== "*" && superset && fragmentIndex.has("*")) - g.push(fragmentIndex.get("*")); - - if (!g.length) return res; - - groups.push(g); - } - - groups.sort( - (a, b) => - a.reduce((prev, cur) => prev + cur.size, 0) - - b.reduce((prev, cur) => prev + cur.size, 0) - ); - - const smallestGroup = groups.shift(); - for (const set of smallestGroup) { - for (let p of set) { - for (const group of groups) { - let found = false; - for (const s of group) { - if (s.has(p)) { - found = true; - break; - } - } - - if (!found) { - p = null; - break; - } + if (!fragmentIndexSet1) { + if (!fragmentIndexSet2) return []; + if (!res) res = [...fragmentIndexSet2]; + else res = res.filter(r => fragmentIndexSet2.has(r)); + } else if (!fragmentIndexSet2) { + if (!res) res = [...fragmentIndexSet1]; + else res = res.filter(r => fragmentIndexSet1.has(r)); + } else { + if (!res) { + res = [...fragmentIndexSet1, ...fragmentIndexSet2]; + } else { + res = res.filter( + r => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r) + ); } - if (p) res.push(p); } + if (!res.length) return res; } + + if (!res) res = [].concat(...lengthIndex.map(a => [...a])); + else res = res.filter(r => lengthIndex.some(a => a.has(r))); + return res; } } diff --git a/lib/common/path.ts b/lib/common/path.ts index 60fd16f..e13413c 100644 --- a/lib/common/path.ts +++ b/lib/common/path.ts @@ -28,6 +28,7 @@ export default class Path { public readonly wildcard: number; public readonly alias: number; protected _string: string; + protected _stringIndex: number[]; protected static parseAlias( pattern: string, @@ -35,8 +36,8 @@ export default class Path { ): { index: number; alias: Alias } { const alias: Alias = []; while (index < pattern.length && pattern[index] !== "]") { - const res = Path.parsePath(pattern, index); - let j = (index = res.index + 1); + const { index: idx, segments } = Path.parsePath(pattern, index); + let j = (index = idx + 1); while (pattern[j] !== "]" && pattern[j] !== ",") { if (pattern[j] === '"' && index === j) { ++j; @@ -58,7 +59,7 @@ export default class Path { } } - alias.push([new Path(res.segments, res.wildcard, res.alias), value]); + alias.push([new Path(segments), value]); if (pattern[index] === ",") ++index; } @@ -78,18 +79,15 @@ export default class Path { protected static parsePath( pattern: string, index: number - ): { index: number; segments: Segments; wildcard: number; alias: number } { + ): { index: number; segments: Segments } { const segments = []; - let wildcard = 0; - let alias = 0; // Colon separator is needed for parseAlias if (index < pattern.length && pattern[index] !== ":") { for (;;) { if (pattern[index] === "[") { - const res = Path.parseAlias(pattern, index + 1); - index = res.index + 1; - alias |= 1 << segments.length; - segments.push(res.alias); + const { index: idx, alias } = Path.parseAlias(pattern, index + 1); + index = idx + 1; + segments.push(alias); } else { const j = index; while ( @@ -99,7 +97,6 @@ export default class Path { ) ++index; const s = pattern.slice(j, index).trim(); - if (s === "*") wildcard |= 1 << segments.length; segments.push(s); } @@ -111,14 +108,33 @@ export default class Path { } Object.freeze(segments); - return { index, segments, wildcard, alias }; + return { index, segments }; } - protected constructor(segments: Segments, wildcard: number, alias: number) { + protected constructor(segments: Segments) { + let alias = 0; + let wildcard = 0; + const arr = segments.map((s, i) => { + if (Array.isArray(s)) { + alias |= 1 << i; + const parts = s.map( + al => `${al[0].toString()}:${JSON.stringify(al[1])}` + ); + return `[${parts.join(",")}]`; + } else if (s === "*") { + wildcard |= 1 << i; + } + return s; + }); + + let offset = 0; + const stringIndex = arr.map((s, i) => (offset += s.length) + i); + this.segments = segments; this.wildcard = wildcard; this.alias = alias; - this._string = null; + this._string = arr.join("."); + this._stringIndex = stringIndex; } public static parse(str: string): Path { @@ -126,8 +142,9 @@ export default class Path { if (!path) { path = cache2.get(str); if (!path) { - const res = Path.parsePath(str, 0); - path = new Path(res.segments, res.wildcard, res.alias); + const { segments } = Path.parsePath(str, 0); + path = new Path(segments); + if (path.toString() !== str) cache1.set(path.toString(), path); } cache1.set(str, path); } @@ -139,44 +156,75 @@ export default class Path { } public toString(): string { - if (this._string == null) { - this._string = this.segments - .map(s => { - if (Array.isArray(s)) { - const parts = s.map( - al => `${al[0].toString()}:${JSON.stringify(al[1])}` - ); - return `[${parts.join(",")}]`; - } - return s; - }) - .join("."); - } return this._string; } public slice(start: number = 0, end: number = this.segments.length): Path { if (start < 0) start = Math.max(0, this.segments.length + start); if (end < 0) end = Math.max(0, this.segments.length + end); - if (start >= end) return new Path([], 0, 0); - const segments = this.segments.slice(start, end); - const mask = (1 << (end - start)) - 1; - const wildcard = (this.wildcard >> start) & mask; - const alias = (this.alias >> start) & mask; - return new Path(segments, wildcard, alias); + + let str; + if (start >= end) { + str = ""; + } else { + const i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0; + const i2 = + end <= this.segments.length + ? this._stringIndex[end - 1] + : this._string.length; + str = this._string.slice(i1, i2); + } + + let path = cache1.get(str); + if (!path) { + path = cache2.get(str); + if (!path) { + const segments = this.segments.slice(start, end); + Object.freeze(segments); + path = new Path(segments); + } + cache1.set(str, path); + } + + return path; } - public concat(path: Path): Path { - const segments = this.segments.concat(path.segments); - const wildcard = this.wildcard | (path.wildcard << this.segments.length); - const alias = this.alias | (path.alias << this.segments.length); - return new Path(segments, wildcard, alias); + public concat(path2: Path): Path { + if (!path2._string) return this; + else if (!this._string) return path2; + + const str = `${this._string}.${path2._string}`; + + let path = cache1.get(str); + if (!path) { + path = cache2.get(str); + if (!path) { + const segments = this.segments.concat(path2.segments); + Object.freeze(segments); + path = new Path(segments); + } + cache1.set(str, path); + } + + return path; } public stripAlias(): Path { if (!this.alias) return this; const segments = this.segments.map(s => (Array.isArray(s) ? "*" : s)); - return new Path(segments, this.wildcard | this.alias, 0); + const str = segments.join("."); + + let path = cache1.get(str); + if (!path) { + path = cache2.get(str); + if (!path) { + Object.freeze(segments); + path = new Path(segments); + } + cache1.set(str, path); + } + + return path; } } diff --git a/test/path-set.ts b/test/path-set.ts new file mode 100644 index 0000000..a48b4f9 --- /dev/null +++ b/test/path-set.ts @@ -0,0 +1,68 @@ +import ava from "ava"; +import Path from "../lib/common/path"; +import PathSet from "../lib/common/path-set"; + +ava("depth", t => { + const pathSet = new PathSet(); + t.is(pathSet.depth, 0); + pathSet.add(Path.parse("")); + t.is(pathSet.depth, 1); + pathSet.add(Path.parse("a")); + t.is(pathSet.depth, 2); +}); + +ava("add", t => { + const pathSet = new PathSet(); + pathSet.add(Path.parse("a")); + pathSet.add(Path.parse("a")); + t.is(pathSet.find(Path.parse("a"), true, true, 99).length, 1); +}); + +ava("get", t => { + const pathSet = new PathSet(); + pathSet.add(Path.parse("a.*")); + pathSet.add(Path.parse("a.a")); + pathSet.add(Path.parse("*.*")); + + t.is(pathSet.get(Path.parse("a.*")).toString(), "a.*"); + t.is(pathSet.get(Path.parse("*.a")), null); +}); + +ava("find", t => { + const pathSet = new PathSet(); + pathSet.add(Path.parse("a")); + pathSet.add(Path.parse("a.*")); + pathSet.add(Path.parse("a.a")); + pathSet.add(Path.parse("*.a")); + pathSet.add(Path.parse("*.*")); + + t.deepEqual( + pathSet.find(Path.parse(""), true, true, 1).map(p => p.toString()), + ["a"] + ); + + t.deepEqual( + pathSet.find(Path.parse(""), false, false, 2).map(p => p.toString()), + ["a", "a.*", "a.a", "*.a", "*.*"] + ); + + t.deepEqual( + pathSet.find(Path.parse("a.*"), false, false).map(p => p.toString()), + ["a.*"] + ); + + t.deepEqual( + pathSet.find(Path.parse("a.*"), false, true).map(p => p.toString()), + ["a.*", "a.a"] + ); + + t.deepEqual( + pathSet.find(Path.parse("a.*"), true, false).map(p => p.toString()), + ["a.*", "*.*"] + ); + + t.deepEqual( + pathSet.find(Path.parse("a.*"), true, true).map(p => p.toString()), + ["a.*", "a.a", "*.a", "*.*"] + ); +});