Add extra optimization on And, add test

This commit is contained in:
pietervdvn 2022-05-01 04:16:17 +02:00
parent 2f3886d2e0
commit 819f65e18d
3 changed files with 97 additions and 55 deletions

View file

@ -1,16 +1,19 @@
import {TagsFilter} from "./TagsFilter"; import {TagsFilter} from "./TagsFilter";
import {Or} from "./Or"; import {Or} from "./Or";
import {TagUtils} from "./TagUtils"; import {TagUtils} from "./TagUtils";
import {Tag} from "./Tag";
import {RegexTag} from "./RegexTag";
export class And extends TagsFilter { export class And extends TagsFilter {
public and: TagsFilter[] public and: TagsFilter[]
constructor(and: TagsFilter[]) { constructor(and: TagsFilter[]) {
super(); super();
this.and = and this.and = and
} }
public static construct(and: TagsFilter[]): TagsFilter{ public static construct(and: TagsFilter[]): TagsFilter {
if(and.length === 1){ if (and.length === 1) {
return and[0] return and[0]
} }
return new And(and) return new And(and)
@ -50,7 +53,7 @@ export class And extends TagsFilter {
* *
* import {Tag} from "./Tag"; * import {Tag} from "./Tag";
* import {RegexTag} from "./RegexTag"; * import {RegexTag} from "./RegexTag";
* *
* const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)])
* and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ] * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ]
*/ */
@ -142,7 +145,7 @@ export class And extends TagsFilter {
usedKeys(): string[] { usedKeys(): string[] {
return [].concat(...this.and.map(subkeys => subkeys.usedKeys())); return [].concat(...this.and.map(subkeys => subkeys.usedKeys()));
} }
usedTags(): { key: string; value: string }[] { usedTags(): { key: string; value: string }[] {
return [].concat(...this.and.map(subkeys => subkeys.usedTags())); return [].concat(...this.and.map(subkeys => subkeys.usedTags()));
} }
@ -161,97 +164,134 @@ export class And extends TagsFilter {
* ^---------^ * ^---------^
* When the evaluation hits (A=B & X=Y), we know _for sure_ that X=Y does _not_ match, as it would have matched the first clause otherwise. * When the evaluation hits (A=B & X=Y), we know _for sure_ that X=Y does _not_ match, as it would have matched the first clause otherwise.
* This means that the entire 'AND' is considered FALSE * This means that the entire 'AND' is considered FALSE
* *
* new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value") * new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value")
* new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false * new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false
* new And([ new RegexTag("key",/^..*$/) ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value") * new And([ new RegexTag("key",/^..*$/) ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value")
* new And([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true * new And([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true
* *
* // should remove 'club~*' if we know that 'club=climbing' * // should remove 'club~*' if we know that 'club=climbing'
* const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} ) * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} )
* expr.removePhraseConsideredKnown(new Tag("club","climbing"), true) // => new Tag("sport","climbing") * expr.removePhraseConsideredKnown(new Tag("club","climbing"), true) // => new Tag("sport","climbing")
* *
* const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} ) * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} )
* expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr * expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
*/ */
removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean {
const newAnds: TagsFilter[] = [] const newAnds: TagsFilter[] = []
for (const tag of this.and) { for (const tag of this.and) {
if(tag instanceof And){ if (tag instanceof And) {
throw "Optimize expressions before using removePhraseConsideredKnown" throw "Optimize expressions before using removePhraseConsideredKnown"
} }
if(tag instanceof Or){ if (tag instanceof Or) {
const r = tag.removePhraseConsideredKnown(knownExpression, value) const r = tag.removePhraseConsideredKnown(knownExpression, value)
if(r === true){ if (r === true) {
continue continue
} }
if(r === false){ if (r === false) {
return false; return false;
} }
newAnds.push(r) newAnds.push(r)
continue continue
} }
if(value && knownExpression.shadows(tag)){ if (value && knownExpression.shadows(tag)) {
/** /**
* At this point, we do know that 'knownExpression' is true in every case * At this point, we do know that 'knownExpression' is true in every case
* As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true, * As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true,
* we can be sure that 'tag' is true as well. * we can be sure that 'tag' is true as well.
* *
* "True" is the neutral element in an AND, so we can skip the tag * "True" is the neutral element in an AND, so we can skip the tag
*/ */
continue continue
} }
if(!value && tag.shadows(knownExpression)){ if (!value && tag.shadows(knownExpression)) {
/** /**
* We know that knownExpression is unmet. * We know that knownExpression is unmet.
* if the tag shadows 'knownExpression' (which is the case when control flows gets here), * if the tag shadows 'knownExpression' (which is the case when control flows gets here),
* then tag CANNOT be met too, as known expression is not met. * then tag CANNOT be met too, as known expression is not met.
* *
* This implies that 'tag' must be false too! * This implies that 'tag' must be false too!
*/ */
// false is the element which absorbs all // false is the element which absorbs all
return false return false
} }
newAnds.push(tag) newAnds.push(tag)
} }
if(newAnds.length === 0){ if (newAnds.length === 0) {
return true return true
} }
return And.construct(newAnds) return And.construct(newAnds)
} }
optimize(): TagsFilter | boolean { optimize(): TagsFilter | boolean {
if(this.and.length === 0){ if (this.and.length === 0) {
return true return true
} }
const optimizedRaw = this.and.map(t => t.optimize()) const optimizedRaw = this.and.map(t => t.optimize())
.filter(t => t !== true /* true is the neutral element in an AND, we drop them*/ ) .filter(t => t !== true /* true is the neutral element in an AND, we drop them*/)
if(optimizedRaw.some(t => t === false)){ if (optimizedRaw.some(t => t === false)) {
// We have an AND with a contained false: this is always 'false' // We have an AND with a contained false: this is always 'false'
return false; return false;
} }
const optimized = <TagsFilter[]> optimizedRaw; const optimized = <TagsFilter[]>optimizedRaw;
const newAnds : TagsFilter[] = [] {
// Conflicting keys do return false
let containedOrs : Or[] = [] const properties: object = {}
for (const opt of optimized) {
if (opt instanceof Tag) {
properties[opt.key] = opt.value
}
}
for (const opt of optimized) {
if(opt instanceof Tag ){
const k = opt.key
const v = properties[k]
if(v === undefined){
continue
}
if(v !== opt.value){
// detected an internal conflict
return false
}
}
if(opt instanceof RegexTag ){
const k = opt.key
if(typeof k !== "string"){
continue
}
const v = properties[k]
if(v === undefined){
continue
}
if(v !== opt.value){
// detected an internal conflict
return false
}
}
}
}
const newAnds: TagsFilter[] = []
let containedOrs: Or[] = []
for (const tf of optimized) { for (const tf of optimized) {
if(tf instanceof And){ if (tf instanceof And) {
newAnds.push(...tf.and) newAnds.push(...tf.and)
}else if(tf instanceof Or){ } else if (tf instanceof Or) {
containedOrs.push(tf) containedOrs.push(tf)
} else { } else {
newAnds.push(tf) newAnds.push(tf)
} }
} }
{ {
let dirty = false; let dirty = false;
do { do {
const cleanedContainedOrs : Or[] = [] const cleanedContainedOrs: Or[] = []
outer: for (let containedOr of containedOrs) { outer: for (let containedOr of containedOrs) {
for (const known of newAnds) { for (const known of newAnds) {
// input for optimazation: (K=V & (X=Y | K=V)) // input for optimazation: (K=V & (X=Y | K=V))
@ -278,10 +318,10 @@ export class And extends TagsFilter {
cleanedContainedOrs.push(containedOr) cleanedContainedOrs.push(containedOr)
} }
containedOrs = cleanedContainedOrs containedOrs = cleanedContainedOrs
} while(dirty) } while (dirty)
} }
containedOrs = containedOrs.filter(ca => { containedOrs = containedOrs.filter(ca => {
const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or) const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or)
// If 'isShadowed', then at least one part of the 'OR' is matched by the outer and, so this means that this OR isn't needed at all // If 'isShadowed', then at least one part of the 'OR' is matched by the outer and, so this means that this OR isn't needed at all
@ -290,51 +330,51 @@ export class And extends TagsFilter {
}) })
// Extract common keys from the OR // Extract common keys from the OR
if(containedOrs.length === 1){ if (containedOrs.length === 1) {
newAnds.push(containedOrs[0]) newAnds.push(containedOrs[0])
}else if(containedOrs.length > 1){ } else if (containedOrs.length > 1) {
let commonValues : TagsFilter [] = containedOrs[0].or let commonValues: TagsFilter [] = containedOrs[0].or
for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++){ for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++) {
const containedOr = containedOrs[i]; const containedOr = containedOrs[i];
commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv))) commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv)))
} }
if(commonValues.length === 0){ if (commonValues.length === 0) {
newAnds.push(...containedOrs) newAnds.push(...containedOrs)
}else{ } else {
const newOrs: TagsFilter[] = [] const newOrs: TagsFilter[] = []
for (const containedOr of containedOrs) { for (const containedOr of containedOrs) {
const elements = containedOr.or const elements = containedOr.or
.filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) .filter(candidate => !commonValues.some(cv => cv.shadows(candidate)))
newOrs.push(Or.construct(elements)) newOrs.push(Or.construct(elements))
} }
commonValues.push(And.construct(newOrs)) commonValues.push(And.construct(newOrs))
const result = new Or(commonValues).optimize() const result = new Or(commonValues).optimize()
if(result === false){ if (result === false) {
return false return false
}else if(result === true){ } else if (result === true) {
// neutral element: skip // neutral element: skip
}else{ } else {
newAnds.push(result) newAnds.push(result)
} }
} }
} }
if(newAnds.length === 0){ if (newAnds.length === 0) {
return true return true
} }
if(TagUtils.ContainsOppositeTags(newAnds)){ if (TagUtils.ContainsOppositeTags(newAnds)) {
return false return false
} }
TagUtils.sortFilters(newAnds, true) TagUtils.sortFilters(newAnds, true)
return And.construct(newAnds) return And.construct(newAnds)
} }
isNegative(): boolean { isNegative(): boolean {
return !this.and.some(t => !t.isNegative()); return !this.and.some(t => !t.isNegative());
} }
} }

