diff --git a/spec/model/implementation/effective-module-specification.spec.ts b/spec/model/implementation/effective-module-specification.spec.ts new file mode 100644 index 00000000..220499ca --- /dev/null +++ b/spec/model/implementation/effective-module-specification.spec.ts @@ -0,0 +1,70 @@ +import { + EffectiveModuleSpecification, + EffectiveModuleSpecificationClause, +} from '../../../src/model/implementation/modules/effective-module-specification'; +import { expect } from 'chai'; + +describe('EffectiveModuleSpecification', () => { + describe('simplify()', () => { + it('removes duplicate simple clauses', () => { + const input = new EffectiveModuleSpecification({ + orCombinedClauses: [ + new EffectiveModuleSpecificationClause({ andCombinedModules: ['a'] }), + new EffectiveModuleSpecificationClause({ andCombinedModules: ['b'] }), + new EffectiveModuleSpecificationClause({ andCombinedModules: ['a'] }), + ], + }); + const output = input.simplify(); + expect(output.toString()).to.equal('a, b'); + }); + + it('removes duplicate multi-module clauses', () => { + const input = new EffectiveModuleSpecification({ + orCombinedClauses: [ + new EffectiveModuleSpecificationClause({ andCombinedModules: ['a', 'c'] }), + new EffectiveModuleSpecificationClause({ andCombinedModules: ['b'] }), + new EffectiveModuleSpecificationClause({ andCombinedModules: ['a', 'c'] }), + ], + }); + const output = input.simplify(); + expect(output.toString()).to.equal('b, a && c'); + }); + + it('removes redundant clauses', () => { + const input = new EffectiveModuleSpecification({ + orCombinedClauses: [ + new EffectiveModuleSpecificationClause({ andCombinedModules: ['a', 'c'] }), + new EffectiveModuleSpecificationClause({ andCombinedModules: ['b'] }), + new EffectiveModuleSpecificationClause({ andCombinedModules: ['a', 'c', 'd'] }), + ], + }); + const output = input.simplify(); + expect(output.toString()).to.equal('b, a && c'); + }); + + it('works with a more complicated case', () => { + const input = new EffectiveModuleSpecification({ + orCombinedClauses: [ + new EffectiveModuleSpecificationClause({ andCombinedModules: ['shipping'] }), + new EffectiveModuleSpecificationClause({ + andCombinedModules: ['shipping', 'dangerous_goods'], + }), + new EffectiveModuleSpecificationClause({ + andCombinedModules: ['tms', 'dangerous_goods'], + }), + new EffectiveModuleSpecificationClause({ + andCombinedModules: ['shipping', 'dangerous_goods', 'extra1'], + }), + new EffectiveModuleSpecificationClause({ + andCombinedModules: ['tms', 'dangerous_goods', 'extra1'], + }), + new EffectiveModuleSpecificationClause({ + andCombinedModules: ['extra1'], + }), + ], + }); + const output = input.simplify(); + expect(output.toString()).to.equal('extra1, shipping, dangerous_goods && tms'); + }); + }); +}); diff --git a/src/model/implementation/modules/effective-module-specification.ts b/src/model/implementation/modules/effective-module-specification.ts index 7ceb6802..e3b42fa0 100644 --- a/src/model/implementation/modules/effective-module-specification.ts +++ b/src/model/implementation/modules/effective-module-specification.ts @@ -29,18 +29,33 @@ export class EffectiveModuleSpecification { } /** - * Sorts the clauses by name and removes duplicates + * Sorts the clauses by name and removes duplicates and redundant clauses (e.g. ["a", "a && b"] is simplified to ["a"]) */ simplify(): EffectiveModuleSpecification { - const map = new Map( - this.orCombinedClauses.map((clause) => { - const normalized = clause.normalize(); - return [normalized.toString(), normalized]; - }), - ); - const keys = Array.from(map.keys()).sort(); - const normalizedClauses = keys.map((k) => map.get(k)!); - return new EffectiveModuleSpecification({ orCombinedClauses: normalizedClauses }); + // sort by length so we prefer shorter clauses + // sort by toString() secondarily to get a normalized representation + const clausesSortedByLength = this.orCombinedClauses + .map((c) => c.normalize()) + .sort((a, b) => a.toString().localeCompare(b.toString())) + .sort((a, b) => a.andCombinedModules.length - b.andCombinedModules.length); + const nonRedundantClauses: EffectiveModuleSpecificationClause[] = []; + // this has quadratic runtime w.r.t. number atoms, but we don't expect huge module specifications + for (const clause of clausesSortedByLength) { + const modulesInClause = new Set(clause.andCombinedModules); + let clauseIsRedundant = false; + for (const existingClause of nonRedundantClauses) { + if (existingClause.andCombinedModules.every((m) => modulesInClause.has(m))) { + // existingClause implies clause + clauseIsRedundant = true; + break; + } + } + if (!clauseIsRedundant) { + nonRedundantClauses.push(clause); + } + } + + return new EffectiveModuleSpecification({ orCombinedClauses: nonRedundantClauses }); } orCombineWith(other: EffectiveModuleSpecification) { @@ -68,6 +83,10 @@ export class EffectiveModuleSpecification { }); return combined.simplify(); } + + toString() { + return this.orCombinedClauses.map((c) => c.toString()).join(', '); + } } export class EffectiveModuleSpecificationClause {