konsultaner/jsonOdm

View on GitHub
src/query.js

Summary

Maintainability
F
1 wk
Test Coverage
"use strict";

// for code climate recognition
if (typeof jsonOdm === "undefined") {
    var jsonOdm;
    if(typeof module === "undefined"){
        jsonOdm = new JsonOdm();
    }else{
        jsonOdm = global.jsonOdm;
    }
}

/** @namespace jsonOdm.Query */

/**
 * The query object that holds the collection to be queried
 * @param {jsonOdm.Collection} [collection]
 * @constructor
 * @example //This example shows how to query a collection
 * var myCollection = new jsonOdm('myCollection');
 * var $q = myCollection.query();
 * $q.$and(
 *    $q.$or(
 *        $q.$branch('child','id').$eq(1,2),
 *        $q.$branch('child').$isNull()
 *    ),
 *    $q.$each('enabled').$eq(1,true)
 * ).$all();
 * @example //This example shows how to delete some entries of a collection
 * var myCollection = new jsonOdm('myCollection');
 * var $q = myCollection.query();
 * $q.$branch('child','id').$eq(1,2).$delete();
 */
jsonOdm.Query = function (collection) {
    this.$$commandQueue = [];
    this.$$aggregationBeforeCollectQueue = [];
    this.$$aggregationResultQueue = [];
    this.$$collection = collection || [];
    this.$$accumulation = false;
};

/**
 * Returns a collection containing all matching elements
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * collection.$query()
 *    .$branch("id").$gt(500)
 *    .$delete();
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$delete = function () {
    if (this.$$commandQueue.length < 1) {
        return this;
    }
    for (var i = 0; i < this.$$collection.length; ) {
        var validCollection = true;
        for (var j = 0; j < this.$$commandQueue.length; j++) {
            if (!(validCollection = validCollection && this.$$commandQueue[j](this.$$collection[i]))) {
                break;
            }
        }
        if (validCollection) {
            this.$$collection.splice(i, 1);
        } else {
            i++;
        }
    }
    return this;
};

/**
 * Returns a collection containing all matching elements within the given range
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * collection.$query()
 *    .$branch("id").$eq(2,9)
 *    .$result(1,3);
 * @param {int} [start] return a subset starting at n; default = 0
 * @param {int} [length] return a subset with the length n; default = collection length
 * @return {*}
 */
jsonOdm.Query.prototype.$result = function (start, length) {
    if (this.$$commandQueue.length < 1 && this.$$aggregationBeforeCollectQueue < 1) {
        return this.$$collection;
    }
    start = typeof start === "undefined" ? 0 : start;
    length = typeof length === "undefined" ? this.$$collection.length : length;

    var filterCollection = new jsonOdm.Collection(),
        resultingElement, i, j;

    for (i = 0; i < this.$$collection.length; i++) {
        var validCollection = true;
        for (j = 0; j < this.$$commandQueue.length; j++) {
            var commandResult = this.$$commandQueue[j](this.$$collection[i]);
            if (!(validCollection = validCollection && commandResult !== null && commandResult !== false && typeof commandResult !== "undefined")) {
                break;
            }
        }
        if (validCollection) {
            if (start > 0) {
                start--;
                continue;
            }
            if (length <= 0) {
                return filterCollection;
            }

            resultingElement = this.$$collection[i];
            for (j = 0; j < this.$$aggregationBeforeCollectQueue.length; j++) {
                resultingElement = this.$$aggregationBeforeCollectQueue[j](i, resultingElement);
            }
            filterCollection.push(resultingElement);
            length--;
        }
    }
    for (i = 0; i < this.$$aggregationResultQueue.length; i++) {
        filterCollection = this.$$aggregationResultQueue[i](filterCollection);
    }
    return filterCollection;
};

/**
 * Returns a collection containing all matching elements
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * collection.$query()
 *    .$branch("id").$eq(2,9)
 *    .$all();
 * @return {jsonOdm.Collection}
 */
jsonOdm.Query.prototype.$all = function () {
    return this.$result();
};

/**
 * Short hand version for $all(true)
 * @return {jsonOdm.Collection}
 */
jsonOdm.Query.prototype.$first = function () {
    return this.$result(0, 1)[0];
};

//////////////////////////////////// COLLECTION AGGREGATION

