finitedomain
Version:
A fast feature rich finite domain solver
337 lines (282 loc) • 11.4 kB
JavaScript
// This file only concerns itself with subtracting two domains
// The algorithm is conceptually simple but the support
// for both array and numbered domains makes it a little
// bloated. However, since it saves significant we do it
// anyways.
// Conceptually: range1-range2 = [max(0, lo1-hi2), max(0, hi1-lo2)]
// [5, 10] - [20, 30]
// [5-30, 30-5] -> [-25, 25] --> [0, 25]
// optimization shortcut: if both domains contain a zero the result
// is [0, max(domain1)] because we drop negative numbers;
// [lo1 - hi2, hi1 - lo2] -> [0 - hi2, hi1 - 0] -> [0, hi1]
// a big table of all input/output for small domains can be found at
// https://gist.github.com/qfox/fce6912ef17503b1055aac28fa34e8d1 (view
// with text editor that doesn't wrap). Spoiler: it doesn't help :)
import {
SMALL_MAX_NUM,
SOLVED_FLAG,
SUB,
ASSERT,
ASSERT_NUMDOM,
ASSERT_STRDOM,
ASSERT_NORDOM,
} from '../helpers';
import {
EMPTY,
EMPTY_STR,
STR_VALUE_SIZE,
STR_RANGE_SIZE,
domain_createRange,
domain_num_createRange,
domain_numnum_createRangeZeroToMax,
domain_createValue,
domain_str_closeGaps,
domain_num_containsValue,
domain_str_decodeValue,
domain_str_encodeRange,
domain_max,
domain_min,
domain_str_simplify,
domain_toSmallest,
} from '../domain';
let MAX = Math.max;
// BODY_START
/**
* Subtract one domain from the other
*
* @param {$domain} domain1
* @param {$domain} domain2
* @returns {$domain}
*/
function domain_minus(domain1, domain2) {
ASSERT_NORDOM(domain1);
ASSERT_NORDOM(domain2);
// note: this is not x-0=x. this is nothing-something=nothing because the domains contain no value
if (!domain1) return EMPTY;
if (!domain2) return EMPTY;
// optimize an easy path: if both domains contain zero the
// result will always be [0, max(domain1)], because:
// d1-d2 = [lo1-hi2, hi1-lo2] -> [0-hi2, hi1-0] -> [0, hi1]
if (domain_min(domain1) === 0 && domain_min(domain2) === 0) {
return domain_createRange(0, domain_max(domain1));
}
let isNum1 = typeof domain1 === 'number';
let isNum2 = typeof domain2 === 'number';
if (isNum1) {
// note: if domain1 is a small domain the result is always a small domain
if (isNum2) return _domain_minusNumNum(domain1, domain2);
return _domain_minusNumStr(domain1, domain2);
}
let result;
if (isNum2) result = _domain_minusStrNumStr(domain1, domain2); // cannot swap minus args!
else result = _domain_minusStrStrStr(domain1, domain2);
return domain_toSmallest(domain_str_simplify(result));
}
function _domain_minusStrStrStr(domain1, domain2) {
ASSERT_STRDOM(domain1);
ASSERT_STRDOM(domain2);
// Simplify the domains by closing gaps since when we add
// the domains, the gaps will close according to the
// smallest interval width in the other domain.
let domains = domain_str_closeGaps(domain1, domain2);
domain1 = domains[0];
domain2 = domains[1];
ASSERT(typeof domain1 === 'string', 'make sure closeGaps doesnt "optimize"');
ASSERT(typeof domain2 === 'string', 'make sure closeGaps doesnt "optimize"');
let newDomain = EMPTY_STR;
for (let index = 0, len = domain1.length; index < len; index += STR_RANGE_SIZE) {
let lo = domain_str_decodeValue(domain1, index);
let hi = domain_str_decodeValue(domain1, index + STR_VALUE_SIZE);
newDomain += _domain_minusRangeStrStr(lo, hi, domain2);
}
return newDomain;
}
function _domain_minusNumNum(domain1, domain2) {
if (domain1 >= SOLVED_FLAG) {
let solvedValue = domain1 ^ SOLVED_FLAG;
if (domain2 >= SOLVED_FLAG) {
let result = solvedValue - (domain2 ^ SOLVED_FLAG);
if (result < 0) return EMPTY;
return domain_createValue(result);
}
if (solvedValue <= SMALL_MAX_NUM) return _domain_minusRangeNumNum(solvedValue, solvedValue, domain2);
else return _domain_minusRangeNumStr(solvedValue, solvedValue, domain2);
}
return _domain_minusNumNumNum(domain1, domain2);
}
function _domain_minusNumNumNum(domain1, domain2) {
ASSERT_NUMDOM(domain1);
ASSERT_NUMDOM(domain2);
ASSERT(domain1 !== EMPTY && domain2 !== EMPTY, 'SHOULD_BE_CHECKED_ELSEWHERE');
ASSERT(domain_max(domain1) - domain_min(domain2) <= SMALL_MAX_NUM, 'MAX-MIN_MUST_NOT_EXCEED_NUMDOM_RANGE');
ASSERT(domain1 < SOLVED_FLAG, 'solved domain1 is expected to be caught elsewhere');
if (domain_num_containsValue(domain1, 0) && domain_num_containsValue(domain2, 0)) return domain_numnum_createRangeZeroToMax(domain1);
let flagIndex = 0;
// find the first set bit. must find something because small domain and not empty
while ((domain1 & (1 << flagIndex)) === 0) ++flagIndex;
let lo = flagIndex;
let hi = flagIndex;
let flagValue = 1 << ++flagIndex;
let newDomain = EMPTY;
while (flagValue <= domain1 && flagIndex <= SMALL_MAX_NUM) {
if ((flagValue & domain1) > 0) {
if (hi !== flagIndex - 1) { // there's a gap so push prev range now
newDomain |= _domain_minusRangeNumNum(lo, hi, domain2);
lo = flagIndex;
}
hi = flagIndex;
}
flagValue = 1 << ++flagIndex;
}
return newDomain | _domain_minusRangeNumNum(lo, hi, domain2);
}
function _domain_minusNumStr(domain_num, domain_str) {
if (domain_num >= SOLVED_FLAG) {
let solvedValue = domain_num ^ SOLVED_FLAG;
if (solvedValue <= SMALL_MAX_NUM) return _domain_minusRangeStrNum(solvedValue, solvedValue, domain_str);
else return _domain_minusRangeStrStr(solvedValue, solvedValue, domain_str);
}
return _domain_minusNumStrNum(domain_num, domain_str);
}
function _domain_minusNumStrNum(domain_num, domain_str) {
ASSERT_NUMDOM(domain_num);
ASSERT_STRDOM(domain_str);
ASSERT(domain_num !== EMPTY && domain_str !== EMPTY, 'SHOULD_BE_CHECKED_ELSEWHERE');
ASSERT(domain_max(domain_num) - domain_min(domain_str) <= SMALL_MAX_NUM, 'MAX-MIN_MUST_NOT_EXCEED_NUMDOM_RANGE');
if (domain_num >= SOLVED_FLAG) {
let solvedValue = domain_num ^ SOLVED_FLAG;
return _domain_minusRangeStrNum(solvedValue, solvedValue, domain_str);
}
// since any number above the small domain max ends up with negative, which is truncated, use the max of domain1
if (domain_num_containsValue(domain_num, 0) && domain_min(domain_str) === 0) return domain_numnum_createRangeZeroToMax(domain_num);
let flagIndex = 0;
// find the first set bit. must find something because small domain and not empty
while ((domain_num & (1 << flagIndex)) === 0) ++flagIndex;
let lo = flagIndex;
let hi = flagIndex;
let flagValue = 1 << ++flagIndex;
let newDomain = EMPTY;
while (flagValue <= domain_num && flagIndex <= SMALL_MAX_NUM) {
if ((flagValue & domain_num) > 0) {
if (hi !== flagIndex - 1) { // there's a gap so push prev range now
newDomain |= _domain_minusRangeStrNum(lo, hi, domain_str);
lo = flagIndex;
}
hi = flagIndex;
}
flagValue = 1 << ++flagIndex;
}
return newDomain | _domain_minusRangeStrNum(lo, hi, domain_str);
}
function _domain_minusRangeNumNum(loi, hii, domain_num) {
ASSERT_NUMDOM(domain_num);
ASSERT(domain_num !== EMPTY, 'SHOULD_BE_CHECKED_ELSEWHERE');
if (domain_num >= SOLVED_FLAG) {
let solvedValue = domain_num ^ SOLVED_FLAG;
return _domain_minusRangeRangeNum(loi, hii, solvedValue, solvedValue);
}
let flagIndex = 0;
// find the first set bit. must find something because small domain and not empty
while ((domain_num & (1 << flagIndex)) === 0) ++flagIndex;
let lo = flagIndex;
let hi = flagIndex;
let flagValue = 1 << ++flagIndex;
let newDomain = EMPTY;
while (flagValue <= domain_num && flagIndex <= SMALL_MAX_NUM) {
if ((flagValue & domain_num) > 0) {
if (hi !== flagIndex - 1) { // there's a gap so push prev range now
newDomain |= _domain_minusRangeRangeNum(loi, hii, lo, hi);
lo = flagIndex;
}
hi = flagIndex;
}
flagValue = 1 << ++flagIndex;
}
return newDomain | _domain_minusRangeRangeNum(loi, hii, lo, hi);
}
function _domain_minusStrNumStr(domain_str, domain_num) {
ASSERT_NUMDOM(domain_num);
ASSERT_STRDOM(domain_str);
ASSERT(domain_num !== EMPTY && domain_str !== EMPTY, 'SHOULD_BE_CHECKED_ELSEWHERE');
// optimize an easy path: if both domains contain zero the
// result will always be [0, max(domain1)], because:
// d1-d2 = [lo1-hi2, hi1-lo2] -> [0-hi2, hi1-0] -> [0, hi1]
if (domain_min(domain_str) === 0 && domain_min(domain_num) === 0) {
return domain_createRange(0, domain_max(domain_str));
}
let newDomain = EMPTY_STR;
for (let index = 0, len = domain_str.length; index < len; index += STR_RANGE_SIZE) {
let lo = domain_str_decodeValue(domain_str, index);
let hi = domain_str_decodeValue(domain_str, index + STR_VALUE_SIZE);
newDomain += _domain_minusRangeNumStr(lo, hi, domain_num);
}
return newDomain;
}
function _domain_minusRangeNumStr(loi, hii, domain_num) {
ASSERT_NUMDOM(domain_num);
if (domain_num === EMPTY) return EMPTY;
if (domain_num >= SOLVED_FLAG) {
let solvedValue = domain_num ^ SOLVED_FLAG;
return _domain_minusRangeRangeStr(loi, hii, solvedValue, solvedValue);
}
let flagIndex = 0;
// find the first set bit. must find something because small domain and not empty
while ((domain_num & (1 << flagIndex)) === 0) ++flagIndex;
let lo = flagIndex;
let hi = flagIndex;
let flagValue = 1 << ++flagIndex;
let newDomain = EMPTY_STR;
while (flagValue <= domain_num && flagIndex <= SMALL_MAX_NUM) {
if ((flagValue & domain_num) > 0) {
if (hi !== flagIndex - 1) { // there's a gap so push prev range now
newDomain += _domain_minusRangeRangeStr(loi, hii, lo, hi);
lo = flagIndex;
}
hi = flagIndex;
}
flagValue = 1 << ++flagIndex;
}
return newDomain + _domain_minusRangeRangeStr(loi, hii, lo, hi);
}
function _domain_minusRangeStrStr(loi, hii, domain_str) {
ASSERT_STRDOM(domain_str);
let newDomain = EMPTY_STR;
for (let index = 0, len = domain_str.length; index < len; index += STR_RANGE_SIZE) {
let lo = domain_str_decodeValue(domain_str, index);
let hi = domain_str_decodeValue(domain_str, index + STR_VALUE_SIZE);
newDomain += _domain_minusRangeRangeStr(loi, hii, lo, hi);
}
return newDomain;
}
function _domain_minusRangeStrNum(loi, hii, domain_str) {
ASSERT_STRDOM(domain_str);
let newDomain = EMPTY;
for (let index = 0, len = domain_str.length; index < len; index += STR_RANGE_SIZE) {
let lo = domain_str_decodeValue(domain_str, index);
let hi = domain_str_decodeValue(domain_str, index + STR_VALUE_SIZE);
newDomain |= _domain_minusRangeRangeNum(loi, hii, lo, hi);
}
return newDomain;
}
function _domain_minusRangeRangeStr(loi, hii, loj, hij) {
let hi = hii - loj;
if (hi >= SUB) { // silently ignore results that are OOB
let lo = MAX(SUB, loi - hij);
return domain_str_encodeRange(lo, hi);
}
return EMPTY_STR;
}
function _domain_minusRangeRangeNum(loi, hii, loj, hij) {
let hi = hii - loj;
if (hi >= SUB) { // silently ignore results that are OOB
let lo = MAX(SUB, loi - hij);
ASSERT(lo <= SMALL_MAX_NUM, 'RESULT_SHOULD_NOT_EXCEED_SMALL_DOMAIN');
ASSERT(hi <= SMALL_MAX_NUM, 'RESULT_SHOULD_NOT_EXCEED_SMALL_DOMAIN');
let domain = domain_num_createRange(lo, hi);
ASSERT(typeof domain === 'number' && domain < SOLVED_FLAG, 'expecting numdom, not soldom');
return domain;
}
return EMPTY;
}
// BODY_STOP
export default domain_minus;