Created
March 19, 2015 10:31
-
-
Save pyrtsa/e82bbf0a416c215cc55c to your computer and use it in GitHub Desktop.
A human-readable, order-preserving serialisation for JSON-like data in JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<meta charset="utf-8"> | |
<script src="orders.js"></script> | |
<style> | |
#display { | |
white-space: pre-wrap; | |
font-family: 'Menlo', monospace; | |
font-size: 12px; | |
line-height: 16px; | |
} | |
.output { | |
color: #777; | |
} | |
</style> | |
<div id="display"></div> | |
<script> | |
// scrutinise.verbose = true | |
testElen() | |
randTestElen() | |
testCerealise() | |
display(scrutinise.stats()) | |
display() | |
display("Examples") | |
display("========") | |
display() | |
function leftAlign(n, s) { | |
s = '' + s | |
return s + repeatString(' ', n - s.length) | |
} | |
function rightAlign(n, s) { | |
s = '' + s | |
return repeatString(' ', n - s.length) + s | |
} | |
function demo(f, x) { | |
var input = scrutinyFcall(f, x) | |
var output = scrutinyStringify(f(x)) | |
if (input.length <= 40) { | |
display('>>> ' + leftAlign(40, input) + ' // ' + output) | |
} else { | |
display('>>> ' + input + '\n // ' + output) | |
} | |
} | |
demo(elenNat, 0) | |
demo(elenNat, 1) | |
demo(elenNat, 2) | |
demo(elenNat, 10) | |
demo(elenNat, 100) | |
demo(elenNat, 1000) | |
demo(elenNat, '1234567890987654321') | |
display() | |
demo(elenInt, 1) | |
demo(elenInt, 0) | |
demo(elenInt, -1) | |
demo(elenInt, -2) | |
demo(elenInt, -10) | |
demo(elenInt, -99) | |
demo(elenInt, -100) | |
demo(elenInt, -1000) | |
demo(elenInt, '-1234567890987654321') | |
display() | |
demo(elenDec, -12) | |
demo(elenDec, -11) | |
demo(elenDec, -10) | |
demo(elenDec, -9) | |
demo(elenDec, -8) | |
demo(elenDec, -7) | |
demo(elenDec, -6) | |
demo(elenDec, -5) | |
demo(elenDec, -4) | |
demo(elenDec, -3) | |
demo(elenDec, -2) | |
demo(elenDec, -1) | |
demo(elenDec, 0) | |
demo(elenDec, 1) | |
demo(elenDec, 2) | |
demo(elenDec, 3) | |
demo(elenDec, 4) | |
demo(elenDec, 5) | |
demo(elenDec, 6) | |
demo(elenDec, 7) | |
demo(elenDec, 8) | |
demo(elenDec, 9) | |
demo(elenDec, 10) | |
demo(elenDec, 11) | |
demo(elenDec, 12) | |
display() | |
demo(elenDec, '-3.13e+100') | |
demo(elenDec, '-3.13e+50') | |
demo(elenDec, '-3.13e+20') | |
demo(elenDec, '-3.13e+10') | |
demo(elenDec, '-3.13e+5') | |
demo(elenDec, '-3.13e+2') | |
demo(elenDec, '-3.13e+1') | |
demo(elenDec, '-3.13') | |
demo(elenDec, '-3.13e-1') | |
demo(elenDec, '-3.13e-2') | |
demo(elenDec, '-3.13e-5') | |
demo(elenDec, '-3.13e-10') | |
demo(elenDec, '-3.13e-20') | |
demo(elenDec, '-3.13e-50') | |
demo(elenDec, '-3.13e-100') | |
demo(elenDec, '+3.13e-100') | |
demo(elenDec, '+3.13e-50') | |
demo(elenDec, '+3.13e-20') | |
demo(elenDec, '+3.13e-10') | |
demo(elenDec, '+3.13e-5') | |
demo(elenDec, '+3.13e-2') | |
demo(elenDec, '+3.13e-1') | |
demo(elenDec, '+3.13') | |
demo(elenDec, '+3.13e+1') | |
demo(elenDec, '+3.13e+2') | |
demo(elenDec, '+3.13e+5') | |
demo(elenDec, '+3.13e+10') | |
demo(elenDec, '+3.13e+20') | |
demo(elenDec, '+3.13e+50') | |
demo(elenDec, '+3.13e+100') | |
display() | |
demo(elenDec, 3.14) | |
demo(elenDec, 3.14e50) | |
demo(elenDec, 3.14e100) | |
demo(elenDec, 3.14e-100) | |
demo(elenDec, 299792458) | |
demo(elenDec, 2432902008176640000) | |
demo(elenDec, '6.02214129e-23') | |
demo(elenDec, -3.14) | |
demo(elenDec, -3.14e100) | |
demo(elenDec, -3.14e-100) | |
display() | |
demo(elenDec, -Infinity) | |
demo(elenDec, "-1.23e1000") | |
demo(elenDec, "-1.23e999") | |
demo(elenDec, "-1.23e987") | |
demo(elenDec, "-1.23e123") | |
demo(elenDec, "-1.23e100") | |
demo(elenDec, "-1.23e99") | |
demo(elenDec, "-1.23e88") | |
demo(elenDec, "-1.23e11") | |
demo(elenDec, "-1.23e10") | |
demo(elenDec, "-1.23e9") | |
demo(elenDec, "-1.23e1") | |
demo(elenDec, "-1.23e0") | |
demo(elenDec, "-1.23e-1") | |
demo(elenDec, "-1.23e-2") | |
demo(elenDec, "-1.23e-1000") | |
demo(elenDec, -12.34) | |
demo(elenDec, -12.3) | |
demo(elenDec, -12) | |
demo(elenDec, -1.23) | |
demo(elenDec, -0.123) | |
demo(elenDec, 0.123) | |
demo(elenDec, 1.23) | |
demo(elenDec, 12) | |
demo(elenDec, 12.3) | |
demo(elenDec, 12.34) | |
demo(elenDec, Infinity) | |
display() | |
demo(cerealise, 0) | |
demo(cerealise, 1) | |
demo(cerealise, 'one') | |
demo(cerealise, 'two') | |
demo(cerealise, []) | |
demo(cerealise, [1, 2, 3]) | |
demo(cerealise, [1, 2, 4]) | |
demo(cerealise, [1, 2, 'four']) | |
demo(cerealise, [[1, 2], 'four']) | |
demo(cerealise, [[123456789, 2], 'four']) | |
demo(cerealise, [[123456789, 2], 'one hundred and fourty-two']) | |
demo(cerealise, [[123456789, 2], 'one thousand!!!']) | |
demo(cerealise, [['doe', 'john', 'jr'], 'deadbeef-1234-...']) | |
demo(cerealise, [['franklin', 'ben'], 'fooo1337-5687-...']) | |
display() | |
demo(cerealise, genArray(randAny)()) | |
display() | |
demo(cerealise, genArray(randAny)()) | |
display() | |
demo(cerealise, genArray(randAny)()) | |
function compareMemoryEfficiency(descr, gen) { | |
var objs = range(100).map(gen) | |
var json = objs.map(function (x) { return JSON.stringify(x).length }).sum() | |
var cere = objs.map(function (x) { return cerealise(x).length }).sum() | |
var fact = Math.round(100 * cere / json) / 100 | |
display('For a random sample of ' + leftAlign(27, descr + ', ') + | |
'cerealise(x) took ' + fact + 'x the space of the equivalent JSON string.') | |
} | |
display() | |
compareMemoryEfficiency('integers 0 .. 9', genNat(10)) | |
compareMemoryEfficiency('integers 0 .. 99', genNat(100)) | |
compareMemoryEfficiency('integers 0 .. 999', genNat(1000)) | |
compareMemoryEfficiency('integers 0 .. 9,999', genNat(10000)) | |
compareMemoryEfficiency('integers 0 .. 99,999', genNat(100000)) | |
compareMemoryEfficiency('integers 0 .. 999,999', genNat(1000000)) | |
compareMemoryEfficiency('integers 0 .. 999,999,999', genNat(1000000000)) | |
compareMemoryEfficiency('32-bit integers', randInt) | |
compareMemoryEfficiency('decimal numbers', randDecimal) | |
compareMemoryEfficiency('floating point numbers', randFloat) | |
compareMemoryEfficiency('arbitrary data', randObject) | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function display(message) { | |
var parent = document.getElementById('display') | |
if (!parent) return | |
var child = document.createElement('div') | |
child.className = 'entry' | |
child.textContent = message || '\n' | |
parent.appendChild(child) | |
console.log.apply(console, arguments) | |
} | |
function identity(x) { return x } | |
function constantly(x) { return function () { return x }} | |
function isSome(x) { return x != null } | |
Array.prototype.sum = function () { | |
return this.reduce(function (a, b) { return a + b }, 0) | |
} | |
Array.prototype.mapSome = function (f) { | |
return this.map(f).filter(isSome) | |
} | |
Array.prototype.groupBy = function (f) { | |
var obj = {} | |
this.forEach(function (x) { | |
var k = f(x) | |
obj[k] = obj[k] || [] | |
obj[k].push(x) | |
}) | |
return obj | |
} | |
Function.prototype.map = function (g) { | |
var f = this | |
return function () { | |
return g(f.apply(null, arguments)) | |
} | |
} | |
function range(n) { | |
return Array.apply(null, Array(Math.max(0, n))).map(function (_, i) { return i }) | |
} | |
function slice(xs, start, end) { | |
var f = Array.prototype.slice | |
return f.apply(xs, f.call(arguments, 1)) | |
} | |
function nanMin(a, b) { | |
return a < b ? a : b < a ? b : a === a ? a : b | |
} | |
function nanMax(a, b) { | |
return a > b ? a : b > a ? b : a === a ? a : b | |
} | |
function repeatedly(f) { | |
var args = slice(arguments, 1) | |
return function () { return f.apply(null, args) } | |
} | |
function sign(x) { | |
return x < 0 ? -1 : x > 0 ? 1 : x == 0 ? 0 : x | |
} | |
function clamp(x, a, b) { | |
return x < a ? a : x > b ? b : x | |
} | |
function randBool() { | |
return Math.random() >= 0.5 | |
} | |
function randSign() { | |
return randBool() ? -1 : 1 | |
} | |
function randUnif(start, end) { | |
if (start == null) start = 0 | |
if (end == null) end = 1 | |
return start + (end - start) * Math.random() | |
} | |
function randInt(start, end) { | |
if (start == null) start = -Math.pow(2, 31) | |
if (end == null) end = Math.pow(2, 31) - 1 | |
return clamp(Math.floor(randUnif(start, end)), start, end - 1) | |
} | |
function genChoose(rands) { | |
if (!Array.isArray(rands) || rands.length < 1) { | |
throw TypeError('rands is not a non-empty array: ' + rands) | |
} | |
return function () { | |
return rands[randNat(rands.length)]() | |
} | |
} | |
function genChooseWeighted(weightedRands) { | |
if (!Array.isArray(weightedRands) || weightedRands.length < 1) { | |
throw TypeError('weightedRands is not a non-empty array: ' + weightedRands) | |
} | |
var totalWeight = 0 | |
var cumulativeWeights = [] | |
for (var i = 0, n = weightedRands.length; i < n; i++) { | |
var weight = +weightedRands[i][0] | |
if (!(0 < weight && weight < Infinity)) { | |
throw TypeError('weightedRands[' + i + '] has invalid weight: ' + weight) | |
} | |
totalWeight += weight | |
cumulativeWeights.push(totalWeight) | |
} | |
return function () { | |
var position = randUnif(0, totalWeight) | |
var i = 0 | |
while (position > cumulativeWeights[i]) i++ | |
return weightedRands[i][1]() | |
} | |
} | |
function randDenormal() { | |
var mant = randNat(1, Math.pow(2, 52)) | |
return Number.MIN_VALUE * mant | |
} | |
function randIntegral() { | |
return Math.floor(Math.pow(2, randInt(0, 54)) * (1 + Math.random())) | |
} | |
function randDecimal() { | |
var precision = randInt(1, 8) | |
var mant = randInt(0, Math.pow(10, precision)) | |
var exp = randInt(-4, 10) - precision | |
return mant * Math.pow(10, exp) | |
} | |
function randSimpleDecimal() { | |
var exp = randInt(-20, 21) | |
var mant = randInt(-9, 10) | |
return mant * Math.pow(10, exp) | |
} | |
function randNormal() { | |
var exp = randInt(-1022, 1024) | |
return (1 + Math.random()) * Math.pow(2, exp) | |
} | |
var randFloat = (function () { | |
var choice = genChooseWeighted([ | |
[1, constantly(0)], | |
[1, constantly(Number.MIN_VALUE)], | |
[1, constantly(0.5)], | |
[1, constantly(1 - Math.pow(2, -53))], | |
[1, constantly(1)], | |
[1, constantly(1 + Math.pow(2, -52))], | |
[1, constantly(2)], | |
[1, constantly(Number.MAX_VALUE)], | |
[1, constantly(Infinity)], | |
[1, randDenormal], | |
[10, randIntegral], | |
[10, randDecimal], | |
[5, randNormal] | |
]) | |
return function () { | |
return randSign() * choice() | |
} | |
}()) | |
function randFloat() { | |
var exp = randSign() * randInt(-1023, 1024) | |
var mantLen = randNat(53) // 0, 1, 2, ..., 52 | |
var mant = randNat(Math.pow(2, mantLen)) | |
return (exp == -1023) ? sgn * Number.MIN_VALUE * mant | |
: sgn * (1 + Math.pow(2, -52) * mant) * Math.pow(2, exp) | |
} | |
function randNat(n) { | |
return randInt(0, n) | |
} | |
function kind(x) { | |
if (x == null) return kind.NULL | |
if (typeof x == 'boolean') return kind.BOOLEAN | |
if (typeof x == 'number') return kind.NUMBER | |
if (typeof x == 'string') return kind.STRING | |
if (Array.isArray(x)) return kind.ARRAY | |
return kind.OBJECT | |
} | |
kind.BOOLEAN = 1 | |
kind.NUMBER = 2 | |
kind.STRING = 3 | |
kind.OBJECT = 4 | |
kind.ARRAY = 5 | |
kind.NULL = 6 | |
function compare(a, b) { | |
if (!(a === a) || !(b === b)) throw new TypeError('NaN') | |
var k = kind(a) | |
var l = kind(b) | |
if (k != l) return sign(k - l) | |
if (k == kind.NULL) return 0 | |
if (k == kind.BOOLEAN) return sign(a - b) | |
if (k == kind.NUMBER) return sign(a - b) | |
if (k == kind.STRING) return a < b ? -1 : a > b ? 1 : 0 | |
if (k == kind.OBJECT) { | |
var as = Object.keys(a); as.sort() | |
var bs = Object.keys(b); bs.sort() | |
var n = Math.min(as.length, bs.length) | |
for (var i = 0; i < n; i++) { | |
var c1 = compare(as[i], bs[i]) | |
if (c1) return c1 | |
var k1 = as[i] | |
var c2 = compare(a[k1], b[k1]) | |
if (c2) return c2 | |
} | |
return sign(as.length - bs.length) | |
} | |
if (k == kind.ARRAY) { | |
var n = Math.min(a.length, b.length) | |
for (var i = 0; i < n; i++) { | |
var c = compare(a[i], b[i]) | |
if (c) return c | |
} | |
return sign(a.length - b.length) | |
} | |
throw new TypeError('unknown kind ' + k) | |
} | |
function comparing(f) { | |
return function (a, b) { | |
return compare(f(a), f(b)) | |
} | |
} | |
function sorted(xs, cmp) { | |
return slice(xs).sort(cmp || compare) | |
} | |
function eq(a, b) { return compare(a, b) === 0 } | |
function ne(a, b) { return compare(a, b) !== 0 } | |
function lt(a, b) { return compare(a, b) < 0 } | |
function gt(a, b) { return compare(a, b) > 0 } | |
function le(a, b) { return compare(a, b) <= 0 } | |
function ge(a, b) { return compare(a, b) >= 0 } | |
function scrutinise(handler, pred, expected, fn) { | |
var args = slice(arguments, 4) | |
var status = 'skip' | |
var actual = null | |
var error = null | |
try { | |
actual = fn.apply(null, args) | |
status = pred(actual, expected) ? 'pass' : 'fail' | |
} catch (e) { | |
error = e | |
status = 'abort' | |
} | |
return handler.call(this, { | |
passed: status === 'pass', | |
status: status, | |
error: error, | |
pred: pred, | |
expected: expected, | |
actual: actual, | |
function: fn, | |
arguments: args | |
}) | |
} | |
scrutinise.stats = function () { | |
var rs = this.results | |
if (!rs.length) return 'Nothing scrutinised yet.' | |
var gs = rs.groupBy(function (r) { return r.status }) | |
var msg = 'Scrutinised ' + (rs.length == 1 ? 'one check' : rs.length + ' checks') | |
if (gs.pass) msg += ', ' + gs.pass.length + ' passed' | |
if (gs.fail) msg += ', ' + gs.fail.length + ' failed' | |
if (gs.abort) msg += ', ' + gs.abort.length + ' aborted' | |
if (gs.skip) msg += ', ' + gs.skip.length + ' skipped' | |
return msg + '.' | |
} | |
scrutinise.results = [] | |
function scrutinyStringify(x) { | |
if (x._scrutinyName) return x._scrutinyName | |
if (x == null) return '' + x | |
if (typeof x == 'number') return '' + x | |
if (typeof x != 'function') return JSON.stringify(x) || '' + x | |
if (x.name) return x.name | |
var s = ('' + x).replace(/\s+/g, ' ') | |
return s.length <= 50 ? s : '<lambda>' | |
} | |
function scrutinyFcall(f) { | |
var fname = scrutinyStringify(f) | |
return fname + '(' + slice(arguments, 1).map(scrutinyStringify).join(', ') + ')' | |
} | |
function scrutinyToString(result) { | |
var predStr = scrutinyFcall(result.pred, result.expected, {_scrutinyName: 'x'}) | |
var xStr = scrutinyFcall.apply(null, [result.function].concat(result.arguments)) | |
var labelStr = predStr + ' where x = ' + xStr | |
if (result.status === 'skip') return 'skipped: ' + labelStr | |
if (result.status === 'abort') return 'aborted: ' + labelStr + ' raised ' + scrutinyStringify('' + result.error) | |
return (result.passed ? 'passed: ' : 'failed: ') + labelStr + ' returned ' + scrutinyStringify(result.actual) | |
} | |
function scrutiniser(handler) { | |
return function (pred, expected, fn) { | |
return scrutinise.apply(null, [handler].concat(slice(arguments))) | |
} | |
} | |
var check = scrutiniser(function (result) { | |
if (this !== window) { | |
this.addResult(result) | |
} else { | |
scrutinise.results.push(result) | |
if (!result.passed || scrutinise.verbose) { | |
display(scrutinyToString(result), result) | |
} | |
} | |
return result.passed | |
}) | |
var verify = scrutiniser(function (result) { | |
if (this !== window) { | |
this.addResult(result) | |
} else { | |
scrutinise.results.push(result) | |
if (!result.passed || scrutinise.verbose) { | |
display(scrutinyToString(result), result) | |
} | |
} | |
if (!result.passed) { | |
throw new Error('verification failed') | |
} else { | |
return true | |
} | |
}) | |
function toNatString(n) { | |
if (!(n >= 0 && n == Math.floor(n))) throw new TypeError('not a natural number: ' + n) | |
if (n < Math.pow(2, 53)) return '' + n | |
throw new TypeError('too big to represent a natural number: ' + n) | |
} | |
function elenNat(n, strict) { | |
n = '' + n | |
if (!/^(0|[1-9]\d*)$/g.test(n)) throw new TypeError('not a natural: ' + n) | |
var prefix = n.length > 1 ? '=' + elenNat(n.length - 1) : '' | |
return (strict ? '=' : '') + prefix + n | |
} | |
function inverseElen(n) { | |
return ('' + n).replace(/[\d=-]/g, function (s) { | |
return s === '-' ? '=' : | |
s === '=' ? '-' : 9 - parseInt(s, 10) | |
}) | |
} | |
function elenInt(n) { | |
n = '' + n | |
if (n == '0') return '0' | |
var m = /^(-?)([1-9]\d*)$/.exec(n) | |
if (!m) throw new TypeError('not an integer: ' + n) | |
if (m[1]) { // negative | |
return inverseElen(elenNat(m[2], 'strict')) | |
} else { // positive | |
return elenNat(m[2]) | |
} | |
} | |
function repeatString(s, n) { | |
return range(n).map(function () { return s }).join('') | |
} | |
function splitDec(d) { | |
function ret(sgn, nat, frac, fmt) { | |
return {positive: sgn, integral: nat, fractional: frac, format: fmt} | |
} | |
d = '' + d | |
var full = /^([+-]?)([1-9]\d*)\.(\d*?)0*$/ | |
var dec = /^([+-]?)([1-9]\d*)$/ | |
var frac = /^([+-]?)(?:0?)\.(0*[1-9]\d*?)0*$/ | |
var eng = /^([+-]?)([1-9]\d*)(?:\.(\d+?)0*)?e([+-]?\d+)$/ | |
var m = null | |
if ((m = full.exec(d))) return ret(m[1] !== '-', m[2] || '', m[3] || '', 'full') | |
if ((m = dec.exec(d))) return ret(m[1] !== '-', m[2] || '', '' , 'dec') | |
if ((m = frac.exec(d))) return ret(m[1] !== '-', '', m[2] || '', 'frac') | |
if ((m = eng.exec(d))) { | |
var e = parseInt(m[4], 10) | |
var g = e + m[2].length | |
var n = m[2] + (m[3] || '') | |
return ret( | |
m[1] !== '-', | |
n.slice(0, Math.max(0, g)) + repeatString('0', g - n.length), | |
repeatString('0', -g) + n.slice(Math.max(0, g)).replace(/0*$/, ''), | |
'eng' | |
) | |
} | |
throw new TypeError('cannot split as decimal number: ' + d) | |
} | |
function splitEng(d) { | |
function ret(sgn, mant, exp, fmt) { | |
return { | |
positive: sgn, | |
mantissa: mant.replace(/0*$/, ''), | |
exponent: exp, | |
format: fmt | |
} | |
} | |
d = '' + d | |
var full = /^([+-]?)([1-9]\d*)\.(\d*?)0*$/ | |
var dec = /^([+-]?)([1-9]\d*)$/ | |
var frac = /^([+-]?)(?:0?)\.(0*[1-9]\d*?)0*$/ | |
var eng = /^([+-]?)([1-9]\d*)(?:\.(\d+?)0*)?e([+-]?\d+)$/ | |
var lead = /^0*/ | |
var m = null | |
if ((m = full.exec(d))) { | |
var i = m[2] || '' | |
var f = m[3] || '' | |
var x = i + f | |
return ret(m[1] !== '-', | |
x.replace(lead, ''), | |
i.length - lead.exec(x)[0].length - 1, | |
'full') | |
} | |
if ((m = dec.exec(d))) { | |
return ret(m[1] !== '-', m[2], m[2].length - 1, 'dec') | |
} | |
if ((m = frac.exec(d))) { | |
return ret(m[1] !== '-', m[2].replace(lead, ''), -lead.exec(m[2])[0].length - 1, 'frac') | |
} | |
if ((m = eng.exec(d))) { | |
var e = parseInt(m[4], 10) | |
var g = e + m[2].length - 1 | |
var n = m[2] + (m[3] || '') | |
return ret(m[1] !== '-', n, g, 'eng') | |
} | |
throw new TypeError('cannot split as decimal number: ' + d) | |
} | |
function elenSmallDec(d) { | |
if (!(-1 < d && d < 1)) throw new TypeError('not a small number within (-1, 1): ' + d) | |
if (d == 0) return '0' | |
var s = splitDec(d) | |
if (s.positive) { | |
return '=' + s.fractional + '-' | |
} else { | |
return '-' + inverseElen(s.fractional) + '=' | |
} | |
} | |
function elenLargeDec(d) { | |
if (d == 0) return '0' | |
if (d == -Infinity) return '-' | |
if (d == Infinity) return '=>' | |
var s = splitDec(d) | |
if (s.positive) { | |
return (s.integral ? elenInt(s.integral) : '0') + s.fractional | |
} else { | |
return (s.integral ? elenInt('-' + s.integral) : '-9') + inverseElen(s.fractional) + '=' | |
} | |
} | |
function elenDec(d) { | |
if (d == '0') return '0' | |
if (d == '-Infinity') return '-' | |
if (d == 'Infinity') return '=>' | |
var s = splitEng(d) | |
if (s.positive) { | |
return '=' + elenInt(s.exponent) + s.mantissa | |
} else { | |
return '-' + inverseElen(elenInt(s.exponent) + s.mantissa) + '=' | |
} | |
} | |
function cerealEscape(s) { | |
return ('' + s).replace(/([\x00-!])/g, '!$1') | |
} | |
function keyvals(x) { | |
var result = [] | |
var keys = Object.keys(x) | |
keys.sort() | |
keys.forEach(function (k) { | |
result.push(k) | |
result.push(x[k]) | |
}) | |
return result | |
} | |
function cerealise(x, depth) { | |
var prefix = depth ? ' ' : '' | |
if (x == null) { | |
return prefix + '~' | |
} | |
if (typeof x == 'boolean') { | |
return prefix + (x ? '#1' : '#0') | |
} | |
if (typeof x == 'number') { | |
return prefix + elenDec(x) // result guaranteed to not need cerealEscape | |
} | |
if (typeof x == 'string') { | |
return prefix + '^' + cerealEscape(x) | |
} | |
if (Array.isArray(x)) { | |
return prefix + '{' + x.map(recur).join('') + ' !}' | |
} | |
// TODO: Objects should be able to define their own order-preserving | |
// serialisation, consisting of the above forms or arrays with quoted | |
// elements (recursively). | |
// Under debate: The unnamed class sorts last and class names should not | |
// the '{' character. I guess we can live with that. | |
if (x.constructor === ({}).constructor) { | |
return prefix + '`' + cerealise(keyvals(x)) | |
} else { | |
var ctor = x.constructor && x.constructor.name || 'null' | |
return prefix + '`' + cerealEscape(ctor) + cerealise(keyvals(x)) | |
} | |
function recur(x) { | |
return cerealise(x, (depth || 0) + 1) | |
} | |
} | |
function genNat(end) { | |
return function () { | |
return randNat(end) | |
} | |
} | |
function genInt(start, end) { | |
return function () { | |
return randInt(start, end) | |
} | |
} | |
function genUnif(start, end) { | |
return function () { | |
return randUnif(start, end) | |
} | |
} | |
function genArray(randElem) { | |
return function () { | |
return range(randInt(1, 5)).map(randElem) | |
} | |
} | |
var randString = (function () { | |
function charsWeighted(weight, minimum, maximum) { | |
var a = minimum.charCodeAt(0) | |
var b = maximum.charCodeAt(0) + 1 | |
return [weight * (b - a), genInt(a, b).map(String.fromCharCode)] | |
} | |
var gen = genChooseWeighted([ | |
charsWeighted( 1, '\x00', '\x31'), // control chars 0..31 | |
charsWeighted( 3, ' ', ' '), | |
charsWeighted( 3, '!', '@'), | |
charsWeighted( 5, 'A', 'Z'), | |
charsWeighted( 3, '[', '`'), | |
charsWeighted(200, 'a', 'z'), | |
charsWeighted( 3, '{', '~'), | |
charsWeighted( 1, '\x7f', '\x7f'), // DEL control char | |
charsWeighted( 1, '\u00a0', '\u02af') | |
]) | |
return function () { | |
var n = Math.floor(Math.pow(10, randUnif(-1, 2))) | |
return range(n).map(gen).join('') | |
} | |
}()) | |
var randIdentifier = (function () { | |
function charsWeighted(weight, minimum, maximum) { | |
var a = minimum.charCodeAt(0) | |
var b = maximum.charCodeAt(0) + 1 | |
return [weight * (b - a), genInt(a, b).map(String.fromCharCode)] | |
} | |
var randAlpha = genChooseWeighted([ | |
charsWeighted(1, 'A', 'Z'), | |
charsWeighted(1, '_', '_'), | |
charsWeighted(1, 'a', 'z') | |
]) | |
var randAlnum = genChooseWeighted([ | |
charsWeighted(1, 'A', 'Z'), | |
charsWeighted(1, '_', '_'), | |
charsWeighted(1, 'a', 'z'), | |
charsWeighted(1, '0', '9') | |
]) | |
return function randIdentifier() { | |
var s = randAlpha() | |
return s + range(randNat(3)).map(randAlnum).join('') | |
} | |
}()) | |
// function randString() { | |
// return range(Math.floor(Math.pow(2, randInt(0, 6))) - 1).map(function (_) { | |
// return String.fromCharCode(randInt(0, 1024)) | |
// }).join('') | |
// } | |
function genObject(keyGen, valGen) { | |
return function () { | |
var n = randInt(1, 6) | |
var result = {} | |
for (var i = 0; i < n; i++) { | |
var k = keyGen() | |
result[k] = valGen() | |
} | |
return result | |
} | |
} | |
function randAny() { | |
return _randAny() | |
} | |
function randObject() { | |
return _randObject() | |
} | |
var _randAny = genChooseWeighted([ | |
[1, constantly(null)], | |
[1, randBool], | |
[5, randFloat], | |
[5, randString], | |
[3, genArray(randAny)], | |
[1, randObject] | |
]) | |
var _randObject = genObject(randIdentifier, randAny) | |
function testElen() { | |
this.check(eq, '0', elenNat, 0) | |
this.check(eq, '1', elenNat, 1) | |
this.check(eq, '2', elenNat, 2) | |
this.check(eq, '3', elenNat, 3) | |
this.check(eq, '4', elenNat, 4) | |
this.check(eq, '9', elenNat, 9) | |
this.check(eq, '=110', elenNat, 10) | |
this.check(eq, '=111', elenNat, 11) | |
this.check(eq, '=112', elenNat, 12) | |
this.check(eq, '=119', elenNat, 19) | |
this.check(eq, '=120', elenNat, 20) | |
this.check(eq, '=199', elenNat, 99) | |
this.check(eq, '=2100', elenNat, 100) | |
this.check(eq, '=2999', elenNat, 999) | |
this.check(eq, '=31000', elenNat, 1000) | |
this.check(eq, '=39999', elenNat, 9999) | |
this.check(eq, '=410000', elenNat, 10000) | |
this.check(eq, '=91234567890', elenNat, 1234567890) | |
this.check(eq, '==11012345678900', elenNat, '12345678900') | |
this.check(eq, '==1991234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890', | |
elenNat, '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890') | |
this.check(eq, '==210012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901', | |
elenNat, '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901') | |
this.check(eq, '---8008765432109876543210987654321098765432109876543210987654321098765432109876543210987654321098765432109', | |
elenInt, '-1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890') | |
this.check(eq, '---789987654321098765432109876543210987654321098765432109876543210987654321098765432109876543210987654321098', | |
elenInt, '-12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901') | |
this.check(eq, '---88987654321098', elenInt, -12345678901) | |
this.check(eq, '---88987654321099', elenInt, -12345678900) | |
this.check(eq, '---88987654321100', elenInt, -12345678899) | |
this.check(eq, '--888', elenInt, -11) | |
this.check(eq, '--889', elenInt, -10) | |
this.check(eq, '-0', elenInt, -9) | |
this.check(eq, '-7', elenInt, -2) | |
this.check(eq, '-8', elenInt, -1) | |
this.check(eq, '0', elenInt, 0) | |
this.check(eq, '1', elenInt, 1) | |
this.check(eq, '2', elenInt, 2) | |
this.check(eq, '9', elenInt, 9) | |
this.check(eq, '=110', elenInt, 10) | |
this.check(eq, '=111', elenInt, 11) | |
this.check(eq, '==11012345678899', elenInt, 12345678899) | |
this.check(eq, '==11012345678900', elenInt, 12345678900) | |
this.check(eq, '==11012345678901', elenInt, 12345678901) | |
this.check(eq, '==1991234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890', | |
elenInt, '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890') | |
this.check(eq, '==210012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901', | |
elenInt, '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901') | |
this.check(eq, '-0004=', elenSmallDec, -0.9995) | |
this.check(eq, '-000=', elenSmallDec, -0.999) | |
this.check(eq, '-9876=', elenSmallDec, -0.0123) | |
this.check(eq, '-98=', elenSmallDec, -0.01) | |
this.check(eq, '-99876=', elenSmallDec, -0.00123) | |
this.check(eq, '-998766=', elenSmallDec, -0.001233) | |
this.check(eq, '-999876=', elenSmallDec, -0.000123) | |
this.check(eq, '0', elenSmallDec, 0) | |
this.check(eq, '=000123-', elenSmallDec, 0.000123) | |
this.check(eq, '=001233-', elenSmallDec, 0.001233) | |
this.check(eq, '=00123-', elenSmallDec, 0.00123) | |
this.check(eq, '=01-', elenSmallDec, 0.01) | |
this.check(eq, '=0123-', elenSmallDec, 0.0123) | |
this.check(eq, '=999-', elenSmallDec, 0.999) | |
this.check(eq, '=9995-', elenSmallDec, 0.9995) | |
this.check(eq, '-', elenLargeDec, -Infinity) | |
this.check(eq, '---8888999999999994=', elenLargeDec, -100000000000.5) | |
this.check(eq, '---889899999999994=', elenLargeDec, -10000000000.5) | |
this.check(eq, '--78994=', elenLargeDec, -100.5) | |
this.check(eq, '--8894=', elenLargeDec, -10.5) | |
this.check(eq, '-6854=', elenLargeDec, -3.145) | |
this.check(eq, '-685=', elenLargeDec, -3.14) | |
this.check(eq, '-898=', elenLargeDec, -1.01) | |
this.check(eq, '-8=', elenLargeDec, -1) | |
this.check(eq, '-998=', elenLargeDec, -0.01) | |
this.check(eq, '-99998766=', elenLargeDec, -0.0001233) | |
this.check(eq, '-9999876=', elenLargeDec, -0.000123) | |
this.check(eq, '0', elenLargeDec, 0) | |
this.check(eq, '0000123', elenLargeDec, 0.000123) | |
this.check(eq, '00001233', elenLargeDec, 0.0001233) | |
this.check(eq, '001', elenLargeDec, 0.01) | |
this.check(eq, '1', elenLargeDec, 1) | |
this.check(eq, '101', elenLargeDec, 1.01) | |
this.check(eq, '314', elenLargeDec, 3.14) | |
this.check(eq, '3145', elenLargeDec, 3.145) | |
this.check(eq, '=1105', elenLargeDec, 10.5) | |
this.check(eq, '=21005', elenLargeDec, 100.5) | |
this.check(eq, '=910000000005', elenLargeDec, 1000000000.5) | |
this.check(eq, '==110100000000005', elenLargeDec, 10000000000.5) | |
this.check(eq, '==1111000000000005', elenLargeDec, 100000000000.5) | |
this.check(eq, '=>', elenLargeDec, Infinity) | |
this.check(eq, '-', elenDec, -Infinity) | |
this.check(eq, '--8898=', elenDec, -1e10) | |
this.check(eq, '-08=', elenDec, -1e9) | |
this.check(eq, '-985=', elenDec, -1.4) | |
this.check(eq, '-986=', elenDec, -1.3) | |
this.check(eq, '-98=', elenDec, -1) | |
this.check(eq, '-=1876=', elenDec, -0.123) | |
this.check(eq, '-=2876=', elenDec, -0.0123) | |
this.check(eq, '-=38766=', elenDec, -0.001233) | |
this.check(eq, '-=3876=', elenDec, -0.00123) | |
this.check(eq, '0', elenDec, 0) | |
this.check(eq, '=-6123', elenDec, 0.00123) | |
this.check(eq, '=-61233', elenDec, 0.001233) | |
this.check(eq, '=-7123', elenDec, 0.0123) | |
this.check(eq, '=-8123', elenDec, 0.123) | |
this.check(eq, '=01', elenDec, 1) | |
this.check(eq, '=013', elenDec, 1.3) | |
this.check(eq, '=014', elenDec, 1.4) | |
this.check(eq, '=91', elenDec, 1e9) | |
this.check(eq, '==1101', elenDec, 1e10) | |
this.check(eq, '=>', elenDec, Infinity) | |
} | |
function randTestElen() { | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randNat(), randNat(), randNat(), randNat()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenNat)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randInt(), randInt(), randInt(), randInt()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenInt)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randUnif(-1, 1), randUnif(-1, 1), randUnif(-1, 1), randUnif(-1, 1)] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenSmallDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [Math.pow(10, randUnif(20)) * randInt(-1, 2), | |
Math.pow(10, randUnif(20)) * randInt(-1, 2), | |
Math.pow(10, randUnif(20)) * randInt(-1, 2), | |
Math.pow(10, randUnif(20)) * randSign(), | |
Math.pow(10, randUnif(20)) * randSign(), | |
Math.pow(10, randUnif(20)) * randSign()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenLargeDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var e = Math.pow(2, -52) | |
var x = randFloat() | |
var xs = [x, (1 + e) * x, (1 + 2 * e) * x, 0.5 * (2 - e) * x, (1 - e) * x] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randDecimal(), randDecimal(), randDecimal(), randDecimal(), randDecimal()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randSimpleDecimal(), randSimpleDecimal(), randSimpleDecimal(), randSimpleDecimal(), randSimpleDecimal()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randIntegral(), randIntegral(), randIntegral(), randIntegral(), randIntegral()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randInt(), randInt(), randInt(), randInt(), randInt()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randDenormal(), randDenormal(), randDenormal(), randDenormal(), randDenormal()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenDec)) | |
} | |
for (var i = 0; i < 1000; i++) { | |
var xs = [randFloat(), randFloat(), randFloat(), randFloat(), randFloat()] | |
this.check(eq, sorted(xs), sorted, xs, comparing(elenDec)) | |
} | |
} | |
function testCerealise() { | |
for (var i = 0; i < 1000; i++) { | |
var xs = genArray(randAny)() | |
this.check(eq, sorted(xs), sorted, xs, comparing(cerealise)) | |
} | |
for (var i = 0; i < 100; i++) { | |
var xs = genArray(genArray(genInt()))() | |
this.check(eq, sorted(xs), sorted, xs, comparing(cerealise)) | |
} | |
function testOne(xs) { | |
this.check(eq, sorted(xs), sorted, xs, comparing(cerealise)) | |
} | |
testOne.call(this, [[1, 2, 3], [1, 1, 2], [2, 1, 2], [10, 1, 2], [1.1, 1.2, 1]]) | |
testOne.call(this, [[[1], 2, 3], [1, [1], 2], [[2], [1], 2], [10, 1, 2], [1.1, 1.2, 1]]) | |
for (var i = 0; i < 100; i++) { | |
var xs = genArray(genArray(randFloat))() | |
this.check(eq, sorted(xs), sorted, xs, comparing(cerealise)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment