752 lines
26 KiB
JavaScript
752 lines
26 KiB
JavaScript
'use strict';
|
|
|
|
/*@
|
|
Ox.api <f> Turns an array into a list API
|
|
`Ox.api` takes an array and returns a function that allows you to run
|
|
complex queries against it. See the examples below for details.
|
|
items <[o]> An array of objects (key/value stores)
|
|
options <o> API Options
|
|
cache <b|false> If true, cache results
|
|
enums <o> Enumerables, for example `{size: ['S', 'M', 'L']}`
|
|
geo <b|false> If true, return combined area with totals
|
|
map <o> Sort map, for example `{name: function(v, o) { return o.sortName; }}`
|
|
sort <[o]|[s]> Default sort, for example `['+name', '-age']`
|
|
sums <[s]> List of keys to be included in totals
|
|
unique <s|'id'> The name of the unique key
|
|
(items, options) -> <f> List API
|
|
(options) -> <o> Results
|
|
(options, callback) -> <o> Results
|
|
area <o> Combined area
|
|
Present if `keys` was undefined and the `geo` option was set
|
|
east <n> Longitude
|
|
north <n> Latitude
|
|
south <n> Latitude
|
|
west <n> Longitude
|
|
items <n|[o]> Number of items or array of items
|
|
Present if `positions` was not passed. Number if `keys` was
|
|
undefined, otherwise array
|
|
positions <o> Position (value) for each id (key)
|
|
Present if `positions` was passed
|
|
* <n> Sum of the values of any key specified in `sums`
|
|
Present if `keys` was undefined
|
|
options <o> Request options
|
|
keys <[s]> Array of keys to be returned, or empty array for all keys
|
|
Leaving `keys` undefined returns totals, not items
|
|
positions <[s]> Array of ids
|
|
Passing `positions` returns positions, not items
|
|
query <o> Query object
|
|
conditions <[o]> Array of condition objects and/or query objects
|
|
Passing a query object instead of a condition object inserts
|
|
a subcondition
|
|
key <s> Key
|
|
operator <s> Operator, like `'='` or `'!='`
|
|
Can be `'='` (contains) `'=='` (is), `'^'` (starts
|
|
with), `'$'` (ends with), `'<'`, `'<='`, `'>'`, `'>='`,
|
|
optionally prefixed with `'!'` (not)
|
|
value <*> Value
|
|
operator <s> `'&'` or `'|'`
|
|
range <[n]> Range of results, like `[100, 200]`
|
|
sort <[s]> Array of sort strings, like `['+name', '-age']`
|
|
callback <f> Callback function
|
|
results <o> Results
|
|
<script>
|
|
Ox.test.api = Ox.api([
|
|
{id: 'foo', n: 2},
|
|
{id: 'bar', n: 2},
|
|
{id: 'baz', n: 1}
|
|
], {
|
|
sums: ['n']
|
|
});
|
|
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}
|
|
],
|
|
operator: '&'
|
|
}
|
|
}),
|
|
4: Ox.test.api({
|
|
keys: [],
|
|
query: {
|
|
conditions: [
|
|
{key: 'id', operator: '=', value: 'O'},
|
|
{key: 'n', operator: '=', value: [1, 2]}
|
|
],
|
|
operator: '|'
|
|
},
|
|
sort: ['+id']
|
|
}),
|
|
5: Ox.test.api({
|
|
keys: [],
|
|
query: {
|
|
conditions: [
|
|
{key: 'id', operator: '=', value: 'f'},
|
|
{
|
|
conditions: [
|
|
{key: 'id', operator: '=', value: 'a'},
|
|
{key: 'id', operator: '=', value: 'z'}
|
|
],
|
|
operator: '&'
|
|
}
|
|
],
|
|
operator: '|'
|
|
},
|
|
sort: ['+id']
|
|
}),
|
|
6: Ox.test.api({
|
|
keys: [],
|
|
range: [1, 2],
|
|
sort: ['+id']
|
|
}),
|
|
7: Ox.test.api({
|
|
positions: ['foo', 'bar'],
|
|
sort: ['+id']
|
|
})
|
|
};
|
|
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: '-'}]
|
|
});
|
|
Ox.test.api = Ox.api([
|
|
{name: 'John Cale', sortName: 'Cale, John'},
|
|
{name: 'Brian Eno', sortName: 'Eno, Brian'}
|
|
], {
|
|
map: {name: function(value, object) { return object.sortName; }},
|
|
unique: 'name'
|
|
});
|
|
Ox.test.apiResults[9] = Ox.test.api({
|
|
keys: ['name'],
|
|
sort: [{key: 'name', operator: '+'}]
|
|
});
|
|
</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.test.apiResults[9].data
|
|
{items: [{name: 'John Cale'}, {name: 'Brian Eno'}]}
|
|
@*/
|
|
Ox.api = function(items, options) {
|
|
|
|
var api = {
|
|
cache: options.cache,
|
|
enums: options.enums ? parseEnums(options.enums) : {},
|
|
geo: options.geo,
|
|
map: options.map || {},
|
|
sort: options.sort || [],
|
|
sums: options.sums || [],
|
|
unique: options.unique || 'id'
|
|
},
|
|
fn = function(options, callback) {
|
|
var data,
|
|
keys,
|
|
map = {},
|
|
result = {data: {}, status: {code: 200, text: 'ok'}};
|
|
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
|
|
keys = [];
|
|
options.sort = parseSort(
|
|
options.sort.concat(api.sort)
|
|
).filter(function(v) {
|
|
var ret = keys.indexOf(v.key) == -1;
|
|
keys.push(v.key);
|
|
return ret;
|
|
});
|
|
options.sort.forEach(function(v) {
|
|
var key = v.key;
|
|
if (api.enums[key]) {
|
|
map[key] = function(value) {
|
|
return api.enums[key].indexOf(value.toLowerCase());
|
|
};
|
|
} else if (api.map[key]) {
|
|
map[key] = api.map[key];
|
|
}/* else if (Ox.isArray(items[0][key])) {
|
|
sort[key] = function(value) {
|
|
return value.join(', ');
|
|
};
|
|
}*/
|
|
});
|
|
if (options.keys || options.positions) {
|
|
result.data.items = sortBy(result.data.items, options.sort, map, options.query);
|
|
}
|
|
}
|
|
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;
|
|
});
|
|
});
|
|
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) {
|
|
/*
|
|
fixme: slow, disabled
|
|
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 = data.items == 0 ? {
|
|
south: -Ox.MAX_LATITUDE,
|
|
west: -179,
|
|
north: Ox.MAX_LATITUDE,
|
|
east: 179
|
|
} : result.data.items.reduce(function(prev, curr) {
|
|
return {
|
|
south: Math.min(prev.south, curr.south),
|
|
west: Math.min(prev.west, curr.west),
|
|
north: Math.max(prev.north, curr.north),
|
|
east: Math.max(prev.east, curr.east)
|
|
};
|
|
}, {
|
|
south: Ox.MAX_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
|
|
result.data.items = result.data.items.slice(
|
|
options.range[0], options.range[1]
|
|
);
|
|
}
|
|
}
|
|
callback && callback(result);
|
|
return result;
|
|
},
|
|
sortBy = Ox.cache(function sortBy(array, by, map, query) {
|
|
return Ox.sortBy(array, by, map);
|
|
}, {
|
|
key: function(args) {
|
|
return JSON.stringify([args[1], args[3]])
|
|
}
|
|
});
|
|
|
|
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;
|
|
});
|
|
}
|
|
|
|
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; },
|
|
'>=': function(a, b) { return a >= b; },
|
|
'^': function(a, b) { return Ox.startsWith(a, b); },
|
|
'$': function(a, b) { return Ox.endsWith(a, b); }
|
|
};
|
|
if (Ox.isString(itemValue)) {
|
|
itemValue = itemValue.toLowerCase();
|
|
}
|
|
if (api.enums[key] && (
|
|
operator.indexOf('<') > -1
|
|
|| operator.indexOf('>') > -1
|
|
)) {
|
|
itemValue = api.enums[key].indexOf(itemValue);
|
|
}
|
|
return test[operator](itemValue, value) == !not;
|
|
}
|
|
|
|
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)
|
|
) {
|
|
return false; // break
|
|
}
|
|
});
|
|
return match;
|
|
}
|
|
|
|
return api.cache ? Ox.cache(fn, {async: true}) : fn;
|
|
|
|
};
|
|
|
|
/*@
|
|
Ox.compact <f> Removes `null` or `undefined` values from an array
|
|
(array) -> <a> Array without `null` or `undefined` values
|
|
> Ox.compact([null,,1,,2,,3])
|
|
[1, 2, 3]
|
|
@*/
|
|
Ox.compact = function(array) {
|
|
return array.filter(function(value) {
|
|
return value != null;
|
|
});
|
|
};
|
|
|
|
/*@
|
|
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
|
|
(array, query[, leading]) -> <[s]> Array of matches
|
|
array <[s]> Array of strings
|
|
query <s> Query string
|
|
leading <b> If true, returns leading matches only
|
|
> 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 matches = [[], []];
|
|
string = string.toLowerCase();
|
|
array.forEach(function(value) {
|
|
var lowerCase = value.toLowerCase(),
|
|
index = lowerCase.indexOf(string);
|
|
index > -1 && matches[index == 0 ? 0 : 1][
|
|
lowerCase == string ? 'unshift' : 'push'
|
|
](value);
|
|
})
|
|
return leading ? matches[0] : matches[0].concat(matches[1]);
|
|
};
|
|
|
|
/*@
|
|
Ox.flatten <f> Flattens an array
|
|
(array) -> <a> Flattened 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)) {
|
|
ret = ret.concat(Ox.flatten(value));
|
|
} else {
|
|
ret.push(value);
|
|
}
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
/*@
|
|
Ox.getIndexById <f> Returns the first array index of an object with a given id
|
|
(array, id) -> <n> Index (or `-1`)
|
|
array <[o]> Array of objects with a unique `'id'` property
|
|
id <s> 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
|
|
(array, id) -> <o> Object (or `null`)
|
|
array <[o]> Array of objects with a unique `'id'` property
|
|
id <s> 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;
|
|
};
|
|
|
|
/*
|
|
Ox.indexOf = function(arr) {
|
|
// indexOf for primitives, test for function, deep equal for others
|
|
};
|
|
*/
|
|
|
|
/*@
|
|
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.makeArray <f> Wraps any non-array in an array.
|
|
(value) -> <a> Array
|
|
value <*> Any value
|
|
> 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;
|
|
};
|
|
|
|
/*@
|
|
Ox.range <f> 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 <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);
|
|
}));
|
|
return array;
|
|
};
|
|
|
|
(function() {
|
|
|
|
var getSortValue = Ox.cache(function getSortValue(value) {
|
|
var sortValue = value;
|
|
function trim(value) {
|
|
return value.replace(/^\W+/, '');
|
|
}
|
|
if (
|
|
Ox.isEmpty(value)
|
|
|| Ox.isNull(value)
|
|
|| Ox.isUndefined(value)
|
|
) {
|
|
sortValue = null;
|
|
} else if (Ox.isString(value)) {
|
|
// make lowercase and remove leading non-word characters
|
|
sortValue = trim(value.toLowerCase());
|
|
// move leading articles to the end
|
|
// and remove leading non-word characters
|
|
Ox.forEach(['a', 'an', 'the'], function(article) {
|
|
if (new RegExp('^' + article + ' ').test(sortValue)) {
|
|
sortValue = trim(sortValue.slice(article.length + 1))
|
|
+ ', ' + sortValue.slice(0, article.length);
|
|
return false; // break
|
|
}
|
|
});
|
|
// pad numbers
|
|
sortValue = sortValue.replace(/\d+/g, function(match) {
|
|
return Ox.pad(match, 'left', 64, '0');
|
|
});
|
|
}
|
|
return sortValue;
|
|
});
|
|
|
|
/*@
|
|
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
|
|
> Ox.sort(['"z"', '10', '9', 'B', 'a'])
|
|
['9', '10', 'a', 'B', '"z"']
|
|
> 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']
|
|
> Ox.sort(['The 9', 'The 10', 'An A', 'A "B"'])
|
|
['The 9', 'The 10', 'An A', 'A "B"']
|
|
@*/
|
|
Ox.sort = function(array, map) {
|
|
var values = getSortValues(map ? array.map(map) : array);
|
|
return array.sort(function(a, b) {
|
|
a = getSortValue(map ? map(a) : a);
|
|
b = getSortValue(map ? map(b) : b);
|
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
});
|
|
};
|
|
|
|
/*@
|
|
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
|
|
> 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) {
|
|
var sortValues = {};
|
|
by = Ox.makeArray(by).map(function(value) {
|
|
return Ox.isString(value) ? {
|
|
key: value.replace(/^[\+\-]/, ''),
|
|
operator: value[0] == '-' ? '-' : '+'
|
|
} : value;
|
|
});
|
|
map = map || {};
|
|
return array.sort(function(a, b) {
|
|
var aValue, bValue, index = 0, key, ret = 0;
|
|
while (ret == 0 && index < by.length) {
|
|
key = by[index].key;
|
|
aValue = getSortValue(
|
|
map[key] ? map[key](a[key], a) : a[key]
|
|
);
|
|
bValue = getSortValue(
|
|
map[key] ? map[key](b[key], b) : b[key]
|
|
);
|
|
if ((aValue === null) != (bValue === null)) {
|
|
ret = aValue === null ? 1 : -1;
|
|
} else if (aValue < bValue) {
|
|
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 {
|
|
Array.prototype.slice.call(document.getElementsByTagName('a'));
|
|
} catch (error) {
|
|
// Handle IE 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> Removes duplicate values from an array
|
|
(array) -> <a> Array without duplicate values
|
|
> 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;
|
|
};
|