/**
 * Helper method for aggregation methods
 * @param {function[]|function} afterValidation Push into the query queue after all commands have been executed. Returning false will result in a skip of this value
 * @param {function[]|function} [beforeCollect] Push into the before collect queue to change or replace the collection element
 * @param {function[]|function} [aggregation] If the result of the whole aggregation changes, i.e. for searching, or ordering
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$aggregateCollection = function (afterValidation, beforeCollect, aggregation) {
    if (typeof afterValidation === "function") {
        afterValidation = [afterValidation];
    }
    if (typeof beforeCollect === "function") {
        beforeCollect = [beforeCollect];
    }
    if (typeof aggregation === "function") {
        aggregation = [aggregation];
    }

    if (jsonOdm.util.isArray(afterValidation)) {
        this.$$commandQueue = this.$$commandQueue.concat(afterValidation);
    }
    if (jsonOdm.util.isArray(beforeCollect)) {
        this.$$aggregationBeforeCollectQueue = this.$$aggregationBeforeCollectQueue.concat(beforeCollect);
    }
    if (jsonOdm.util.isArray(aggregation)) {
        this.$$aggregationResultQueue = this.$$aggregationResultQueue.concat(aggregation);
    }
    return this;
};

/**
 * Groups all elements of a collection by a given grouping schema
 * @param {...jsonOdm.Query}
 * @return {jsonOdm.Query}
 * @example
 * var collection = new jsonOdm.Collection("employees");
 * var $query = collection.$query();
 * var groupedResult = $query.$and($query.$branch("age").$gt(21),$query.$branch("age").$lt(50)) // query before grouping
 *       .$group(
 *            "salaryRate", // as used with $branch
 *            ["salaryGroup","name"], // as used with $branch
 *            // A projection object defining the accumulation
 *            {
 *                 missingDays:$query.$sum("daysAtHome"),
 *                 holidayDays:$query.$sum("daysOnHoliday"),
 *                 averageMissingDays:$query.$avg("daysAtHome"),
 *                 averageHolidayDays:$query.$avg("daysOnHoliday"),
 *                 count:$query.$count()
 *            }
 *       ).$all();
 * // RESULT COULD BE
 * // [
 * //   {"salaryRate":3300,"salaryGroup":{"name":"Developer"},"missingDays":22,"holidayDays":144,"averageMissingDays":11,"averageHolidayDays":72,"count":2},
 * //   {"salaryRate":2800,"salaryGroup":{"name":"Tester"}   ,"missingDays":10,"holidayDays":66 ,"averageMissingDays":5, "averageHolidayDays":33,"count":2},
 * //   {"salaryRate":4800,"salaryGroup":{"name":"Boss"}     ,"missingDays":12,"holidayDays":33 ,"averageMissingDays":12,"averageHolidayDays":33,"count":1}
 * // ]
 */
jsonOdm.Query.prototype.$group = function () {
    var orderByFields = arguments;
    var accumulationProjectionDefinition = false;
    var aggregationResultBuffer = [];
    // last argument might be a projection
    if (
        arguments.length > 1 && !(jsonOdm.util.isArray(arguments[arguments.length - 1])) && !(jsonOdm.util.is(arguments[arguments.length - 1], "string")) &&
        typeof arguments[arguments.length - 1] === "object"
    ) {
        accumulationProjectionDefinition = arguments[arguments.length - 1];
        orderByFields = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
    }
    return this.$aggregateCollection(
        (function (orderBy, aggregationResult) {
            var valueVariations = {};
            return function (collectionElement) {
                // walk through the collection
                var accumulationObject = {},
                    currentValueVariation = valueVariations,
                    value;
                // create result sets matching the order by parameters
                for (var i = 0; i < orderBy.length; i++) {
                    var currentOrderBy = jsonOdm.util.isArray(orderBy[i]) ? orderBy[i] : [orderBy[i]];
                    value = jsonOdm.util.branch(collectionElement, currentOrderBy);
                    if (i < orderBy.length - 1) {
                        if (typeof currentValueVariation["" + value] === "undefined") {
                            currentValueVariation["" + value] = {};
                        }
                        currentValueVariation = currentValueVariation["" + value];
                    }
                    var accumulationObjectBuffer = accumulationObject;
                    for (var j = 0; j < currentOrderBy.length - 1; j++) {
                        if (typeof accumulationObjectBuffer[currentOrderBy[j]] === "undefined") {
                            accumulationObjectBuffer[currentOrderBy[j]] = {};
                        }
                        accumulationObjectBuffer = accumulationObjectBuffer[currentOrderBy[j]];
                    }
                    accumulationObjectBuffer[currentOrderBy[currentOrderBy.length - 1]] = value;
                }
                if (!currentValueVariation["" + value]) {
                    // this valueVariation has not been found before
                    currentValueVariation["" + value] = {
                        accumulationObject: accumulationObject,
                        subResultSet: []
                    };
                    aggregationResult.push(currentValueVariation["" + value]);
                }
                currentValueVariation["" + value].subResultSet.push(collectionElement);

                return true;
            };
        })(orderByFields, aggregationResultBuffer),
        null,
        (function (aggregationResult, accumulationProjection) {
            function falseQueryAccumulation(projection) {
                for (var i in projection) {
                    if (!projection.hasOwnProperty(i)) {
                        continue;
                    }
                    if (projection[i] instanceof jsonOdm.Query) {
                        projection[i].$$accumulation = false;
                    }
                    if (typeof projection[i] === "object") {
                        falseQueryAccumulation(projection[i]);
                    }
                }
            }

            return function () {
                var resultCollection = new jsonOdm.Collection();
                for (var i = 0; i < aggregationResult.length; i++) {
                    if (accumulationProjection === false) {
                        resultCollection.push(aggregationResult[i].accumulationObject);
                    } else {
                        falseQueryAccumulation(accumulationProjection);
                        var projectionResult = {};
                        for (var j = 0; j < aggregationResult[i].subResultSet.length; j++) {
                            projectionResult = jsonOdm.util.projectElement(accumulationProjection, aggregationResult[i].subResultSet[j]);
                        }
                        for (j in aggregationResult[i].accumulationObject) {
                            if (aggregationResult[i].accumulationObject.hasOwnProperty(j)) {
                                projectionResult[j] = aggregationResult[i].accumulationObject[j];
                            }
                        }
                        resultCollection.push(projectionResult);
                    }
                }
                return resultCollection;
            };
        })(aggregationResultBuffer, accumulationProjectionDefinition));
};

/**
 * Projects all elements of the collection into a given schema
 * @param {*} projection The projection definition with nested definitions
 * @return {jsonOdm.Query}
 * @example
 * var collection = new jsonOdm.Collection("myBooks");
 * var $query = collection.$query()
 *    .$branch("id").$gt(12) // query before project
 *    .$project({
 *        title: 1,
 *        isbn: {
 *            prefix: $query.$branch("isbn").$subString(0,3),
 *            group: $query.$branch("isbn").$subString(3,2),
 *            publisher: $query.$branch("isbn").$subString(5,4),
 *            title: $query.$branch("isbn").$subString(9,3),
 *            checkDigit: $query.$branch("isbn").$subString(12,1)
 *         },
 *         lastName: function(element){return element.author.last}, // functions can be used as well
 *         copiesSold: $query.$branch("copies")
 *    });
 * var collectionResult = $query.$all();
 */
jsonOdm.Query.prototype.$project = function (projection) {
    return this.$aggregateCollection(null, function (index, element) {
        return jsonOdm.util.projectElement(projection, element);
    });
};

//////////////////////////////////// COLLECTION QUERYING AND FILTERING

/**
 * Test a collection or collection field against one or more values
 * @param {*} comparables An array of values to test again
 * @param {function} collectionTest the test function to evaluate the values
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$testCollection = function (comparables, collectionTest) {
    var lastCommand = this.$$commandQueue.pop();
    var $testCollection = (function () {
        return function (collection) {
            if (!((lastCommand instanceof jsonOdm.Collection || typeof lastCommand === "function" || typeof lastCommand === "undefined") && typeof collectionTest === "function")) {
                return false;
            }
            var collectionValue = typeof lastCommand === "undefined" ? collection : (lastCommand instanceof jsonOdm.Collection ? lastCommand : lastCommand(collection));
            return !!collectionTest(collectionValue, comparables);
        };
    })();
    this.$$commandQueue.push($testCollection);
    return this;
};

/**
 * Test a collection or collection field against one or more values
 * @param {jsonOdm.Query[]} queries A finite number of operators
 * @param {function} operator the test function to evaluate the values
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$queryOperator = function (queries, operator) {
    var $testCollection = (function (queries, currentOprator) {
        return function (collection) {
            if (typeof currentOprator !== "function") {
                return false;
            }
            var commandResults = [];
            for (var i = 0; i < queries.length; i++) {
                if (queries[i] instanceof jsonOdm.Query) {
                    for (var j = 0; j < queries[i].$$commandQueue.length; j++) {
                        commandResults.push(queries[i].$$commandQueue[j](collection));
                    }
                } else {
                    // also accept raw values
                    commandResults.push(queries[i]);
                }
            }
            return currentOprator(commandResults);
        };
    })(queries, operator);
    var subQuery = new jsonOdm.Query(this.$$collection);
    subQuery.$$commandQueue.push($testCollection);
    return subQuery;
};

/** Go down the property tree of the collection
 * @param {...String} node A variable amount of nodes to traverse down the document tree
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$branch = function (node) {
    if( typeof node === "undefined" ) {
        return this;
    }
    var $branch = (function (nodes) {
        /**
         * @param {*} The collection to go down
         * @return {Query|boolean} The query object with the sub collection or false if querying was impossible
         */
        return function (collection) {
            return jsonOdm.util.branch(collection, nodes);
        };
    })(arguments);
    var subQuery = new jsonOdm.Query(this.$$collection);
    subQuery.$$commandQueue.push($branch);
    return subQuery;
};

// FIELD MODIFIER

/** Modify fields before validation
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$modifyField = function (modifier) {
    var $modifier = (function (currentModifier, lastCommand) {
        /**
         * @param {*} The collection to go down
         * @return {Query|boolean} The query object with the sub collection or false if querying was impossible
         */
        return function (collection) {
            collection = lastCommand !== null ? lastCommand(collection) : collection;
            return typeof currentModifier === "function" ? currentModifier(collection) : collection;
        };
    })(modifier, this.$$commandQueue.length ? this.$$commandQueue[this.$$commandQueue.length - 1] : null);
    this.$$commandQueue.push($modifier);
    return this;
};

