'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, options) -> List API items <[o]> An array of objects (key/value stores) options Options object cache If true, cache results enums Enumerables, for example {size: ['S', 'M', 'L', 'XL']} 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 > 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.starts(a, b); }, '$': function(a, b) { return Ox.ends(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 (array) -> 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 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 Flattens an array (arr) -> 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 > 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 > 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.makeArray 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; }; /*@ 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(arr, fn) { var arr_ = fn ? arr.map(fn) : arr, len, matches = [], sort = {}; // find numbers arr.forEach(function(val, i) { var match; if (Ox.isString(val)) { match = arr_[i].match(/\d+/g); if (match) { matches = matches.concat(match); } } }); // get length of longest number len = Ox.max(Ox.map(matches, function(val) { return val.length; })); // make lowercase, remove leading non-word characters, // pad numbers and move leading articles to the end arr.forEach(function(val, i) { var val_ = arr_[i]; if ( Ox.isEmpty(val_) || Ox.isNull(val_) || Ox.isUndefined(val_) ) { sort[val] = null; } else if (Ox.isString(val_)) { sort[val] = val_.toLowerCase() .replace(/^\W+/, '') .replace(/\d+/g, function(match) { return Ox.pad(match, len); }); Ox.forEach(['a', 'an', 'the'], function(article) { var len; if (new RegExp('^' + article + ' ').test(sort[val])) { len = article.length; sort[val] = sort[val].slice(len + 1) + ', ' + sort[val].slice(0, len); Ox.Break(); } }); } else { sort[val] = val_; } }); 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 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 Returns an array without duplicate values (array) -> 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 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; };