stsys
Version:
String rewriting system (semi-Thue system)
261 lines (238 loc) • 5.93 kB
JavaScript
class Cmp
{
constructor(preorderElementRegex)
{
this.preorder = null;
this.preorderElementRegex = preorderElementRegex || /^[a-zA-Z]$/;
}
checkPreorder(preorder)
{
if (!(preorder instanceof Array) || preorder.length === 0)
throw new Error('invalid preorder: non-empty array of letters expected');
const faultyElement = preorder.find(sym => typeof sym !== 'string' || !this.preorderElementRegex.test(sym));
if (faultyElement)
throw new Error('invalid element in preorder: ' + faultyElement);
for (let i = 0; i < preorder.length - 1; ++i)
{
const sym = preorder[i].length > 1 ? preorder[i].charAt(0) : preorder[i];
for (let j = i + 1; j < preorder.length; ++j)
{
if (sym === preorder[j].charAt(0))
throw new Error('duplicates in preorder: ' + sym);
}
}
}
setPreorder(preorder)
{
this.checkPreorder(preorder);
this.preorder = preorder.map(sym => sym);
}
getPreorder()
{
return this.preorder.map(sym => sym);
}
}
class LpoCmp extends Cmp
{
constructor(preorder)
{
super();
this.setPreorder(preorder);
}
compare(s1, s2)
{
return lpoCmp.call(this, s1, 0, s2, 0, []);
}
getOrdering()
{
return 'lpo';
}
toString()
{
return 'LPO using preorder ' + this.preorder.join(' > ');
}
}
class LexCmp extends Cmp
{
constructor(preorder)
{
super();
this.setPreorder(preorder);
}
compare(s1, s2)
{
let d = s1.length - s2.length;
if (d !== 0)
return d;
for (let i = 0; i < s1.length; i++)
{
d = preorderCmp(this.preorder, s1.charAt(i), s2.charAt(i));
if (d !== 0)
return d;
}
return 0;
}
getOrdering()
{
return 'lex';
}
toString()
{
return 'Length/lexicographic ordering using preorder ' + this.preorder.join(' > ');
}
}
class SylCmp extends Cmp
{
constructor(preorder, defaultDirection)
{
super(/^[a-zA-Z](?::[lr])?$/);
this.directionMap = null;
this.defaultDirection = defaultDirection || 'l';
this.setPreorder(preorder);
}
setPreorder(preorder)
{
this.checkPreorder(preorder);
this.directionMap = {};
this.preorder = preorder.map(sym => {
if (sym.charAt(1) === ':')
{
const firstChar = sym.charAt(0);
this.directionMap[firstChar] = sym.charAt(2);
return firstChar;
}
this.directionMap[sym] = this.defaultDirection;
return sym;
});
}
compare(s1, s2)
{
return sylCmp.call(this, 0, s1, 0, s1.length, s2, 0, s2.length);
}
getOrdering()
{
return 'syl-' + this.defaultDirection;
}
toString()
{
return 'Syllable ordering with default direction ' + this.defaultDirection + ' nested as per preorder ' +
this.preorder.map(sym =>
this.directionMap[sym] === this.defaultDirection ? sym : sym + ':' + this.directionMap[sym]).join(' > ');
}
}
function preorderCmp(preorder, c1, c2)
{
const i1 = preorder.indexOf(c1);
const i2 = preorder.indexOf(c2);
if (i1 < 0 || i2 < 0)
throw new Error('Preorder comparison failed for letters ' + c1 + ' and ' + c2 + ' (preorder incomplete)');
return i2 - i1;
}
function lpoCmp(s1, i1, s2, i2, cmpCash)
{
const l1 = s1.length - i1;
const l2 = s2.length - i2;
if (l1 === 0 && l2 === 0)
return 0;
if (l1 === 0)
return -1;
if (l2 === 0)
return 1;
const cmpCashIdx = i1 * s2.length + i2;
if (l1 === l2 && substringEqual(s1, i1, s2, i2))
return cmpCash[cmpCashIdx] = 0;
if (typeof cmpCash[cmpCashIdx] === 'number')
return cmpCash[cmpCashIdx];
if (lpoCmp.call(this, s1, i1 + 1, s2, i2, cmpCash) >= 0)
return cmpCash[cmpCashIdx] = 1;
if (lpoCmp.call(this, s1, i1, s2, i2 + 1, cmpCash) <= 0)
return cmpCash[cmpCashIdx] = -1;
let c1 = s1.charAt(i1);
let c2 = s2.charAt(i2);
if (c1 === c2)
return cmpCash[cmpCashIdx] = lpoCmp.call(this, s1, i1 + 1, s2, i2 + 1, cmpCash);
else if (preorderCmp(this.preorder, c1, c2) > 0)
return cmpCash[cmpCashIdx] = 1;
return cmpCash[cmpCashIdx] = -1;
}
function sylCmp(poi, s1, from1, to1, s2, from2, to2)
{
if (poi >= this.preorder.length)
return 0;
const poc = this.preorder[poi];
let o1 = countLetterOccurrences(poc, s1, from1, to1);
let o2 = countLetterOccurrences(poc, s2, from2, to2);
if (o1 > o2)
return 1;
if (o1 < o2)
return -1;
if (this.directionMap[poc] === 'l')
while (from1 < to1 || from2 < to2)
{
o1 = s1.indexOf(poc, from1);
o2 = s2.indexOf(poc, from2);
if (o1 < 0 || o1 > to1)
o1 = to1;
if (o2 < 0 || o2 > to2)
o2 = to2;
let r = sylCmp.call(this, poi + 1, s1, from1, o1, s2, from2, o2);
if (r !== 0)
return r;
from1 = o1 + 1;
from2 = o2 + 1;
}
else
while (from1 < to1 || from2 < to2)
{
o1 = s1.lastIndexOf(poc, to1 - 1);
o2 = s2.lastIndexOf(poc, to2 - 1);
if (o1 < from1)
o1 = from1;
else
o1++;
if (o2 < from2)
o2 = from2;
else
o2++;
let r = sylCmp.call(this, poi + 1, s1, o1, to1, s2, o2, to2);
if (r !== 0)
return r;
to1 = o1 - 1;
to2 = o2 - 1;
}
return 0;
}
function substringEqual(s1, i1, s2, i2)
{
while (i1 < s1.length)
{
if (s1.charAt(i1++) !== s2.charAt(i2++))
return false;
}
return true;
}
function countLetterOccurrences(c, str, from, to)
{
let n = 0;
while (from < to)
{
if (str.charAt(from++) === c)
n++;
}
return n;
}
module.exports.createComparer = function(name, preorder)
{
switch (name)
{
case 'lex':
return new LexCmp(preorder);
case 'lpo':
return new LpoCmp(preorder);
case 'syl-l':
return new SylCmp(preorder, 'l');
case 'syl-r':
return new SylCmp(preorder, 'r');
}
throw new Error(name ? 'unknown ordering: ' + name : 'no ordering specified');
};