oxjs/source/Ox/js/Array.js

662 lines
22 KiB
JavaScript
Raw Normal View History

2011-11-05 16:46:53 +00:00
'use strict';
/*@
2012-03-29 14:43:05 +00:00
Ox.api <f> Turns an array into a list API
2012-03-29 19:32:05 +00:00
<code>Ox.api</code> takes an array and returns a function that allows you to
run complex queries against it. See the examples below for details.
(items, options) -> <f> List API
items <[o]> An array of objects (key/value stores)
options <o> Options object
2012-04-03 13:09:39 +00:00
cache <b|false> If true, cache results
enums <o> Enumerables, for example <code>{size: ['S', 'M', 'L']}</code>
geo <b|false> If true, return combined area with totals
sort <[o]|[s]> Default sort, for example <code> ['+name', '-age']
sums <[s]> List of keys to be included in totals
unique <s|'id'> The name of the unique key
2012-03-29 14:43:05 +00:00
<script>
Ox.test.api = Ox.api([
{id: 'foo', n: 2},
{id: 'bar', n: 2},
{id: 'baz', n: 1}
], {
sums: ['n']
});
2012-03-29 14:43:05 +00:00
Ox.test.apiResults = {
0: Ox.test.api(),
1: Ox.test.api({
keys: []
}),
2: Ox.test.api({
keys: ['id'],
sort: ['-n', '+id']
}),
3: Ox.test.api({
keys: [],
query: {
conditions: [
{key: 'id', operator: '!^', value: 'f'},
{key: 'n', operator: '>', value: 1}
2012-03-29 14:43:05 +00:00
],
operator: '&'
}
}),
4: Ox.test.api({
keys: [],
query: {
conditions: [
{key: 'id', operator: '=', value: 'O'},
{key: 'n', operator: '=', value: [1, 2]}
2012-03-29 14:43:05 +00:00
],
operator: '|'
},
sort: ['+id']
}),
5: Ox.test.api({
keys: [],
query: {
conditions: [
{key: 'id', operator: '=', value: 'f'},
2012-03-29 14:43:05 +00:00
{
conditions: [
{key: 'id', operator: '=', value: 'a'},
{key: 'id', operator: '=', value: 'z'}
2012-03-29 14:43:05 +00:00
],
operator: '&'
}
],
operator: '|'
},
sort: ['+id']
}),
6: Ox.test.api({
keys: [],
range: [1, 2],
sort: ['+id']
}),
7: Ox.test.api({
positions: ['foo', 'bar'],
sort: ['+id']
2012-03-29 14:43:05 +00:00
})
};
Ox.test.api = Ox.api([
{i: 0, size: 'S'},
{i: 1, size: 'M'},
{i: 2, size: 'L'}
], {
enums: {size: ['S', 'M', 'L']},
unique: 'i'
});
Ox.test.apiResults[8] = Ox.test.api({
keys: ['size'],
query: {
conditions: [{key: 'size', operator: '>=', value: 'M'}],
operator: '&'
},
sort: [{key: 'size', operator: '-'}]
});
2012-03-29 14:43:05 +00:00
</script>
> Ox.test.apiResults[0].data
{items: 3, n: 5}
> Ox.test.apiResults[1].data
{items: [{id: 'foo', n: 2}, {id: 'bar', n: 2}, {id: 'baz', n: 1}]}
> Ox.test.apiResults[2].data
{items: [{id: 'bar'}, {id: 'foo'}, {id: 'baz'}]}
> Ox.test.apiResults[3].data
{items: [{id: 'bar', n: 2}]}
> Ox.test.apiResults[4].data
{items: [{id: 'baz', n: 1}, {id: 'foo', n: 2}]}
> Ox.test.apiResults[5].data
{items: [{id: 'baz', n: 1}, {id: 'foo', n: 2}]}
> Ox.test.apiResults[6].data
{items: [{id: 'baz', n: 1}]}
> Ox.test.apiResults[7].data
{positions: {foo: 2, bar: 0}}
> Ox.test.apiResults[8].data
{items: [{i: 2, size: 'L'}, {i: 1, size: 'M'}]}
@*/
Ox.api = function(items, options) {
var api = {
2012-04-03 13:09:39 +00:00
cache: options.cache,
enums: options.enums ? parseEnums(options.enums) : {},
geo: options.geo,
sort: options.sort || [],
sums: options.sums || [],
unique: options.unique || 'id'
},
fn = function(options, callback) {
var data,
result = {data: {}, status: {code: 200, text: 'ok'}},
sort = {};
options = options || {};
if (options.query) {
// find
options.query.conditions = parseConditions(options.query.conditions);
result.data.items = items.filter(function(item) {
return testQuery(item, options.query);
});
} else {
result.data.items = Ox.clone(items);
}
if (options.sort && result.data.items.length > 1) {
// sort
2012-05-24 07:45:33 +00:00
options.sort = parseSort(options.sort.concat(api.sort));
2012-04-03 13:09:39 +00:00
options.sort.forEach(function(v) {
var key = v.key;
if (api.enums[key]) {
sort[key] = function(value) {
return api.enums[key].indexOf(value.toLowerCase());
};
} /*else if (Ox.isArray(items[0][key])) {
sort[key] = function(value) {
return value.join(', ');
};
}*/
});
result.data.items = Ox.sortBy(result.data.items, options.sort, sort);
}
if (options.positions) {
// return positions
data = {positions: {}};
options.positions.forEach(function(id) {
data.positions[id] = Ox.indexOf(result.data.items, function(item) {
return item[api.unique] == id;
});
2012-04-03 13:09:39 +00:00
});
result.data = data;
} else if (!options.keys) {
// return totals
data = {};
api.sums.forEach(function(key) {
data[key] = Ox.sum(result.data.items.map(function(item) {
return item[key];
}));
})
data.items = result.data.items.length;
if (api.geo) {
/*
2012-04-04 07:06:55 +00:00
fixme: slow, disabled
2012-04-03 13:09:39 +00:00
data.area = Ox.joinAreas(result.data.items.map(function(item) {
return {
sw: {lat: item.south, lng: item.west},
ne: {lat: item.north, lng: item.east}
};
}));
data.area = {
south: data.area.sw.lat,
west: data.area.sw.lng,
north: data.area.ne.lat,
east: data.area.ne.lng
};
*/
data.area = {
south: Ox.MIN_LATITUDE,
west: -180,
north: Ox.MAX_LATITUDE,
east: 180
}
}
result.data = data;
} else {
// return items
if (!Ox.isEmpty(options.keys)) {
// filter keys
if (options.keys.indexOf(api.unique) == -1) {
options.keys.push(api.unique);
}
result.data.items = result.data.items.map(function(item) {
var ret = {};
options.keys.forEach(function(key) {
ret[key] = item[key];
});
return ret;
});
}
if (options.range) {
// apply range
2012-05-24 09:02:59 +00:00
result.data.items = result.data.items.slice(
options.range[0], options.range[1]
2012-04-03 13:09:39 +00:00
);
}
}
callback && callback(result);
return result;
};
function parseEnums(enums) {
// make enumerable strings lowercase
return Ox.map(enums, function(values) {
return values.map(function(value) {
return value.toLowerCase();
});
});
}
function parseConditions(conditions) {
// make string values lowercase,
// and replace enumerable strings used with the
// <, !<, <=, !<=, >, !>, >= or !>= operator
// with their index
return conditions.map(function(condition) {
var key = condition.key,
operator = condition.operator,
values = Ox.makeArray(condition.value);
if (condition.conditions) {
condition.conditions = parseConditions(condition.conditions);
} else {
values = values.map(function(value) {
if (Ox.isString(value)) {
value = value.toLowerCase();
}
if (api.enums[key] && (
operator.indexOf('<') > -1
|| operator.indexOf('>') > -1
)) {
value = api.enums[key].indexOf(value);
}
return value;
});
condition.value = Ox.isArray(condition.value)
? values : values[0];
}
return condition;
});
}
function parseSort(sort) {
// translate 'foo' to {key: 'foo', operator: '+'}
return sort.map(function(sort) {
return Ox.isString(sort) ? {
key: sort.replace(/^[\+\-]/, ''),
operator: sort[0] == '-' ? '-' : '+'
} : sort;
});
}
2012-03-29 14:43:05 +00:00
function testCondition(item, condition) {
var key = condition.key,
operator = condition.operator.replace('!', ''),
value = condition.value,
not = condition.operator[0] == '!',
itemValue = item[key],
test = {
'=': function(a, b) {
return Ox.isArray(b) ? a >= b[0] && a < b[1]
: Ox.isString(a) ? a.indexOf(b) > -1
: a === b;
},
'==': function(a, b) { return a === b; },
'<': function(a, b) { return a < b; },
'<=': function(a, b) { return a <= b; },
'>': function(a, b) { return a > b; },
2012-03-29 14:43:05 +00:00
'>=': function(a, b) { return a >= b; },
'^': function(a, b) { return Ox.starts(a, b); },
2012-05-25 21:59:48 +00:00
'$': function(a, b) { return Ox.ends(a, b); }
2012-03-29 14:43:05 +00:00
};
if (Ox.isString(itemValue)) {
itemValue = itemValue.toLowerCase();
}
if (api.enums[key] && (
operator.indexOf('<') > -1
|| operator.indexOf('>') > -1
)) {
itemValue = api.enums[key].indexOf(itemValue);
}
2012-03-29 14:43:05 +00:00
return test[operator](itemValue, value) == !not;
}
2012-03-29 14:43:05 +00:00
function testQuery(item, query) {
var match = true;
Ox.forEach(query.conditions, function(condition) {
if (condition.conditions) {
match = testQuery(item, condition);
} else {
match = testCondition(item, condition)
}
if (
(query.operator == '&' && !match)
|| (query.operator == '|' && match)
) {
Ox.Break();
2012-03-29 14:43:05 +00:00
}
});
return match;
}
2012-04-03 13:09:39 +00:00
return api.cache ? Ox.cache(fn, {async: true}) : fn;
2012-03-29 14:43:05 +00:00
};
/*@
2012-03-29 10:11:45 +00:00
Ox.compact <f> Removes <code>null</code> or <code>undefined</code> values
(array) -> <a> Array
> Ox.compact([null,,1,,2,,3])
[1, 2, 3]
@*/
Ox.compact = function(array) {
return array.filter(function(value) {
return !Ox.isNull(value) && !Ox.isUndefined(value);
});
};
/*@
Ox.find <f> Returns array elements that match a string
Returns an array of case-insensitive matches: exact match first, then
leading matches, then other matches
> Ox.find(['Bar', 'Barfoo', 'Foo', 'Foobar'], 'foo')
['Foo', 'Foobar', 'Barfoo']
> Ox.find(['Bar', 'Barfoo', 'Foo', 'Foobar'], 'foo', true)
['Foo', 'Foobar']
@*/
Ox.find = function(array, string, leading) {
var ret = [[], []];
string = string.toLowerCase();
array.forEach(function(value) {
var lowerCase = value.toLowerCase(),
index = lowerCase.indexOf(string);
index > -1 && ret[index == 0 ? 0 : 1][
lowerCase == string ? 'unshift' : 'push'
](value);
})
return leading ? ret[0] : ret[0].concat(ret[1]);
};
/*@
Ox.flatten <f> Flattens an array
2012-05-23 07:01:36 +00:00
(arr) -> <a> Array
> Ox.flatten([1, [2, [3], 2], 1])
[1, 2, 3, 2, 1]
@*/
Ox.flatten = function(array) {
var ret = [];
array.forEach(function(value) {
if (Ox.isArray(value)) {
Ox.flatten(value).forEach(function(value) {
ret.push(value);
});
} else {
ret.push(value);
}
});
return ret;
};
/*@
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(array, id) {
return Ox.indexOf(array, 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(array, id) {
var index = Ox.getIndexById(array, id);
return index > -1 ? array[index] : null;
};
/*
2012-05-19 08:30:43 +00:00
Ox.indexOf = function(arr) {
// indexOf for primitives, test for function, deep equal for others
};
*/
2012-05-19 08:30:43 +00:00
/*@
Ox.makeArray <f> Wraps any non-array in an array.
> Ox.makeArray('foo')
['foo']
> Ox.makeArray(['foo'])
['foo']
@*/
Ox.makeArray = function(value) {
var ret, type = Ox.typeOf(value);
if (type == 'arguments') {
ret = Ox.toArray(value);
} else if (type == 'array') {
ret = value;
} else {
ret = [value];
}
return ret;
};
2012-01-07 07:20:02 +00:00
/*@
Ox.range <f> Python-style range
(stop) -> <[n]> range
Returns an array of integers from <code>0</code> (inclusive) to
<code>stop</code> (exclusive).
(start, stop) -> <[n]> range
Returns an array of integers from <code>start</code> (inclusive) to
<code>stop</code> (exclusive).
(start, stop, step) -> <[n]> range
Returns an array of numbers from <code>start</code> (inclusive) to
2012-03-29 10:11:45 +00:00
<code>stop</code> (exclusive), incrementing by <code>step</code>.
2012-01-07 07:20:02 +00:00
start <n> Start value
stop <n> Stop value
step <n> 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 array = [];
Ox.loop.apply(null, Ox.toArray(arguments).concat(function(index) {
array.push(index);
2012-03-29 10:19:42 +00:00
}));
return array;
2012-01-07 07:20:02 +00:00
};
2012-01-11 10:47:06 +00:00
(function() {
function getSortValues(array, map) {
var mappedArray = map ? array.map(map) : array,
length, matches = [], sort = {};
// find numbers
array.forEach(function(value, i) {
var match, mappedValue = mappedArray[i];
if (Ox.isString(mappedValue)) {
match = mappedValue.match(/\d+/g);
2012-05-24 07:55:18 +00:00
if (match) {
matches = matches.concat(match);
}
}
2012-01-11 10:47:06 +00:00
});
// get length of longest number
length = Ox.max(Ox.map(matches, function(value) {
return value.length;
2012-01-11 10:47:06 +00:00
}));
// make lowercase, remove leading non-word characters,
// pad numbers and move leading articles to the end
array.forEach(function(value, i) {
var mappedValue = mappedArray[i];
if (
Ox.isEmpty(mappedValue)
|| Ox.isNull(mappedValue)
|| Ox.isUndefined(mappedValue)
) {
sort[value] = null;
} else if (Ox.isString(mappedValue)) {
sort[value] = mappedValue.toLowerCase()
.replace(/^\W+/, '')
.replace(/\d+/g, function(match) {
return Ox.pad(match, length);
});
Ox.forEach(['a', 'an', 'the'], function(article) {
var length;
if (new RegExp('^' + article + ' ').test(sort[value])) {
length = article.length;
sort[value] = sort[value].slice(length + 1) + ', '
+ sort[value].slice(0, length);
Ox.Break();
}
});
} else {
sort[value] = mappedValue;
}
2012-01-11 10:47:06 +00:00
});
return sort;
}
/*@
Ox.sort <f> Sorts an array, handling articles and digits, ignoring capitalization
(array) -> <a> Sorted array
(array, map) -> <a> Sorted array
array <a> Array
map <f|u> Optional map function that returns the value for the array element
2012-01-12 10:39:05 +00:00
> Ox.sort(['"z"', '10', '9', 'B', 'a'])
['9', '10', 'a', 'B', '"z"']
2012-01-11 10:47:06 +00:00
> Ox.sort([{id: 0, name: '80 Days'}, {id: 1, name: '8 Women'}], function(v) {return v.name})
[{id: 1, name: '8 Women'}, {id: 0, name: '80 Days'}]
> Ox.sort(['In 80 Days Around the World', 'In 9 Minutes Around the World'])
['In 9 Minutes Around the World', 'In 80 Days Around the World']
> Ox.sort(['Man', 'A Plan', 'The Canal'])
['The Canal', 'Man', 'A Plan']
2012-01-11 10:47:06 +00:00
@*/
Ox.sort = function(array, map) {
var values = getSortValues(map ? array.map(map) : array);
return array.sort(function(a, b) {
a = map ? map(a) : a;
b = map ? map(b) : b;
2012-01-11 10:47:06 +00:00
var ret = 0;
if (values[a] < values[b]) {
2012-01-11 10:47:06 +00:00
ret = -1;
} else if (values[a] > values[b]) {
2012-01-11 10:47:06 +00:00
ret = 1;
}
return ret;
});
};
/*@
Ox.sortBy <f> Sorts an array of objects by given properties
(array, by[, map]) -> <a> Sorted array
array <[o]> Array of objects
by <[s]> Array of object keys (asc: 'foo' or '+foo', desc: '-foo')
map <o> Optional map functions, per key, that return the sort value
2012-01-11 10:47:06 +00:00
> Ox.sortBy([{x: 1, y: 1}, {x: 1, y: 2}, {x: 2, y: 2}], ['+x', '-y'])
[{x: 1, y: 2}, {x: 1, y: 1}, {x: 2, y: 2}]
> Ox.sortBy([{id: 0, name: '80 Days'}, {id: 1, name: '8 Women'}], ['name'])
[{id: 1, name: '8 Women'}, {id: 0, name: '80 Days'}]
@*/
Ox.sortBy = function(array, by, map) {
2012-04-11 20:36:27 +00:00
var values = {};
by = Ox.makeArray(by);
map = map || {};
by = by.map(function(value) {
return Ox.isString(value) ? {
key: value.replace(/^[\+\-]/, ''),
operator: value[0] == '-' ? '-' : '+'
} : value;
2012-01-11 10:47:06 +00:00
});
by.map(function(value) {
return value.key;
2012-01-11 10:47:06 +00:00
}).forEach(function(key) {
values[key] = getSortValues(array.map(function(value) {
return value[key];
}), map[key]);
2012-01-11 10:47:06 +00:00
});
return array.sort(function(a, b) {
2012-01-11 10:47:06 +00:00
var aValue, bValue, index = 0, key, ret = 0;
2012-04-11 20:36:27 +00:00
while (ret == 0 && index < by.length) {
2012-01-11 10:47:06 +00:00
key = by[index].key;
aValue = values[key][a[key]];
bValue = values[key][b[key]];
if ((aValue === null) != (bValue === null)) {
ret = aValue === null ? 1 : -1;
} else if (aValue < bValue) {
2012-01-11 10:47:06 +00:00
ret = by[index].operator == '+' ? -1 : 1;
} else if (aValue > bValue) {
ret = by[index].operator == '+' ? 1 : -1;
} else {
index++;
}
}
return ret;
});
};
}());
/*@
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']
@*/
Ox.toArray = function(collection) {
return Ox.slice(collection);
};
try {
2012-05-25 17:07:40 +00:00
Array.prototype.slice.call(document.getElementsByTagName('a'));
} catch (error) {
// Handle MSIE NodeLists
Ox.toArray = function(collection) {
var i, length, ret = [];
try {
ret = Ox.slice(collection);
} catch (error) {
length = collection.length;
for (i = 0; i < length; i++) {
ret[i] = collection[i];
}
}
return ret;
};
}
/*@
Ox.unique <f> Returns an array without duplicate values
(array) -> <a> Array
> Ox.unique([1, 2, 3, 2, 1])
[1, 2, 3]
> Ox.unique([NaN, NaN])
[]
@*/
Ox.unique = function(array) {
return Ox.filter(array, function(value, index) {
return array.indexOf(value) == index;
});
};
/*@
Ox.zip <f> Zips an array of arrays
> Ox.zip([[0, 1], [2, 3], [4, 5]])
[[0, 2, 4], [1, 3, 5]]
> Ox.zip([0, 1, 2], [3, 4, 5])
[[0, 3], [1, 4], [2, 5]]
@*/
Ox.zip = function() {
var args = arguments.length == 1 ? arguments[0] : Ox.toArray(arguments),
array = [];
args[0].forEach(function(value, index) {
array[index] = [];
args.forEach(function(value) {
array[index].push(value[index]);
});
});
return array;
};