/** A generation for all native String.prototype methods to make them available via $modifyField <br/>
 * Supported Methods are: "charAt", "charCodeAt", "concat", "fromCharCode", "indexOf", "lastIndexOf", "localeCompare", "match", "replace", "search", "slice", "split", "substr", "substring", "toLocaleLowerCase", "toLocaleUpperCase", "toLowerCase", "toUpperCase", "trim", "valueOf"
 * @param {*} [args] The string methods parameter
 * @return {jsonOdm.Query}
 * @method StringPrototype
 * @memberof jsonOdm.Query.prototype
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * var $query = collection.$query();
 *    $query.$branch("explosionBy").$trim().$substr(0,3).$toUpperCase().$eq("TNT").$all();
 */
jsonOdm.Query.stringFiledModifyer = ["charAt", "charCodeAt", "concat", "fromCharCode", "indexOf", "lastIndexOf", "localeCompare", "match", "replace", "search", "slice", "split", "substr", "substring", "toLocaleLowerCase", "toLocaleUpperCase", "toLowerCase", "toUpperCase", "trim", "valueOf"];
function createQueryStringModifier(modifyer) {
    return function () {
        return this.$modifyField((function (args, modifyer) {
            return function (value) {
                return typeof value === "string" && String.prototype.hasOwnProperty(modifyer) ? String.prototype[modifyer].apply(value, args) : value;
            };
        })(arguments, modifyer));
    };
}
for (var i = 0; i < jsonOdm.Query.stringFiledModifyer.length; i++) {
    jsonOdm.Query.prototype["$" + jsonOdm.Query.stringFiledModifyer[i]] = createQueryStringModifier(jsonOdm.Query.stringFiledModifyer[i]);
}

// ACCUMULATION FUNCTIONS

/** Go down the property tree of the collection
 * @param {String[]} nodes A variable of nodes to traverse down the json tree
 * @param {function} accumulator
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$accumulator = function (nodes, accumulator) {
    nodes = typeof nodes === "string" ? [nodes] : nodes;
    var subQuery = new jsonOdm.Query(this.$$collection);
    var $accumulator = (function (nodes, accumulator, query, parentQuery) {
        /**
         * @param {*} The collection to go down
         * @return {Query|boolean} The query object with the sub collection or false if querying was impossible
         */
        return function (collection) {
            var value = nodes !== null ? jsonOdm.util.branch(collection, nodes) : nodes;
            query.$$accumulation = accumulator(value, query.$$accumulation, collection);
            parentQuery.$$accumulation = query.$$accumulation;
            return value;
        };
    })(nodes, accumulator, subQuery, this);
    subQuery.$$commandQueue.push($accumulator);
    return subQuery;
};

/**
 * Performs the accumulation sum of a field. It's integrated to be used with $group. May as well be used as stand alone.
 * @param {...String} branch Internally calls the $branch method to receive the field values
 * @return {jsonOdm.Query}
 * @example
 * // SHOULD BE USED WITH $group
 * var collection = new jsonOdm.Collection("employees");
 * var $query = collection.$query();
 * $query.$sum("daysOff").$all();
 * console.log($query.$$accumulation);
 */
jsonOdm.Query.prototype.$sum = function (branch) {
    return this.$accumulator(branch, function (value, accumulation) {
        if (accumulation === false) {
            accumulation = 0;
        }
        return value + accumulation;
    });
};

/**
 * Performs the accumulation average of a field. It's integrated to be used with $group. May as well be used as stand alone.
 * @param {...String} branch Internally calls the $branch method to receive the field values
 * @return {jsonOdm.Query}
 * @example
 * // SHOULD BE USED WITH $group
 * var collection = new jsonOdm.Collection("employees");
 * var $query = collection.$query();
 * $query.$avg("daysOff").$all();
 * console.log($query.$$accumulation);
 */
jsonOdm.Query.prototype.$avg = function (branch) {
    var count, sum;
    return this.$accumulator(branch, function (value, accumulation) {
        if (accumulation === false) {
            count = 0;
            sum = 0;
        }
        sum += value;
        count++;
        return sum / count;
    });
};

/**
 * Performs the accumulation max of a field. It's integrated to be used with $group. May as well be used as stand alone.
 * @param {...String} branch Internally calls the $branch method to receive the field values
 * @return {jsonOdm.Query}
 * @example
 * // SHOULD BE USED WITH $group
 * var collection = new jsonOdm.Collection("employees");
 * var $query = collection.$query();
 * $query.$max("daysOff").$all();
 * console.log($query.$$accumulation);
 */
jsonOdm.Query.prototype.$max = function (branch) {
    return this.$accumulator(branch, function (value, accumulation) {
        if (accumulation === false) {
            accumulation = value;
        }
        return Math.max(value, accumulation);
    });
};

/**
 * Performs the accumulation min of a field. It's integrated to be used with $group. May as well be used as stand alone.
 * @param {...String} branch Internally calls the $branch method to receive the field values
 * @return {jsonOdm.Query}
 * @example
 * // SHOULD BE USED WITH $group
 * var collection = new jsonOdm.Collection("employees");
 * var $query = collection.$query();
 * $query.$max("daysOff").$all();
 * console.log($query.$$accumulation);
 */
jsonOdm.Query.prototype.$min = function (branch) {
    return this.$accumulator(branch, function (value, accumulation) {
        if (accumulation === false) {
            accumulation = value;
        }
        return Math.min(value, accumulation);
    });
};

