'use strict'; /*@ Ox.avg Returns the average of an array's values, or an object's properties (collection) -> Average value collection <[n]|o> Array or object with numerical values > Ox.avg([-1, 0, 1]) 0 > Ox.avg({a: 1, b: 2, c: 3}) 2 > Ox.avg('avg is 0.1') 0.1 @*/ Ox.avg = function(obj) { return Ox.sum(obj) / Ox.len(obj); }; /*@ Ox.contains Tests if a collection contains a value > Ox.contains(['foo', 'bar'], 'foo') true > Ox.contains({foo: 'bar'}, 'bar') true > Ox.contains({foo: 'bar'}, 'foo') false > Ox.contains("foobar", "bar") true @*/ Ox.contains = function(col, val) { /* // fixme: rename to Ox.has or Ox.isIn? // then it'd become convenient for arrays */ return (Ox.isObject(col) ? Ox.values(col) : col).indexOf(val) > -1; }; /*@ Ox.copy Returns a (shallow or deep) copy of an object or array > (function() { var a = ['v'], b = Ox.copy(a); a[0] = null; return b[0]; }()) 'v' > (function() { var a = {k: 'v'}, b = Ox.copy(a); a.k = null; return b.k; }()) 'v' > Ox.clone(0) 0 @*/ Ox.copy = Ox.clone = function(col, deep) { // fixme: remove references to Ox.clone // fixme: is there any use case for shallow copy? var ret = Ox.isArray(col) ? [] : {}; if (deep) { Ox.forEach(col, function(val, key) { ret[key] = ['array', 'object'].indexOf(Ox.typeOf(val)) > -1 ? Ox.clone(val, true) : val; }); } else { ret = Ox.isArray(col) ? col.slice() : Ox.isObject(col) ? Ox.extend({}, col) : col; } return ret; }; /*@ Ox.count Counts the occurences of values in a collection > Ox.count(['f', 'o', 'o']) {f: 1, o: 2} > Ox.count({a: 'f', b: 'o', c: 'o'}) {f: 1, o: 2} > Ox.count('foo') {f: 1, o: 2} @*/ Ox.count = function(arr) { var obj = {}; Ox.forEach(arr, function(v) { obj[v] = (obj[v] || 0) + 1; }); return obj; }; /*@ Ox.every Tests if every element of a collection satisfies a given condition Unlike [].every(), Ox.every() works for arrays, objects and strings. > Ox.every([0, 1, 2], function(v, i) { return i == v; }) true > Ox.every({a: 1, b: 2, c: 3}, function(v) { return v == 1; }) false > Ox.every("foo", function(v) { return v == 'f'; }) false > Ox.every([true, true, true]) true @*/ Ox.every = function(col, fn) { return Ox.filter(Ox.values(col), fn || function(v) { return v; }).length == Ox.len(col); }; /*@ Ox.filter Filters a collection by a given condition Unlike [].filter(), Ox.filter() works for arrays, objects and strings. > Ox.filter([2, 1, 0], function(v, i) { return v == i; }) [1] > Ox.filter({a: 'c', b: 'b', c: 'a'}, function(v, k) { return v == k; }) {b: 'b'} > Ox.filter(' foo bar ', function(v) { return v != ' '; }) 'foobar' @*/ Ox.filter = function(col, fn) { var type = Ox.typeOf(col), ret = type == 'array' ? [] : type == 'object' ? {} : ''; Ox.forEach(col, function(val, key) { if (fn(val, key)) { if (type == 'array') { ret.push(val); } else if (type == 'object') { ret[key] = val; } else { ret += val; } } }); return ret; }; /*@ Ox.find Returns array elements that match a string Returns an array of two arrays, the first containing leading matches (exact match first), the second containing non-leading matches > Ox.find(['foo', 'bar', 'foobar', 'barfoo'], 'foo') [['foo', 'foobar'], ['barfoo']] @*/ // fixme: wouldn't it make more sense to return just one array? Ox.find = function(arr, str) { var ret = [[], []]; str = str.toLowerCase(); arr.map(function(v) { return v.toLowerCase(); }).forEach(function(v, i) { var index = v.indexOf(str); index > -1 && ret[index == 0 ? 0 : 1][v == str ? 'unshift' : 'push'](arr[i]); }); return ret; }; /*@ Ox.forEach forEach loop Ox.forEach() loops over arrays, objects and strings. Returning false from the iterator function acts like a break statement (unlike [].forEach(), like $.each()). The arguments of the iterator function are (value, key) (like [].forEach(), unlike $.each()). (collection, callback) The collection (collection, callback, includePrototype) The collection collection An array, object or string callback Callback function value <*> Value key Key includePrototype If true, include prototype properties > Ox.test.string "012abcfoo" @*/ Ox.forEach = function(col, fn, includePrototype) { var isObject = Ox.isObject(col), key; // Safari will not loop through an arguments array col = Ox.isArguments(col) ? Ox.makeArray(col) : col; for (key in col) { key = isObject ? key : parseInt(key); // fixme: fn.call(context, obj[key], key, obj) may be more standard... if (( includePrototype || Object.hasOwnProperty.call(col, key) ) && fn(col[key], key) === false) { break; } } return col; }; /*@ Ox.getIndex Returns the first array index of an object where obj[key] is val > Ox.getIndex([{a: 1}, {a: 2}, {a: 1}], 'a', 2) 1 > Ox.getIndex([{a: 1}, {a: 2}, {a: 1}], 'a', 1) 0 > Ox.getIndex([{a: 1}, {a: 2}, {a: 1}], 'a', 0) -1 @*/ Ox.getIndex = function(arr, key, val) { var ret = -1; Ox.forEach(arr, function(obj, ind) { if (obj[key] === val) { ret = ind; return false; } }); return ret; }; /*@ Ox.getIndexById Returns the first array index of an object with a given id > Ox.getIndexById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'foo') 0 @*/ Ox.getIndexById = function(arr, id) { return Ox.getIndex(arr, 'id', id); }; /*@ Ox.getObject Returns the first object in an array where obj[key] is val > Ox.getObject([{a: 1, i: 0}, {a: 2, i: 1}, {a: 1, i: 2}], 'a', 2) {a: 2, i: 1} > Ox.getObject([{a: 1, i: 0}, {a: 2, i: 1}, {a: 1, i: 2}], 'a', 1) {a: 1, i: 0} > Ox.getObject([{a: 1, i: 0}, {a: 2, i: 1}, {a: 1, i: 2}], 'a', 0) null @*/ Ox.getObject = function(arr, key, val) { var ret = null; Ox.forEach(arr, function(obj) { if (obj[key] === val) { ret = obj; return false; } }); return ret; }; /*@ Ox.getObjectById Returns the first object in an array with a given id > Ox.getObjectById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'foo') {id: "foo", str: "Foo"} @*/ Ox.getObjectById = function(arr, id) { return Ox.getObject(arr, 'id', id); }; /*@ Ox.getset Generic getter and setter function See examples for details. # Usage -------------------------------------------------------------------- Ox.getset(options, args=[]) -> all options Ox.getset(options, args=[key]) -> <*> options[key] Ox.getset(options, args=[key, value], callback, context) -> context sets options[key] to value and calls fn(key, value) if the key/value pair was added or modified Ox.getset(options, args=[{key: value}], callback, context) -> context sets multiple options and calls fn(key, value) for every key/value pair that was added or modified # Arguments ---------------------------------------------------------------- options Options object (key/value pairs) args The arguments "array" of the caller function callback Callback function The callback is called for every key/value pair that was added or modified. key Key value <*> Value context The parent object of the caller function (for chaining) # Examples ----------------------------------------------------------------- > Ox.test.object.options("key", "val").options("key") "val" > Ox.test.object.options({foo: "foo", bar: "bar"}).options() {"key": "val", "foo": "foo", "bar": "bar"} @*/ Ox.getset = function(obj, args, callback, context) { var obj_ = Ox.clone(obj), ret; if (args.length == 0) { // [] ret = obj_; } else if (args.length == 1 && !Ox.isObject(args[0])) { // [key] ret = Ox.clone(obj[args[0]]); } else { // [key, val] or [{key: val, ...}] args = Ox.makeObject(args); obj = Ox.extend(obj, args); Ox.forEach(args, function(val, key) { if (!obj_ || !Ox.isEqual(obj_[key], val)) { callback && callback(key, val); } }); ret = context; } return ret; } /*@ Ox.isEmpty Returns true if a collection is empty > Ox.isEmpty([]) true > Ox.isEmpty({}) true > Ox.isEmpty('') true > Ox.isEmpty(function() {}) true > Ox.isEmpty(function(a) {}) false > Ox.isEmpty(null) false > Ox.isEmpty() false @*/ Ox.isEmpty = function(val) { return Ox.len(val) == 0; }; /*@ Ox.keys Returns the keys of a collection Unlike Object.keys(), Ox.keys() works for arrays, objects and strings. > Ox.keys([1, 2, 3]) [0, 1, 2] > Ox.keys([1,,3]) [0, 2] # fixme? # > Ox.keys([,]) # [0] > Ox.keys({a: 1, b: 2, c: 3}) ['a', 'b', 'c'] > Ox.keys('abc') [0, 1, 2] @*/ // fixme: is this really needed? arrays... ok... but strings?? Ox.keys = function(obj) { var keys = []; Ox.forEach(obj, function(v, k) { keys.push(k); }); return keys.sort(); }; /*@ Ox.last Gets or sets the last element of an array Unlike foobarbaz[foobarbaz.length - 1], Ox.last(foobarbaz) is short. > Ox.last(Ox.test.array) 3 > Ox.last(Ox.test.array, 4) [1, 2, 4] > Ox.test.array [1, 2, 4] @*/ Ox.last = function(arr, val) { var ret; if (arguments.length == 1) { ret = arr[arr.length - 1]; } else { arr[arr.length - 1] = val; ret = arr; } return ret; }; /*@ Ox.len Returns the length of an array, function, object or string Not to be confused with Ox.length, which is the length property of the Ox function (1). // FIXME: 1 becomes 67 in DocPanel > Ox.len([1, 2, 3]) 3 > Ox.len([,]) 1 > Ox.len({a: 1, b: 2, c: 3}) 3 > Ox.len(function(a, b, c) {}) 3 > Ox.len('abc') 3 @*/ Ox.len = function(col) { var type = Ox.typeOf(col); return ['array', 'function', 'string'].indexOf(type) > -1 ? col.length : type == 'object' ? Ox.values(col).length : void 0; }; /*@ Ox.loop For-loop, functional-style Returning false from the iterator function acts like a break statement. Unlike a for loop, Ox.loop doesn't leak its counter variable to the outer scope, but returns it. (stop, callback) -> Next value equivalent to for (var i = 0; i < stop; i++) (start, stop, callback) -> Next value equivalent to for (var i = start; i < stop; i++) or, if start is larger than stop, for (var i = start; i > stop; i--) (start, stop, step, callback) -> Next value equivalent to for (var i = start; i < stop; i += step) or, if step is negative, for (var i = start; i > stop; i += step) start Start value stop Stop value (exclusive) step Step value callback Iterator function i Counter value > Ox.loop(10, function(i) { return i != 4; }) 4 > Ox.loop(0, 3, 2, function() {}) 4 @*/ Ox.loop = function() { var len = arguments.length, start = len > 2 ? arguments[0] : 0, stop = arguments[len > 2 ? 1 : 0], step = len == 4 ? arguments[2] : (start <= stop ? 1 : -1), callback = arguments[len - 1], i; for (i = start; step > 0 ? i < stop : i > stop; i += step) { if (callback(i) === false) { break; }; } return i; }; /*@ Ox.makeArray Takes an array-like object and returns a true array Alias for Array.prototype.slice.call (value) -> True array value <*> Array-like object > (function() { return Ox.makeArray(arguments); }("foo", "bar")) ["foo", "bar"] > Ox.makeArray("foo") ["f", "o", "o"] > Ox.makeArray({0: "f", 1: "o", 2: "o", length: 3}) ["f", "o", "o"] @*/ Ox.makeArray = /MSIE/.test(navigator.userAgent) ? function(col) { var i, len, ret = []; try { ret = Array.prototype.slice.call(col); } catch(e) { // handle MSIE NodeLists len = col.length; for (i = 0; i < len; i++) { ret[i] = col[i]; } } return ret; } : function(col) { return Array.prototype.slice.call(col); }; /*@ Ox.makeObject Takes an array and returns an object Ox.makeObject is a helper for functions with two alternative signatures like ('key', 'val') and ({key: 'val'}). > (function() { return Ox.makeObject(arguments); }({foo: 1, bar: 2})) {foo: 1, bar: 2} > (function() { return Ox.makeObject(arguments); }('foo', 1)) {foo: 1} > (function() { return Ox.makeObject(arguments); }('foo')) {foo: void 0} > (function() { return Ox.makeObject(arguments); }()) {} @*/ Ox.makeObject = function(obj) { var ret = {}; if (Ox.isObject(obj[0])) { // ({foo: 'bar'}) ret = obj[0]; } else if (obj.length) { // ('foo', 'bar') ret[obj[0]] = obj[1] } return ret; }; /*@ Ox.map Transforms the values of an array, object or string Unlike [].map(), Ox.map() works for arrays, objects and strings. Returning null from the iterator function will remove the element from the collection. > Ox.map([0, 0, 0], function(v, i) { return v == i; }) [true, false, false] > Ox.map({a: 'a', b: 'a', c: 'a'}, function(v, k) { return v == k; }) {a: true, b: false, c: false} > Ox.map("000", function(v, i) { return v == i; }) [true, false, false] > Ox.map([0, 1, 2, 4], function(v, i) { return v ? i == v : null; }) [true, true, false] # fixme? # > Ox.map([,], function(v, i) { return i; }) # [0] @*/ // FIXME: it would sometimes be nice to have Ox.map(3, function(i) {...}) // instead of Ox.range(3).map(function(i) {...}) Ox.map = function(obj, fn) { // fixme: return null to filter out may be a bit esoteric var isObject = Ox.isObject(obj), ret = isObject ? {} : []; Ox.forEach(obj, function(val, key) { var map; if ((map = fn(val, key)) !== null) { ret[isObject ? key : ret.length] = map; } }); return ret; }; /*@ Ox.max Returns the maximum value of a collection > Ox.max([1, 2, 3]) 3 > Ox.max({a: 1, b: 2, c: 3}) 3 > Ox.max('123') 3 @*/ Ox.max = function(col) { return Math.max.apply(Math, Ox.values(col)); }; /*@ Ox.min Returns the minimum value of a collection > Ox.min([1, 2, 3]) 1 > Ox.min({a: 1, b: 2, c: 3}) 1 > Ox.min('123') 1 @*/ Ox.min = function(col) { return Math.min.apply(Math, Ox.values(col)); }; /*@ Ox.range Python-style range (stop) -> <[n]> range Returns an array of integers from 0 (inclusive) to stop (exclusive). (start, stop) -> <[n]> range Returns an array of integers from start (inclusive) to stop (exclusive). (start, stop, step) -> <[n]> range Returns an array of numbers from start (inclusive) to stop (exclusive), incrementing by step. start Start value stop Stop value step Step value > Ox.range(3) [0, 1, 2] > Ox.range(1, 4) [1, 2, 3] > Ox.range(3, 0) [3, 2, 1] > Ox.range(1, 2, 0.5) [1, 1.5] > Ox.range(-1, -2, -0.5) [-1, -1.5] @*/ Ox.range = function() { var args = Ox.makeArray(arguments), arr = []; args.push(function(i) { arr.push(i); }); Ox.loop.apply(null, args); return arr; }; /*@ Ox.setPropertyOnce Sets a property once Given a array of objects, each of which has a property with a boolean value, this sets exactly one of these to true, and returns the index of the object whose property is true. > Ox.setPropertyOnce([{selected: false}, {selected: false}], 'selected') 0 > Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected') 1 > Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected') 0 @*/ // fixme: strange name, and shouldn't it return the full array? Ox.setPropertyOnce = function(arr, str) { var pos = -1; Ox.forEach(arr, function(v, i) { if (pos == -1 && arr[i][str]) { pos = i; } else if (pos > -1 && arr[i][str]) { delete arr[i][str]; } }); if (pos == -1) { arr[0][str] = true; pos = 0; } return pos; }; /*@ Ox.shuffle Randomizes the order of values within a collection > Ox.shuffle([1, 2, 3]).length 3 > Ox.len(Ox.shuffle({a: 1, b: 2, c: 3})) 3 > Ox.shuffle('123').length 3 @*/ Ox.shuffle = function(col) { var keys, ret, type = Ox.typeOf(col), values; function sort() { return Math.random() - 0.5; } if (type == 'array') { ret = col.sort(sort); } else if (type == 'object') { keys = Object.keys(col); values = Ox.values(col).sort(sort); ret = {}; keys.forEach(function(key, i) { ret[key] = values[i] }); } else if (type == 'string') { ret = col.split('').sort(sort).join(''); } return ret; }; /*@ Ox.some Tests if one or more elements of a collection satisfy a given condition Unlike [].some(), Ox.some() works for arrays, objects and strings. > Ox.some([2, 1, 0], function(i, v) { return i == v; }) true > Ox.some({a: 1, b: 2, c: 3}, function(v) { return v == 1; }) true > Ox.some("foo", function(v) { return v == 'f'; }) true @*/ Ox.some = function(obj, fn) { return Ox.filter(Ox.values(obj), fn || function(v) { return v; }).length > 0; }; /*@ Ox.sub Returns a substring or sub-array Ox.sub behaves like collection[start:stop] in Python (or, for strings, like str.substring() with negative values for stop) > Ox.sub([1, 2, 3], 1, -1) [2] > Ox.sub('foobar', 1) "oobar" > Ox.sub('foobar', -1) "r" > Ox.sub('foobar', 1, 5) "ooba" > Ox.sub('foobar', 1, -1) "ooba" > Ox.sub('foobar', -5, 5) "ooba" > Ox.sub('foobar', -5, -1) "ooba" > Ox.sub('foo', -1, 0) "" @*/ Ox.sub = function(col, start, stop) { stop = Ox.isUndefined(stop) ? col.length : stop; start = start < 0 ? col.length + start : start; stop = stop < 0 ? col.length + stop : stop; return Ox.isArray(col) ? Ox.filter(col, function(val, key) { return key >= start && key < stop; }) : col.substring(start, Math.max(start, stop)); } /*@ Ox.sum Returns the sum of the values of a collection > Ox.sum(1, 2, 3) 6 > Ox.sum([1, 2, 3]) 6 > Ox.sum({a: 1, b: 2, c: 3}) 6 > Ox.sum('123') 6 > Ox.sum('123foo') 6 > Ox.sum('08', -2, 'foo') 6 @*/ Ox.sum = function(col) { var sum = 0; col = arguments.length > 1 ? Ox.makeArray(arguments) : col; Ox.forEach(col, function(val) { val = +val; sum += Ox.isNumber(val) ? val : 0; }); return sum; }; Ox.toArray = function(obj) { // fixme: can this be thrown out? /* >>> Ox.toArray('foo') ['foo'] >>> Ox.toArray(['foo']) ['foo'] */ var arr; if (Ox.isArray(obj)) { arr = obj; } else if (Ox.isArguments(obj)) { arr = Ox.makeArray(obj); } else { arr = [obj]; } return arr; }; /*@ Ox.values Returns the values of a collection > Ox.values([1, 2, 3]) [1, 2, 3] > Ox.values({a: 1, b: 2, c: 3}) [1, 2, 3] > Ox.values('abc') ['a', 'b', 'c'] > Ox.values([1,,3]) [1, 3] @*/ Ox.values = function(col) { // Ox.values(str) is identical to str.split('') var values = []; Ox.forEach(col, function(val) { values.push(val); }); return values; }; /*@ Ox.walk Recursively walk a tree-like key/value store > Ox.test.number 6 @*/ Ox.walk = function(obj, fn) { Ox.forEach(obj, function(val, key) { fn(val, key, obj); Ox.walk(obj[key], fn); }); };