(function($, undefined) {

// Default settings:
///////////////////////////////////////////////////////////////////////////////

var 
    defaults = {
        // The default output mode is tooltip because it requires no 
        // dependencies:
        outputMode:"tooltip",
        
        // The this property is set to true, validity will scroll the browser 
        // viewport so that the first error is visible when validation fails:
        scrollTo:false,

        // If this setting is true, modal errors will disappear when they are 
        // clicked on:
        modalErrorsClickable:true,

        //if set to true will include title with rendered errors
        useInfer: true,

        // If a field name cannot be otherwise inferred, this will be used:
        defaultFieldName:"This field",
        
        // jQuery selector to filter down to validation-supported elements:
        elementSupport:[
            ":text", 
            ":password", 
            "textarea", 
            "select", 
            ":radio", 
            ":checkbox", 
            "input[type='hidden']", 
            "input[type='tel']", 
            "input[type='email']",
            "input[type='number']",
            "input[type='file']"
        ].join(", "),
        
        // Function to stringify argments for use when generating error 
        // messages. Primarily, it just generates pretty date strings:
        argToString:function(val) {
            return val.getDate ? [
                    val.getMonth() + 1, 
                    val.getDate(), 
                    val.getFullYear() 
                ].join("/") :
                val + "";
        },
        
        debugPrivates:false
    },
    
    __private;

// Static functions and properties:
///////////////////////////////////////////////////////////////////////////////

$.validity = {

    // Clone the defaults. They can be overridden with the setup function:
    settings:$.extend(defaults, {}),

    // Built-in library of format-checking tools for use with the match 
    // validator as well as the nonHtml validator:
    patterns:{
        integer:/^\d+$/,
        
        // Used to use Date.parse(), which was the cause of Issue 9,  where the 
        // function would accept 09/80/2009 as parseable. The fix is to use a 
        // RegExp that will only accept American Middle-Endian form. See the 
        // Internationalization section in the documentation for how to cause 
        // it to support other date formats:
        date:/^((0?\d)|(1[012]))[\/-]([012]?\d|30|31)[\/-]\d{1,4}$/, 
        
        email:/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i,
        usd:/^\$?((\d{1,3}(,\d{3})*)|\d+)(\.(\d{2})?)?$/,            
        url:/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i,
        
        // Number should accept floats or integers, be they positive or 
        // negative. It should also support scientific-notation, written as a 
        // lower or capital 'E' followed by the radix. Number assumes base 10. 
        // Unlike the native parseFloat or parseInt functions, this should not 
        // accept trailing Latin characters.
        number:/^[+-]?(\d+(\.\d*)?|\.\d+)([Ee]-?\d+)?$/,
        
        zip:/^\d{5}(-\d{4})?$/,
        phone:/^[2-9]\d{2}-\d{3}-\d{4}$/,
        guid:/^(\{?([0-9a-fA-F]){8}-(([0-9a-fA-F]){4}-){3}([0-9a-fA-F]){12}\}?)$/,
        time12:/^((0?\d)|(1[012])):[0-5]\d?\s?[aApP]\.?[mM]\.?$/,
        time24:/^(20|21|22|23|[01]\d|\d)(([:][0-5]\d){1,2})$/,

        nonHtml:/^[^<>]*$/
    },

    // Built-in set of default error messages (for use when a message isn't 
    // specified):
    messages:{

        require:"#{field} is required.",

        // Format validators:
        match:"#{field} is in an invalid format.",
        integer:"#{field} must be a positive, whole number.",
        date:"#{field} must be formatted as a date. (mm/dd/yyyy)",
        email:"#{field} must be formatted as an email.",
        usd:"#{field} must be formatted as a US Dollar amount.",
        url:"#{field} must be formatted as a URL.",
        number:"#{field} must be formatted as a number.",
        zip:"#{field} must be formatted as a zipcode ##### or #####-####.",
        phone:"#{field} must be formatted as a phone number ###-###-####.",
        guid:"#{field} must be formatted as a guid like {3F2504E0-4F89-11D3-9A0C-0305E82C3301}.",
        time24:"#{field} must be formatted as a 24 hour time: 23:00.",
        time12:"#{field} must be formatted as a 12 hour time: 12:00 AM/PM",

        // Value range messages:
        lessThan:"#{field} must be less than #{max}.",
        lessThanOrEqualTo:"#{field} must be less than or equal to #{max}.",
        greaterThan:"#{field} must be greater than #{min}.",
        greaterThanOrEqualTo:"#{field} must be greater than or equal to #{min}.",
        range:"#{field} must be between #{min} and #{max}.",

        // Value length messages:
        tooLong:"#{field} cannot be longer than #{max} characters.",
        tooShort:"#{field} cannot be shorter than #{min} characters.",

        // Composition validators:
        nonHtml:"#{field} cannot contain HTML characters.",
        alphabet:"#{field} contains disallowed characters.",

        minCharClass:"#{field} cannot have more than #{min} #{charClass} characters.",
        maxCharClass:"#{field} cannot have less than #{min} #{charClass} characters.",
        
        // Aggregate validator messages:
        equal:"Values don't match.",
        distinct:"A value was repeated.",
        sum:"Values don't add to #{sum}.",
        sumMax:"The sum of the values must be less than #{max}.",
        sumMin:"The sum of the values must be greater than #{min}.",

        // Radio validator messages:
        radioChecked:"The selected value is not valid.",

        // Checkbox validator message:
        checkboxChecked:"#{field} must be checked.",
		
        generic:"Invalid."
    },
    
    // Abstract output:
    out:{
        start:function() {
            this.defer("start");
        },
        end:function(results) {
            this.defer("end", results);
        },
        raise:function($obj, msg) {
            this.defer("raise", $obj, msg);
        },
        raiseAggregate:function($obj, msg) {
            this.defer("raiseAggregate", $obj, msg);
        },
        defer:function(name) {
            var 
                v = $.validity,
                o = v.outputs[v.settings.outputMode];
            
            o[name].apply(
                o,
                Array.prototype.slice.call(arguments, 1)
            );
        }
    },
    
    // Character classes can be used to determine the quantity of a given type 
    // of character in a string:
    charClasses:{
        alphabetical:/\w/g,
        numeric:/\d/g,
        alphanumeric:/[A-Za-z0-9]/g,
        symbol:/[^A-Za-z0-9]/g
    },

    // Object to contain the output modes. The three built-in output modes are 
    // installed later on in this script.
    outputs:{},
    
    // By default, private members are not exposed.
    __private:undefined,

    // Override the default settings with user-specified ones.
    setup:function(options) {
        this.settings = $.extend(this.settings, options);
        
        if (this.settings.debugPrivates) {
            this.__private = __private;
        }
        else {
            this.__private = undefined;
        }
    },

    // Object to store information about ongoing validation. When validation 
    // starts, this will be set to a report object. When validators fail, they 
    // will inform this object. When validation is completed, this object will 
    // contain the information of whether it succeeded:
    report:null,

    // Determine whether validity is in the middle of validation:
    isValidating:function() {
        return !!this.report;
    },

    // Function to prepare validity to start validating:
    start:function() {

        // The output mode should be notified that validation is starting. This 
        // usually means that the output mode will erase errors from the 
        // document in whatever way the mode needs to:
        this.out.start();
        
        // Initialize the report object:
        this.report = { errors:0, valid:true, reductionObjects:[] };
    },

    // Function called when validation is over to examine the results and 
    // clean-up:
    end:function() {

        // Null coalescence: fix for Issue 5:
        var results = this.report || { errors: 0, valid: true };

        for (var idx = 0; idx < this.report.reductionObjects.length; ++idx) {
            delete this.report.reductionObjects[idx].reduction;
        }

        this.report = null;
        
        // Notify the current output mode that validation is over:
        this.out.end(results);
        
        return results;
    },

    // Remove validiation errors:
    clear:function() {
        this.start();
        this.end();
    },

    registerReduction:function(obj) {
        this.report.reductionObjects.push(obj);
    }
};

// jQuery instance methods:
///////////////////////////////////////////////////////////////////////////////

$.fn.extend({

    // The validity function is how validation can be bound to forms. The user 
    // may pass in a validation function or, as a shortcut, pass in a string of 
    // a CSS selector that grabs all the inputs to require:
    validity:function(arg) {
    
        return this.each(
        
            function() {

                var self = this;
            
                // Only operate on forms:
                if (this.tagName.toLowerCase() == "form") {
                    var f = null;

                    // If the user entered a string to select the inputs to 
                    // require, then make the validation logic ad hoc:
                    if (typeof (arg) == "string") {
                        f = function() {
                            $(arg).require();
                        };
                    }

                    // If the user entered a validation function then just call
                    // that at the appropriate time:
                    else if ($.isFunction(arg)) {
                        f = arg;
                    }

                    if (arg) {
                        $(this).bind(
                            "submit",
                            function() {
                                $.validity.start();
                                f.apply(self);
                                return $.validity.end().valid;
                            }
                        );
                    }
                }
            }
        );
    },

    // Validators:
    ///////////////////////////////////////////////////////////////////////////

    // Common validators:
    ///////////////////////////////////////////////////////////////////////////
    
    // Validate whether the field has a value.
    require:function(msg) {
        return validate(
            this,
            function(obj) {
                if ($(obj).val()) {
                    return $(obj).val() != null && !!$(obj).val().length;
                }
                return false;
            },
            msg || $.validity.messages.require
        );
    },

    // Validate whether the field matches a regex.
    match:function(rule, msg) {

        // If a default message is to be used:
        if (!msg) {
            // First grab the generic one:
            msg = $.validity.messages.match;

            // If there's a more specific one, use that.
            if (typeof (rule) === "string" && $.validity.messages[rule]) {
                msg = $.validity.messages[rule];
            }
        }

        // If the rule is named, rather than specified:
        if (typeof (rule) == "string") {
            rule = $.validity.patterns[rule];
        }

        return validate(
            this,

            // Some of the named rules can be functions, such as 'date'. If the 
            // discovered rule is a function use it as such. Otherwise, assume 
            // it's a RegExp.
            $.isFunction(rule) ?

                function(obj) {
                    return !obj.value.length || rule(obj.value);
                } :

                function(obj) {
                    // Fix for regexes where the global flag is set. Make sure 
                    // to test from the start of the string.
                    if (rule.global) {
                        rule.lastIndex = 0;
                    }
                
                    return !obj.value.length || rule.test(obj.value);
                },

            msg
        );
    },

    range:function(min, max, msg) {
        return validate(
            this,

            min.getTime && max.getTime ?

                // If both arguments are dates then use them that way.
                orEmpty(function(obj) {
                    var d = new Date(obj.value);
                    return d >= new Date(min) && d <= new Date(max);
                }) :

                min.substring && max.substring && Big ?

                    // If both arguments are strings then parse them  using the 
                    // Arbitrary-Precision library.
                    orEmpty(function(obj) {
                        var n = new Big(obj.value);
                        return (
                            n.greaterThanOrEqualTo(new Big(min)) && 
                            n.lessThanOrEqualTo(new Big(max))
                        );
                    }) :

                    // Otherwise treat them like floats.
                    orEmpty(function(obj) {
                        var f = parseFloat(obj.value);
                        return f >= min && f <= max;
                    }),

            msg || format(
                $.validity.messages.range, {
                    min:$.validity.settings.argToString(min),
                    max:$.validity.settings.argToString(max)
                }
            )
        );
    },

    greaterThan:function(min, msg) {
        return validate(
            this,

            min.getTime ?
                orEmpty(function(obj) {
                    return new Date(obj.value) > min;
                }) :

                min.substring && Big ?

                    orEmpty(function(obj) {
                       return new Big(obj.value).greaterThan(new Big(min)); 
                    }) :

                    orEmpty(function(obj) {
                        return parseFloat(obj.value) > min;
                    }),

            msg || format(
                $.validity.messages.greaterThan, {
                    min:$.validity.settings.argToString(min)
                }
            )
        );
    },

    greaterThanOrEqualTo:function(min, msg) {
        return validate(
            this,

            min.getTime ?
                orEmpty(function(obj) {
                    return new Date(obj.value) >= min;
                }) :

                min.substring && Big ?

                    orEmpty(function(obj) {
                        return new Big(obj.value).greaterThanOrEqualTo(new Big(min));
                    }) :

                    orEmpty(function(obj) {
                        return parseFloat(obj.value) >= min;
                    }),

            msg || format(
                $.validity.messages.greaterThanOrEqualTo, {
                    min:$.validity.settings.argToString(min)
                }
            )
        );
    },

    lessThan:function(max, msg) {
        return validate(
            this,

            max.getTime ?
                orEmpty(function(obj) {
                    return new Date(obj.value) < max;
                }) :

                max.substring && Big ?

                    orEmpty(function(obj) {
                        return new Big(obj.value).lessThan(new Big(max));
                    }) :

                    orEmpty(function(obj) {
                        return parseFloat(obj.value) < max;
                    }),

            msg || format(
                $.validity.messages.lessThan, {
                    max:$.validity.settings.argToString(max)
                }
            )
        );
    },

    lessThanOrEqualTo:function(max, msg) {
        return validate(
            this,

            max.getTime ?
                orEmpty(function(obj) {
                    return new Date(obj.value) <= max;
                }) :

                max.substring && Big ?

                    orEmpty(function(obj) {
                        return new Big(obj.value).lessThanOrEqualTo(new Big(max));
                    }) :

                    orEmpty(function(obj) {
                        return parseFloat(obj.value) <= max;
                    }),

            msg || format(
                $.validity.messages.lessThanOrEqualTo, {
                    max:$.validity.settings.argToString(max)
                }
            )
        );
    },

    maxLength:function(max, msg) {
        return validate(
            this,
            orEmpty(function(obj) {
                return obj.value.length <= max;
            }),
            msg || format(
                $.validity.messages.tooLong, {
                    max:max
                }
            )
        );
    },

    minLength:function(min, msg) {
        return validate(
            this,
            orEmpty(function(obj) {
                return obj.value.length >= min;
            }),
            msg || format(
                $.validity.messages.tooShort, {
                    min:min
                }
            )
        );
    },
    
    alphabet:function(alpha, msg) {
        var chars = [];
        
        return validate(
            this,
            function(obj) {
            
                // For each character in the string, ensure that it's in the 
                // alphabet definition:
                for (var idx = 0; idx < obj.value.length; ++idx) {
                    if (alpha.indexOf(obj.value.charAt(idx)) == -1) {
                        chars.push(obj.value.charAt(idx));
                        return false;
                    }
                }
                
                return true;
            },
            msg || format(
                $.validity.messages.alphabet, {
                    chars:chars.join(", ")
                }
            )
        );
    },
    
    minCharClass:function(charClass, min, msg) {
        if (typeof(charClass) == "string") {
            charClass = charClass.toLowerCase();
        
            if ($.validity.charClasses[charClass]) {
                charClass = $.validity.charClasses[charClass];
            }
        }
        
        return validate(
            this,
            orEmpty(function(obj) {
                return (obj.value.match(charClass) || []).length >= min;
            }),
            msg || format(
                $.validity.messages.minCharClass, {
                    min:min,
                    charClass:charClass
                }
            )
        );
    },
    
    maxCharClass:function(charClass, max, msg) {
        if (typeof(charClass) == "string") {
            charClass = charClass.toLowerCase();
        
            if ($.validity.charClasses[charClass]) {
                charClass = $.validity.charClasses[charClass];
            }
        }
        
        return validate(
            this,
            function(obj) {
                return (obj.value.match(charClass) || []).length <= max;
            },
            msg || format(
                $.validity.messages.maxCharClass, {
                    max:max,
                    charClass:charClass
                }
            )
        );
    },
    
    // Validate that the input does not contain potentially dangerous strings.
    nonHtml:function(msg) {
        return validate(
            this,

            function(obj) {
                return $.validity.patterns.nonHtml.test(obj.value);
            },

            msg || $.validity.messages.nonHtml
        );
    },
    
    // Aggregate validators:
    ///////////////////////////////////////////////////////////////////////////
    
    // Validate that all matched elements bear the same values. Accepts a 
    // function to transform the values for testing.
    equal:function(arg0, arg1) {
        $.validity.registerReduction(this);

        var 
            // If a reduced set is attached, use it. Also, remove unsupported 
            // elements.
            $reduction = (this.reduction || this)
                .filter($.validity.settings.elementSupport),

            transform = function(val) {
                return val;
            },

            msg = $.validity.messages.equal;

        if ($reduction.length) {

            // Figure out what arguments were specified.
            if ($.isFunction(arg0)) {
                transform = arg0;

                if (typeof (arg1) == "string") {
                    msg = arg1;
                }
            }

            else if (typeof (arg0) == "string") {
                msg = arg0;
            }

            var 
                map = $.map(
                    $reduction,
                    function(obj) {
                        return transform(obj.value);
                    }
                ),

                // Get the first value.
                first = map[0],
                valid = true;

            // If any value is not equal to the first value, then they aren't 
            // all equal, and it's not valid.
            for (var i in map) {
                if (map[i] != first) {
                    valid = false;
                }
            }

            if (!valid) {
                raiseAggregateError($reduction, msg);

                // The set reduces to zero valid elements.
                this.reduction = $([]);
            }
        }

        return this;
    },

    // Validate that all matched elements bear distinct values. Accepts an 
    // optional function to transform the values for testing.
    distinct:function(arg0, arg1) {
        $.validity.registerReduction(this);

        var 
            // If a reduced set is attached, use it.
            // Also, remove unsupported elements.
            $reduction =  (this.reduction || this).filter($.validity.settings.elementSupport),

            transform = function(val) {
                return val;
            },

            msg = $.validity.messages.distinct,

            // An empty array to store already-examined values
            subMap = [],

            // An array with repeated values
            repeatedVal = [],

            valid = true;

        if ($reduction.length) {

            // Figure out what arguments were specified.
            if ($.isFunction(arg0)) {
                transform = arg0;

                if (typeof (arg1) == "string") {
                    msg = arg1;
                }
            }

            else if (typeof (arg0) == "string") {
                msg = arg0;
            }

            // Map all the matched values into an array.    
            var map = $.map(
                $reduction,
                function(obj) {
                    return transform(obj.value);
                }
            );

            // For each transformed value:
            for (var i1 = 0; i1 < map.length; ++i1) {
                // Unless it's an empty string:
                if (map[i1].length) {
                    // For each value we've already looked at:
                    for (var i2 = 0; i2 < subMap.length; ++i2) {
                        // If we've already seen the transformed value:
                        if (subMap[i2] == map[i1]) {
                            valid = false;
                            // Collect repeated values
                            repeatedVal.push(map[i1]);
                        }
                    }

                    // We looked at the value.
                    subMap.push(map[i1]);
                }
            }

            if (!valid) {
                // Remove duplicates of duplicates
                repeatedVal = $.unique(repeatedVal);
                
                // For all repeated values found...
                for (
                    var 
                        i = 0, 
                        repeatedLength = repeatedVal.length; 
                    i < repeatedLength; 
                    ++i) {
                     
                     // raise the error - aggregate will use last repeated value
                     raiseAggregateError(
                        $reduction.filter(
                            "[value=\'" + repeatedVal[i] + "\']"
                        ),
                        msg
                    );
                }

                // The set reduces to zero valid elements.
                this.reduction = $([]);
            }
        }

        return this;
    },

    // Validate that the numeric sum of all values is equal to a given value.
    sum:function(sum, msg) {
        $.validity.registerReduction(this);

        // If a reduced set is attached, use it. Also, remove unsupported 
        // elements.
        var $reduction =  (this.reduction || this).filter($.validity.settings.elementSupport);

        if ($reduction.length && sum != numericSum($reduction)) {
            raiseAggregateError(
                $reduction,
                msg || format(
                    $.validity.messages.sum,
                    { sum:sum }
                )
            );

            // The set reduces to zero valid elements.
            this.reduction = $([]);
        }

        return this;
    },

    // Validates an inclusive upper-bound on the numeric sum of the values of 
    // all matched elements.
    sumMax:function(max, msg) {
        $.validity.registerReduction(this);

        // If a reduced set is attached, use it. Also, remove unsupported 
        // elements.
        var $reduction =  (this.reduction || this).filter($.validity.settings.elementSupport);

        if ($reduction.length && max < numericSum($reduction)) {
            raiseAggregateError(
                $reduction,
                msg || format(
                    $.validity.messages.sumMax,
                    { max:max }
                )
            );

            // The set reduces to zero valid elements.
            this.reduction = $([]);
        }

        return this;
    },

    // Validates an inclusive lower-bound on the numeric sum of the values of 
    // all matched elements.
    sumMin:function(min, msg) {
        $.validity.registerReduction(this);

        // If a reduced set is attached, use it. Also, remove unsupported 
        // elements.
        var $reduction =  (this.reduction || this).filter($.validity.settings.elementSupport);

        if ($reduction.length && min > numericSum($reduction)) {
            raiseAggregateError(
                $reduction,
                msg || format(
                    $.validity.messages.sumMin,
                    { min:min }
                )
            );

            // The set reduces to zero valid elements.
            this.reduction = $([]);
        }

        return this;
    },
    
    // Radio group validators:
    ///////////////////////////////////////////////////////////////////////////
    
    radioChecked:function(val, msg) {
        $.validity.registerReduction(this);

        // If a reduced set is attached, use it. Also, remove unsupported 
        // elements.
        var $reduction =  (this.reduction || this).filter($.validity.settings.elementSupport);
        
        if ($reduction.is(":radio") && $reduction.find(":checked").val() != val) {
            raiseAggregateError(
                $reduction,
                msg || $.validity.messages.radioChecked
            );
        }
    },
    
    radioNotChecked:function(val, msg) {
        $.validity.registerReduction(this);

        // If a reduced set is attached, use it. Also, remove unsupported 
        // elements.
        var $reduction =  (this.reduction || this).filter($.validity.settings.elementSupport);
        
        if ($reduction.is(":radio") && $reduction.filter(":checked").val() == val) {
            raiseAggregateError(
                $reduction,
                msg || $.validity.messages.radioChecked
            );
        }
    },
    
    // Checkbox validators:
    ///////////////////////////////////////////////////////////////////////////
    
    checkboxChecked:function(msg) {
        return validate(
            this,

            function(obj) {
                return !$(obj).is(":checkbox") || $(obj).is(":checked");
            },

            msg || $.validity.messages.checkboxChecked
        );
    },
    
    // Specialized validators:
    ///////////////////////////////////////////////////////////////////////////

    // If expression is a function, it will be called on each matched element.
    // Otherwise, it is treated as a boolean, and the determines the validity 
    // of elements in an aggregate fashion.
    assert:function(expression, msg) {

        $.validity.registerReduction(this);

        // If a reduced set is attached, use it. Do not reduce to supported 
        // elements.
        var $reduction = this.reduction || this;

        if ($reduction.length) {

            // In the case that 'expression' is a function,  use it as a 
            // regimen on each matched element individually:
            if ($.isFunction(expression)) {
                return validate(
                    this,
                    expression,
                    msg || $.validity.messages.generic
                );
            }

            // Otherwise map it to a boolean and consider this as an aggregate 
            // validation:
            else if (!expression) {

                raiseAggregateError(
                    $reduction,
                    msg || $.validity.messages.generic
                );

                // The set reduces to zero valid elements.
                this.reduction = $([]);
            }
        }

        return this;
    },
    
    fail:function(msg) {
        return this.assert(false, msg);
    }
});

// Private utilities:
///////////////////////////////////////////////////////////////////////////////

// Do non-aggregate validation. Subject each element in $obj to the regimen.
// Raise the specified error on failures. This function is the heart of 
// validity:
function validate($obj, regimen, message) {

    $.validity.registerReduction($obj);

    var 
        // If a reduced set is attached, use it Also, remove any unsupported 
        // elements.
        $reduction = ($obj.reduction || $obj).filter($.validity.settings.elementSupport),

        // Array to store only elements that pass the regimen.
        elements = [];

    // For each in the reduction.
    $reduction.each(
        function() {
            // If the element passes the regimen, include it in the reduction.
            if (regimen(this)) {
                elements.push(this);
            }

            // Else give the element an error message.
            else {
                raiseError(
                    this,
                    format(message, {
                        field:infer(this)
                    })
                );
            }
        }
    );

    // Attach a (potentially) reduced set of only elements that passed.
    $obj.reduction = $(elements);

    // Return the full set with attached reduction.
    return $obj;
}

// Inform the report object that there was a failure.
function addToReport() {
    if ($.validity.isValidating()) {
        $.validity.report.errors++;
        $.validity.report.valid = false;
    }
}

// Inform the report of a failure and display an error according to the  idiom 
// of the current output mode.
function raiseError(obj, msg) {
    addToReport();

    $.validity.out.raise($(obj), msg);
}

// Inform the report of a failure and display an aggregate error according to 
// the idiom of the current output mode.
function raiseAggregateError($obj, msg) {
    addToReport();

    $.validity.out.raiseAggregate($obj, msg);
}

// Yield the sum of the values of all fields matched in obj that can be parsed.
function numericSum(obj) {
    var accumulator = 0;
    obj.each(
        function() {
            var n = parseFloat(this.value);

            accumulator += isNaN(n) ? 0 : n;
        }
    );
    return accumulator;
}

// Using the associative array supplied as the 'obj' argument, replace tokens 
// of the format #{<key>} in the 'str' argument with that key's value.
function format(str, obj) {
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            str = str.replace(
                new RegExp("#\\{" + p + "\\}", "g"), 
                obj[p]
            );
        }
    }
    return capitalize(str);
}

