finitedomain
Version:
A fast feature rich finite domain solver
1,564 lines (1,261 loc) • 81.4 kB
JavaScript
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