View file

@ -68,7 +68,7 @@ export class Tag extends TagsFilter {
if (shorten) { if (shorten) {
v = Utils.EllipsesAfter(v, 25); v = Utils.EllipsesAfter(v, 25);
} }
if (v === "" || v === undefined) { if (v === "" || v === undefined && currentProperties !== undefined) {
// This tag will be removed if in the properties, so we indicate this with special rendering // This tag will be removed if in the properties, so we indicate this with special rendering
if (currentProperties !== undefined && (currentProperties[this.key] ?? "") === "") { if (currentProperties !== undefined && (currentProperties[this.key] ?? "") === "") {
// This tag is not present in the current properties, so this tag doesn't change anything // This tag is not present in the current properties, so this tag doesn't change anything
@ -122,10 +122,6 @@ export class Tag extends TagsFilter {
return [{k: this.key, v: this.value}]; return [{k: this.key, v: this.value}];
} }
AsJson() {
return this.asHumanString(false, false)
}
optimize(): TagsFilter | boolean { optimize(): TagsFilter | boolean {
return this; return this;
} }

View file

@ -30,6 +30,12 @@ describe("Tag optimalization", () => {
const opt = t.optimize() const opt = t.optimize()
expect(opt).eq(true) expect(opt).eq(true)
}) })
it("should return false on conflicting tags", () => {
const t = new And([new Tag("key","a"), new Tag("key","b")])
const opt = t.optimize()
expect(opt).eq(false)
})
it("with nested ors and common property should be extracted", () => { it("with nested ors and common property should be extracted", () => {