// Infer the field name of the passed DOM element. If a title is specified, its 
// value is returned. Otherwise, attempt to parse a field name out of the id 
// attribute. If that doesn't work, return the default field name in the 
// configuration.
function infer(field) {
    if (!$.validity.settings.useInfer) {
        return $.validity.settings.defaultFieldName;
    }

    var 
        $f = $(field),
        id = $f.prop("id"),
        ret = $.validity.settings.defaultFieldName;

    // Check for title.
    if ($f.prop("title").length) {
        ret = $f.prop("title");
    }

    // Check for UpperCamelCase.
    else if (/^([A-Z0-9][a-z]*)+$/.test(id)) {
        ret = id.replace(/([A-Z0-9])[a-z]*/g, " $&");
    }

    // Check for lowercase_separated_by_underscores
    else if (/^[a-z0-9]+(_[a-z0-9]+)*$/.test(id)) {
        var arr = id.split("_");

        for (var i = 0; i < arr.length; ++i) {
            arr[i] = capitalize(arr[i]);
        }

        ret = arr.join(" ");
    }

    return $.trim(ret);
}

// Capitalize the first character of the string argument.
function capitalize(sz) {
    return sz.substring ?
        sz.substring(0, 1).toUpperCase() + sz.substring(1, sz.length) :
        sz;
}

function orEmpty(fn) {
    return function(obj) {
        return obj.value.length == 0 || fn(obj);
    };
}

__private = { 
    validate:validate,
    addToReport:addToReport,
    raiseError:raiseError,
    raiseAggregateError:raiseAggregateError,
    numericSum:numericSum,
    format:format,
    infer:infer,
    capitalize:capitalize,
    orEmpty:orEmpty
};

})(jQuery);
