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 {Or} from "./Or";
import {TagUtils} from "./TagUtils";
import {Tag} from "./Tag";
import {RegexTag} from "./RegexTag";
export class And extends TagsFilter {
public and: TagsFilter[]
constructor(and: TagsFilter[]) {
super();
this.and = and
}
public static construct(and: TagsFilter[]): TagsFilter{
if(and.length === 1){
public static construct(and: TagsFilter[]): TagsFilter {
if (and.length === 1) {
return and[0]
}
return new And(and)
@ -177,21 +180,21 @@ export class And extends TagsFilter {
removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean {
const newAnds: TagsFilter[] = []
for (const tag of this.and) {
if(tag instanceof And){
if (tag instanceof And) {
throw "Optimize expressions before using removePhraseConsideredKnown"
}
if(tag instanceof Or){
if (tag instanceof Or) {
const r = tag.removePhraseConsideredKnown(knownExpression, value)
if(r === true){
if (r === true) {
continue
}
if(r === false){
if (r === false) {
return false;
}
newAnds.push(r)
continue
}
if(value && knownExpression.shadows(tag)){
if (value && knownExpression.shadows(tag)) {
/**
* 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,
@ -201,7 +204,7 @@ export class And extends TagsFilter {
*/
continue
}
if(!value && tag.shadows(knownExpression)){
if (!value && tag.shadows(knownExpression)) {
/**
* We know that knownExpression is unmet.
@ -217,31 +220,68 @@ export class And extends TagsFilter {
newAnds.push(tag)
}
if(newAnds.length === 0){
if (newAnds.length === 0) {
return true
}
return And.construct(newAnds)
}
optimize(): TagsFilter | boolean {
if(this.and.length === 0){
if (this.and.length === 0) {
return true
}
const optimizedRaw = this.and.map(t => t.optimize())
.filter(t => t !== true /* true is the neutral element in an AND, we drop them*/ )
if(optimizedRaw.some(t => t === false)){
.filter(t => t !== true /* true is the neutral element in an AND, we drop them*/)
if (optimizedRaw.some(t => t === false)) {
// We have an AND with a contained false: this is always 'false'
return false;
}
const optimized = <TagsFilter[]> optimizedRaw;
const optimized = <TagsFilter[]>optimizedRaw;
const newAnds : TagsFilter[] = []
{
// Conflicting keys do return false
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
}
}
}
}
let containedOrs : Or[] = []
const newAnds: TagsFilter[] = []
let containedOrs: Or[] = []
for (const tf of optimized) {
if(tf instanceof And){
if (tf instanceof And) {
newAnds.push(...tf.and)
}else if(tf instanceof Or){
} else if (tf instanceof Or) {
containedOrs.push(tf)
} else {
newAnds.push(tf)
@ -251,7 +291,7 @@ export class And extends TagsFilter {
{
let dirty = false;
do {
const cleanedContainedOrs : Or[] = []
const cleanedContainedOrs: Or[] = []
outer: for (let containedOr of containedOrs) {
for (const known of newAnds) {
// input for optimazation: (K=V & (X=Y | K=V))
@ -278,7 +318,7 @@ export class And extends TagsFilter {
cleanedContainedOrs.push(containedOr)
}
containedOrs = cleanedContainedOrs
} while(dirty)
} while (dirty)
}
@ -290,17 +330,17 @@ export class And extends TagsFilter {
})
// Extract common keys from the OR
if(containedOrs.length === 1){
if (containedOrs.length === 1) {
newAnds.push(containedOrs[0])
}else if(containedOrs.length > 1){
let commonValues : TagsFilter [] = containedOrs[0].or
for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++){
} else if (containedOrs.length > 1) {
let commonValues: TagsFilter [] = containedOrs[0].or
for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++) {
const containedOr = containedOrs[i];
commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv)))
}
if(commonValues.length === 0){
if (commonValues.length === 0) {
newAnds.push(...containedOrs)
}else{
} else {
const newOrs: TagsFilter[] = []
for (const containedOr of containedOrs) {
const elements = containedOr.or
@ -310,20 +350,20 @@ export class And extends TagsFilter {
commonValues.push(And.construct(newOrs))
const result = new Or(commonValues).optimize()
if(result === false){
if (result === false) {
return false
}else if(result === true){
} else if (result === true) {
// neutral element: skip
}else{
} else {
newAnds.push(result)
}
}
}
if(newAnds.length === 0){
if (newAnds.length === 0) {
return true
}
if(TagUtils.ContainsOppositeTags(newAnds)){
if (TagUtils.ContainsOppositeTags(newAnds)) {
return false
}

View file

@ -68,7 +68,7 @@ export class Tag extends TagsFilter {
if (shorten) {
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
if (currentProperties !== undefined && (currentProperties[this.key] ?? "") === "") {
// 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}];
}
AsJson() {
return this.asHumanString(false, false)
}
optimize(): TagsFilter | boolean {
return this;
}

View file

@ -31,6 +31,12 @@ describe("Tag optimalization", () => {
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", () => {
// foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))