'use strict'; /*@ Ox.api 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 API Options cache If true, cache results enums Enumerables, for example `{size: ['S', 'M', 'L']}` geo If true, return combined area with totals sort <[o]|[s]> Default sort, for example `['+name', '-age']` sums <[s]> List of keys to be included in totals unique The name of the unique key (items, options) -> List API (options) -> Results (options, callback) -> Results area Combined area Present if `keys` was undefined and the `geo` option was set east Longitude north Latitude south Latitude west Longitude items Number of items or array of items Present if `positions` was not passed. Number if `keys` was undefined, otherwise array positions Position (value) for each id (key) Present if `positions` was passed * Sum of the values of any key specified in `sums` Present if `keys` was undefined options 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 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 Key operator Operator, like `'='` or `'!='` Can be `'='` (contains) `'=='` (is), `'^'` (starts with), `'$'` (ends with), `'<'`, `'<='`, `'>'`, `'>='`, optionally prefixed with `'!'` (not) value <*> Value operator `'&'` or `'|'` range <[n]> Range of results, like `[100, 200]` sort <[s]> Array of sort strings, like `['+name', '-age']` callback Callback function results Results > 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 = { 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 options.sort = parseSort(options.sort.concat(api.sort)); 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; }); }); 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 = { 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 result.data.items = result.data.items.slice( options.range[0], options.range[1] ); } } 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; }); } 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) ) { Ox.Break(); } }); return match; } return api.cache ? Ox.cache(fn, {async: true}) : fn; }; /*@ Ox.compact Removes `null` or `undefined` values from an array (array) -> 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 !Ox.isNull(value) && !Ox.isUndefined(value); }); }; /*@ Ox.find 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]) -> Array of matches array <[s]> Array of strings query Query string leading 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 Flattens an array (array) -> 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)) { Ox.flatten(value).forEach(function(value) { ret.push(value); }); } else { ret.push(value); } }); return ret; }; /*@ Ox.getIndexById Returns the first array index of an object with a given id (array, id) -> Index (or `-1`) array <[o]> Array of objects with a unique `'id'` property id 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 Returns the first object in an array with a given id (array, id) -> Object (or `null`) array <[o]> Array of objects with a unique `'id'` property id 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 Gets or sets the last element of an array Unlike `arrayWithALongName[arrayWithALongName.length - 1]`, `Ox.last(arrayWithALongName)` 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('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 Wraps any non-array in an array. (value) -> 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 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 array = []; Ox.loop.apply(null, Ox.toArray(arguments).concat(function(index) { array.push(index); })); return array; }; (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); if (match) { matches = matches.concat(match); } } }); // get length of longest number length = Ox.max(Ox.map(matches, function(value) { return value.length; })); // 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(parseInt(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; } }); return sort; } /*@ Ox.sort Sorts an array, handling articles and digits, ignoring capitalization (array) -> Sorted array (array, map) -> Sorted array array Array map 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 = 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; var ret = 0; if (values[a] < values[b]) { ret = -1; } else if (values[a] > values[b]) { ret = 1; } return ret; }); }; /*@ Ox.sortBy Sorts an array of objects by given properties (array, by[, map]) -> Sorted array array <[o]> Array of objects by <[s]> Array of object keys (asc: 'foo' or '+foo', desc: '-foo') map 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 values = {}; by = Ox.makeArray(by); map = map || {}; by = by.map(function(value) { return Ox.isString(value) ? { key: value.replace(/^[\+\-]/, ''), operator: value[0] == '-' ? '-' : '+' } : value; }); by.map(function(value) { return value.key; }).forEach(function(key) { values[key] = getSortValues(array.map(function(value) { return value[key]; }), map[key]); }); 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 = values[key][a[key]]; bValue = values[key][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 Takes an array-like object and returns a true array (value) -> 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 Removes duplicate values from an array (array) -> 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 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; };