Optimize Path and PathSet
This commit is contained in:
1 parent
c5553d95c5
commit
4b1d2daafb
3 files changed
+200
-121
No files matched your search
+43
-80
@@ -22,10 +22,12 @@ import Path from "./path";
|
|||||||
export default class PathSet {
|
export default class PathSet {
|
||||||
private lengthIndex: Set<Path>[];
|
private lengthIndex: Set<Path>[];
|
||||||
private fragmentIndex: Map<string, Set<Path>>[];
|
private fragmentIndex: Map<string, Set<Path>>[];
|
||||||
|
private stringIndex: Map<string, Path>;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.lengthIndex = [];
|
this.lengthIndex = [];
|
||||||
this.fragmentIndex = [];
|
this.fragmentIndex = [];
|
||||||
|
this.stringIndex = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get depth(): number {
|
public get depth(): number {
|
||||||
@@ -38,9 +40,12 @@ export default class PathSet {
|
|||||||
|
|
||||||
if (p) return p;
|
if (p) return p;
|
||||||
|
|
||||||
|
this.stringIndex.set(path.toString(), path);
|
||||||
|
|
||||||
while (this.lengthIndex.length <= path.length) {
|
while (this.lengthIndex.length <= path.length) {
|
||||||
this.lengthIndex.push(new Set());
|
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];
|
const lengthIndex = this.lengthIndex[path.length];
|
||||||
@@ -63,41 +68,7 @@ export default class PathSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get(path: Path): Path {
|
public get(path: Path): Path {
|
||||||
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
return this.stringIndex.get(path.toString()) || null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public find(
|
public find(
|
||||||
@@ -107,61 +78,53 @@ export default class PathSet {
|
|||||||
depth: number = path.length
|
depth: number = path.length
|
||||||
): Path[] {
|
): Path[] {
|
||||||
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
||||||
const res = [];
|
|
||||||
const groups = [[]];
|
|
||||||
|
|
||||||
for (let i = path.length; i <= depth; ++i)
|
const len = path.length;
|
||||||
if (this.lengthIndex[i]) groups[0].push(this.lengthIndex[i]);
|
|
||||||
|
|
||||||
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];
|
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 fragment = path.segments[i] as string;
|
||||||
|
const fragmentIndexSet1 = fragmentIndex.get(fragment);
|
||||||
|
|
||||||
if (fragment === "*" && subset) continue;
|
if (!fragmentIndexSet1) {
|
||||||
|
if (!fragmentIndexSet2) return [];
|
||||||
const g = [];
|
if (!res) res = [...fragmentIndexSet2];
|
||||||
|
else res = res.filter(r => fragmentIndexSet2.has(r));
|
||||||
const s = fragmentIndex.get(fragment);
|
} else if (!fragmentIndexSet2) {
|
||||||
if (s) g.push(s);
|
if (!res) res = [...fragmentIndexSet1];
|
||||||
|
else res = res.filter(r => fragmentIndexSet1.has(r));
|
||||||
if (fragment !== "*" && superset && fragmentIndex.has("*"))
|
} else {
|
||||||
g.push(fragmentIndex.get("*"));
|
if (!res) {
|
||||||
|
res = [...fragmentIndexSet1, ...fragmentIndexSet2];
|
||||||
if (!g.length) return res;
|
} else {
|
||||||
|
res = res.filter(
|
||||||
groups.push(g);
|
r => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r)
|
||||||
}
|
|
||||||
|
|
||||||
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 (!res.length) return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res) res = [].concat(...lengthIndex.map(a => [...a]));
|
||||||
|
else res = res.filter(r => lengthIndex.some(a => a.has(r)));
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
p = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (p) res.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+89
-41
@@ -28,6 +28,7 @@ export default class Path {
|
|||||||
public readonly wildcard: number;
|
public readonly wildcard: number;
|
||||||
public readonly alias: number;
|
public readonly alias: number;
|
||||||
protected _string: string;
|
protected _string: string;
|
||||||
|
protected _stringIndex: number[];
|
||||||
|
|
||||||
protected static parseAlias(
|
protected static parseAlias(
|
||||||
pattern: string,
|
pattern: string,
|
||||||
@@ -35,8 +36,8 @@ export default class Path {
|
|||||||
): { index: number; alias: Alias } {
|
): { index: number; alias: Alias } {
|
||||||
const alias: Alias = [];
|
const alias: Alias = [];
|
||||||
while (index < pattern.length && pattern[index] !== "]") {
|
while (index < pattern.length && pattern[index] !== "]") {
|
||||||
const res = Path.parsePath(pattern, index);
|
const { index: idx, segments } = Path.parsePath(pattern, index);
|
||||||
let j = (index = res.index + 1);
|
let j = (index = idx + 1);
|
||||||
while (pattern[j] !== "]" && pattern[j] !== ",") {
|
while (pattern[j] !== "]" && pattern[j] !== ",") {
|
||||||
if (pattern[j] === '"' && index === j) {
|
if (pattern[j] === '"' && index === j) {
|
||||||
++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;
|
if (pattern[index] === ",") ++index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,18 +79,15 @@ export default class Path {
|
|||||||
protected static parsePath(
|
protected static parsePath(
|
||||||
pattern: string,
|
pattern: string,
|
||||||
index: number
|
index: number
|
||||||
): { index: number; segments: Segments; wildcard: number; alias: number } {
|
): { index: number; segments: Segments } {
|
||||||
const segments = [];
|
const segments = [];
|
||||||
let wildcard = 0;
|
|
||||||
let alias = 0;
|
|
||||||
// Colon separator is needed for parseAlias
|
// Colon separator is needed for parseAlias
|
||||||
if (index < pattern.length && pattern[index] !== ":") {
|
if (index < pattern.length && pattern[index] !== ":") {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (pattern[index] === "[") {
|
if (pattern[index] === "[") {
|
||||||
const res = Path.parseAlias(pattern, index + 1);
|
const { index: idx, alias } = Path.parseAlias(pattern, index + 1);
|
||||||
index = res.index + 1;
|
index = idx + 1;
|
||||||
alias |= 1 << segments.length;
|
segments.push(alias);
|
||||||
segments.push(res.alias);
|
|
||||||
} else {
|
} else {
|
||||||
const j = index;
|
const j = index;
|
||||||
while (
|
while (
|
||||||
@@ -99,7 +97,6 @@ export default class Path {
|
|||||||
)
|
)
|
||||||
++index;
|
++index;
|
||||||
const s = pattern.slice(j, index).trim();
|
const s = pattern.slice(j, index).trim();
|
||||||
if (s === "*") wildcard |= 1 << segments.length;
|
|
||||||
segments.push(s);
|
segments.push(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,14 +108,33 @@ export default class Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.freeze(segments);
|
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.segments = segments;
|
||||||
this.wildcard = wildcard;
|
this.wildcard = wildcard;
|
||||||
this.alias = alias;
|
this.alias = alias;
|
||||||
this._string = null;
|
this._string = arr.join(".");
|
||||||
|
this._stringIndex = stringIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parse(str: string): Path {
|
public static parse(str: string): Path {
|
||||||
@@ -126,8 +142,9 @@ export default class Path {
|
|||||||
if (!path) {
|
if (!path) {
|
||||||
path = cache2.get(str);
|
path = cache2.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
const res = Path.parsePath(str, 0);
|
const { segments } = Path.parsePath(str, 0);
|
||||||
path = new Path(res.segments, res.wildcard, res.alias);
|
path = new Path(segments);
|
||||||
|
if (path.toString() !== str) cache1.set(path.toString(), path);
|
||||||
}
|
}
|
||||||
cache1.set(str, path);
|
cache1.set(str, path);
|
||||||
}
|
}
|
||||||
@@ -139,44 +156,75 @@ export default class Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
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;
|
return this._string;
|
||||||
}
|
}
|
||||||
|
|
||||||
public slice(start: number = 0, end: number = this.segments.length): Path {
|
public slice(start: number = 0, end: number = this.segments.length): Path {
|
||||||
if (start < 0) start = Math.max(0, this.segments.length + start);
|
if (start < 0) start = Math.max(0, this.segments.length + start);
|
||||||
if (end < 0) end = Math.max(0, this.segments.length + end);
|
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);
|
let str;
|
||||||
const mask = (1 << (end - start)) - 1;
|
if (start >= end) {
|
||||||
const wildcard = (this.wildcard >> start) & mask;
|
str = "";
|
||||||
const alias = (this.alias >> start) & mask;
|
} else {
|
||||||
return new Path(segments, wildcard, alias);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public concat(path: Path): Path {
|
let path = cache1.get(str);
|
||||||
const segments = this.segments.concat(path.segments);
|
if (!path) {
|
||||||
const wildcard = this.wildcard | (path.wildcard << this.segments.length);
|
path = cache2.get(str);
|
||||||
const alias = this.alias | (path.alias << this.segments.length);
|
if (!path) {
|
||||||
return new Path(segments, wildcard, alias);
|
const segments = this.segments.slice(start, end);
|
||||||
|
Object.freeze(segments);
|
||||||
|
path = new Path(segments);
|
||||||
|
}
|
||||||
|
cache1.set(str, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
public stripAlias(): Path {
|
||||||
if (!this.alias) return this;
|
if (!this.alias) return this;
|
||||||
const segments = this.segments.map(s => (Array.isArray(s) ? "*" : s));
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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", "*.*"]
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user