/**
 * Counts the grouped elements
 * @return {jsonOdm.Query}
 * @example
 * // SHOULD BE USED WITH $group
 * var collection = new jsonOdm.Collection("employees");
 * var $query = collection.$count();
 * $query.$count("daysOff").$all();
 * expect(collection.length).toBe($query.$$accumulation);
 */
jsonOdm.Query.prototype.$count = function () {
    return this.$accumulator(null, function (value, accumulation) {
        if (accumulation === false) {
            accumulation = 0;
        }
        return ++accumulation;
    });
};

/**
 * adds the grouped elements to result set
 * @return {jsonOdm.Query}
 * @example
 * // SHOULD BE USED WITH $group
 * var collection = new jsonOdm.Collection("employees");
 * var $query = collection.$query();
 * $query.$push("daysOff").$all()
 * expect(collection.length).toBe($query.$$accumulation.length);
 */
jsonOdm.Query.prototype.$push = function () {
    var subQuery = new jsonOdm.Query(this.$$collection);
    var $push = (function (query, parentQuery) {
        return function (collectionElement) {
            query.$$accumulation = query.$$accumulation === false ? [] : query.$$accumulation;
            query.$$accumulation.push(collectionElement);
            parentQuery.$$accumulation = query.$$accumulation;
            return true;
        };
    })(subQuery, this);
    subQuery.$$commandQueue.push($push);
    return subQuery;
};

// ARITHMETIC FUNCTIONS

/**
 * Performs an arithmetic addition on two or more field values
 * @param {jsonOdm.Query|Number} branch1
 * @param {...jsonOdm.Query|...Number} branch2
 * @return jsonOdm.Query
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * var $query = collection.$query();
 *    $query.$add(
 *        $query.$branch("firstValue"),
 *        $query.$subtract(
 *            $query.$branch("lastValue"),
 *            $query.$branch(["otherValues","firstValue"])
 *        )
 *    ).$eq(12).$all();
 */
jsonOdm.Query.prototype.$add = function (branch1, branch2) {
    return this.$queryOperator(arguments, function (queryResults) {
        var result = queryResults.length > 0 ? queryResults[0] : 0;
        for (var i = 1; i < queryResults.length; i++) {
            result += queryResults[i];
        }
        return result;
    });
};

/**
 * Performs an arithmetic subtraction on two or more field values
 * @param {...jsonOdm.Query|...Number} branch
 * @return jsonOdm.Query
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * var $query = collection.$query();
 *    $query.$subtract(
 *        $query.$branch("firstValue"),
 *        $query.$add(
 *            $query.$branch("lastValue"),
 *            $query.$branch(["otherValues","firstValue"])
 *        )
 *    ).$eq(12).$all();
 */
jsonOdm.Query.prototype.$subtract = function (branch) {
    if( typeof branch === "undefined" ) {
        return this;
    }
    return this.$queryOperator(arguments, function (queryResults) {
        var result = queryResults.length > 0 ? queryResults[0] : 0;
        for (var i = 1; i < queryResults.length; i++) {
            result -= queryResults[i];
        }
        return result;
    });
};

/**
 * Performs an arithmetic multiplication on two or more field values
 * @param {...jsonOdm.Query|...Number} branch
 * @return jsonOdm.Query
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * var $query = collection.$query();
 *    $query.$multiply(
 *        $query.$branch("firstValue"),
 *        $query.$add(
 *            $query.$branch("lastValue"),
 *            $query.$branch(["otherValues","firstValue"])
 *        )
 *    ).$eq(12).$all();
 */
jsonOdm.Query.prototype.$multiply = function (branch) {
    if( typeof branch === "undefined" ) {
        return this;
    }
    return this.$queryOperator(arguments, function (queryResults) {
        var result = queryResults.length > 0 ? queryResults[0] : 0;
        for (var i = 1; i < queryResults.length; i++) {
            result *= queryResults[i];
        }
        return result;
    });
};

/**
 * Performs an arithmetic divition on two or more field values
 * @param {...jsonOdm.Query|...Number} branch
 * @return jsonOdm.Query
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * var $query = collection.$query();
 *    $query.$divide(
 *        $query.$branch("firstValue"),
 *        $query.$add(
 *            $query.$branch("lastValue"),
 *            $query.$branch(["otherValues","firstValue"])
 *        )
 *    ).$eq(12).$all();
 */
jsonOdm.Query.prototype.$divide = function (branch) {
    if( typeof branch === "undefined" ) {
        return this;
    }
    return this.$queryOperator(arguments, function (queryResults) {
        var result = queryResults.length > 0 ? queryResults[0] : 0;
        for (var i = 1; i < queryResults.length; i++) {
            result /= queryResults[i];
        }
        return result;
    });
};

/**
 * Performs an arithmetic modulo on two or more field values
 * @param {jsonOdm.Query|Number} branch
 * @param {...jsonOdm.Query|...Number} module
 * @return jsonOdm.Query
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * var $query = collection.$query();
 *    $query.$modulo(
 *        $query.$branch("firstValue"),
 *        $query.$add(
 *            $query.$branch("lastValue"),
 *            $query.$branch(["otherValues","firstValue"])
 *        )
 *    ).$eq(12).$all();
 */
