UNPKG

finitedomain

Version:

A fast feature rich finite domain solver

1,564 lines (1,261 loc) 81.4 kB
import expect from '../fixtures/mocha_proxy.fixt'; import { fixt_arrdom_nums, fixt_arrdom_range, fixt_arrdom_ranges, fixt_dom_ranges, stripAnonVarsFromArrays, } from '../fixtures/domain.fixt'; import { countSolutions, } from '../fixtures/lib'; import Solver from '../../src/solver'; import { LOG_NONE, LOG_STATS, LOG_SOLVES, LOG_MAX, LOG_MIN, SUB, SUP, } from '../../src/helpers'; describe('solver.spec', function() { this.timeout(60000); // takes long under istanbul / even longer under travis describe('api', function() { describe('solver constructor', function() { it('should exist', function() { expect(typeof Solver).to.be.equal('function'); }); it('should not require the options arg', function() { let solver = new Solver(); expect(solver).to.be.an('object'); expect(solver instanceof Solver); }); it('should accept a string for distribution options', function() { let solver = new Solver({distribute: 'naive'}); expect(solver).to.be.an('object'); expect(solver instanceof Solver); }); it('should throw for unknown distribute strings', function() { expect(_ => new Solver({distribute: 'fail'}).solve()).to.throw('distribution.get_defaults: Unknown preset: fail'); expect(_ => new Solver().solve({distribute: 'fail'})).to.throw('distribution.get_defaults: Unknown preset: fail'); }); it('should accept an object for distribution options', function() { let solver = new Solver({distribute: {}}); expect(solver).to.be.an('object'); expect(solver instanceof Solver); }); }); describe('solver.num', function() { it('num(false)', function() { let solver = new Solver(); expect(_ => solver.num(false)).to.throw('Solver#num: expecting a number, got false (a boolean)'); }); it('num(true)', function() { let solver = new Solver(); expect(_ => solver.num(true)).to.throw('Solver#num: expecting a number, got true (a boolean)'); }); it('num(0)', function() { let solver = new Solver(); let name = solver.num(0); expect(name).to.be.a('string'); }); it('num(10)', function() { let solver = new Solver(); let name = solver.num(10); expect(name).to.be.a('string'); }); it('should throw for undefined', function() { let solver = new Solver(); expect(_ => solver.num(undefined)).to.throw('Solver#num: expecting a number, got undefined (a undefined)'); }); it('should throw for null', function() { let solver = new Solver(); expect(_ => solver.num(null)).to.throw('Solver#num: expecting a number, got null (a object)'); }); it('should throw for NaN', function() { let solver = new Solver(); expect(_ => solver.num(NaN)).to.throw('Solver#num: expecting a number, got NaN'); }); }); describe('solver.decl', function() { it('should work', function() { let solver = new Solver(); expect(solver.decl('foo')).to.equal('foo'); }); it('should accept a flat array for domain', function() { let solver = new Solver(); solver.decl('foo', [0, 10, 20, 30]); // dont use fixtures because small domain expect(solver.config.initialDomains[solver.config.allVarNames.indexOf('foo')]).to.eql(fixt_dom_ranges([0, 10], [20, 30])); }); it('should no longer accept a legacy nested array for domain', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [[0, 10], [20, 30]])).to.throw('SHOULD_BE_GTE 0'); }); describe('legacy', function() { it('should throw for bad legacy domain ', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [[0]])).to.throw('SHOULD_CONTAIN_RANGES'); }); it('should throw for bad legacy domain with multiple ranges', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [[0], [20, 30]])).to.throw('SHOULD_BE_LTE 100000000'); }); }); it('should throw for domains with numbers <SUB', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [SUB - 2, SUB - 1])).to.throw('SHOULD_BE_GTE'); }); it('should throw for domains with numbers >SUP', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [SUP + 1, SUP + 2])).to.throw('SHOULD_BE_LTE'); }); it('should throw for domains with NaNs', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [0, NaN])).to.throw('SHOULD_BE_LTE'); let solver2 = new Solver(); expect(_ => solver2.decl('foo', [NaN, 1])).to.throw('SHOULD_BE_GTE'); let solver3 = new Solver(); expect(_ => solver3.decl('foo', [NaN, NaN])).to.throw('SHOULD_BE_GTE'); }); it('should throw for domains with inverted range', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [2, 1])).to.throw('NON_EMPTY_DOMAIN'); }); it('should throw for legacy domains with inverted range', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [[2, 1]])).to.throw('SHOULD_CONTAIN_RANGES'); }); it('should throw for domains with garbage', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [{}, {}])).to.throw('SHOULD_BE_GTE 0'); }); it('should throw for legacy domains with garbage', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [[{}]])).to.throw('SHOULD_CONTAIN_RANGES'); }); it('should throw for domains with one number', function() { let solver = new Solver(); expect(_ => solver.decl('foo', [1])).to.throw('SHOULD_CONTAIN_RANGES'); }); }); describe('solver.plus', function() { it('should work without result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver.plus('A', 'B')).to.be.a('string'); }); it('should work with a result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); expect(solver.plus('A', 'B', 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions', function() { let solver = new Solver(); solver.decl('B', 100); solver.decl('C', 100); expect(solver.plus(1, 'B', 'C')).to.equal('C'); let solver2 = new Solver(); solver2.decl('A', 100); solver2.decl('C', 100); expect(solver2.plus('A', 2, 'C')).to.equal('C'); let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(solver3.plus('A', 'B', 3)).to.be.a('string'); }); it('should throw for bad result name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(_ => solver3.plus(['A', 'B'], {})).to.throw('all var names should be strings or numbers or undefined'); }); it('should always return the result var name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); solver3.decl('C', 100); expect(solver3.plus('A', 'B')).to.be.a('string'); expect(solver3.plus(1, 'B')).to.be.a('string'); expect(solver3.plus('A', 1)).to.be.a('string'); expect(solver3.plus(1, 2)).to.be.a('string'); expect(solver3.plus('A', 'B', 'C')).to.eql('C'); expect(solver3.plus(1, 'B', 'C')).to.eql('C'); expect(solver3.plus('A', 2, 'C')).to.eql('C'); expect(solver3.plus(1, 2, 'C')).to.eql('C'); expect(solver3.plus('A', 'B', 3)).to.be.a('string'); expect(solver3.plus(1, 'B', 3)).to.be.a('string'); expect(solver3.plus('A', 2, 3)).to.be.a('string'); expect(solver3.plus(1, 2, 3)).to.be.a('string'); }); }); describe('solver.minus', function() { it('should work without result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver.minus('A', 'B')).to.be.a('string'); }); it('should work with a result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); expect(solver.minus('A', 'B', 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions', function() { let solver = new Solver(); solver.decl('B', 100); solver.decl('C', 100); expect(solver.minus(1, 'B', 'C')).to.equal('C'); let solver2 = new Solver(); solver2.decl('A', 100); solver2.decl('C', 100); expect(solver2.minus('A', 2, 'C')).to.equal('C'); let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(solver3.minus('A', 'B', 3)).to.be.a('string'); }); it('should throw for bad result name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(_ => solver3.minus(['A', 'B'], {})).to.throw('all var names should be strings or numbers or undefined'); }); it('should always return the result var name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); solver3.decl('C', 100); expect(solver3.minus('A', 'B')).to.be.a('string'); expect(solver3.minus(1, 'B')).to.be.a('string'); expect(solver3.minus('A', 1)).to.be.a('string'); expect(solver3.minus(1, 2)).to.be.a('string'); expect(solver3.minus('A', 'B', 'C')).to.eql('C'); expect(solver3.minus(1, 'B', 'C')).to.eql('C'); expect(solver3.minus('A', 2, 'C')).to.eql('C'); expect(solver3.minus(1, 2, 'C')).to.eql('C'); expect(solver3.minus('A', 'B', 3)).to.be.a('string'); expect(solver3.minus(1, 'B', 3)).to.be.a('string'); expect(solver3.minus('A', 2, 3)).to.be.a('string'); expect(solver3.minus(1, 2, 3)).to.be.a('string'); }); }); describe('solver.mul', function() { it('should work without result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver.mul('A', 'B')).to.be.a('string'); }); it('should work with a result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); expect(solver.mul('A', 'B', 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions', function() { let solver = new Solver(); solver.decl('B', 100); solver.decl('C', 100); expect(solver.mul(1, 'B', 'C')).to.equal('C'); let solver2 = new Solver(); solver2.decl('A', 100); solver2.decl('C', 100); expect(solver2.mul('A', 2, 'C')).to.equal('C'); let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(solver3.mul('A', 'B', 3)).to.be.a('string'); }); it('should throw for bad result name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(_ => solver3.mul(['A', 'B'], {})).to.throw('all var names should be strings or numbers or undefined'); }); it('should always return the result var name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); solver3.decl('C', 100); expect(solver3.mul('A', 'B')).to.be.a('string'); expect(solver3.mul(1, 'B')).to.be.a('string'); expect(solver3.mul('A', 1)).to.be.a('string'); expect(solver3.mul(1, 2)).to.be.a('string'); expect(solver3.mul('A', 'B', 'C')).to.eql('C'); expect(solver3.mul(1, 'B', 'C')).to.eql('C'); expect(solver3.mul('A', 2, 'C')).to.eql('C'); expect(solver3.mul(1, 2, 'C')).to.eql('C'); expect(solver3.mul('A', 'B', 3)).to.be.a('string'); expect(solver3.mul(1, 'B', 3)).to.be.a('string'); expect(solver3.mul('A', 2, 3)).to.be.a('string'); expect(solver3.mul(1, 2, 3)).to.be.a('string'); }); }); describe('solver.div', function() { it('should work without result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver.div('A', 'B')).to.be.a('string'); }); it('should work with a result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); expect(solver.div('A', 'B', 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions', function() { let solver = new Solver(); solver.decl('B', 100); solver.decl('C', 100); expect(solver.div(1, 'B', 'C')).to.equal('C'); let solver2 = new Solver(); solver2.decl('A', 100); solver2.decl('C', 100); expect(solver2.div('A', 2, 'C')).to.equal('C'); let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(solver3.div('A', 'B', 3)).to.be.a('string'); }); it('should throw for bad result name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(_ => solver3.div(['A', 'B'], {})).to.throw('all var names should be strings or numbers or undefined'); }); it('should always return the result var name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); solver3.decl('C', 100); expect(solver3.div('A', 'B')).to.be.a('string'); expect(solver3.div(1, 'B')).to.be.a('string'); expect(solver3.div('A', 1)).to.be.a('string'); expect(solver3.div(1, 2)).to.be.a('string'); expect(solver3.div('A', 'B', 'C')).to.eql('C'); expect(solver3.div(1, 'B', 'C')).to.eql('C'); expect(solver3.div('A', 2, 'C')).to.eql('C'); expect(solver3.div(1, 2, 'C')).to.eql('C'); expect(solver3.div('A', 'B', 3)).to.be.a('string'); expect(solver3.div(1, 'B', 3)).to.be.a('string'); expect(solver3.div('A', 2, 3)).to.be.a('string'); expect(solver3.div(1, 2, 3)).to.be.a('string'); }); }); describe('solver.product', function() { it('should work without result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver.product(['A', 'B'])).to.be.a('string'); }); it('should work with a result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); expect(solver.product(['A', 'B'], 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions', function() { let solver = new Solver(); solver.decl('B', 100); solver.decl('C', 100); expect(solver.product([1, 'B'], 'C')).to.equal('C'); let solver2 = new Solver(); solver2.decl('A', 100); solver2.decl('C', 100); expect(solver2.product(['A', 2], 'C')).to.equal('C'); let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(solver3.product(['A', 'B'], 3)).to.be.a('string'); }); it('should throw for bad result name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(_ => solver3.product(['A', 'B'], {})).to.throw('expecting result var name to be absent or a number or string:'); }); it('should always return the result var name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); solver3.decl('C', 100); expect(solver3.product(['A', 'B'])).to.be.a('string'); expect(solver3.product([1, 'B'])).to.be.a('string'); expect(solver3.product(['A', 1])).to.be.a('string'); expect(solver3.product([1, 2])).to.be.a('string'); expect(solver3.product(['A', 'B'], 'C')).to.eql('C'); expect(solver3.product([1, 'B'], 'C')).to.eql('C'); expect(solver3.product(['A', 2], 'C')).to.eql('C'); expect(solver3.product([1, 2], 'C')).to.eql('C'); expect(solver3.product(['A', 'B'], 3)).to.be.a('string'); expect(solver3.product([1, 'B'], 3)).to.be.a('string'); expect(solver3.product(['A', 2], 3)).to.be.a('string'); expect(solver3.product([1, 2], 3)).to.be.a('string'); }); }); describe('solver.sum', function() { it('should work without result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver.sum(['A', 'B'])).to.be.a('string'); }); it('should work with a result var', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); expect(solver.sum(['A', 'B'], 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions; 1', function() { let solver = new Solver(); solver.decl('B', 100); solver.decl('C', 100); expect(solver.sum([1, 'B'], 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions; 2', function() { let solver2 = new Solver(); solver2.decl('A', 100); solver2.decl('C', 100); expect(solver2.sum(['A', 2], 'C')).to.equal('C'); }); it('should accept numbers on either of the three positions; 3', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(solver3.sum(['A', 'B'], 3)).to.be.a('string'); }); it('should throw for bad result name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(_ => solver3.sum(['A', 'B'], {})).to.throw('expecting result var name to be absent or a number or string:'); }); it('should always return the result var name, regardless', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); solver3.decl('C', 100); expect(solver3.sum(['A', 'B'])).to.be.a('string'); expect(solver3.sum([1, 'B'])).to.be.a('string'); expect(solver3.sum(['A', 1])).to.be.a('string'); expect(solver3.sum([1, 2])).to.be.a('string'); expect(solver3.sum(['A', 'B'], 'C')).to.eql('C'); expect(solver3.sum([1, 'B'], 'C')).to.be.eql('C'); expect(solver3.sum(['A', 2], 'C')).to.be.eql('C'); expect(solver3.sum([1, 2], 'C')).to.eql('C'); expect(solver3.sum(['A', 'B'], 3)).to.be.a('string'); expect(solver3.sum([1, 'B'], 3)).to.be.a('string'); expect(solver3.sum(['A', 2], 3)).to.be.a('string'); expect(solver3.sum([1, 2], 3)).to.be.a('string'); }); }); describe('solver.distinct', function() { it('should work', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); solver.decl('D', 100); solver.decl('E', 100); expect(solver.distinct(['A', 'B', 'C', 'D'], 'E')).to.equal(undefined); }); it('accept zero vars', function() { let solver = new Solver(); expect(_ => solver.distinct([])).not.to.throw(); }); it('accept one var', function() { let solver = new Solver(); solver.decl('A', 100); expect(solver.distinct(['A'])).to.equal(undefined); }); it('accept two vars', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver.distinct(['A', 'B'])).to.equal(undefined); }); }); describe('solver comparison with .eq and .neq', function() { function alias(method) { it('should work', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver[method]('A', 'B')).to.equal(undefined); // returns v1 }); it('should work with a number left', function() { let solver = new Solver(); solver.decl('B', 100); expect(solver[method](1, 'B')).to.eql(undefined); }); it('should work with a number right', function() { let solver = new Solver(); solver.decl('A', 100); expect(solver[method]('A', 2)).to.eql(undefined); }); it('should work with an empty array left', function() { let solver = new Solver(); solver.decl('B', 100); expect(solver[method]([], 'B')).to.equal(undefined); // returns v2! }); it('should work with an empty array right', function() { let solver = new Solver(); solver.decl('A', 100); expect(solver[method]('A', [])).to.equal(undefined); // returns v2! }); it('should work with an empty array left and right', function() { let solver = new Solver(); expect(solver[method]([], [])).to.equal(undefined); // returns v2! }); it('should work with an array of one element left', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver[method](['A'], 'B')).to.equal(undefined); }); it('should work with an array of one element right', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver[method]('A', ['B'])).to.equal(undefined); }); it('should work with an array of multiple elements left', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); solver.decl('D', 100); expect(solver[method](['A', 'C', 'D'], 'B')).to.equal(undefined); }); it('should work with an array of multiple elements right', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); solver.decl('D', 100); expect(solver[method]('B', ['A', 'C', 'D'])).to.equal(undefined); }); it('should work with an array of multiple elements on both sides', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); solver.decl('D', 100); expect(solver[method](['A', 'B', 'C', 'D'], ['A', 'B', 'C', 'D'])).to.equal(undefined); }); } alias('eq'); alias('neq'); }); describe('solver relative comparisons', function() { function alias(method) { describe('method [' + method + ']', function() { it('should work', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver[method]('A', 'B')).to.equal(undefined); }); it('should work with a number left', function() { let solver = new Solver(); solver.decl('B', 100); expect(solver[method](1, 'B')).to.equal(undefined); // if we change anonymous var naming, this'll break }); it('should work with a number right', function() { let solver = new Solver(); solver.decl('A', 100); expect(solver[method]('A', 2)).to.equal(undefined); // if we change anonymous var naming, this'll break }); it('should not work with an empty array', function() { let solver = new Solver(); solver.decl('B', 100); expect(_ => solver[method]([], 'B')).to.throw('NOT_ACCEPTING_ARRAYS'); }); it('should work with an array of one element', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(_ => solver[method](['A'], 'B')).to.throw('NOT_ACCEPTING_ARRAYS'); }); it('should work with an array of multiple elements', function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); solver.decl('D', 100); expect(_ => solver[method](['A', 'C', 'D'], 'B')).to.throw('NOT_ACCEPTING_ARRAYS'); }); }); } alias('gte'); alias('gt'); alias('lte'); alias('lt'); }); describe('solver reifiers', function() { function alias(method) { describe('method = ' + method, function() { it('should work:' + method, function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); expect(solver[method]('A', 'B')).to.be.a('string'); }); it('should work with a number left: ' + method, function() { let solver = new Solver(); solver.decl('B', 100); expect(solver[method](1, 'B')).to.be.a('string'); }); it('should work with a number right: ' + method, function() { let solver = new Solver(); solver.decl('A', 100); expect(solver[method]('A', 2)).to.be.a('string'); }); it('should accept a result name: ' + method, function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 100); solver.decl('C', 100); expect(solver[method]('A', 'B', 'C')).to.equal('C'); }); it('should accept a result number: ' + method, function() { let solver = new Solver(); solver.decl('A', 100); solver.decl('B', 1); expect(solver[method]('A', 'B', 1)).to.be.a('string'); }); it('should throw for bad result name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); expect(_ => solver3[method](['A', 'B'], {})).to.throw('all var names should be strings or numbers or undefined'); }); it('should always return the result var name', function() { let solver3 = new Solver(); solver3.decl('A', 100); solver3.decl('B', 100); solver3.decl('C', 100); expect(solver3[method]('A', 'B')).to.be.a('string'); expect(solver3[method](1, 'B')).to.be.a('string'); expect(solver3[method]('A', 1)).to.be.a('string'); expect(solver3[method](1, 2)).to.be.a('string'); expect(solver3[method]('A', 'B', 'C')).to.eql('C'); expect(solver3[method](1, 'B', 'C')).to.eql('C'); expect(solver3[method]('A', 2, 'C')).to.eql('C'); expect(solver3[method](1, 2, 'C')).to.eql('C'); expect(solver3[method]('A', 'B', 3)).to.be.a('string'); expect(solver3[method](1, 'B', 3)).to.be.a('string'); expect(solver3[method]('A', 2, 3)).to.be.a('string'); expect(solver3[method](1, 2, 3)).to.be.a('string'); }); }); } alias('isNeq'); alias('isEq'); alias('isGt'); alias('isGte'); alias('isLt'); alias('isLte'); }); describe('solver.solve', function() { it('should solve a trivial case when targeted', function() { let solver = new Solver({}); solver.declRange('A', 1, 2); expect(solver.solve({vars: ['A']})).to.eql([{A: 1}, {A: 2}]); }); it('should solve a trivial case when not targeted', function() { let solver = new Solver({}); solver.declRange('A', 1, 2); expect(solver.solve()).to.eql([{A: [1, 2]}]); }); function forLevel(level) { it('should accept all log levels (' + level + ')', function() { let solver = new Solver(); expect(solver.solve({log: level})).to.eql([{}]); }); it('should accept all dbg levels (' + level + ')', function() { let solver = new Solver(); expect(solver.solve({dbg: level})).to.eql([{}]); }); } forLevel(undefined); forLevel(null); forLevel(false); forLevel(true); forLevel(LOG_NONE); forLevel(LOG_STATS); forLevel(LOG_SOLVES); forLevel(LOG_MAX); forLevel(LOG_MIN); }); describe('solver._prepare', function() { it('should prepare for war', function() { let solver = new Solver(); solver._prepare({}); expect(true).to.equal(true); }); it('should not require options object', function() { let solver = new Solver(); solver._prepare({}); expect(true).to.equal(true); }); }); describe('Solver.domainFromList', function() { it('should map to domainFromList', function() { expect(Solver.domainFromList([1, 2, 4, 5, 7, 9, 10, 11, 12, 13, 15, 118])).to.eql(fixt_arrdom_ranges([1, 2], [4, 5], [7, 7], [9, 13], [15, 15], [118, 118])); }); it('should always return an array even for small domains', function() { expect(Solver.domainFromList([1, 2, 4, 5, 7, 9, 10, 11, 12, 13, 15])).to.eql([1, 2, 4, 5, 7, 7, 9, 13, 15, 15]); }); }); }); describe('API integration tests', function() { it('4 branch 2 level example w/ string vars (binary)', function() { /* A 1 2 - B 3 1 2 3 C 1 2 - D 3 1 2 3 */ let solver = new Solver(); // branch vars solver.decls(['A', 'C', 'B', 'D']); // path vars let Avars = ['A1', 'A2', 'A3']; let Bvars = ['B1', 'B2', 'B3']; let Cvars = ['C1', 'C2', 'C3']; let Dvars = ['D1', 'D2', 'D3']; solver.decls([].concat(Avars, Bvars, Cvars, Dvars), fixt_arrdom_range(0, 1)); // path to branch binding solver.sum(Avars, 'A'); solver.sum(Bvars, 'B'); solver.sum(Cvars, 'C'); solver.sum(Dvars, 'D'); // root branches must be on solver.eq('A', 1); solver.eq('C', 1); // child-parent binding solver.eq('B', 'A2'); solver.eq('D', 'C2'); // D & B counterpoint solver.isEq('B', 'D', solver.declRange('BsyncD', 0, 1)); let BD1 = solver.isEq('B1', 'D1'); solver.gte(BD1, 'BsyncD'); let BD2 = solver.isEq('B2', 'D2'); solver.gte(BD2, 'BsyncD'); let BD3 = solver.isEq('B3', 'D3'); solver.gte(BD3, 'BsyncD'); solver.solve(); expect(countSolutions(solver)).to.equal(19); }); it('4 branch 2 level example w/ var objs (binary)', function() { /* A 1 2 - B 3 1 2 3 C 1 2 - D 3 1 2 3 */ let solver = new Solver(); let branches = { A: 3, B: 3, C: 3, D: 3, }; let pathCount = 3; for (let branchId in branches) { solver.decl(branchId, fixt_arrdom_range(0, 1)); let pathVars = []; for (let i = 1; i <= pathCount; ++i) { pathVars.push(branchId + i); } solver.decls(pathVars); // path to branch binding solver.sum(pathVars, branchId); } // root branches must be on solver.eq('A', 1); solver.eq('C', 1); // child-parent binding solver.eq('B', 'A2'); solver.eq('D', 'C2'); // D & B counterpoint //S.isEq 'B', 'D', S.decl('BsyncD') let BD = solver.isEq('B', 'D'); solver.lte(BD, solver.isEq('B1', 'D1')); solver.lte(BD, solver.isEq('B2', 'D2')); solver.lte(BD, solver.isEq('B3', 'D3')); solver.solve(); expect(countSolutions(solver), 'solution count').to.equal(19); }); it('4 branch 2 level example w/ var objs (non-binary)', function() { /* A 1 2 - B 3 1 2 3 C 1 2 - D 3 1 2 3 */ let solver = new Solver(); solver.declRange('A', 0, 3); solver.declRange('B', 0, 3); solver.declRange('C', 0, 3); solver.declRange('D', 0, 3); // root branches must be on solver.gte('A', 1); solver.gte('C', 1); // child-parent binding let A = solver.isEq('A', 2); let B = solver.isGt('B', 0); solver.eq(A, B); let C = solver.isEq('C', 2); let D = solver.isGt('D', 0); solver.eq(C, D); // Synchronize D & B if possible // if B > 0 and D > 0, then B == D solver.gte( solver.isEq('B', 'D'), solver.isEq( solver.isGt('B', 0), solver.isGt('D', 0) ) ); solver.solve(); expect(countSolutions(solver)).to.equal(19); }); }); describe('plain tests', function() { it('should solve a sparse domain', function() { let solver = new Solver({}); solver.decl('item1', fixt_arrdom_range(1, 5)); solver.decl('item2', [2, 2, 4, 5]); // TODO: restore to specDomainCreateRanges([2, 2], [4, 5])); solver.decl('item3', fixt_arrdom_range(1, 5)); solver.decl('item4', fixt_arrdom_range(4, 4)); solver.decl('item5', fixt_arrdom_range(1, 5)); solver.lt('item1', 'item2'); solver.lt('item2', 'item3'); solver.lt('item3', 'item4'); solver.lt('item4', 'item5'); let solutions = solver.solve(); expect(countSolutions(solver)).to.equal(1); expect(solutions[0].item1, 'item1').to.equal(1); expect(solutions[0].item2, 'item2').to.equal(2); }); it('should reject a simple > test (regression)', function() { // regression: x>y was wrongfully mapped to y<=x let solver = new Solver({}); solver.decl('item5', fixt_arrdom_range(1, 5)); solver.decl('item4', [2, 2, 3, 5]); // TODO: restore to specDomainCreateRanges([2, 2], [3, 5])); solver.decl('item3', fixt_arrdom_range(1, 5)); solver.decl('item2', fixt_arrdom_range(4, 4)); solver.decl('item1', fixt_arrdom_range(1, 5)); solver.eq('item5', 5); solver.gt('item1', 'item2'); solver.gt('item2', 'item3'); solver.gt('item3', 'item4'); solver.gt('item4', 'item5'); // there is no solution since item 5 must be 5 and item 2 must be 4 solver.solve(); expect(countSolutions(solver), 'solution count').to.equal(0); }); it('should solve a simple >= test', function() { let solver = new Solver({}); solver.decl('item5', fixt_arrdom_range(1, 5)); solver.decl('item4', fixt_arrdom_nums(2, 3, 5)); solver.decl('item3', fixt_arrdom_range(1, 5)); solver.decl('item2', fixt_arrdom_range(4, 5)); solver.decl('item1', fixt_arrdom_range(1, 5)); solver.eq('item5', 5); solver.gte('item1', 'item2'); solver.gte('item2', 'item3'); solver.gte('item3', 'item4'); solver.gte('item4', 'item5'); solver.solve({}); // only solution is where everything is `5` expect(countSolutions(solver)).to.equal(1); }); it('should solve a simple < test', function() { let solver = new Solver({}); solver.decl('item5', fixt_arrdom_range(1, 5)); solver.decl('item4', fixt_arrdom_range(4, 4)); solver.decl('item3', fixt_arrdom_range(1, 5)); solver.decl('item2', fixt_arrdom_nums(2, 3, 5)); solver.decl('item1', fixt_arrdom_range(1, 5)); solver.eq('item5', 5); solver.lt('item1', 'item2'); solver.lt('item2', 'item3'); solver.lt('item3', 'item4'); solver.lt('item4', 'item5'); solver.solve(); // only solution is where each var is prev+1, 1 2 3 4 5 expect(countSolutions(solver)).to.equal(1); }); it('should solve a simple / test', function() { let solver = new Solver({}); solver.declRange('A', 50, 100); solver.declRange('B', 5, 10); solver.declRange('C', 0, 100); solver.div('A', 'B', 'C'); solver.eq('C', 15); let solutions = solver.solve(); // there are two integer solutions (75/5 and 90/6) and // 9 fractional solutions whose floor result in 15 expect(countSolutions(solver)).to.equal(11); // there are two cases where A/B=15 with input ranges: expect(stripAnonVarsFromArrays(solutions)).to.eql([{ A: 75, B: 5, C: 15, }, { A: 76, B: 5, C: 15, // floored }, { A: 77, B: 5, C: 15, // floored }, { A: 78, B: 5, C: 15, // floored }, { A: 79, B: 5, C: 15, // floored }, { A: 90, B: 6, C: 15, }, { A: 91, B: 6, C: 15, // floored }, { A: 92, B: 6, C: 15, // floored }, { A: 93, B: 6, C: 15, // floored }, { A: 94, B: 6, C: 15, // floored }, { A: 95, B: 6, C: 15, // floored }]); }); it('should solve another simple / test', function() { let solver = new Solver({}); solver.declRange('A', 3, 5); solver.declRange('B', 2, 2); solver.declRange('C', 0, 100); solver.div('A', 'B', 'C'); solver.eq('C', 2); let solutions = solver.solve(); // expecting two solutions; one integer division and one floored fractional division expect(countSolutions(solver)).to.equal(2); // there is only one case where 3~5 / 2 equals 2 and that is when A is 4. // but when flooring results, 5/2=2.5 -> 2, so there are two answers expect(stripAnonVarsFromArrays(solutions)).to.eql([{ A: 4, B: 2, C: 2, }, { A: 5, B: 2, C: 2, // floored }]); }); it('should solve a simple * test', function() { let solver = new Solver({}); solver.declRange('A', 3, 8); solver.declRange('B', 2, 10); solver.declRange('C', 0, 100); solver.mul('A', 'B', 'C'); solver.eq('C', 30); let solutions = solver.solve(); expect(countSolutions(solver)).to.equal(3); // 3*10=30 // 5*6=30 // 6*5=30 expect(stripAnonVarsFromArrays(solutions)).to.eql([{ A: 3, B: 10, C: 30, }, { A: 5, B: 6, C: 30, }, { A: 6, B: 5, C: 30, }]); }); it('should solve a simple - test', function() { let solver = new Solver({}); solver.decl('A', 400); solver.decl('B', 50); solver.decl('C', fixt_arrdom_range(0, 10000)); solver.minus('A', 'B', 'C'); let solutions = solver.solve(); expect(solutions).to.eql([{ A: 400, B: 50, C: 350, }]); }); it('should not skip over when a var only has one propagator and is affected', function() { // this is more thoroughly tested with space unit tests let solver = new Solver({}); solver.decl('A', fixt_arrdom_range(0, 1)); solver.decl('B', fixt_arrdom_range(0, 1)); solver.neq('A', 'B'); solver.solve(); expect(countSolutions(solver)).to.eql(2); // 0 1, and 1 0 }); }); describe('targeting vars', function() { it('should want to solve all vars if targets are not set at all', function() { let solver = new Solver(); solver.declRange('A', 0, 1); solver.declRange('B', 0, 1); solver.declRange('C', 0, 1); solver.isEq('A', 'B', solver.decl('AnotB', [0, 1])); let solutions = solver.solve({}); // a, b, c are not constrained in any way, so 2^3=8 // no var is targeted so they should all solve // however, the constraint will force A and B to solve // to a single value, where C is left as "any" expect(countSolutions(solver)).to.equal(8); expect(solutions).to.eql([ {A: 0, B: 0, C: [0, 1], AnotB: 1}, {A: 0, B: 1, C: [0, 1], AnotB: 0}, {A: 1, B: 0, C: [0, 1], AnotB: 0}, {A: 1, B: 1, C: [0, 1], AnotB: 1}, ]); }); it('should throw if explicitly targeting no vars', function() { let solver = new Solver(); solver.declRange('A', 0, 1); solver.declRange('B', 0, 1); solver.declRange('C', 0, 1); solver.isEq('A', 'B', solver.decl('AnotB')); expect(_ => solver.solve({vars: []})).to.throw('ONLY_USE_WITH_SOME_TARGET_VARS'); }); it('should ignore C when only A and B are targeted', function() { let solver = new Solver(); solver.declRange('A', 0, 1); solver.declRange('B', 0, 1); solver.declRange('C', 0, 1); solver.isEq('A', 'B', solver.decl('AnotB', [0, 1])); let solutions = solver.solve({vars: ['A', 'B']}); // A and B are targeted, they have [0,1] [0,1] so // 4 solutions. the result of C is irrelevant here // so that's x2=8 (pretty much same as before) expect(countSolutions(solver)).to.equal(8); expect(solutions).to.eql([ {A: 0, B: 0, C: [0, 1], AnotB: 1}, {A: 0, B: 1, C: [0, 1], AnotB: 0}, {A: 1, B: 0, C: [0, 1], AnotB: 0}, {A: 1, B: 1, C: [0, 1], AnotB: 1}, ]); }); it('should ignore A when only B and C are targeted', function() { let solver = new Solver(); solver.declRange('A', 0, 1); solver.declRange('B', 0, 1); solver.declRange('C', 0, 1); solver.isEq('A', 'B'); solver.solve({vars: ['B', 'C']}); // B and C are targeted, they have [0,1] [0,1] so // 4 solutions. the result of A is irrelevant so x2 // and here the reifier is not a constraint on its // so that's another x2 total(l)ing 16. expect(countSolutions(solver)).to.equal(16); }); it('should not solve anonymous vars if no targets given', function() { let solver = new Solver(); solver.declRange('A', 0, 1); solver.declRange('B', 0, 1); solver.isEq('A', 'B'); solver.solve({}); // internally there will be three vars; A B and the reifier result var // make sure we don't accidentally require to solve that one too expect(countSolutions(solver)).to.equal(4); }); it('should be capable of solving an anonymous var', function() { let solver = new Solver(); solver.declRange('A', 0, 1); solver.declRange('B', 0, 1); let anon = solver.isEq('A', 'B'); solver.solve({vars: [anon]}); // the anonymous var will be boolean. since we only target // that var, there ought to be two solutions (0 and 1) for // it and any for the others, 2x2x2=8 expect(countSolutions(solver)).to.equal(8); }); }); describe('targeting values', function() { it('should support a function comparator', function() { let solver = new Solver(); solver.decl('a', fixt_arrdom_range(0, 100)); solver.decl('b', fixt_arrdom_range(0, 100)); solver.neq('a', 'b'); let called = false; solver.solve({max: 1, distribute: {valueStrategy: function(space, varIndex, choiceIndex) { called = true; expect(space._class).to.eql('$space'); expect(varIndex).to.be.a('number'); expect(choiceIndex).to.be.a('number'); }}}); expect(called, 'the callback should be called at least once').to.eql(true); }); }); describe('brute force entire space', function() { it('should solve a single unconstrainted var', function() { let solver = new Solver({}); solver.declRange('A', 1, 2); solver.solve(); // A solves to 1 or 2 expect(countSolutions(solver)).to.eql(2); }); it('should combine multiple unconstrained vars when targeted', function() { let solver = new Solver({}); solver.declRange('_ROOT_BRANCH_', 0, 1); solver.decl('SECTION', 1); solver.decl('VERSE_INDEX', fixt_arrdom_nums(2, 4, 9)); solver.declRange('ITEM_INDEX', 1, 2); solver.declRange('align', 1, 2); solver.declRange('text_align', 1, 2); solver.decl('SECTION&n=1', 1); solver.decl('VERSE_INDEX&n=1', fixt_arrdom_nums(5, 6, 8)); solver.decl('ITEM_INDEX&n=1', 2); solver.declRange('align&n=1', 1, 2); solver.declRange('text_align&n=1', 1, 2); solver.decl('SECTION&n=2', 1); solver.decl('VERSE_INDEX&n=2', fixt_arrdom_nums(1, 3, 7)); solver.decl('ITEM_INDEX&n=2', 3); solver.declRange('align&n=2', 1, 2); solver.declRange('text_align&n=2', 1, 2); solver.solve({max: 10000, vars: solver.config.allVarNames.slice(0)}); // 2×3×2×2×2×3×2×2×3×2×2 (size of each domain multiplied) // there are no constraints so it's just all combinations expect(countSolutions(solver)).to.eql(6912); }); it('should return all domains as is when not targeted', function() { let solver = new Solver({}); solver.declRange('_ROOT_BRANCH_', 0, 1); solver.decl('SECTION', 1); solver.decl('VERSE_INDEX', fixt_arrdom_nums(2, 4, 9)); solver.declRange('ITEM_INDEX', 1, 2); solver.declRange('align', 1, 2); solver.declRange('text_align', 1, 2); solver.decl('SECTION&n=1', 1); solver.decl('VERSE_INDEX&n=1', fixt_arrdom_nums(5, 6, 8)); solver.decl('ITEM_INDEX&n=1', 2); solver.declRange('align&n=1', 1, 2); solver.declRange('text_align&n=1', 1, 2); solver.decl('SECTION&n=2', 1); solver.decl('VERSE_INDEX&n=2', fixt_arrdom_nums(1, 3, 7)); solver.decl('ITEM_INDEX&n=2', 3); solver.declRange('align&n=2', 1, 2); solver.declRange('text_align&n=2', 1, 2); solver.solve({max: 10000}); // same as before but none are targeted so algo considers them // "solved" and returns all valid values (=init) so it becomes // a multiplication of all the number of options... // 2x1x3x2x2x2x1x3x1x2x2x1x3x1x2x2=6912 expect(countSolutions(solver)).to.eql(6912); }); it('should constrain one var to be equal to another', function() { let solver = new Solver({}); solver.declRange('_ROOT_BRANCH_', 0, 1); solver.decl('SECTION', 1); solver.decl('VERSE_INDEX', fixt_arrdom_nums(2, 4, 9)); solver.declRange('ITEM_INDEX', 1, 2); solver.declRange('align', 1, 2); solver.declRange('text_align', 1, 2); solver.decl('SECTION&n=1', 1); solver.decl('VERSE_INDEX&n=1', fixt_arrdom_nums(5, 6, 8)); solver.decl('ITEM_INDEX&n=1', 2); solver.declRange('align&n=1', 1, 2); solver.declRange('text_align&n=1', 1, 2); solver.decl('SECTION&n=2', 1); solver.decl('VERSE_INDEX&n=2', fixt_arrdom_nums(1, 3, 7)); solver.decl('ITEM_INDEX&n=2', 3); solver.declRange('align&n=2', 1, 2); solver.declRange('text_align&n=2', 1, 2); solver.eq('_ROOT_BRANCH_', 'SECTION'); solver.solve({max: 10000, vars: solver.config.allVarNames.slice(0)}); // same as 'combine multiple unconstrained vars' but one var has one instead of two options, so /2 // note: must target all vars explicitly or you'll validly get just one solution back. expect(countSolutions(solver)).to.eql(6912 / 2); }); it('should allow useless constraints', function() { let solver = new Solver({}); solver.decl('x2', 1); solver.declRange('_ROOT_BRANCH_', 0, 1); // becomes 1 solver.decl('SECTION', 1); solver.decl('VERSE_INDEX', fixt_arrdom_nums(2, 4, 9)); solver.declRange('ITEM_INDEX', 1, 2); // becomes 2 solver.declRange('align', 1, 2); solver.declRange('text_align', 1, 2); solver.decl('SECTION&n=1', 1); solver.decl('VERSE_INDEX&n=1', fixt_arrdom_nums(5, 6, 8)); solver.decl('ITEM_INDEX&n=1', 2); solver.declRange('align&n=1', 1, 2); solver.declRange('text_align&n=1', 1, 2); solver.decl('SECTION&n=2', 1); solver.decl('VERSE_INDEX&n=2', fixt_arrdom_nums(1, 3, 7)); solver.decl('ITEM_INDEX&n=2', 3); solver.declRange('align&n=2', 1, 2); solver.declRange('text_align&n=2', 1, 2); solver.eq('_ROOT_BRANCH_', 'SECTION'); // root branch can only be 1 because section only has 1 // these are meaningless since 'x2' is [0,1] and all the rhs have no zeroes solver.lte('x2', 'SECTION'); solver.lte('x2', 'VERSE_INDEX'); solver.lte('x2', 'ITEM_INDEX'); solver.lte('x2', 'align'); solver.lte('x2', 'text_align'); solver.lte('x2', 'SECTION&n=1'); solver.lte('x2', 'VERSE_INDEX&n