Commit 5d3af8bd authored by Tristan Cavelier's avatar Tristan Cavelier

WIP: Great changes on complex queries

Change to a module compatible with nodejs, requirejs, web workers and classic
browsers. Provides classes Query, SimpleQuery, ComplexQuery. Documentation
comming soon.
parent 1c0444c6
...@@ -4,11 +4,27 @@ ...@@ -4,11 +4,27 @@
* http://www.gnu.org/licenses/lgpl.html * http://www.gnu.org/licenses/lgpl.html
*/ */
(function (scope) { /**
* Provides some function to use complex queries with item list
*
* @module complex_queries
*/
var complex_queries;
(function () {
"use strict"; "use strict";
Object.defineProperty(scope, "ComplexQueries", { var to_export = {}, module_name = "complex_queries";
configurable: false, /**
enumerable: false, * Add a secured (write permission denied) property to an object.
writable: false, * @method defineProperty
value: {} * @param {Object} object The object to fill
}); * @param {String} key The object key where to store the property
* @param {Any} value The value to store
*/
function _export(key, value) {
Object.defineProperty(to_export, key, {
"configurable": false,
"enumerable": true,
"writable": false,
"value": value
});
}
// XXX
var ComplexQuery = newClass(Query, function () {
/**
* Filter the item list only if all the sub queries match this item according
* to the logical operator.
* See {{#crossLink "Query/exec:method"}}{{/crossLink}}
*/
this.exec = function () {
todo
};
});
todo
query_class_dict.complex = ComplexQuery;
_export("ComplexQuery", ComplexQuery);
}(jIO)); if (typeof define === "function" && define.amd) {
define(to_export);
} else if (typeof window === "object") {
Object.defineProperty(window, module_name, {
configurable: false,
enumerable: true,
writable: false,
value: to_export
});
} else if (typeof exports === "object") {
var i;
for (i in to_export) {
if (to_export.hasOwnProperty(i)) {
exports[i] = to_export[i];
}
}
} else {
complex_queries = to_export;
}
}());
Object.defineProperty(scope.ComplexQueries, "parse", { /**
configurable: false, * Parse a text request to a json query tree
enumerable: false, * @method parseStringToObject
writable: false, * @param {String} string The string to parse
value: function (string) { * @return {Object} The json query tree
*/
function parseStringToObject(string) {
return result; return result;
} }
}); }; // parseStringToQuery
Object.defineProperty(scope.ComplexQueries,"query",{ /**
configurable:false,enumerable:false,writable:false, * The query to use to filter a list of objects.
value: function (query, object_list) { * This is an abstract class.
var wildcard_character = typeof query.wildcard_character === 'string' ? *
query.wildcard_character : '%', * @class Query
operator_actions = { * @constructor
'=': function (value1, value2) { */
value1 = '' + value1; var Query = newClass(function() {
return value1.match (convertToRegexp ( /**
value2, wildcard_character * Creates a new item list with matching item only
)) || false && true; *
}, * @method exec
'!=': function (value1, value2) { * @param {Array} item_list The list of object
value1 = '' + value1; * @param {Object} [option={}] Some operation option
return !(value1.match (convertToRegexp ( * @param {String} [option.wildcard_character="%"] The wildcard character
value2, wildcard_character * @return {Array} The new item list
))); */
}, this.exec = function (item_list, option) {};
'<': function (value1, value2) { return value1 < value2; },
'<=': function (value1, value2) { return value1 <= value2; }, /**
'>': function (value1, value2) { return value1 > value2; }, * Test if an item matches this query
'>=': function (value1, value2) { return value1 >= value2; }, * @method match
'AND': function (item, query_list) { * @param {Object} item The object to test
var i; * @return {Boolean} true if match, false otherwise
for (i=0; i<query_list.length; ++i) { */
if (! itemMatchesQuery (item, query_list[i])) { this.match = function (item, wildcard_character) {};
return false;
}
} /**
return true; * Convert this query to a parsable string.
}, * @method toString
'OR': function (item, query_list) { * @return {String} The string version of this query
var i; */
for (i=0; i<query_list.length; ++i) { this.toString = function () {};
if (itemMatchesQuery (item, query_list[i])) {
return true; /**
} * Convert this query to an jsonable object in order to be remake thanks to
} * QueryFactory class.
return false; *
}, * @method serialized
'NOT': function (item, query_list) { * @return {Object} The jsonable object
return !itemMatchesQuery(item, query_list[0]); */
} this.serialized = function () {};
}, }, {"static_methods": {
convertToRegexp = function (string) { // XXX
return subString('^' + string.replace( "filterListSelect": function (select_option, list) {
new RegExp( list.forEach(function (item, index) {
'([\\{\\}\\(\\)\\^\\$\\&\\.\\*\\?\\\/\\+\\|\\[\\]\\-\\\\])'. var new_item = {};
replace (wildcard_character? select_option.forEach(function (key) {
'\\'+wildcard_character:undefined,''), new_item[key] = item[key];
'g' });
), list[index] = new_item;
'\\$1' });
) + '$',(wildcard_character||undefined), '.*'); },
}, // XXX
subString = function (string, substring, newsubstring) { "sortOn": function (sort_on_option, list) {
var res = '', i = 0; var sort_index;
if (substring === undefined) { for (sort_index = sort_on_option.length - 1; sort_index >= 0;
return string; sort_index += 1) {
} list.sort(sortFunction(
while (1) { sort_on_option[sort_index][0],
var tmp = string.indexOf(substring,i); sort_on_option[sort_index][1]
if (tmp === -1) { ));
break; }
} },
for (; i < tmp; ++i) { "parseStringToQuery": parseStringToQuery
res += string[i]; }});
}
res += newsubstring; _export("Query", Query);
i += substring.length;
}
for (; i<string.length; ++i) { function query(query, object_list) {
res += string[i]; var wildcard_character = typeof query.wildcard_character === "string" ?
} query.wildcard_character : "%",
return res; // A list of methods according to operators
}, operator_actions = {
itemMatchesQuery = function (item, query_object) { "=": function (value1, value2) {
var i; value1 = value1.toString();
if (query_object.type === 'complex') { return value1.match(convertToRegexp(
return operator_actions[query_object.operator]( value2, wildcard_character
item, query_object.query_list )) || false && true;
); },
} else { '!=': function (value1, value2) {
if (query_object.id) { value1 = value1.toString();
if (typeof item[query_object.id] !== 'undefined') { return !(value1.match(convertToRegexp(
return operator_actions[query_object.operator]( value2, wildcard_character
item[query_object.id], query_object.value )));
); },
} else { '<': function (value1, value2) { return value1 < value2; },
return false; '<=': function (value1, value2) { return value1 <= value2; },
} '>': function (value1, value2) { return value1 > value2; },
} else { '>=': function (value1, value2) { return value1 >= value2; },
return true; 'AND': function (item, query_list) {
} var i;
} for (i=0; i<query_list.length; ++i) {
}, if (! itemMatchesQuery (item, query_list[i])) {
select = function (list, select_list) { return false;
var i;
if (select_list.length === 0) {
return;
}
for (i=0; i<list.length; ++i) {
var list_value = {}, k;
for (k=0; k<select_list.length; ++k) {
list_value[select_list[k]] =
list[i][select_list[k]];
}
list[i] = list_value;
}
},
sortFunction = function (key, asc) {
if (asc === 'descending') {
return function (a,b) {
return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0;
};
}
return function (a,b) {
return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
};
},
mergeList = function (list, list_to_merge, index) {
var i,j;
for (i = index,j = 0; i < list_to_merge.length + index; ++i, ++j) {
list[i] = list_to_merge[j];
}
},
sort = function (list, sort_list) {
var i, tmp, key, asc, sortAndMerge = function() {
sort(tmp,sort_list.slice(1));
mergeList(list,tmp,i-tmp.length);
tmp = [list[i]];
};
if (list.length < 2) {
return;
}
if (sort_list.length === 0) {
return;
}
key = sort_list[0][0];
asc = sort_list[0][1];
list.sort (sortFunction (key,asc));
tmp = [list[0]];
for (i = 1; i < list.length; ++i) {
if (tmp[0][key] === list[i][key]) {
tmp.push(list[i]);
} else {
sortAndMerge();
}
}
sortAndMerge();
},
limit = function (list, limit_list) {
var i;
if (typeof limit_list[0] !== 'undefined') {
if (typeof limit_list[1] !== 'undefined') {
if (list.length > limit_list[1] + limit_list[0]) {
list.length = limit_list[1] + limit_list[0];
}
list.splice(0,limit_list[0]);
} else {
list.length = limit_list[0];
}
}
},
////////////////////////////////////////////////////////////
result_list = [], result_list_tmp = [], j;
object_list = object_list || [];
if (query.query === undefined) {
result_list = object_list;
} else {
for (j=0; j<object_list.length; ++j) {
if ( itemMatchesQuery (
object_list[j], scope.ComplexQueries.parse (query.query)
)) {
result_list.push(object_list[j]);
}
} }
} }
if (query.filter) { return true;
select(result_list,query.filter.select_list || []); },
sort(result_list,query.filter.sort_on || []); 'OR': function (item, query_list) {
limit(result_list,query.filter.limit || []); var i;
for (i=0; i<query_list.length; ++i) {
if (itemMatchesQuery (item, query_list[i])) {
return true;
}
}
return false;
},
'NOT': function (item, query_list) {
return !itemMatchesQuery(item, query_list[0]);
}
},
convertToRegexp = function (string) {
return subString('^' + string.replace(
new RegExp(
'([\\{\\}\\(\\)\\^\\$\\&\\.\\*\\?\\\/\\+\\|\\[\\]\\-\\\\])'.
replace(wildcard_character ?
"\\" + wildcard_character : undefined, ""),
"g"
),
"\\$1"
) + '$',(wildcard_character||undefined), '.*');
},
subString = function (string, substring, newsubstring) {
var res = '', i = 0;
if (substring === undefined) {
return string;
}
while (true) {
var tmp = string.indexOf(substring, i);
if (tmp === -1) {
break;
}
for (; i < tmp; i += 1) {
res += string[i];
} }
return result_list; res += newsubstring;
i += substring.length;
}
for (; i < string.length; i += 1) {
res += string[i];
}
return res;
},
itemMatchesQuery = function (item, query_object) {
var i;
if (query_object.type === 'complex') {
return operator_actions[query_object.operator](
item, query_object.query_list
);
} else {
if (query_object.id) {
if (typeof item[query_object.id] !== 'undefined') {
return operator_actions[query_object.operator](
item[query_object.id], query_object.value
);
} else {
return false;
}
} else {
return true;
}
}
},
select = function (list, select_list) {
var i;
if (select_list.length === 0) {
return;
}
for (i=0; i<list.length; ++i) {
var list_value = {}, k;
for (k=0; k<select_list.length; ++k) {
list_value[select_list[k]] =
list[i][select_list[k]];
}
list[i] = list_value;
}
},
sortFunction = function (key, asc) {
if (asc === 'descending') {
return function (a,b) {
return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0;
};
}
return function (a,b) {
return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
};
},
mergeList = function (list, list_to_merge, index) {
var i,j;
for (i = index,j = 0; i < list_to_merge.length + index; ++i, ++j) {
list[i] = list_to_merge[j];
}
},
sort = function (list, sort_list) {
var i, tmp, key, asc, sortAndMerge = function() {
sort(tmp,sort_list.slice(1));
mergeList(list,tmp,i-tmp.length);
tmp = [list[i]];
};
if (list.length < 2) {
return;
}
if (sort_list.length === 0) {
return;
}
key = sort_list[0][0];
asc = sort_list[0][1];
list.sort (sortFunction (key,asc));
tmp = [list[0]];
for (i = 1; i < list.length; ++i) {
if (tmp[0][key] === list[i][key]) {
tmp.push(list[i]);
} else {
sortAndMerge();
}
}
sortAndMerge();
},
limit = function (list, limit_list) {
var i;
if (typeof limit_list[0] !== 'undefined') {
if (typeof limit_list[1] !== 'undefined') {
if (list.length > limit_list[1] + limit_list[0]) {
list.length = limit_list[1] + limit_list[0];
}
list.splice(0,limit_list[0]);
} else {
list.length = limit_list[0];
}
}
},
////////////////////////////////////////////////////////////
result_list = [], result_list_tmp = [], j;
object_list = object_list || [];
if (query.query === undefined) {
result_list = object_list;
} else {
for (j=0; j<object_list.length; ++j) {
if ( itemMatchesQuery (
object_list[j], scope.ComplexQueries.parse (query.query)
)) {
result_list.push(object_list[j]);
}
} }
}); }
if (query.filter) {
select(result_list,query.filter.select_list || []);
sort(result_list,query.filter.sort_on || []);
limit(result_list,query.filter.limit || []);
}
return result_list;
}
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global _export: true, ComplexQuery: true, SimpleQuery: true,
newClass: true,
sortFunction: true, convertSearchTextToRegExp: true */
// XXX
var query_class_dict = {}, query_factory = {};
newClass.apply(query_factory, [{
"secure_methods": true
}, function () {
// XXX
this.create = function (object) {
if (typeof object.type === "string" &&
query_class_dict[object.type]) {
return new query_class_dict[object.type](object);
}
return null;
};
}]); // end QueryFactory
_export("factory", query_factory);
Object.defineProperty(scope.ComplexQueries,"serialize",{ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
configurable:false,enumerable:false,writable:false,value:function(query){ /*global _export: true, to_export: true */
var str_list = [], i;
if (query.type === 'complex') { function objectToSearchText(query) {
str_list.push ( '(' ); var str_list = [];
for (i=0; i<query.query_list.length; ++i) { if (query.type === "complex") {
str_list.push( scope.ComplexQueries.serialize(query.query_list[i]) ); str_list.push("(");
str_list.push( query.operator ); (query.query_list || []).forEach(function (sub_query) {
} str_list.push(objectToSearchText(sub_query));
str_list.length --; str_list.push(query.operator);
str_list.push ( ')' ); });
return str_list.join(' '); str_list.length -= 1;
} else if (query.type === 'simple') { str_list.push(")");
return query.id + (query.id?': ':'') + query.operator + ' "' + query.value + '"'; return str_list.join(" ");
} }
return query; if (query.type === "simple") {
} return query.id + (query.id ? ": " : "") + (query.operator || "=") + ' "' +
}); query.value + '"';
}
throw new TypeError("This object is not a query");
}
_export("objectToSearchText", objectToSearchText);
/**
* The SimpleQuery inherits from Query, and compares one metadata value
*
* @class SimpleQuery
* @param {Object} [spec={}] The specifications
* @param {String} [spec.operator="="] The compare method to use
* @param {String} spec.key The metadata key
* @param {String} spec.value The value of the metadata to compare
* @param {String} [spec.wildcard_character="%"] The wildcard character
*/
var SimpleQuery = newClass(Query, function (spec) {
/**
* Operator to use to compare object values
*
* @property operator
* @type String
* @default "="
*/
this.operator = spec.operator || "=";
/**
* Key of the object which refers to the value to compare
*
* @property key
* @type String
*/
this.key = spec.key;
/**
* Value is used to do the comparison with the object value
*
* @property value
* @type String
*/
this.value = spec.value;
/**
* The wildcard character used to extend comparison action
*
* @property wildcard_character
* @type String
*/
this.wildcard_character = spec.wildcard_character || "%";
/**
* #crossLink "Query/exec:method"
*/
this.exec = function (item_list, option) {
var new_item_list = [];
item_list.forEach(function (item) {
if (!this.match(item, option.wildcard_character)) {
new_item_list.push(item);
}
});
if (option.sort_on) {
Query.sortOn(option.sort_on, new_item_list);
}
if (option.limit) {
new_item_list = new_item_list.slice(
option.limit[0],
option.limit[1] + option.limit[0] + 1
);
}
if (option.select_list) {
Query.filterListSelect(option.select_list, new_item_list);
}
return new_item_list;
};
/**
* #crossLink "Query/match:method"
*/
this.match = function (item, wildcard_character) {
this[this.operator](item[this.key], this.value, wildcard_character);
};
/**
* #crossLink "Query/toString:method"
*/
this.toString = function () {
return (this.key ? this.key + ": " : "") + (this.operator || "=") + ' "' +
this.value + '"';
};
/**
* #crossLink "Query/serialized:method"
*/
this.serialized = function () {
return {
"type": "simple",
"operator": this.operator,
"key": this.key,
"value": this.value
};
};
// XXX
this["="] = function (object_value, comparison_value,
wildcard_character) {
return convertSearchTextToRegExp(
comparison_value.toString(),
wildcard_character || this.wildcard_character
).test(object_value.toString());
};
// XXX
this["!="] = function (object_value, comparison_value,
wildcard_character) {
return !convertSearchTextToRegExp(
comparison_value.toString(),
wildcard_character || this.wildcard_character
).test(object_value.toString());
};
// XXX
this["<"] = function (object_value, comparison_value) {
return object_value < comparison_value;
};
// XXX
this["<="] = function (object_value, comparison_value) {
return object_value <= comparison_value;
};
// XXX
this[">"] = function (object_value, comparison_value) {
return object_value > comparison_value;
};
// XXX
this[">="] = function (object_value, comparison_value) {
return object_value >= comparison_value;
};
});
query_class_dict.simple = SimpleQuery;
_export("SimpleQuery", SimpleQuery);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global _export: true */
#!/usr/bin/env node
// keywords: js, javascript, new class creator, generator
/**
* Create a class, manage inheritance, static methods,
* protected attributes and can hide methods or/and secure methods
*
* @method newClass
* @param {Class} Class Classes to inherit from (0..n)
* @param {Object} option Class option (0..n)
* @param {Boolean} [option.secure_methods=false] Make methods not configurable
* and not writable
* @param {Boolean} [option.hide_methods=false] Make methods not enumerable
* @param {Object} [option.static_methods={}] Add static methods
* @param {Function} constructor The new class constructor
* @return {Class} The new class
*/
function newClass() {
var j, k, constructors = [], option, new_class;
for (j = 0; j < arguments.length; j += 1) {
if (typeof arguments[j] === "function") {
constructors.push(arguments[j]);
} else if (typeof arguments[j] === "object") {
option = option || {};
for (k in arguments[j]) {
if (arguments[j].hasOwnProperty(k)) {
option[j] = arguments[j][k];
}
}
}
}
function postCreate(that) {
// modify the object according to 'option'
var key;
if (option) {
for (key in that) {
if (that.hasOwnProperty(key)) {
if (typeof that[key] === "function") {
Object.defineProperty(that, key, {
configurable: option.secure_methods ? false : true,
enumerable: option.hide_methods ? false : true,
writable: option.secure_methods ? false : true,
value: that[key]
});
}
}
}
}
}
new_class = function (spec, my) {
var i;
spec = spec || {};
my = my || {};
// don't use forEach !
for (i = 0; i < constructors.length; i += 1) {
constructors[i].apply(this, [spec, my]);
}
postCreate(this);
return this;
};
for (j in (option.static_methods || {})) {
if (option.static_methods.hasOwnProperty(j)) {
new_class[j] = option.static_methods[j];
}
}
postCreate(new_class);
return new_class;
}
/**
* Escapes regexp special chars from a string.
* @method regexpEscapeString
* @param {String} string The string to escape
* @return {String} The escaped string
*/
function regexpEscapeString(string) {
if (typeof string === "string") {
return string.replace(/([\\\.\$\[\]\(\)\{\}\^\?\*\+\-])/g, "\\$1");
}
}
_export("regexpEscapeString", regexpEscapeString);
/**
* Convert a search text to a regexp.
* @method convertSearchTextToRegExp
* @param {String} string The string to convert
* @param {String} [wildcard_character=undefined] The wildcard chararter
* @return {RegExp} The search text regexp
*/
function convertSearchTextToRegExp(string, wildcard_character) {
return new RegExp("^" + regexpEscapeString(string).replace(
regexpEscapeString(wildcard_character),
'.*'
) + "$");
}
_export("convertSearchTextToRegExp", convertSearchTextToRegExp);
// XXX
function sortFunction(key, way) {
if (way === 'descending') {
return function (a,b) {
return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0;
};
}
return function (a,b) {
return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
};
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment