From 5cb7673485a56e4307c74d4a3f31e3f3b0efe673 Mon Sep 17 00:00:00 2001 From: Yurii Rashkovskii Date: Thu, 23 Dec 2010 01:51:26 -0800 Subject: [PATCH] Added assert.js (and its dependency, narwhal/util.js). Not sure how permanent this solution is, but we need to run CommonJS tests. --- Makefile | 3 + lib/assert.js | 323 ++++++++++++ lib/narwhal/util.js | 1131 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1457 insertions(+) create mode 100644 lib/assert.js create mode 100644 lib/narwhal/util.js diff --git a/Makefile b/Makefile index 06f9528..2b01601 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,9 @@ dependencies: deps/erlv8/ebin/erlv8.beam test: compile @./rebar eunit +test-commonjs: compile + @./beamjs -norepl -jspath lib -load deps/commonjs/tests/unit-testing/1.0/program.js + compile: dependencies @./rebar compile @cat ebin/beamjs.app | sed s/%sha%/`git log -1 --pretty=format:%h`/ > ebin/beamjs.app diff --git a/lib/assert.js b/lib/assert.js new file mode 100644 index 0000000..042a934 --- /dev/null +++ b/lib/assert.js @@ -0,0 +1,323 @@ +// Originally from narwhal +// http://wiki.commonjs.org/wiki/Unit_Testing/1.0 + +// -- zaach Zachary Carter +// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License +// original skeleton +// -- felixge Felix Geisendörfer +// editions backported from NodeJS +// -- Karl Guertin +// editions backported from NodeJS +// -- ashb Ash Berlin +// contributions annotated + +var util = require("narwhal/util"); +var sys = require('sys'); + +// 1. The assert module provides functions that throw +// AssertionError's when particular conditions are not met. The +// assert module must conform to the following interface. + +var assert = exports; + +// 2. The AssertionError is defined in assert. +// new assert.AssertionError({message: message, actual: actual, expected: expected}) + +assert.AssertionError = function (options) { + if (typeof options == "string") + options = {"message": options}; + this.name = "AssertionError"; + this.message = options.message; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + + // V8 specific + if (Error.captureStackTrace) { + Error.captureStackTrace(this, (this.fail || assert.fail)); + // Node specific, removes the node machinery stack frames + // XXX __filename will probably not always be the best way to detect Node + if (typeof __filename !== undefined) { + var stack = this.stack.split("\n"); + for (var i = stack.length - 1; i >= 0; i--) { + if (stack[i].indexOf(__filename) != -1) { + this.stack = stack.slice(0, i + 2).join("\n"); + break; + } + } + } + } + +}; + +// XXX extension +// Ash Berlin +assert.AssertionError.prototype.toString = function (){ + if (this.message) { + return [ + this.name + ":", + this.message + ].join(" "); + } else { + return [ + this.name + ":", + sys.inspect(this.expected), + this.operator, + sys.inspect(this.actual) + ].join(" "); + } +} + +// XXX extension +// Ash Berlin +assert.AssertionError.prototype.toSource = function () { + return "new (require('assert').AssertionError)(" + Object.prototype.toSource.call(this) + ")"; +}; + +// assert.AssertionError instanceof Error + +assert.AssertionError.prototype = Object.create(Error.prototype); + +// At present only the three keys mentioned above are used and +// understood by the spec. Implementations or sub modules can pass +// other keys to the AssertionError's constructor - they will be +// ignored. + +// 3. All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. + +assert.fail = function (options) { + throw new assert.AssertionError(options); +}; + +// XXX extension +// stub for logger protocol +assert.pass = function () { +}; + +// XXX extension +// stub for logger protocol +assert.error = function () { +}; + +// XXX extension +// stub for logger protocol +assert.section = function () { + return this; +}; + +// 4. Pure assertion tests whether a value is truthy, as determined +// by !!guard. +// assert.ok(guard, message_opt); +// This statement is equivalent to assert.equal(true, guard, +// message_opt);. To test strictly for the value true, use +// assert.strictEqual(true, guard, message_opt);. + +assert.ok = function (value, message) { + if (!!!value) + (this.fail || assert.fail)({ + "actual": value, + "expected": true, + "message": message, + "operator": "==" + }); + else + (this.pass || assert.pass)(message); +}; + +// 5. The equality assertion tests shallow, coercive equality with +// ==. +// assert.equal(actual, expected, message_opt); + +assert.equal = function (actual, expected, message) { + if (actual != expected) + (this.fail || assert.fail)({ + "actual": actual, + "expected": expected, + "message": message, + "operator": "==" + }); + else + (this.pass || assert.pass)(message); +}; + + +// 6. The non-equality assertion tests for whether two objects are not equal +// with != assert.notEqual(actual, expected, message_opt); + +assert.notEqual = function (actual, expected, message) { + if (actual == expected) + (this.fail || assert.fail)({ + "actual": actual, + "expected": expected, + "message": message, + "operator": "!=" + }); + else + (this.pass || assert.pass)(message); +}; + +// 7. The equivalence assertion tests a deep equality relation. +// assert.deepEqual(actual, expected, message_opt); + +assert.deepEqual = function (actual, expected, message) { + if (!deepEqual(actual, expected)) + (this.fail || assert.fail)({ + "actual": actual, + "expected": expected, + "message": message, + "operator": "deepEqual" + }); + else + (this.pass || assert.pass)(message); +}; + +function deepEqual(actual, expected) { + + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (actual instanceof Date && expected instanceof Date) { + return actual.getTime() === expected.getTime(); + + // 7.3. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + } else if (typeof actual != 'object' && typeof expected != 'object') { + return actual == expected; + + // XXX specification bug: this should be specified + } else if (typeof expected == "string" || typeof actual == "string") { + return expected === actual; + + // 7.4. For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical "prototype" property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return actual.prototype === expected.prototype && objEquiv(actual, expected); + } +} + +function objEquiv(a, b, stack) { + return ( + !util.no(a) && !util.no(b) && + arrayEquiv( + util.sort(util.object.keys(a)), + util.sort(util.object.keys(b)) + ) && + util.object.keys(a).every(function (key) { + return deepEqual(a[key], b[key], stack); + }) + ); +} + +function arrayEquiv(a, b, stack) { + return util.isArrayLike(b) && + a.length == b.length && + util.zip(a, b).every(util.apply(function (a, b) { + return deepEqual(a, b, stack); + })); +} + +// 8. The non-equivalence assertion tests for any deep inequality. +// assert.notDeepEqual(actual, expected, message_opt); + +assert.notDeepEqual = function (actual, expected, message) { + if (deepEqual(actual, expected)) + (this.fail || assert.fail)({ + "actual": actual, + "expected": expected, + "message": message, + "operator": "notDeepEqual" + }); + else + (this.pass || assert.pass)(message); +}; + +// 9. The strict equality assertion tests strict equality, as determined by ===. +// assert.strictEqual(actual, expected, message_opt); + +assert.strictEqual = function (actual, expected, message) { + if (actual !== expected) + (this.fail || assert.fail)({ + "actual": actual, + "expected": expected, + "message": message, + "operator": "===" + }); + else + (this.pass || assert.pass)(message); +}; + +// 10. The strict non-equality assertion tests for strict inequality, as determined by !==. +// assert.notStrictEqual(actual, expected, message_opt); + +assert.notStrictEqual = function (actual, expected, message) { + if (actual === expected) + (this.fail || assert.fail)({ + "actual": actual, + "expected": expected, + "message": message, + "operator": "!==" + }); + else + (this.pass || assert.pass)(message); +}; + +// 11. Expected to throw an error: +// assert.throws(block, Error_opt, message_opt); + +assert["throws"] = function (block, Error, message) { + var threw = false, + exception = null; + + // (block) + // (block, message:String) + // (block, Error) + // (block, Error, message) + + if (typeof Error == "string") { + message = Error; + Error = undefined; + } + + try { + block(); + } catch (e) { + threw = true; + exception = e; + } + + if (!threw) { + (this.fail || assert.fail)({ + "message": message, + "operator": "throw" + }); + } else if (Error) { + if (exception instanceof Error) + (this.pass || assert.pass)(message); + else + throw exception; + } else { + (this.pass || assert.pass)(message); + } + +}; + +// XXX extension +assert.Assert = function (log) { + var self = Object.create(assert); + self.pass = log.pass.bind(log); + self.fail = log.fail.bind(log); + self.error = log.error.bind(log); + self.section = log.section.bind(log); + return self; +}; diff --git a/lib/narwhal/util.js b/lib/narwhal/util.js new file mode 100644 index 0000000..1a0a37a --- /dev/null +++ b/lib/narwhal/util.js @@ -0,0 +1,1131 @@ +// Originally from narwhal +// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License +// -- isaacs Isaac Schlueter +// -- nrstott Nathan Stott +// -- fitzgen Nick Fitzgerald +// -- nevilleburnell Neville Burnell +// -- cadorn Christoph Dorn + +// a decorator for functions that curry "polymorphically", +// that is, that return a function that can be tested +// against various objects if they're only "partially +// completed", or fewer arguments than needed are used. +// +// this enables the idioms: +// [1, 2, 3].every(lt(4)) eq true +// [1, 2, 3].map(add(1)) eq [2, 3, 4] +// [{}, {}, {}].forEach(set('a', 10)) +// +exports.operator = function (name, length, block) { + var operator = function () { + var args = exports.array(arguments); + var completion = function (object) { + if ( + typeof object == "object" && + object !== null && // seriously? typeof null == "object" + name in object && // would throw if object === null + // not interested in literal objects: + !Object.prototype.hasOwnProperty.call(object, name) + ) + return object[name].apply(object, args); + return block.apply( + this, + [object].concat(args) + ); + }; + if (arguments.length < length) { + // polymoprhic curry, delayed completion + return completion; + } else { + // immediate completion + return completion.call(this, args.shift()); + } + }; + operator.name = name; + operator.displayName = name; + operator.length = length; + operator.operator = block; + return operator; +}; + +exports.no = function (value) { + return value === null || value === undefined; +}; + +// object + +exports.object = exports.operator('toObject', 1, function (object) { + var items = object; + if (!items.length) + items = exports.items(object); + var copy = {}; + for (var i = 0; i < items.length; i++) { + var item = items[i]; + var key = item[0]; + var value = item[1]; + copy[key] = value; + } + return copy; +}); + +exports.object.copy = function (object) { + var copy = {}; + exports.object.keys(object).forEach(function (key) { + copy[key] = object[key]; + }); + return copy; +}; + +exports.object.deepCopy = function (object) { + var copy = {}; + exports.object.keys(object).forEach(function (key) { + copy[key] = exports.deepCopy(object[key]); + }); + return copy; +}; + +exports.object.eq = function (a, b, stack) { + return ( + !exports.no(a) && !exports.no(b) && + exports.array.eq( + exports.sort(exports.object.keys(a)), + exports.sort(exports.object.keys(b)) + ) && + exports.object.keys(a).every(function (key) { + return exports.eq(a[key], b[key], stack); + }) + ); +}; + +exports.object.len = function (object) { + return exports.object.keys(object).length; +}; + +exports.object.has = function (object, key) { + return Object.prototype.hasOwnProperty.call(object, key); +}; + +exports.object.keys = function (object) { + var keys = []; + for (var key in object) { + if (exports.object.has(object, key)) + keys.push(key); + } + return keys; +}; + +exports.object.values = function (object) { + var values = []; + exports.object.keys(object).forEach(function (key) { + values.push(object[key]); + }); + return values; +}; + +exports.object.items = function (object) { + var items = []; + exports.object.keys(object).forEach(function (key) { + items.push([key, object[key]]); + }); + return items; +}; + +/** + * Updates an object with the properties from another object. + * This function is variadic requiring a minimum of two arguments. + * The first argument is the object to update. Remaining arguments + * are considered the sources for the update. If multiple sources + * contain values for the same property, the last one with that + * property in the arguments list wins. + * + * example usage: + * util.update({}, { hello: "world" }); // -> { hello: "world" } + * util.update({}, { hello: "world" }, { hello: "le monde" }); // -> { hello: "le monde" } + * + * @returns Updated object + * @type Object + * + */ +exports.object.update = function () { + return variadicHelper(arguments, function(target, source) { + var key; + for (key in source) { + if (exports.object.has(source, key)) { + target[key] = source[key]; + } + } + }); +}; + +exports.object.deepUpdate = function (target, source) { + var key; + for (key in source) { + if(exports.object.has(source, key)) { + if(typeof source[key] == "object" && exports.object.has(target, key)) { + exports.object.deepUpdate(target[key], source[key]); + } else { + target[key] = source[key]; + } + } + } +}; + +/** + * Updates an object with the properties of another object(s) if those + * properties are not already defined for the target object. First argument is + * the object to complete, the remaining arguments are considered sources to + * complete from. If multiple sources contain the same property, the value of + * the first source with that property will be the one inserted in to the + * target. + * + * example usage: + * util.complete({}, { hello: "world" }); // -> { hello: "world" } + * util.complete({ hello: "narwhal" }, { hello: "world" }); // -> { hello: "narwhal" } + * util.complete({}, { hello: "world" }, { hello: "le monde" }); // -> { hello: "world" } + * + * @returns Completed object + * @type Object + * + */ +exports.object.complete = function () { + return variadicHelper(arguments, function(target, source) { + var key; + for (key in source) { + if ( + exports.object.has(source, key) && + !exports.object.has(target, key) + ) { + target[key] = source[key]; + } + } + }); +}; + +exports.object.deepComplete = function () { + return variadicHelper(arguments, function (target, source) { + var key; + for (key in source) { + if ( + exports.object.has(source, key) && + !exports.object.has(target, key) + ) { + target[key] = exports.deepCopy(source[key]); + } + } + }); +}; + +exports.object.repr = function (object) { + return "{" + + exports.object.keys(object) + .map(function (key) { + return exports.enquote(key) + ": " + + exports.repr(object[key]); + }).join(", ") + + "}"; +}; + +/** + * @param args Arguments list of the calling function + * First argument should be a callback that takes target and source parameters. + * Second argument should be target. + * Remaining arguments are treated a sources. + * + * @returns Target + * @type Object + */ +var variadicHelper = function (args, callback) { + var sources = Array.prototype.slice.call(args); + var target = sources.shift(); + + sources.forEach(function(source) { + callback(target, source); + }); + + return target; +}; + +// array + +exports.array = function (array) { + if (exports.no(array)) + return []; + if (!exports.isArrayLike(array)) { + if ( + array.toArray && + !Object.prototype.hasOwnProperty.call(array, 'toArray') + ) { + return array.toArray(); + } else if ( + array.forEach && + !Object.prototype.hasOwnProperty.call(array, 'forEach') + ) { + var results = []; + array.forEach(function (value) { + results.push(value); + }); + return results; + } else if (typeof array === "string") { + return Array.prototype.slice.call(array); + } else { + return exports.items(array); + } + } + return Array.prototype.slice.call(array); +}; + +exports.array.coerce = function (array) { + if (!Array.isArray(array)) + return exports.array(array); + return array; +}; + +exports.isArrayLike = function(object) { + return Array.isArray(object) || exports.isArguments(object); +}; + +// from http://code.google.com/p/google-caja/wiki/NiceNeighbor +// by "kangax" +// +// Mark Miller posted a solution that will work in ES5 compliant +// implementations, that may provide future insight: +// (http://groups.google.com/group/narwhaljs/msg/116097568bae41c6) +exports.isArguments = function (object) { + // ES5 reliable positive + if (Object.prototype.toString.call(object) == "[object Arguments]") + return true; + // for ES5, we will still need a way to distinguish false negatives + // from the following code (in ES5, it is possible to create + // an object that satisfies all of these constraints but is + // not an Arguments object). + // callee should exist + if ( + !typeof object == "object" || + !Object.prototype.hasOwnProperty.call(object, 'callee') || + !object.callee || + // It should be a Function object ([[Class]] === 'Function') + Object.prototype.toString.call(object.callee) !== '[object Function]' || + typeof object.length != 'number' + ) + return false; + for (var name in object) { + // both "callee" and "length" should be { DontEnum } + if (name === 'callee' || name === 'length') return false; + } + return true; +}; + +exports.array.copy = exports.array; + +exports.array.deepCopy = function (array) { + return array.map(exports.deepCopy); +}; + +exports.array.len = function (array) { + return array.length; +}; + +exports.array.has = function (array, value) { + return Array.prototype.indexOf.call(array, value) >= 0; +}; + +exports.array.put = function (array, key, value) { + array.splice(key, 0, value); + return array; +}; + +exports.array.del = function (array, begin, end) { + array.splice(begin, end === undefined ? 1 : (end - begin)); + return array; +}; + +exports.array.eq = function (a, b, stack) { + return exports.isArrayLike(b) && + a.length == b.length && + exports.zip(a, b).every(exports.apply(function (a, b) { + return exports.eq(a, b, stack); + })); +}; + +exports.array.lt = function (a, b) { + var length = Math.max(a.length, b.length); + for (var i = 0; i < length; i++) + if (!exports.eq(a[i], b[i])) + return exports.lt(a[i], b[i]); + return false; +}; + +exports.array.repr = function (array) { + return "[" + exports.map(array, exports.repr).join(', ') + "]"; +}; + +exports.array.first = function (array) { + return array[0]; +}; + +exports.array.last = function (array) { + return array[array.length - 1]; +}; + +exports.apply = exports.operator('apply', 2, function (args, block) { + return block.apply(this, args); +}); + +exports.copy = exports.operator('copy', 1, function (object) { + if (exports.no(object)) + return object; + if (exports.isArrayLike(object)) + return exports.array.copy(object); + if (object instanceof Date) + return object; + if (typeof object == 'object') + return exports.object.copy(object); + return object; +}); + +exports.deepCopy = exports.operator('deepCopy', 1, function (object) { + if (exports.no(object)) + return object; + if (exports.isArrayLike(object)) + return exports.array.deepCopy(object); + if (typeof object == 'object') + return exports.object.deepCopy(object); + return object; +}); + +exports.repr = exports.operator('repr', 1, function (object) { + if (exports.no(object)) + return String(object); + if (exports.isArrayLike(object)) + return exports.array.repr(object); + if (typeof object == 'object' && !(object instanceof Date)) + return exports.object.repr(object); + if (typeof object == 'string') + return exports.enquote(object); + return object.toString(); +}); + +exports.keys = exports.operator('keys', 1, function (object) { + if (exports.isArrayLike(object)) + return exports.range(object.length); + else if (typeof object == 'object') + return exports.object.keys(object); + return []; +}); + +exports.values = exports.operator('values', 1, function (object) { + if (exports.isArrayLike(object)) + return exports.array(object); + else if (typeof object == 'object') + return exports.object.values(object); + return []; +}); + +exports.items = exports.operator('items', 1, function (object) { + if (exports.isArrayLike(object) || typeof object == "string") + return exports.enumerate(object); + else if (typeof object == 'object') + return exports.object.items(object); + return []; +}); + +exports.len = exports.operator('len', 1, function (object) { + if (exports.isArrayLike(object)) + return exports.array.len(object); + else if (typeof object == 'object') + return exports.object.len(object); +}); + +exports.has = exports.operator('has', 2, function (object, value) { + if (exports.isArrayLike(object)) + return exports.array.has(object, value); + else if (typeof object == 'object') + return exports.object.has(object, value); + return false; +}); + +exports.get = exports.operator('get', 2, function (object, key, value) { + if (typeof object == "string") { + if (!typeof key == "number") + throw new Error("TypeError: String keys must be numbers"); + if (!exports.has(exports.range(object.length), key)) { + if (arguments.length == 3) + return value; + throw new Error("KeyError: " + exports.repr(key)); + } + return object.charAt(key); + } + if (typeof object == "object") { + if (!exports.object.has(object, key)) { + if (arguments.length == 3) + return value; + throw new Error("KeyError: " + exports.repr(key)); + } + return object[key]; + } + throw new Error("Object does not have keys: " + exports.repr(object)); +}); + +exports.set = exports.operator('set', 3, function (object, key, value) { + object[key] = value; + return object; +}); + +exports.getset = exports.operator('getset', 3, function (object, key, value) { + if (!exports.has(object, key)) + exports.set(object, key, value); + return exports.get(object, key); +}); + +exports.del = exports.operator('del', 2, function (object, begin, end) { + if (exports.isArrayLike(object)) + return exports.array.del(object, begin, end); + delete object[begin]; + return object; +}); + +exports.cut = exports.operator('cut', 2, function (object, key) { + var result = exports.get(object, key); + exports.del(object, key); + return result; +}); + +exports.put = exports.operator('put', 2, function (object, key, value) { + if (exports.isArrayLike(object)) + return exports.array.put(object, key, value); + return exports.set(object, key, value); +}); + +exports.first = exports.operator('first', 1, function (object) { + return object[0]; +}); + +exports.last = exports.operator('last', 1, function (object) { + return object[object.length - 1]; +}); + +exports.update = exports.operator('update', 2, function () { + var args = Array.prototype.slice.call(arguments); + return exports.object.update.apply(this, args); +}); + +exports.deepUpdate = exports.operator('deepUpdate', 2, function (target, source) { + exports.object.deepUpdate(target, source); +}); + +exports.complete = exports.operator('complete', 2, function (target, source) { + var args = Array.prototype.slice.call(arguments); + return exports.object.complete.apply(this, args); +}); + +exports.deepComplete = exports.operator('deepComplete', 2, function (target, source) { + var args = Array.prototype.slice.call(arguments); + return exports.object.deepComplete.apply(this, args); +}); + +exports.remove = exports.operator('remove', 2, function (list, value) { + var index; + if ((index = list.indexOf(value))>-1) + list.splice(index,1); + return list; +}); + +// TODO insert +// TODO discard + +exports.range = function () { + var start = 0, stop = 0, step = 1; + if (arguments.length == 1) { + stop = arguments[0]; + } else { + start = arguments[0]; + stop = arguments[1]; + step = arguments[2] || 1; + } + var range = []; + for (var i = start; i < stop; i += step) + range.push(i); + return range; +}; + +exports.forEach = function (array, block) { + Array.prototype.forEach.call( + exports.array.coerce(array), + block + ); +}; + +exports.forEachApply = function (array, block) { + Array.prototype.forEach.call( + exports.array.coerce(array), + exports.apply(block) + ); +}; + +exports.map = function (array, block, context) { + return Array.prototype.map.call( + exports.array.coerce(array), + block, + context + ); +}; + +exports.mapApply = function (array, block) { + return Array.prototype.map.call( + exports.array.coerce(array), + exports.apply(block) + ); +}; + +exports.every = exports.operator('every', 2, function (array, block, context) { + return exports.all(exports.map(array, block, context)); +}); + +exports.some = exports.operator('some', 2, function (array, block, context) { + return exports.any(exports.map(array, block, context)); +}); + +exports.all = exports.operator('all', 1, function (array) { + array = exports.array.coerce(array); + for (var i = 0; i < array.length; i++) + if (!array[i]) + return false; + return true; +}); + +exports.any = exports.operator('any', 1, function (array) { + array = exports.array.coerce(array); + for (var i = 0; i < array.length; i++) + if (array[i]) + return true; + return false; +}); + +exports.reduce = exports.operator('reduce', 2, function (array, block, basis) { + array = exports.array.coerce(array); + return array.reduce.apply(array, arguments); +}); + +exports.reduceRight = exports.operator('reduceRight', 2, function (array, block, basis) { + array = exports.array.coerce(array); + return array.reduceRight.apply(array, arguments); +}); + +exports.zip = function () { + return exports.transpose(arguments); +}; + +exports.transpose = function (array) { + array = exports.array.coerce(array); + var transpose = []; + var length = Math.min.apply(this, exports.map(array, function (row) { + return row.length; + })); + for (var i = 0; i < array.length; i++) { + var row = array[i]; + for (var j = 0; j < length; j++) { + var cell = row[j]; + if (!transpose[j]) + transpose[j] = []; + transpose[j][i] = cell; + } + } + return transpose; +}; + +exports.enumerate = function (array, start) { + array = exports.array.coerce(array); + if (exports.no(start)) + start = 0; + return exports.zip( + exports.range(start, start + array.length), + array + ); +}; + +// arithmetic, transitive, and logical operators + +exports.is = function (a, b) { + // + if (a === b) + // 0 === -0, but they are not identical + return a !== 0 || 1/a === 1/b; + // NaN !== NaN, but they are identical. + // NaNs are the only non-reflexive value, i.e., if a !== a, + // then a is a NaN. + return a !== a && b !== b; + // +}; + +exports.eq = exports.operator('eq', 2, function (a, b, stack) { + if (!stack) + stack = []; + if (a === b) + return true; + if (typeof a !== typeof b) + return false; + if (exports.no(a)) + return exports.no(b); + if (a instanceof Date) + return a.valueOf() === b.valueOf(); + if (a instanceof RegExp) + return a.source === b.source && + a.global === b.global && + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline; + if (typeof a === "function") { + var caller = stack[stack.length - 1]; + // XXX what is this for? can it be axed? + // it comes from the "equiv" project code + return caller !== Object && + typeof caller !== "undefined"; + } + if (exports.isArrayLike(a)) + return exports.array.eq( + a, b, + stack.concat([a.constructor]) + ); + if (typeof a === 'object') + return exports.object.eq( + a, b, + stack.concat([a.constructor]) + ); + return false; +}); + +exports.ne = exports.operator('ne', 2, function (a, b) { + return !exports.eq(a, b); +}); + +exports.lt = exports.operator('lt', 2, function (a, b) { + if (exports.no(a) != exports.no(b)) + return exports.no(a) > exports.no(b); + if (exports.isArrayLike(a) && exports.isArrayLike(b)) + return exports.array.lt(a, b); + return a < b; +}); + +exports.gt = exports.operator('gt', 2, function (a, b) { + return !(exports.lt(a, b) || exports.eq(a, b)); +}); + +exports.le = exports.operator('le', 2, function (a, b) { + return exports.lt(a, b) || exports.eq(a, b); +}); + +exports.ge = exports.operator('ge', 2, function (a, b) { + return !exports.lt(a, b); +}); + +exports.mul = exports.operator('mul', 2, function (a, b) { + if (typeof a == "string") + return exports.string.mul(a, b); + return a * b; +}); + +/*** by + returns a `comparator` that compares + values based on the values resultant from + a given `relation`. + accepts a `relation` and an optional comparator. + + To sort a list of objects based on their + "a" key:: + + objects.sort(by(get("a"))) + + To get those in descending order:: + + objects.sort(by(get("a")), desc) + + `by` returns a comparison function that also tracks + the arguments you used to construct it. This permits + `sort` and `sorted` to perform a Schwartzian transform + which can increase the performance of the sort + by a factor of 2. +*/ +exports.by = function (relation) { + var compare = arguments[1]; + if (exports.no(compare)) + compare = exports.compare; + var comparator = function (a, b) { + a = relation(a); + b = relation(b); + return compare(a, b); + }; + comparator.by = relation; + comparator.compare = compare; + return comparator; +}; + +exports.compare = exports.operator('compare', 2, function (a, b) { + if (exports.no(a) !== exports.no(b)) + return exports.no(b) - exports.no(a); + if (typeof a === "number" && typeof b === "number") + return a - b; + return exports.eq(a, b) ? 0 : exports.lt(a, b) ? -1 : 1; +}); + +/*** sort + an in-place array sorter that uses a deep comparison + function by default (compare), and improves performance if + you provide a comparator returned by "by", using a + Schwartzian transform. +*/ +exports.sort = function (array, compare) { + if (exports.no(compare)) + compare = exports.compare; + if (compare.by) { + /* schwartzian transform */ + array.splice.apply( + array, + [0, array.length].concat( + array.map(function (value) { + return [compare.by(value), value]; + }).sort(function (a, b) { + return compare.compare(a[0], b[0]); + }).map(function (pair) { + return pair[1]; + }) + ) + ); + } else { + array.sort(compare); + } + return array; +}; + +/*** sorted + returns a sorted copy of an array using a deep + comparison function by default (compare), and + improves performance if you provide a comparator + returned by "by", using a Schwartzian transform. +*/ +exports.sorted = function (array, compare) { + return exports.sort(exports.array.copy(array), compare); +}; + +exports.reverse = function (array) { + return Array.prototype.reverse.call(array); +}; + +exports.reversed = function (array) { + return exports.reverse(exports.array.copy(array)); +}; + +exports.hash = exports.operator('hash', 1, function (object) { + return '' + object; +}); + +exports.unique = exports.operator('unique', 1, function (array, eq, hash) { + var visited = {}; + if (!eq) eq = exports.eq; + if (!hash) hash = exports.hash; + return array.filter(function (value) { + var bucket = exports.getset(visited, hash(value), []); + var finds = bucket.filter(function (other) { + return eq(value, other); + }); + if (!finds.length) + bucket.push(value); + return !finds.length; + }); +}); + +// string + +exports.string = exports.operator('toString', 1, function (object) { + return '' + object; +}); + +exports.string.mul = function (string, n) { + return exports.range(n).map(function () { + return string; + }).join(''); +}; + +/*** escape + escapes all characters of a string that are + special to JavaScript and many other languages. + Recognizes all of the relevant + control characters and formats all other + non-printable characters as Hex byte escape + sequences or Unicode escape sequences depending + on their size. + + Pass ``true`` as an optional second argument and + ``escape`` produces valid contents for escaped + JSON strings, wherein non-printable-characters are + all escaped with the Unicode ``\u`` notation. +*/ +/* more Steve Levithan flagrence */ +var escapeExpression = /[^ !#-[\]-~]/g; +/* from Doug Crockford's JSON library */ +var escapePatterns = { + '\b': '\\b', '\t': '\\t', + '\n': '\\n', '\f': '\\f', '\r': '\\r', + '"' : '\\"', '\\': '\\\\' +}; +exports.escape = function (value, strictJson) { + if (typeof value != "string") + throw new Error( + module.path + + "#escape: requires a string. got " + + exports.repr(value) + ); + return value.replace( + escapeExpression, + function (match) { + if (escapePatterns[match]) + return escapePatterns[match]; + match = match.charCodeAt(); + if (!strictJson && match < 256) + return "\\x" + exports.padBegin(match.toString(16), 2); + return '\\u' + exports.padBegin(match.toString(16), 4); + } + ); +}; + +/*** enquote + transforms a string into a string literal, escaping + all characters of a string that are special to + JavaScript and and some other languages. + + ``enquote`` uses double quotes to be JSON compatible. + + Pass ``true`` as an optional second argument to + be strictly JSON compliant, wherein all + non-printable-characters are represented with + Unicode escape sequences. +*/ +exports.enquote = function (value, strictJson) { + return '"' + exports.escape(value, strictJson) + '"'; +}; + +/** + * remove adjacent characters + * todo: i'm not sure if this works correctly without the second argument + */ +exports.squeeze = function (s) { + var set = arguments.length > 0 ? "["+Array.prototype.slice.call(arguments, 1).join('')+"]" : ".|\\n", + regex = new RegExp("("+set+")\\1+", "g"); + + return s.replace(regex, "$1"); +}; + +/*** expand + transforms tabs to an equivalent number of spaces. +*/ +// TODO special case for \r if it ever matters +exports.expand = function (str, tabLength) { + str = String(str); + tabLength = tabLength || 4; + var output = [], + tabLf = /[\t\n]/g, + lastLastIndex = 0, + lastLfIndex = 0, + charsAddedThisLine = 0, + tabOffset, match; + while (match = tabLf.exec(str)) { + if (match[0] == "\t") { + tabOffset = ( + tabLength - 1 - + ( + (match.index - lastLfIndex) + + charsAddedThisLine + ) % tabLength + ); + charsAddedThisLine += tabOffset; + output.push( + str.slice(lastLastIndex, match.index) + + exports.mul(" ", tabOffset + 1) + ); + } else if (match[0] === "\n") { + output.push(str.slice(lastLastIndex, tabLf.lastIndex)); + lastLfIndex = tabLf.lastIndex; + charsAddedThisLine = 0; + } + lastLastIndex = tabLf.lastIndex; + } + return output.join("") + str.slice(lastLastIndex); +}; + +var trimBeginExpression = /^\s\s*/g; +exports.trimBegin = function (value) { + return String(value).replace(trimBeginExpression, ""); +}; + +var trimEndExpression = /\s\s*$/g; +exports.trimEnd = function (value) { + return String(value).replace(trimEndExpression, ""); +}; + +exports.trim = function (value) { + return String(value).replace(trimBeginExpression, "").replace(trimEndExpression, ""); +}; + +/* generates padBegin and padEnd */ +var augmentor = function (augment) { + return function (value, length, pad) { + if (exports.no(pad)) pad = '0'; + if (exports.no(length)) length = 2; + value = String(value); + while (value.length < length) { + value = augment(value, pad); + } + return value; + }; +}; + +/*** padBegin + + accepts: + - a `String` or `Number` value + - a minimum length of the resultant `String`: + by default, 2 + - a pad string: by default, ``'0'``. + + returns a `String` of the value padded up to at least + the minimum length. adds the padding to the begining + side of the `String`. + +*/ +exports.padBegin = augmentor(function (value, pad) { + return pad + value; +}); + +/*** padEnd + + accepts: + - a `String` or `Number` value + - a minimum length of the resultant `String`: + by default, 2 + - a pad string: by default, ``'0'``. + + returns a `String` of the value padded up to at least + the minimum length. adds the padding to the end + side of the `String`. + +*/ +exports.padEnd = augmentor(function (value, pad) { + return value + pad; +}); + +/*** splitName + splits a string into an array of words from an original + string. +*/ +// thanks go to Steve Levithan for this regular expression +// that, in addition to splitting any normal-form identifier +// in any case convention, splits XMLHttpRequest into +// "XML", "Http", and "Request" +var splitNameExpression = /[a-z]+|[A-Z](?:[a-z]+|[A-Z]*(?![a-z]))|[.\d]+/g; +exports.splitName = function (value) { + return String(value).match(splitNameExpression); +}; + +/*** joinName + joins a list of words with a given delimiter + between alphanumeric words. +*/ +exports.joinName = function (delimiter, parts) { + if (exports.no(delimiter)) delimiter = '_'; + parts.unshift([]); + return parts.reduce(function (parts, part) { + if ( + part.match(/\d/) && + exports.len(parts) && parts[parts.length-1].match(/\d/) + ) { + return parts.concat([delimiter + part]); + } else { + return parts.concat([part]); + } + }).join(''); +}; + +/*** upper + converts a name to ``UPPER CASE`` using + a given delimiter between numeric words. + + see: + - `lower` + - `camel` + - `title` + +*/ +exports.upper = function (value, delimiter) { + if (exports.no(delimiter)) + return value.toUpperCase(); + return exports.splitName(value).map(function (part) { + return part.toUpperCase(); + }).join(delimiter); +}; + +/*** lower + converts a name to a ``lower case`` using + a given delimiter between numeric words. + + see: + - `upper` + - `camel` + - `title` + +*/ +exports.lower = function (value, delimiter) { + if (exports.no(delimiter)) + return String(value).toLowerCase(); + return exports.splitName(value).map(function (part) { + return part.toLowerCase(); + }).join(delimiter); +}; + +/*** camel + converts a name to ``camel Case`` using + a given delimiter between numeric words. + + see: + - `lower` + - `upper` + - `title` + +*/ +exports.camel = function (value, delimiter) { + return exports.joinName( + delimiter, + exports.mapApply( + exports.enumerate(exports.splitName(value)), + function (n, part) { + if (n) { + return ( + part.substring(0, 1).toUpperCase() + + part.substring(1).toLowerCase() + ); + } else { + return part.toLowerCase(); + } + } + ) + ); +}; + +/*** title + converts a name to ``Title Case`` using + a given delimiter between numeric words. + + see: + - `lower` + - `upper` + - `camel` + +*/ +exports.title = function (value, delimiter) { + return exports.joinName( + delimiter, + exports.splitName(value).map(function (part) { + return ( + part.substring(0, 1).toUpperCase() + + part.substring(1).toLowerCase() + ); + }) + ); +};