diff --git a/README.md b/README.md index fd24553..7dbbb52 100644 --- a/README.md +++ b/README.md @@ -103,11 +103,59 @@ In some cases, it is desired to stop the engine as soon as a specific rule has f This is achieved by settings the respective rules' property `final` to `true`. Default, of course, is `false`. +### Optimization of premises (`when`) + +It is very common that different rules partially share the same premises. +Rools will merge identical premises into one. +You are free to use references or just to repeat the same premise. +Both cases are working fine. + +Example 1: by reference +```js +const isApplicable = facts => facts.user.salery >= 2000; +const rule1 = { + when: isApplicable, + ... +}; +const rule2 = { + when: isApplicable, + ... +}; +``` + +Example 2: repeat premise +```js +const rule1 = { + when: facts => facts.user.salery >= 2000, + ... +}; +const rule2 = { + when: facts => facts.user.salery >= 2000, + ... +}; +``` + +TL;DR + +Technically, this is achieved by hashing the premises +(remember, a function is an object in JavaScript). +This can be a classic function or an ES6 arrow function. +This can be a reference or the function directly. +It's tested with Node 8 and 9 (see unit tests `premises.spec.js`). + +``` +const md5 = require('md5'); +const hash1 = md5(facts => facts.user.salery > 2000); +const hash2 = md5(facts => facts.user.salery > 2000); +const hash3 = md5(facts => facts.user.salery > 3000); +console.log(hash1 === hash2); // true +console.log(hash1 === hash3); // false +``` + ### Todos Some of the features on my list are: * Conflict resolution by specificity - * Optimization: merge identical premises (`when`) into one * Optimization: re-evaluate only those premises (`when`) that are relying on modified facts * Support asynchronous actions (`then`) * More unit tests diff --git a/src/index.js b/src/index.js index 58c4ced..30c9d81 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ const assert = require('assert'); +const md5 = require('md5'); const actionId = require('uniqueid')('a'); const premiseId = require('uniqueid')('p'); @@ -8,6 +9,7 @@ class Rools { } = { logErrors: true, logDebug: false, logDelegate: null }) { this.actions = []; this.premises = []; + this.premisesByHash = {}; this.maxSteps = 100; this.logErrors = logErrors; this.logDebug = logDebug; @@ -30,13 +32,18 @@ class Rools { this.actions.push(action); const whens = Array.isArray(rule.when) ? rule.when : [rule.when]; whens.forEach((when) => { - const premise = { - id: premiseId(), - name: rule.name, - when, - }; - action.premises.push(premise); - this.premises.push(premise); + const hash = md5(when); // is function already introduced by other rule? + let premise = this.premisesByHash[hash]; + if (!premise) { // create new premise + premise = { + id: premiseId(), + name: rule.name, + when, + }; + this.premisesByHash[hash] = premise; // add to hash + this.premises.push(premise); // add to premises + } + action.premises.push(premise); // add to action }); }); } diff --git a/test/premises.spec.js b/test/premises.spec.js new file mode 100644 index 0000000..969b34d --- /dev/null +++ b/test/premises.spec.js @@ -0,0 +1,125 @@ +const Rools = require('../src'); +require('./setup'); + +describe('Rules.register() / optimization of premises', () => { + it('should not merge premises if not identical', () => { + const rule1 = { + name: 'rule1', + when: facts => facts.user.name === 'frank', + then: () => {}, + }; + const rule2 = { + name: 'rule2', + when: facts => facts.user.name === 'michael', + then: () => {}, + }; + const rools = new Rools(); + rools.register(rule1, rule2); + expect(rools.premises.length).to.be.equal(2); + }); + + it('should merge premises if identical / reference / arrow function', () => { + const isFrank = facts => facts.user.name === 'frank'; + const rule1 = { + name: 'rule1', + when: isFrank, + then: () => {}, + }; + const rule2 = { + name: 'rule2', + when: isFrank, + then: () => {}, + }; + const rools = new Rools(); + rools.register(rule1, rule2); + expect(rools.premises.length).to.be.equal(1); + }); + + it('should merge premises if identical / reference / classic function', () => { + function isFrank(facts) { + return facts.user.name === 'frank'; + } + const rule1 = { + name: 'rule1', + when: isFrank, + then: () => {}, + }; + const rule2 = { + name: 'rule2', + when: isFrank, + then: () => {}, + }; + const rools = new Rools(); + rools.register(rule1, rule2); + expect(rools.premises.length).to.be.equal(1); + }); + + it('should merge premises if identical / hash / arrow function', () => { + const rule1 = { + name: 'rule1', + when: facts => facts.user.name === 'frank', + then: () => {}, + }; + const rule2 = { + name: 'rule2', + when: facts => facts.user.name === 'frank', + then: () => {}, + }; + const rools = new Rools(); + rools.register(rule1, rule2); + expect(rools.premises.length).to.be.equal(1); + }); + + it('should merge premises if identical / hash / classic function()', () => { + const rule1 = { + name: 'rule1', + when: function p(facts) { + return facts.user.name === 'frank'; + }, + then: () => {}, + }; + const rule2 = { + name: 'rule2', + when: function p(facts) { + return facts.user.name === 'frank'; + }, + then: () => {}, + }; + const rools = new Rools(); + rools.register(rule1, rule2); + expect(rools.premises.length).to.be.equal(1); + }); + + it('should merge premises if identical / hash / slightly different', () => { + const rule1 = { + name: 'rule1', + when: facts => facts.user.name === 'frank', + then: () => {}, + }; + const rule2 = { + name: 'rule2', + when: facts => facts.user.name === "frank", // eslint-disable-line quotes + then: () => {}, + }; + const rools = new Rools(); + rools.register(rule1, rule2); + expect(rools.premises.length).to.be.equal(1); + }); + + it('should merge premises if identical / with Date object', () => { + const date = new Date('2000-01-01'); + const rule1 = { + name: 'rule1', + when: facts => facts.user.birthdate > date, + then: () => {}, + }; + const rule2 = { + name: 'rule2', + when: facts => facts.user.birthdate > date, + then: () => {}, + }; + const rools = new Rools(); + rools.register(rule1, rule2); + expect(rools.premises.length).to.be.equal(1); + }); +});