oxjs/source/Ox/js/Collection.js

572 lines
16 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(collection) {
return Ox.sum(collection) / Ox.len(collection);
};
/*@
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(collection, deep) {
// fixme: copy or clone?
var ret, type = Ox.typeOf(collection);
if (deep) {
ret = type == 'array' ? [] : {};
Ox.forEach(collection, function(value, key) {
type = Ox.typeOf(value);
ret[key] = type == 'array' || type == 'object'
? Ox.clone(value, true) : value;
});
} else {
ret = type == 'array' ? collection.slice()
: type == 'object' ? Ox.extend({}, collection)
: collection;
}
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
@*/
// FIXME: a shorter name would be nice (but IE8 doesn't like 'in')
Ox.contains = function(collection, value) {
return (
Ox.isObject(collection) ? Ox.values(collection) : collection
).indexOf(value) > -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(collection) {
var ret = {};
Ox.forEach(collection, function(value) {
ret[value] = (ret[value] || 0) + 1;
});
return ret;
};
/*@
Ox.every <f> 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 v == i; })
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(collection, iterator) {
return Ox.filter(
Ox.values(collection), iterator || Ox.identity
).length == Ox.len(collection);
};
/*@
Ox.filter <f> 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(collection, iterator, that) {
var ret, type = Ox.typeOf(collection);
iterator = iterator || Ox.identity;
if (type == 'object') {
ret = {};
Ox.forEach(collection, function(value, key) {
if (iterator.call(that, value, key, collection)) {
ret[value] = key;
}
});
} else {
ret = Ox.toArray(collection).filter(iterator, that);
if (type == 'string') {
ret = ret.join('');
}
}
return ret;
};
// FIXME: documentation is outdated!
/*@
Ox.forEach <f> 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, index)` (more like `[].forEach()` than like `$.each()`).
(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> Iterator 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(collection, iterator, that) {
var i, key, type = Ox.typeOf(collection);
if (type != 'array' && type != 'object') {
collection = Ox.toArray(collection);
}
try {
if (type == 'object') {
for (key in collection) {
if (Ox.hasOwn(collection, key)) {
// iterator.call(that, collection[key], key, collection);
if (iterator.call(that, collection[key], key, collection) === false) {
console.warn('Returning false in Ox.forEach is deprecated.');
break;
}
}
}
} else {
for (i = 0; i < collection.length; i++) {
if (i in collection) {
// iterator.call(that, collection[i], i, collection);
if (iterator.call(that, collection[i], i, collection) === false) {
console.warn('Returning false in Ox.forEach is deprecated.');
break;
}
}
}
}
} catch (error) {
if (error !== Ox.BreakError) {
throw error;
}
}
return type == 'object' ? key : i;
};
/*@
Ox.indexOf <f> Returns the first index of a collection element that passes a test
> Ox.indexOf([1, 2, 3], function(val) { return val % 2 == 0; })
1
> Ox.indexOf({a: 1, b: 2, c: 3}, function(val) { return val % 2 == 0; })
'b'
> Ox.indexOf('FooBar', function(val) { return val == val.toUpperCase(); })
0
> Ox.indexOf([1, 2, 3], function(val) { return val == 0; })
-1
@*/
Ox.indexOf = function(collection, test) {
var index = Ox.forEach(collection, function(value) {
test(value) && Ox.Break();
});
return index == collection.length ? -1 : index;
};
/*@
Ox.indicesOf <f> Returns all indices of collection elements that pass a test
> Ox.indicesOf([1, 2, 3], function(val) { return val % 2 == 1; })
[0, 2]
> Ox.indicesOf({a: 1, b: 2, c: 3}, function(val) { return val % 2 == 1; })
['a', 'c']
> Ox.indicesOf('FooBar', function(val) { return val == val.toUpperCase(); })
[0, 3]
> Ox.indicesOf([1, 2, 3], function(val) { return val == 0; })
[]
@*/
Ox.indicesOf = function(collection, test) {
var ret = [];
Ox.forEach(collection, function(value, index) {
test(value) && ret.push(index);
});
return ret;
};
/*@
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(value) {
return Ox.len(value) === 0;
};
/*@
Ox.last <f> Gets or sets the last element of an array
Unlike `arrayWithALongName[arrayWithALongName.length - 1]`,
`Ox.last(arrayWithALongName)` 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(array, value) {
var ret;
if (arguments.length == 1) {
ret = array[array.length - 1];
} else {
array[array.length - 1] = value;
ret = array;
}
return ret;
};
/*@
Ox.len <f> Returns the length of an array, node list, object or string
Not to be confused with `Ox.length`, which is the `length` property of the
`Ox` function (`1`).
> 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
@*/
// FIXME: Ox.size() ?
Ox.len = function(collection) {
var ret, type = Ox.typeOf(collection);
if (
type == 'arguments' || type == 'array'
|| type == 'nodelist' || type == 'string'
) {
ret = collection.length;
} else if (type == 'object') {
ret = Object.keys(collection).length;
}
return ret;
};
/*@
Ox.map <f> Transforms the values of an array, object or string
Unlike `[].map()`, `Ox.map()` works for arrays, objects and strings.
> Ox.map([2, 1, 0], function(v, i) { return v == i; })
[false, true, false]
> Ox.map({a: 'b', b: 'b', c: 'b'}, function(v, k) { return v == k; })
{a: false, b: true, c: false}
> Ox.map('foo', function(v) { return v.toUpperCase(); })
'FOO'
> Ox.map([,], function(v, i) { return i; })
[,]
@*/
Ox.map = function(collection, iterator, that) {
var ret, type = Ox.typeOf(collection);
if (type == 'object') {
ret = {};
Ox.forEach(collection, function(value, key) {
ret[key] = iterator.call(that, value, key, collection);
});
} else {
ret = Ox.toArray(collection).map(iterator);
if (type == 'string') {
ret = ret.join('');
}
}
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(collection) {
var ret, values = Ox.values(collection);
if (values.length < Ox.STACK_LENGTH) {
ret = Math.max.apply(null, values)
} else {
ret = values.reduce(function(previousValue, currentValue) {
return Math.max(previousValue, currentValue);
}, -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(collection) {
var ret, values = Ox.values(collection);
if (values.length < Ox.STACK_LENGTH) {
ret = Math.min.apply(null, values)
} else {
ret = values.reduce(function(previousValue, currentValue) {
return Math.min(previousValue, currentValue);
}, 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(collection) {
return Ox.isArray(collection)
? Ox.clone(collection).reverse()
: collection.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?
// fixme: unused, there is OptionGroup (which should be part of Ox.js)
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'
@*/
Ox.shuffle = function(collection) {
var keys, ret, type = Ox.typeOf(collection), values;
if (type == 'object') {
keys = Object.keys(collection);
values = Ox.shuffle(Ox.values(collection));
ret = {};
keys.forEach(function(key, index) {
ret[key] = values[index];
});
} else {
ret = [];
Ox.toArray(collection).forEach(function(value, index) {
var random = Math.floor(Math.random() * (index + 1));
ret[index] = ret[random];
ret[random] = value;
});
if (type == 'string') {
ret = ret.join('');
}
}
return ret;
};
/*@
Ox.slice <f> Alias for `Array.prototype.slice.call`
> (function() { return Ox.slice(arguments, 1, -1); }(1, 2, 3))
[2]
> (function() { return Ox.slice(arguments, 1); }(1, 2, 3))
[2, 3]
@*/
Ox.slice = function(value, start, stop) {
return Array.prototype.slice.call(value, start, stop);
};
// IE8 returns an empty array if undefined is passed as stop
// and an array of null values if a string is passed as value.
// Firefox 3.6 returns an array of undefined values
// if a string is passed as value.
if (
Ox.slice([0]).length == 0
|| Ox.slice('0')[0] === null
|| Ox.slice('0')[0] === void 0
) {
Ox.slice = function(value, start, stop) {
if (Ox.typeOf(value) == 'string') {
value = value.split('');
}
return stop === void 0
? Array.prototype.slice.call(value, start)
: Array.prototype.slice.call(value, start, stop);
};
}
/*@
Ox.some <f> Tests if one or more elements of a collection meet 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([false, null, 0, '', void 0])
false
@*/
Ox.some = function(collection, iterator) {
return Ox.filter(Ox.values(collection), iterator || Ox.identity).length > 0;
};
/*@
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(collection) {
var ret = 0;
collection = arguments.length > 1 ? Ox.toArray(arguments) : collection;
Ox.forEach(collection, function(value) {
value = +value;
ret += isFinite(value) ? value : 0;
});
return ret;
};
/*@
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(collection) {
var ret, type = Ox.typeOf(collection);
if (type == 'array') {
ret = Ox.clone(collection);
} else if (type == 'object') {
ret = [];
Ox.forEach(collection, function(value) {
ret.push(value);
});
} else if (type == 'string') {
ret = collection.split('');
}
return ret;
};
/*@
Ox.walk <f> Iterates over a nested data structure
<script>
Ox.test.number = 0;
Ox.walk({a: 1, b: {c: 2, d: 3}}, function(value) {
Ox.test.number += Ox.isNumber(value) ? value : 0;
});
Ox.test.array = [];
Ox.walk({a: 1, b: {c: 2, d: 3}}, function(value, keys) {
Ox.isNumber(value) && Ox.test.array.push(keys)
});
</script>
> Ox.test.number
6
> Ox.test.array
[['a'], ['b', 'c'], ['b', 'd']]
@*/
Ox.walk = function(collection, iterator, keys) {
keys = keys || [];
Ox.forEach(collection, function(value, key) {
var keys_ = keys.concat(key);
iterator(value, keys_, collection);
Ox.walk(collection[key], iterator, keys_);
});
};