Add extra optimization on And, add test
This commit is contained in:
parent
2f3886d2e0
commit
819f65e18d
3 changed files with 97 additions and 55 deletions
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue