'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 = Ox.sub( result.data.items, 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 (arr) -> Array > Ox.compact([null,,1,,2,,3]) [1, 2, 3] @*/ Ox.compact = function(arr) { return arr.filter(function(val) { return !Ox.isNull(val) && !Ox.isUndefined(val); }); }; /*@ Ox.flatten Flattens an array (arr) -> Array > Ox.flatten([1, [2, [3], 2], 1]) [1, 2, 3, 2, 1] @*/ Ox.flatten = function(arr) { var ret = []; arr.forEach(function(val) { if (Ox.isArray(val)) { Ox.flatten(val).forEach(function(val) { ret.push(val); }); } else { ret.push(val); } }); return ret; }; /* Ox.indexOf = function(arr) { // indexOf for primitives, test for function, deep equal for others }; */ /*@ Ox.merge Merges an array with one or more other arrays For convenience, literals are treated as arrays with one element (arr, arr[, arr[, ...]]) -> Array > Ox.merge([1], [2, 3, 2], [1]) [1, 2, 3, 2, 1] > Ox.merge(1, [2, 3, 2], 1) [1, 2, 3, 2, 1] @*/ // FIXME: [].concat exists! Ox.merge = function(arr) { arr = Ox.makeArray(arr); Ox.slice(arguments, 1).forEach(function(arg) { arg = Ox.makeArray(arg); if (arg.length < Ox.STACK_SIZE) { arr.push.apply(arr, arg); } else { arg.forEach(function(val) { arr.push(val); }); } }); return arr; }; /*@ 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 arr = []; Ox.loop.apply(null, Ox.toArray(arguments).concat(function(i) { arr.push(i); })); return arr; }; (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); match && 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].substr(len + 1) + ', ' + sort[val].substr(0, len); Ox.break(); } }); } else { sort[val] = val_; } }); return sort; } /*@ Ox.sort Sorts an array, handling articles and digits, ignoring capitalization (arr) -> Sorted array (arr, fn) -> Sorted array arr Array fn 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(arr, fn) { var sort = getSortValues(fn ? arr.map(fn) : arr); return arr.sort(function(a, b) { a = fn ? fn(a) : a; b = fn ? fn(b) : b; var ret = 0; if (sort[a] < sort[b]) { ret = -1; } else if (sort[a] > sort[b]) { ret = 1; } return ret; }); }; /*@ Ox.sortBy Sorts an array of objects by given properties (arr, by) -> Sorted array arr <[o]> Array of objects by <[s]> Array of object keys (asc: 'foo' or '+foo', desc: '-foo') fn Optional 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(arr, by, fn) { var values = {}; by = Ox.makeArray(by); fn = fn || {}; by = by.map(function(v) { return Ox.isString(v) ? { key: v.replace(/^[\+\-]/, ''), operator: v[0] == '-' ? '-' : '+' } : v; }); by.map(function(v) { return v.key; }).forEach(function(key) { values[key] = getSortValues(arr.map(function(v) { return v[key]; }), fn[key]); }); return arr.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.unique Returns an array without duplicate values > Ox.unique([1, 2, 3, 2, 1]) [1, 2, 3] > Ox.unique([NaN, NaN]) [] @*/ Ox.unique = function(arr) { return Ox.filter(arr, function(val, i) { return arr.indexOf(val) == i; }); }; /*@ 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), arr = []; args[0].forEach(function(v, i) { arr[i] = []; args.forEach(function(v) { arr[i].push(v[i]); }); }); return arr; };