jsonOdm.Query.prototype.$modulo = function (branch, module) {
    if( typeof branch === "undefined" ) {
        return this;
    }
    return this.$queryOperator(arguments, function (queryResults) {
        var result = queryResults.length > 0 ? queryResults[0] : 0;
        for (var i = 1; i < queryResults.length; i++) {
            result = result % queryResults[i];
        }
        return result;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $eq('1','2','4') so 1 or 2 or 4 are valid fields
 * @param {...*} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$eq = function (comparable) {
    if( typeof comparable === "undefined" ) {
        return this;
    }
    return this.$testCollection(arguments, function (collectionValue, possibleValues) {
        for (var i = 0; i < possibleValues.length; i++) {
            if (possibleValues[i] === collectionValue) {
                return true;
            }
        }
        return false;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $in(['1','2','4']) so 1 or 2 or 4 are valid fields
 * @param {Array} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$in = function (comparable) {
    if( typeof comparable === "undefined" ) {
        comparable = [];
    }
    return this.$testCollection(comparable, function (collectionValue, possibleValues) {
        for (var i = 0; i < possibleValues.length; i++) {
            if (possibleValues[i] === collectionValue) {
                return true;
            }
        }
        return false;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $ne('1','2','4') so 1 or 2 or 4 are not valid fields
 * @param {...*} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$ne = function (comparable) {
    if( typeof comparable === "undefined" ) {
        return this
    }
    return this.$testCollection(arguments, function (collectionValue, possibleValues) {
        for (var i = 0; i < possibleValues.length; i++) {
            if (possibleValues[i] === collectionValue) {
                return false;
            }
        }
        return true;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $nin(['1','2','4']) so 1 or 2 or 4 are not valid fields
 * @param {Array} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$nin = function (comparable) {
    if( typeof comparable === "undefined" ) {
        comparable = [];
    }
    return this.$testCollection(comparable, function (collectionValue, possibleValues) {
        for (var i = 0; i < possibleValues.length; i++) {
            if (possibleValues[i] === collectionValue) {
                return false;
            }
        }
        return true;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $gt('1') field values greater then 1 are valid
 * @param {*} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$gt = function (comparable) {
    return this.$testCollection(comparable, function (collectionValue, possibleValues) {
        return possibleValues < collectionValue;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $gte('1') field values greater then or equal to 1 are valid
 * @param {*} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$gte = function (comparable) {
    return this.$testCollection(comparable, function (collectionValue, possibleValues) {
        return possibleValues <= collectionValue;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $lt('1') field values less then 1 are valid
 * @param {*} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$lt = function (comparable) {
    return this.$testCollection(comparable, function (collectionValue, possibleValues) {
        return possibleValues > collectionValue;
    });
};

/**
 * Compares the current sub collection value with the comparable
 * like this $lte('1') field values less then or equal to 1 are valid
 * @param {*} comparable Values to compare the current field with
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$lte = function (comparable) {
    return this.$testCollection(comparable, function (collectionValue, possibleValues) {
        return possibleValues >= collectionValue;
    });
};

/**
 * Compares the current sub collection value to be null or undefined
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$isNull = function () {
    return this.$testCollection(null, function (collectionValue) {
        return typeof collectionValue === "undefined" || collectionValue === null;
    });
};

/**
 * Compares the current sub collection value to not be undefined
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$exists = function () {
    return this.$testCollection(null, function (collectionValue) {
        return typeof collectionValue !== "undefined";
    });
};

/**
 * Compares the current sub collection against the given types using the binary of and the JavaScript typeof
 * Supported (case insensitive) types are: number, string, undefined, object, array and RegExp, ArrayBuffer, null, boolean plus all other [object *] types
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * collection.$query()
 *    // id is string or number and not undefined or null
 *    .$branch("id").$type("string","number")
 *    .$all();
 * @param {...string} type A list of allowed types for the selected field
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$type = function (type) {
    if(typeof type === "undefined" ) {
        return this.$testCollection([type], function () {
            return false;
        });
    }

    return this.$testCollection(arguments, function (collectionValue, possibleTypes) {
        return jsonOdm.util.is(collectionValue, possibleTypes);
    });
};

/**
 * Compares the given reminder against the selected field value modulo the given divisor
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * collection.$query()
 *    // get every fourth element, so elements with id 4,8,12,... when starting with id 1
 *    .$branch("id").$mod(4,0)
 *    .$all();
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$mod = function (divisor, remainder) {
    if( typeof divisor === "undefined" || typeof remainder === "undefined" ){
        return this;
    }
    return this.$testCollection(arguments, function (collectionValue, args) {
        return collectionValue % args[0] === args[1];
    });
};

/**
 * Tests a selected field against the regular expression
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * collection.$query()
 *    // gets all elements with a name of "Richard","RiChI","RichI","richard",...
 *    .$branch("name").$regex(/rich(i|ard)/i)
 *    .$all();
 * @example
 * var collection = new jsonOdm.Collection("myCollection");
 * collection.$query()
 *    // gets all elements with a name of "Richard","RiChI","RichI","richard",...
 *    .$branch("name").$regex("rich(i|ard)","i")
 *    .$all();
 * @param {RegExp|string} regex The regular expression to test against
 * @param {string} [options] The regular expression options, i.e. "i" for case insensitivity
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$regex = function (regex, options) {
    if (typeof regex === "string") {
        regex = typeof options === "string" ? new RegExp(regex, options) : new RegExp(regex);
    }
    return this.$testCollection(regex, function (collectionValue, regex) {
        return regex.test(collectionValue);
    });
};

/**
 * Performs a text search on a given collection with the same notation used by mongodb<br/>
 * In contrast to mongodb this method does not implement stop words elimination or word stamming at the moment
 * @example
 * collection.$query()
 *    // Must find "Ralf Tomson" and ("Jack" or "Josh") and not("Matteo")
 *    .$branch("name").$text("Jack Josh \"Ralf Tomson\" -Matteo")
 *    .$all();
 * @param {String} text
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$text = function (text) {
    var notRegExp = /(^| )-([^ ]+)( |$)/g;
    var andRegExp = /"([^"]+)"/g;
    var nots = [], ands = [];
    var notMatches, andMatches;
    while ((notMatches = notRegExp.exec(text)) !== null) {
        nots.push(notMatches[2]);
    }
    text = text.replace(notRegExp, "");
    while ((andMatches = andRegExp.exec(text)) !== null) {
        ands.push(andMatches[1]);
    }
    text = text.replace(andRegExp, "");
    var ors = text.split(" ");
    return this.$testCollection([nots, ands, ors], function (collectionValue, logics) {
        // nots
        for (var i = 0; i < logics[0].length; i++) {
            if (collectionValue.indexOf(logics[0][i]) > -1) {
                return false;
            }
        }
        // ands
        for (i = 0; i < logics[1].length; i++) {
            if (collectionValue.indexOf(logics[1][i]) < 0) {
                return false;
            }
        }
        // ors
        for (i = 0; i < logics[2].length; i++) {
            if (collectionValue.indexOf(logics[2][i]) > -1) {
                return true;
            }
        }
        // if there are no ors, matching all ands is enough
        return !!logics[1].length;
    });
};

/**
 * Performs a query selection by a self defined function of function body string. The function context (this) will be the current collection or a value selected by $branch.
 * @example
 * // !!!! NOT SUPPORTED ANYMORE !!!! using a string to find Harry
 * collection.$query().$where("return this.name == 'Harry';").$first();
 * // using a function to find Harry
 * collection.$query().$where(function(){return this.name == 'Harry';}).$first();
 * // using $where after selecting a branch
 * collection.$query().$('name').$where(function(){return this == 'Harry';}).$first();
 * @param {string|Function} evaluation
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$where = function (evaluation) {
    return this.$testCollection(evaluation, function (collectionValue, evaluation) {
        if (typeof evaluation !== "function") {
            return false;
        }
        return evaluation.apply(collectionValue);
    });
};

/*-------- GEO ----------*/
/**
 * Checks whether the current field geometry is within the given geometry object <br/>
 * <strong style="color:#ff0000">Warning:</strong> The coordinate reference system is <a href="http://spatialreference.org/ref/epsg/4326/" target="_blank">WGS 84</a>witch uses the coordinate order [<strong>longitude</strong>,<strong>latitude</strong>]!<br/>
 * The method automatically transforms arrays into the assumed GeoJSON definitions where: <br/>
 * [10,10] transforms into a jsonOdm.Geo.Point <br/>
 * [[10,10],[10,12],...] transforms into a jsonOdm.Geo.LineString <br/>
 * [[[10,10],[10,12],...],...] transforms into a jsonOdm.Geo.Polygon <br/>
 * [[[[10,10],[10,12],...],...],...] transforms into a jsonOdm.Geo.MultiPolygon <br/>
 * or simply use a GeoJSON object definition from jsonOdm.Geo
 * @example
 * {
 *     "geo":[
 *         {
 *             "type": "Feature",
 *             "properties": {...},
 *             "geometry": {
 *                 "type": "Polygon",
 *                 "coordinates": [ ... ]
 *             }
 *         },
 *         {
 *             "type": "Feature",
 *             "properties": {...},
 *             "geometry": {
 *                 "type": "Polygon",
 *                 "coordinates": [ ... ]
 *             }
 *         },
 *         ...
 *     ]
 * }
 *
 * var collection = new jsonOdm.Collection("geo"),
 *     q = collection.$query().$branch("geometry").$geoWithin(new jsonOdm.Geo.BoundaryBox([129.049317,-31.434555,139.464356,-19.068644]));
 *     //found geometries
 *     geometries = q.$all();
 * @param {Array|jsonOdm.Geo.BoundaryBox|jsonOdm.Geo.Point|jsonOdm.Geo.MultiPoint|jsonOdm.Geo.LineString|jsonOdm.Geo.MultiLineString|jsonOdm.Geo.Polygon|jsonOdm.Geo.MultiPolygon|jsonOdm.Geo.GeometryCollection} geometry
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$geoWithin = function (geometry) {
    return this.$testCollection(jsonOdm.Geo.detectAsGeometry(geometry), function (collectionValue, geometry) {
        return jsonOdm.Geo[collectionValue.type] && jsonOdm.Geo[collectionValue.type].within && jsonOdm.Geo[collectionValue.type].within(collectionValue, geometry);
    });
};

/**
 * Checks whether the current field geometry intersects the given geometry object <br/>
 * <strong style="color:#ff0000">Warning:</strong> The coordinate reference system is <a href="http://spatialreference.org/ref/epsg/4326/" target="_blank">WGS 84</a>witch uses the coordinate order [<strong>longitude</strong>,<strong>latitude</strong>]!<br/>
 * The method automatically transforms arrays into the assumed GeoJSON definitions where: <br/>
 * [10,10] transforms into a jsonOdm.Geo.Point <br/>
 * [[10,10],[10,12],...] transforms into a jsonOdm.Geo.LineString <br/>
 * [[[10,10],[10,12],...],...] transforms into a jsonOdm.Geo.Polygon <br/>
 * [[[[10,10],[10,12],...],...],...] transforms into a jsonOdm.Geo.MultiPolygon <br/>
 * or simply use a GeoJSON object definition from jsonOdm.Geo
 * @example
 * {
 *     "geo":[
 *         {
 *             "type": "Feature",
 *             "properties": {...},
 *             "geometry": {
 *                 "type": "Polygon",
 *                 "coordinates": [ ... ]
 *             }
 *         },
 *         {
 *             "type": "Feature",
 *             "properties": {...},
 *             "geometry": {
 *                 "type": "Polygon",
 *                 "coordinates": [ ... ]
 *             }
 *         },
 *         ...
 *     ]
 * }
 *
 * var collection = new jsonOdm.Collection("geo"),
 *     q = collection.$query().$branch("geometry").$geoIntersects(new jsonOdm.Geo.BoundaryBox([129.049317,-31.434555,139.464356,-19.068644]));
 *     //found geometries
 *     geometries = q.$all();
 * @param {Array|jsonOdm.Geo.BoundaryBox|jsonOdm.Geo.Point|jsonOdm.Geo.MultiPoint|jsonOdm.Geo.LineString|jsonOdm.Geo.MultiLineString|jsonOdm.Geo.Polygon|jsonOdm.Geo.MultiPolygon|jsonOdm.Geo.GeometryCollection} geometry
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$geoIntersects = function (geometry) {
    return this.$testCollection(jsonOdm.Geo.detectAsGeometry(geometry), function (collectionValue, geometry) {
        return jsonOdm.Geo[collectionValue.type] && jsonOdm.Geo[collectionValue.type].intersects && jsonOdm.Geo[collectionValue.type].intersects(collectionValue, geometry);
    });
};

/*-------- Logic ---------*/
/**
 * Compares sub query results using the boolean and
 * @param {...jsonOdm.Query} queries A finite number of operators
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$and = function (queries) {
    // TODO optimize with generators to only query paths that are needed
    return this.$queryOperator(arguments, function (queryResults) {
        for (var i = 0; i < queryResults.length; i++) {
            if (!queryResults[i]) {
                return false;
            }
        }
        return true;
    });
};

/**
 * Compares sub query results using the boolean nand
 * @param {...jsonOdm.Query} queries A finite number of operators
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$nand = function (queries) {
    // TODO optimize with generators to only query paths that are needed
    return this.$queryOperator(arguments, function (queryResults) {
        for (var i = 0; i < queryResults.length; i++) {
            if (!queryResults[i]) {
                return true;
            }
        }
        return false;
    });
};


/**
 * An alisa for $nand
 * @see jsonOdm.Query.$nand
 * @method $not
 * @memberof jsonOdm.Query.prototype
 * @param {...jsonOdm.Query} queries A finite number of operators
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$not = jsonOdm.Query.prototype.$nand;

/**
 * Compares sub query results using the boolean or
 * @param {...jsonOdm.Query} queries A finite number of operators
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$or = function (queries) {
    // TODO optimize with generators to only query paths that are needed
    return this.$queryOperator(arguments, function (queryResults) {
        for (var i = 0; i < queryResults.length; i++) {
            if (queryResults[i]) {
                return true;
            }
        }
        return false;
    });
};

/**
 * Compares sub query results using the boolean nor
 * @param {...jsonOdm.Query} queries A finite number of operators
 * @return {jsonOdm.Query}
 */
jsonOdm.Query.prototype.$nor = function (queries) {
    // TODO optimize with generators to only query paths that are needed
    return this.$queryOperator(arguments, function (queryResults) {
        for (var i = 0; i < queryResults.length; i++) {
            if (queryResults[i]) {
                return false;
            }
        }
        return true;
    });
};

if (typeof module !== "undefined" && module.exports) {
    module.exports = jsonOdm.Query;
}