krl-stdlib
Advanced tools
Comparing version 0.44.0 to 0.45.0
{ | ||
"name": "krl-stdlib", | ||
"version": "0.44.0", | ||
"version": "0.45.0", | ||
"description": "Standard library for KRL", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
147
src/index.js
@@ -37,29 +37,18 @@ var _ = require("lodash"); | ||
// some guidelines/suggestions: | ||
// 0. effectively check arguments.length when not fixed by the grammar (and consider null versus omitted) | ||
// 1. convert NaN's/void 0's when needed (i.e. use cleanNulls) | ||
// 2. don't mutate arguments (array/map) | ||
// 3. where strings/numbers/arrays are expected, convert to them when reasonable (don't coerce arrays to maps) | ||
// 4. prioritize errors on val's type (if applicable), then argument values/types/0. from left to right | ||
// 5. try to return the logical noop value (e.g. false, [], val (unchanged by 3.)) for missing or unrecoverably wrongly typed arguments | ||
// 6. don't worry about call stack limits when processing deep objects - Lodash is incorrect there too | ||
// 7. the wiki's docs take precedence over the above | ||
var stdlib = {}; | ||
//Infix operators/////////////////////////////////////////////////////////////// | ||
var ltEqGt = function(left, right){ | ||
if(types.typeOf(left) !== types.typeOf(right)){ | ||
return NaN; // unlike -1/0/1, all comparisons with 0 are false | ||
var a = types.toNumberOrNull(left); | ||
var b = types.toNumberOrNull(right); | ||
if(a === null || b === null){ | ||
// if both are not numbers, fall back on string comparison | ||
a = types.toString(left); | ||
b = types.toString(right); | ||
} | ||
left = types.cleanNulls(left); | ||
right = types.cleanNulls(right); | ||
if(_.isEqual(left, right)){ | ||
return 0; | ||
} | ||
if(types.isArrayOrMap(left)){ | ||
return NaN; // don't compare unequal arrays or maps | ||
} | ||
return (left > right) ? 1 : -1; | ||
// at this point a and b are both numbers or both strings | ||
return a === b ? 0 : (a > b ? 1 : -1); | ||
}; | ||
var stdlib = {}; | ||
//Infix operators/////////////////////////////////////////////////////////////// | ||
stdlib["<"] = function(ctx, left, right){ | ||
@@ -96,3 +85,3 @@ return ltEqGt(left, right) < 0; | ||
stdlib["-"] = function(ctx, left, right){ | ||
var leftNumber = types.numericCast(left); | ||
var leftNumber = types.toNumberOrNull(left); | ||
if(arguments.length < 3){ | ||
@@ -104,3 +93,3 @@ if(leftNumber === null){ | ||
} | ||
var rightNumber = types.numericCast(right); | ||
var rightNumber = types.toNumberOrNull(right); | ||
if(leftNumber === null || rightNumber === null){ | ||
@@ -112,4 +101,4 @@ throw new TypeError(types.toString(right) + " cannot be subtracted from " + types.toString(left)); | ||
stdlib["*"] = function(ctx, left, right){ | ||
var leftNumber = types.numericCast(left); | ||
var rightNumber = types.numericCast(right); | ||
var leftNumber = types.toNumberOrNull(left); | ||
var rightNumber = types.toNumberOrNull(right); | ||
if(leftNumber === null || rightNumber === null){ | ||
@@ -121,4 +110,4 @@ throw new TypeError(types.toString(left) + " cannot be multiplied by " + types.toString(right)); | ||
stdlib["/"] = function(ctx, left, right){ | ||
var leftNumber = types.numericCast(left); | ||
var rightNumber = types.numericCast(right); | ||
var leftNumber = types.toNumberOrNull(left); | ||
var rightNumber = types.toNumberOrNull(right); | ||
if(leftNumber === null || rightNumber === null){ | ||
@@ -128,3 +117,4 @@ throw new TypeError(types.toString(left) + " cannot be divided by " + types.toString(right)); | ||
if(rightNumber === 0){ | ||
throw new RangeError(leftNumber + " / 0 is not a number"); | ||
ctx.emit("debug", "[DIVISION BY ZERO] " + leftNumber + " / 0"); | ||
return 0; | ||
} | ||
@@ -134,4 +124,4 @@ return leftNumber / rightNumber; | ||
stdlib["%"] = function(ctx, left, right){ | ||
var leftNumber = types.numericCast(left); | ||
var rightNumber = types.numericCast(right); | ||
var leftNumber = types.toNumberOrNull(left); | ||
var rightNumber = types.toNumberOrNull(right); | ||
if(leftNumber === null || rightNumber === null){ | ||
@@ -166,17 +156,8 @@ throw new TypeError("Cannot calculate " + types.toString(left) + " modulo " + types.toString(right)); | ||
stdlib["<=>"] = function(ctx, left, right){ | ||
var leftNumber = types.numericCast(left); | ||
var rightNumber = types.numericCast(right); | ||
if(leftNumber !== null && rightNumber !== null){ | ||
return ltEqGt(leftNumber, rightNumber); | ||
} | ||
var result = ltEqGt(left, right); | ||
if(_.isNaN(result)){ | ||
throw new TypeError("The <=> operator will not compare " + types.toString(left) + " with " + types.toString(right)); | ||
} | ||
return result; | ||
return ltEqGt(left, right); | ||
}; | ||
stdlib.cmp = function(ctx, left, right){ | ||
var leftStr = types.toString(left); | ||
var rightStr = types.toString(right); | ||
return ltEqGt(leftStr, rightStr); | ||
left = types.toString(left); | ||
right = types.toString(right); | ||
return left === right ? 0 : (left > right ? 1 : -1); | ||
}; | ||
@@ -213,13 +194,3 @@ | ||
if(type === "Number"){ | ||
if(val_type === "Null"){ | ||
return 0; | ||
}else if(val_type === "Boolean"){ | ||
return val ? 1 : 0; | ||
}else if(val_type === "String"){ | ||
var n = parseFloat(val); | ||
return types.isNumber(n) | ||
? n | ||
: null; | ||
} | ||
return null; | ||
return types.toNumberOrNull(val); | ||
} | ||
@@ -289,3 +260,3 @@ if(type === "RegExp"){ | ||
stdlib.chr = function(ctx, val){ | ||
var code = types.numericCast(val); | ||
var code = types.toNumberOrNull(val); | ||
if(code === null){ | ||
@@ -297,6 +268,9 @@ return null; | ||
stdlib.range = function(ctx, val, end){ | ||
var startNumber = types.numericCast(val); | ||
var endNumber = types.numericCast(end); | ||
if(arguments.length < 3){ | ||
return []; | ||
} | ||
var startNumber = types.toNumberOrNull(val); | ||
var endNumber = types.toNumberOrNull(end); | ||
if(startNumber === null || endNumber === null){ | ||
return []; // we could return [number] if one of them is a number | ||
return []; | ||
} | ||
@@ -375,11 +349,11 @@ if(startNumber < endNumber){ | ||
stdlib.substr = function(ctx, val, start, len){ | ||
start = types.numericCast(start); | ||
val = types.toString(val); | ||
start = types.toNumberOrNull(start); | ||
len = types.toNumberOrNull(len); | ||
if(start === null){ | ||
return val; | ||
} | ||
val = types.toString(val); | ||
if(start > val.length){ | ||
return null; | ||
return ""; | ||
} | ||
len = types.numericCast(len); | ||
var end; | ||
@@ -519,3 +493,2 @@ if(len === null){ | ||
}; | ||
//works for maps for weak typing purposes | ||
stdlib.length = function(ctx, val){ | ||
@@ -525,3 +498,3 @@ if(types.isArrayOrMap(val) || types.isString(val)){ | ||
} | ||
return 0; // we could check function.prototype.length | ||
return 0; | ||
}; | ||
@@ -624,4 +597,3 @@ stdlib.map = function*(ctx, val, iter){ | ||
}else if(val.length === 0){ | ||
ctx.emit("error", new Error("Cannot .slice() an empty array")); | ||
return null; | ||
return []; | ||
} | ||
@@ -631,3 +603,3 @@ if(arguments.length === 2){ | ||
} | ||
var firstIndex = types.numericCast(start); | ||
var firstIndex = types.toNumberOrNull(start); | ||
if(firstIndex === null){ | ||
@@ -638,8 +610,7 @@ throw new TypeError("The .slice() operator cannot use " + types.toString(start) + " as an index"); | ||
if(firstIndex > val.length){ | ||
ctx.emit("error", new RangeError("Cannot .slice() an array of length " + val.length + " from 0 to " + firstIndex)); | ||
return null; | ||
return []; | ||
} | ||
return _.slice(val, 0, firstIndex + 1); | ||
} | ||
var secondIndex = types.numericCast(end); | ||
var secondIndex = types.toNumberOrNull(end); | ||
if(secondIndex === null){ | ||
@@ -656,33 +627,25 @@ throw new TypeError("The .slice() operator cannot use " + types.toString(end) + " as the other index"); | ||
} | ||
ctx.emit("error", new RangeError("Cannot .slice() an array of length " + val.length + " from " + firstIndex + " to " + secondIndex)); | ||
return null; | ||
return []; | ||
}; | ||
stdlib.splice = function(ctx, val, start, n_elms, value){ | ||
stdlib.splice = function(ctx, val, start, nElements, value){ | ||
if(!types.isArray(val)){ | ||
val = [val]; | ||
}else if(val.length === 0){ | ||
throw new Error("Cannot .splice() an empty array"); | ||
return []; | ||
} | ||
if(arguments.length < 4){ | ||
throw new Error("The .splice() operator needs more than one argument"); | ||
} | ||
var startIndex = types.numericCast(start); | ||
var startIndex = types.toNumberOrNull(start); | ||
if(startIndex === null){ | ||
throw new TypeError("The .splice() operator cannot use " + types.toString(start) + "as an index"); | ||
} | ||
if(startIndex < 0){ | ||
throw new RangeError("Cannot start .splice() starting at index " + startIndex); | ||
startIndex = Math.min(Math.max(startIndex, 0), val.length - 1); | ||
var n_elm = types.toNumberOrNull(nElements); | ||
if(n_elm === null){ | ||
throw new TypeError("The .splice() operator cannot use " + types.toString(nElements) + "as a number of elements"); | ||
} | ||
if(startIndex >= val.length){ | ||
throw new RangeError("Cannot .splice() an array of length " + val.length + " starting at index " + startIndex); | ||
if(n_elm < 0 || startIndex + n_elm > val.length){ | ||
n_elm = val.length - startIndex; | ||
} | ||
var n_elms_number = types.numericCast(n_elms); | ||
if(n_elms_number === null){ | ||
throw new TypeError("The .splice() operator cannot use " + types.toString(n_elms) + "as a number of elements"); | ||
} | ||
if(n_elms_number < 0 || startIndex + n_elms_number > val.length){ | ||
n_elms_number = val.length - startIndex; | ||
} | ||
var part1 = _.slice(val, 0, startIndex); | ||
var part2 = _.slice(val, startIndex + n_elms); | ||
var part2 = _.slice(val, startIndex + n_elm); | ||
if(arguments.length < 5){ | ||
@@ -689,0 +652,0 @@ return _.concat(part1, part2); |
136
src/tests.js
@@ -61,2 +61,6 @@ var _ = require("lodash"); | ||
if(!message){ | ||
message = "testing stdlib[\"" + fn + "\"]"; | ||
} | ||
if(useStrict(expected)){ | ||
@@ -78,12 +82,2 @@ strictDeepEquals(t, stdlib[fn].apply(null, [emitCTX].concat(args)), expected, message); | ||
var tfMatrix = function(tf, args, exp){ | ||
var i; | ||
for(i=0; i < exp.length; i++){ | ||
var j; | ||
for(j=0; j < args.length; j++){ | ||
tf(exp[i][0], args[j], exp[i][j+1]); | ||
} | ||
} | ||
}; | ||
var ytfMatrix = function*(ytf, obj, args, exp){ | ||
@@ -164,4 +158,4 @@ var i; | ||
tfe("-", ["zero"], "TypeError"); | ||
tfe("-", [[0]], "TypeError"); | ||
tfe("-", [{}], "TypeError"); | ||
tf("-", [[0]], -1); | ||
tf("-", [{}], 0); | ||
@@ -173,3 +167,4 @@ tf("-", [1, 3], -2); | ||
tfe("-", ["two", 1], "TypeError"); | ||
tfe("-", [[], "-1"], "TypeError"); | ||
tf("-", [[], "-1"], 1); | ||
tf("-", [{a:1,b:1,c:1}, [1]], 2, "map.length() - array.length()"); | ||
@@ -186,32 +181,19 @@ tf("==", [null, NaN], true); | ||
tf("==", [true, 1], false); | ||
tf("==", [{a: ["b"]}, {a: ["b"]}], true); | ||
tf("==", [{a: ["b"]}, {a: ["c"]}], false); | ||
tfMatrix(tf, [ | ||
[2, 10], // 1 | ||
[6, 6], // 2 | ||
[10, 2], // 3 | ||
["2", "10"], // 4 | ||
["6", "6"], // 5 | ||
["10", "2"], // 6 | ||
[NaN, null], // 7 | ||
[["a", 0], ["a", 0]], // 8 | ||
[{"a": 0}, {"a": 0}], // 9 | ||
[["a", 0], ["b", 1]], // 10 | ||
[{"a": 1}, {"b": 0}], // 11 | ||
], [ // 1 2 3 4 5 6 7 8 9 10 11 | ||
["<", true, false, false, false, false, true, false, false, false, false, false], | ||
[">", false, false, true, true, false, false, false, false, false, false, false], | ||
["<=", true, true, false, false, true, true, true, true, true, false, false], | ||
[">=", false, true, true, true, true, false, true, true, true, false, false], | ||
["==", false, true, false, false, true, false, true, true, true, false, false], | ||
["!=", true, false, true, true, false, true, false, false, false, true, true], | ||
]); | ||
tf("*", [5, 2], 10); | ||
tfe("*", ["two", 1], "TypeError"); | ||
tfe("*", [[], "-1"], "TypeError"); | ||
tfe("*", [1, _.noop], "TypeError"); | ||
tf("/", [4, 2], 2); | ||
tfe("/", ["two", 1], "TypeError"); | ||
tfe("/", [[], "-1"], "TypeError"); | ||
tfe("/", ["1", "0"], "RangeError"); | ||
tf("/", ["2", 1], 2); | ||
tfe("/", [1, _.noop], "TypeError"); | ||
t.equals(stdlib["/"]({ | ||
emit: function(kind, err){ | ||
t.equals(kind + err, "debug[DIVISION BY ZERO] 9 / 0"); | ||
} | ||
}, 9, 0), 0); | ||
@@ -221,3 +203,4 @@ tf("%", [4, 2], 0); | ||
tfe("%", [1, "two"], "TypeError"); | ||
tfe("%", [[], "-1"], "TypeError"); | ||
tf("%", [[1,2,3,4], [1,2]], 0, ".length() % .length()"); | ||
tfe("%", [_.noop, 1], "TypeError"); | ||
@@ -232,11 +215,38 @@ tf("like", ["wat", /a/], true); | ||
tf("<=>", ["10", 5], 1); | ||
tf("<=>", [{" ":-.5}, {" ":-.5}], 0); | ||
tf("<=>", [NaN, void 0], 0); | ||
tfe("<=>", [null, 0], "TypeError"); | ||
tfe("<=>", [[0, 1], [1, 1]], "TypeError"); | ||
tf("<=>", [null, 0], 0); | ||
tf("<=>", [null, false], 0); | ||
tf("<=>", [true, 1], 0); | ||
tf("<=>", [true, false], 1); | ||
tf("<=>", [[0, 1], [1, 1]], 0); | ||
tf("<=>", [20, 3], 1); | ||
tf("<=>", ["20", 3], 1); | ||
tf("<=>", [20, "3"], 1); | ||
tf("<=>", ["20", "3"], 1, "parse numbers then compare"); | ||
tf("<=>", [".2", .02], 1, "parse numbers then compare"); | ||
tf("<=>", [["a", "b"], 2], 0, ".length() of arrays"); | ||
tf("<=>", [{" ":-.5}, 1], 0, ".length() of maps"); | ||
tf("<=>", [[1,2,3], {a:"b",z:"y",c:"d"}], 0, "compare the .length() of each"); | ||
tf("<=>", [_.noop, "[Function]"], 0, "Functions drop down to string compare"); | ||
tf("<=>", [action, "[Action]"], 0, "Actions drop down to string compare"); | ||
tf("<=>", [/good/, "re#good#"], 0, "RegExp drop down to string compare"); | ||
tf("<=>", [1, "a"], -1, "if both can't be numbers, then string compare"); | ||
tf("<=>", ["to", true], -1, "if both can't be numbers, then string compare"); | ||
// <, >, <=, >= all use <=> under the hood | ||
tf("<", ["3", "20"], true); | ||
tf(">", ["a", "b"], false); | ||
tf("<=", ["a", "a"], true); | ||
tf("<=", ["a", "b"], true); | ||
tf("<=", ["b", "a"], false); | ||
tf(">=", ["a", "a"], true); | ||
tf(">=", ["a", "b"], false); | ||
tf(">=", ["b", "a"], true); | ||
tf("cmp", ["aab", "abb"], -1); | ||
tf("cmp", ["aab", "aab"], 0); | ||
tf("cmp", ["abb", "aab"], 1); | ||
tf("cmp", [void 0, NaN], 0); | ||
tf("cmp", [void 0, NaN], 0, "\"null\" === \"null\""); | ||
tf("cmp", ["5", "10"], 1); | ||
@@ -248,2 +258,3 @@ tf("cmp", [5, "5"], 0); | ||
tf("cmp", [null, 0], 1); | ||
tf("cmp", [20, 3], -1, "cmp always converts to string then compares"); | ||
@@ -282,4 +293,5 @@ t.end(); | ||
tf("as", ["foo", "Number"], null); | ||
tf("as", [[1,2], "Number"], null); | ||
tf("as", [arguments, "Number"], null); | ||
tf("as", [{}, "Number"], 0); | ||
tf("as", [[1,2], "Number"], 2); | ||
tf("as", [{a:"b",z:"y",c:"d"}, "Number"], 3); | ||
@@ -352,4 +364,5 @@ t.equals(stdlib.as(defaultCTX, "^a.*z$", "RegExp").source, /^a.*z$/.source); | ||
tf("range", [-4], []); | ||
tf("range", [null, 0], []); | ||
tf("range", [0, []], []); | ||
tf("range", [-4, _.noop], []); | ||
tf("range", [null, 0], [0], "range auto convert null -> 0"); | ||
tf("range", [0, [1,2,3]], [0, 1, 2, 3], "0.range(.length())"); | ||
@@ -431,5 +444,5 @@ tf("sprintf", [.25], ""); | ||
tf("substr", ["This is a string", 16, -1], "g"); | ||
tf("substr", ["This is a string", 25], null); | ||
tf("substr", [["Not a string", void 0]], ["Not a string", void 0]); | ||
tf("substr", [void 0, "Not an index", 2], void 0); | ||
tf("substr", ["This is a string", 25], ""); | ||
tf("substr", [["Not a string", void 0]], "[Array]"); | ||
tf("substr", [void 0, "Not an index", 2], "null"); | ||
@@ -536,3 +549,3 @@ tf("uc", ["loWer"], "LOWER"); | ||
}); | ||
yield ytf("collect", [null, collectFn], {"y": [null]}); | ||
yield ytf("collect", [null, collectFn], {"x": [null]}); | ||
yield ytf("collect", [[], fnDontCall], {}); | ||
@@ -644,9 +657,10 @@ yield ytf("collect", [[7]], {}); | ||
tf("slice", [veggies, 0, 0], ["corn"]); | ||
tf("slice", [veggies, null, NaN], ["corn"]); | ||
tf("slice", [[], 0, 0], []); | ||
tf("slice", [{"0": "0"}, 0, 0], [{"0": "0"}]); | ||
tf("slice", [[], _.noop], null, "error", "Error"); | ||
tfe("slice", [veggies, _.noop], "TypeError"); | ||
tfe("slice", [veggies, 1, _.noop], "TypeError"); | ||
tfe("slice", [veggies, -1, _.noop], "TypeError"); | ||
tf("slice", [veggies, 14], null, "error", "RangeError"); | ||
tf("slice", [veggies, 2, -1], null, "error", "RangeError"); | ||
tf("slice", [veggies, 14], []); | ||
tf("slice", [veggies, 2, -1], []); | ||
t.deepEquals(veggies, ["corn","tomato","tomato","tomato","sprouts","lettuce","sprouts"], "should not be mutated"); | ||
@@ -661,8 +675,14 @@ | ||
tf("splice", [veggies, 1, 10, []], ["corn"]); | ||
tfe("splice", [[], NaN], "Error"); | ||
tfe("splice", [void 0, NaN, []], "TypeError"); | ||
tfe("splice", [void 0, -1, []], "RangeError"); | ||
tfe("splice", [veggies, 7, []], "RangeError"); | ||
tfe("splice", [veggies, 6, []], "TypeError"); | ||
tf("splice", [void 0, 0, 0, []], [void 0]); | ||
tf("splice", [[], 0, 1], []); | ||
tf("splice", [[], NaN], []); | ||
tfe("splice", [veggies, _.noop, 1] , "TypeError"); | ||
tfe("splice", [veggies, 0, _.noop] , "TypeError"); | ||
tf("splice", [veggies, 0, 0], veggies); | ||
tf("splice", [veggies, 0, veggies.length], []); | ||
tf("splice", [veggies, 0, 999], []); | ||
tf("splice", [veggies, 0, -1], []); | ||
tf("splice", [veggies, 0, -999], []); | ||
tf("splice", [veggies, -1, 0], veggies); | ||
tf("splice", [veggies, -999, 0], veggies); | ||
tf("splice", [void 0, 0, 0], [void 0]); | ||
t.deepEquals(veggies, ["corn","tomato","tomato","tomato","sprouts","lettuce","sprouts"], "should not be mutated"); | ||
@@ -669,0 +689,0 @@ |
@@ -97,14 +97,22 @@ var _ = require("lodash"); | ||
types.numericCast = function(val, round){ | ||
var roundFn = round ? _.round : _.identity; | ||
if(types.isNumber(val)){ | ||
return roundFn(val); | ||
types.toNumberOrNull = function(val){ | ||
switch(types.typeOf(val)){ | ||
case "Null": | ||
return 0; | ||
case "Boolean": | ||
return val ? 1 : 0; | ||
case "String": | ||
var n = parseFloat(val); | ||
return types.isNumber(n) ? n : null; | ||
case "Number": | ||
return val; | ||
case "Array": | ||
case "Map": | ||
return _.size(val); | ||
case "RegExp": | ||
case "Function": | ||
case "Action": | ||
} | ||
if(!types.isString(val)){ | ||
return null; | ||
} | ||
var n = parseFloat(val); | ||
return types.isNumber(n) | ||
? roundFn(n) | ||
: null; | ||
return null; | ||
}; | ||
@@ -111,0 +119,0 @@ |
66756
1927