oxjs/source/Ox/js/Collection.js

761 lines
No EOL
21 KiB
JavaScript

'use strict';
/*@
Ox.avg <f> Returns the average of an array's values, or an object's properties
(collection) -> <n> 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.break = function() {
throw Ox.BreakError;
};
Ox.BreakError = new SyntaxError('Illegal My.break() statement');
/*@
Ox.clone <f> Returns a (shallow or deep) copy of an object or array
> (function() { var a = ['v'], b = Ox.clone(a); a[0] = null; return b[0]; }())
'v'
> (function() { var a = {k: 'v'}, b = Ox.clone(a); a.k = null; return b.k; }())
'v'
> Ox.clone(0)
0
@*/
Ox.clone = Ox.copy = function(col, deep) {
// fixme: copy or clone?
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.contains <f> 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 = Ox.in = function(col, val) {
return (Ox.isObject(col) ? Ox.values(col) : col).indexOf(val) > -1;
};
/*@
Ox.count <f> 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 <f> Tests if every element of a collection satisfies a given condition
Unlike <code>[].every()</code>, <code>Ox.every()</code> 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 || Ox.identity).length == Ox.len(col);
};
/*@
Ox.filter <f> Filters a collection by a given condition
Unlike <code>[].filter()</code>, <code>Ox.filter()</code> 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, that) {
var ret, type = Ox.typeOf(col);
if (type == 'object') {
ret = {};
Ox.forEach(col, function(val, key) {
if (fn.call(that, val, key, col)) {
ret[val] = key;
}
});
} else {
ret = Ox.toArray(col).filter(fn, that);
if (type == 'string') {
ret = ret.join('');
}
}
return ret;
};
/*@
Ox.find <f> 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(); // fixme: don't loop twice!!
}).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 <f> forEach loop
<code>Ox.forEach()</code> loops over arrays, objects and strings.
Returning <code>false</code> from the iterator function acts like a
<code>break</code> statement (unlike <code>[].forEach()</code>, like
<code>$.each()</code>). The arguments of the iterator function are
<code>(value, key, index)</code> (more like <code>[].forEach()</code>
than like <code>$.each()</code>).
(collection, callback) <a|o|s> The collection
(collection, callback, includePrototype) <a|o|s> The collection
collection <a|o|s> An array, object or string
callback <f> Callback function
value <*> Value
key <n|s> Key
includePrototype <b|false> If true, include prototype properties
<script>
Ox.test.string = "";
Ox.forEach(["f", "o", "o"], function(v, i) { Ox.test.string += i; });
Ox.forEach({a: "f", b: "o", c: "o"}, function(v, k) { Ox.test.string += k; });
Ox.forEach("foo", function(v) { Ox.test.string += v; });
</script>
> Ox.test.string
"012abcfoo"
@*/
Ox.forEach = function(col, fn, that) {
var i, key, type = Ox.typeOf(col);
if (type != 'array' && type != 'object') {
col = Ox.toArray(col);
}
try {
if (type == 'object') {
for (key in col) {
// Ox.hasOwn(obj, key) && fn.call(that, col[key], key, col);
if (Ox.hasOwn(col, key) && fn.call(that, col[key], key, col) === false) {
console.warn('Returning false in Ox.forEach is deprecated.')
break;
}
}
} else {
for (i = 0; i < col.length; i++) {
// fn.call(that, col[i], i, col);
if (fn.call(that, col[i], i, col) === false) {
console.warn('Returning false in Ox.forEach is deprecated.')
break;
}
}
}
} catch(e) {
if (e !== Ox.BreakError) {
throw e;
}
}
return type == 'object' ? key : i;
};
/*@
Ox.getIndexById <f> Returns the first array index of an object with a given id
> Ox.getIndexById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'bar')
1
> Ox.getIndexById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'baz')
-1
@*/
Ox.getIndexById = function(arr, id) {
return Ox.indexOf(arr, function(obj) {
return obj.id === id;
});
};
/*@
Ox.getObjectById <f> Returns the first object in an array with a given id
> Ox.getObjectById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'bar')
{id: "bar", str: "Bar"}
> Ox.getObjectById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'baz')
null
@*/
Ox.getObjectById = function(arr, id) {
var index = Ox.getIndexById(arr, id);
return index > -1 ? arr[index] : null;
};
/*@
Ox.getset <f> Generic getter and setter function
See examples for details.
# Usage --------------------------------------------------------------------
Ox.getset(options, args=[]) -> <o> all options
Ox.getset(options, args=[key]) -> <*> options[key]
Ox.getset(options, args=[key, value], callback, context) -> <f|o> 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) -> <f|o> context
sets multiple options and calls fn(key, value)
for every key/value pair that was added or modified
# Arguments ----------------------------------------------------------------
options <obj> Options object (key/value pairs)
args <arr> The arguments "array" of the caller function
callback <fun> Callback function
The callback is called for every key/value pair that was added or
modified.
key <s> Key
value <*> Value
context <obj> The parent object of the caller function (for chaining)
# Examples -----------------------------------------------------------------
<script>
Ox.test.object = new function() {
var options = {},
setOption = function(key, value) {
// handle added or modified options
},
that = this;
that.options = function() {
return Ox.getset(options, arguments, setOption, that);
};
return that;
};
</script>
> 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.indexOf <f> <code>indexOf</code> with a test function
> Ox.indexOf([1, 2, 3], function(val) { return val % 2 == 0; })
1
> Ox.indexOf('fooBar', function(val) { return val == val.toUpperCase(); })
3
@*/
Ox.indexOf = function(col, fn) {
var index = Ox.forEach(col, function(val) {
fn(val) && Ox.break();
});
return index == col.length ? -1 : index;
};
/*@
Ox.isEmpty <f> Tests if a value is an empty array, object or string
(value) -> <b> True if the value is an empty array, object or string
value <*> Any value
> Ox.isEmpty([])
true
> Ox.isEmpty({})
true
> Ox.isEmpty('')
true
> Ox.isEmpty(function() {})
false
> Ox.isEmpty(false)
false
> Ox.isEmpty(null)
false
> Ox.isEmpty(0)
false
> Ox.isEmpty()
false
@*/
Ox.isEmpty = function(val) {
return Ox.len(val) === 0;
};
/*@
Ox.keys <f> Returns the keys of a collection
Unlike <code>Object.keys()</code>, <code>Ox.keys()</code> 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 <f> Gets or sets the last element of an array
Unlike <code>arrayWithALongName[arrayWithALongName.length - 1]</code>,
<code>Ox.last(arrayWithALongName)</code> is short.
<script>
Ox.test.array = [1, 2, 3];
</script>
> Ox.last(Ox.test.array)
3
> Ox.last(Ox.test.array, 4)
[1, 2, 4]
> Ox.test.array
[1, 2, 4]
> Ox.last('123')
'3'
@*/
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 <f> Returns the length of an array, node list, object or string
Not to be confused with <code>Ox.length</code>, which is the
<code>length</code> property of the <code>Ox</code> function
(<code>1</code>). // FIXME: 1 becomes 67 in DocPanel
> Ox.len((function() { return arguments; }(1, 2, 3)))
3
> Ox.len([1, 2, 3])
3
> Ox.len([,])
1
> Ox.typeOf(Ox.len(document.getElementsByTagName('a')))
'number'
> Ox.len({a: 1, b: 2, c: 3})
3
> Ox.len('abc')
3
> Ox.len(function(a, b, c) {})
undefined
@*/
Ox.len = function(col) {
var len, type = Ox.typeOf(col);
if (
type == 'arguments' || type == 'array'
|| type == 'nodelist' || type == 'string'
) {
len = col.length;
} else if (type == 'object') {
len = Object.keys(col).length;
}
return len;
};
/*@
Ox.makeArray <f> Wraps any non-array in an array.
> Ox.makeArray('foo')
['foo']
> Ox.makeArray(['foo'])
['foo']
@*/
Ox.makeArray = function(obj) {
var arr;
if (Ox.isArray(obj)) {
arr = obj;
} else if (Ox.isArguments(obj)) {
arr = Ox.toArray(obj);
} else {
arr = [obj];
}
return arr;
};
/*@
Ox.makeObject <f> Takes an array and returns an object
<code>Ox.makeObject</code> is a helper for functions with two alternative
signatures like <code>('key', 'val')</code> and <code>({key: 'val'})</code>.
> (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(arr) {
var ret = {}, type = Ox.typeOf(arr[0]);
if (type == 'object') {
// ({foo: 'bar'})
ret = arr[0];
} else if (type == 'string') {
// ('foo', 'bar')
ret[arr[0]] = arr[1];
}
return ret;
};
/*@
Ox.map <f> Transforms the values of an array, object or string
Unlike <code>[].map()</code>, <code>Ox.map()</code> works for arrays,
objects and strings. Returning <code>null</code> 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) {
// FIXME: is there any reason for this strange assignment?
var map;
if ((map = fn(val, key)) !== null) {
ret[isObject ? key : ret.length] = map;
}
});
return ret;
};
/*@
Ox.max <f> 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) {
var ret, values = Ox.values(col);
if (values.length < Ox.STACK_LENGTH) {
ret = Math.max.apply(null, values)
} else {
ret = values.reduce(function(pre, val) {
return Math.max(pre, val);
}, -Infinity);
}
return ret;
};
/*@
Ox.min <f> 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) {
var ret, values = Ox.values(col);
if (values.length < Ox.STACK_LENGTH) {
ret = Math.min.apply(null, values)
} else {
ret = values.reduce(function(pre, val) {
return Math.min(pre, val);
}, Infinity);
}
return ret;
};
/*@
Ox.reverse <f> Reverses an array or string
> Ox.reverse([1, 2, 3])
[3, 2, 1]
> Ox.reverse('foobar')
'raboof'
@*/
Ox.reverse = function(col) {
return Ox.isArray(col)
? Ox.clone(col).reverse()
: col.toString().split('').reverse().join('');
};
/*@
Ox.setPropertyOnce <f> 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 <f> 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').split('').sort().join('')
'123'
@*/
// FIXME: this doesn't actually randomize the order
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.slice <f> Alias for <code>Array.prototype.slice.call</code>
> (function() { return Ox.slice(arguments, 1, -1); }(1, 2, 3))
[2]
@*/
Ox.slice = function(val, start, stop) {
return Array.prototype.slice.call(val, start, stop);
};
/*@
Ox.some <f> Tests if one or more elements of a collection meet a given condition
Unlike <code>[].some()</code>, <code>Ox.some()</code> 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([false, null, 0, '', void 0])
false
@*/
Ox.some = function(obj, fn) {
return Ox.filter(Ox.values(obj), fn || function(v) {
return v;
}).length > 0;
};
/*@
Ox.sub <f> 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 <f> 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.toArray(arguments) : col;
Ox.forEach(col, function(val) {
val = +val;
sum += isFinite(val) ? val : 0;
});
return sum;
};
/*@
Ox.toArray <f> Takes an array-like object and returns a true array
(value) -> <a> True array
value <*> Array-like object
> (function() { return Ox.toArray(arguments); }("foo", "bar"))
["foo", "bar"]
> Ox.toArray("foo")
["f", "o", "o"]
> Ox.toArray({0: "f", 1: "o", 2: "o", length: 3})
["f", "o", "o"]
@*/
// rewrite this so that it uses a try/catch test
Ox.toArray = /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.values <f> 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) {
var ret, type = Ox.typeOf(col);
if (type == 'array') {
ret = col;
} else if (type == 'object') {
ret = [];
Ox.forEach(col, function(val) {
ret.push(val);
});
} else if (type == 'string') {
ret = col.split('');
}
return ret;
};
/*@
Ox.walk <f> Recursively walk a tree of key/value pairs
<script>
Ox.test.number = 0;
Ox.walk({a: 1, b: {c: 2, d: 3}}, function (v) {
Ox.test.number += Ox.isNumber(v) ? v : 0;
});
</script>
> Ox.test.number
6
@*/
Ox.walk = function(obj, fn) {
Ox.forEach(obj, function(val, key) {
fn(val, key, obj);
Ox.walk(obj[key], fn);
});
};