/* Common form validation module
   Author: Jonathan Hurshman
   Created: 23 Aug 2000
   
   --Modification record-------------------------------------------------------------------------
	31 Jul 2001 (Jonathan Hurshman)
		Added "len" attribute for exact string lengths.
	 8 Jan 2002 (Jonathan Hurshman)
		Added support for columns of fields.
	21 Jan 2002 (Jonathan Hurshman)
		Added "validateIf" attribute.
	12 Mar 2002 (Jonathan Hurshman)
		Added "alpha", "strictalpha", and "email" types.
	25 Mar 2002 (Jonathan Hurshman)
		Fixed currency type validation (Javascript math bug was causing false errors).
	 6 May 2002 (Jonathan Hurshman)
		Added "strictalphanum" type.
	 8 May 2002 (Jonathan Hurshman)
		Improved error messages. Many tweaks to code.
	 9 May 2002 (Jonathan Hurshman)
		Fixed currency check, date range check, and additional validation function call.
		Added "mutuallydependent" and "exactlyone" relationships.
	14 Aug 2002 (Jonathan Hurshman)
		Allow "=" characters in attribute values (useful for validateIf conditions).
		Added URL type.
	 3 Sep 2002 (Jonathan Hurshman)
		Added date format capability. Default format is M/D/YYYY.
	11 Sep 2002 (Jonathan Hurshman)
		Added capacity for multiple date formats, as well as a text description of the
		date format for error messages.
	15 Jan 2003 (Jonathan Hurshman)
		Added support for date formats which don't include the day (e.g. YYYY-MM).
	 3 Feb 2003 (Jonathan Hurshman)
	 	Added numberstring type.
   ----------------------------------------------------------------------------------------------
   
   Note: The names of all functions and variables used internally in this module
   begin with an underscore to prevent conflicts with identifiers in the main code.
      
   How to use this module:
   	1.	Define all the fields you wish to validate by using 
			defineField(fieldObj, description, parameter_string)
		Error messages will look best if the description is title case or lower case.
		Fields will be checked in the order they are defined.
		For the parameter string attributes, see below.
		Call defineField only after the field exists (i.e. after its HTML has been rendered).
		
		
	2.	Call validateFields (usually onSubmit) to validate (returns true or false).
	    To prevent the form from submitting on error, use onSubmit="return validateFields()".
	
	
   Field attributes usually have two components: the attribute name and its value.
   These are separated by a "=" character. Some attributes are boolean and any
   supplied value is ignored (even if the value is "false").
   
   Attributes are separated by ";" characters.
   
   Recognized attributes are the following:
   	 * validateIf: provide an expression to be evaluated at validation time.
	               If it evaluates to false, no validation will be performed.
		   
     * required: field cannot have blank or null value. (BOOLEAN)
     * type: determines basic validation. If not specified, "string" is assumed.
             Valid values are the following:
             * numeric: all numeric values
             * integer: numeric values with 0 digits after the decimal point
             * currency: numeric values with no more than 2 digits after the decimal point
             * string: any value; this is the default type
			 * alpha: strings containing only letters, space, comma, and period
			 * strictalpha: strings containing only letters
			 * strictalphanum: strings containing only letters and numbers
			 * numstring: string containing only digits
			 * email: strings in user@host.domain format
  			 * url: strings in http://... or https://... format
             * date: valid dates (default format: M/D/YYYY)
 
     * min: minimum value, either numeric or date
     * max: maximum value, either numeric or date
     * positive: value must be positive (greater than 0), numeric types only (BOOLEAN)
     * negative: value must be negative (less than 0), numeric types only (BOOLEAN)
     
     * len: exact length, string types only
	 * minlen: minimum length, string types only
     * maxlen: maximum length, string types only
	 
	 * dateFormat: pattern for date format, using the following:
	 			   * D = 1- or 2-digit day
				   * DD = 2-digit day
				   * M = 1- or 2-digit month
				   * MM = 2-digit month
				   * MMM = 3-character month name abbreviations
				   * YY = 2-digit year
				   * YYYY = 4-digit year
				   Multiple instances of the dateFormat attribute are allowed,
				   which indicate alternative formats accepted.
	 * dateFormatDescr: a user-friendly description of the dateFormat.
	 					If not provided, a list of the valid dateFormats is used.
				   

     * relatedTo: global reference to another field to relate this one to.
	              The other field must also have a field definition object.
     * relationship: one of the following values:
                   * dependent: if the current field is populated, the other must be also.
				   * mutuallydependent: each field is dependent on the other
                   * mutuallyexclusive: neither or only one of the two fields may be populated.
				   * exactlyone: exactly one of the two fields is required (not both or neither)
                   * atleastonerequired: at least one of the two fields must be populated.
				       
     * compareTo: global reference to another field to compare this one to.
	              The other field must also have a field definition object.
     * compareHow: one of the following values:
                   * greaterthan: current field's value must be greater than the other field's.
                   * lessthan: current field's value must be less than the other field's.
				   * greaterthanorequal: current field's value must be greater than or equal to the other field's
				   * lessthanorequal: current field's value must be less than or equal to the other field's
    
     * function: name of function to be called after basic validation passes for
                 the field. This is where custom validation can be done.
                 Function must return true if validation passes, or false otherwise.
                 Function will be called with the field object as its first parameter.
     * funcparams: additional parameters to supply to function named in 'function'
                   attribute. These should be comma-separated. Any objects should
                   be referenced globally.
				   
	Columns of fields (multiple fields with the same name) are supported.
	The validation criteria will be applied to each instance in turn. 
	In the case of comparisons or relationships, if the other field is also a column of fields,
	the comparison or relationship check will be performed on the corresponding field that has the same
	index as the current one.
	Note: If there are different numbers of fields in each column, the unmatched fields will not be checked
	for comparison/relationship. All other validations will occur.
*/

var _fieldArray = new Array();
var _numericTypes = "|numeric|integer|currency|";
var _stringTypes = "|string|alpha|strictalpha|strictalphanum|email|url|numstring|";
var _dateTypes = "|date|";
var _validTypes = _numericTypes + _stringTypes + _dateTypes;

// Construct a field validation object
// The "parameters" string should not contain any other "=" or ";" characters.
function _Field (fieldObj, label, parameters) {


	//------------------------------
	// Date format object
	function _dateFormat(formatStr) {

		// Functions
		function _getRegExp(formatStr) {
			formatStr = formatStr.replace(/M{3}/i, "([A-Za-z]{3})");
			formatStr = formatStr.replace(/M{2}/i, "([0-9]{2})");
			formatStr = formatStr.replace(/M{1}/i, "([0-9]{1,2})");
			
			formatStr = formatStr.replace(/D{2}/i, "([0-9]{2})");
			formatStr = formatStr.replace(/D{1}/i, "([0-9]{1,2})");
			
			formatStr = formatStr.replace(/Y{4}/i, "([0-9]{4})");
			formatStr = formatStr.replace(/Y{2}/i, "([0-9]{2})");
			return new RegExp("^" + formatStr + "$");
		}
		
		function _getOrder(formatStr) {
			formatStr = formatStr.toUpperCase();
			formatStr = formatStr.replace(/[^DMY]/g, "");
			formatStr = formatStr.replace(/D+/g, "D");
			formatStr = formatStr.replace(/M+/g, "M");
			formatStr = formatStr.replace(/Y+/g, "Y");
			
			var outArr = new Array();
			for (var i = 0; i < 3; i++) {
				outArr[formatStr.substring(i,i + 1)] = i + 1;
			}
			
			return outArr;
		}
		
		function _alphaMonth(formatStr) {
			return (/M{3}/i.test(formatStr));
		}
		
		function _validate(str) {
			return this.regExp.test(trimStr(str));
		}
		
		function _parse(str) {
			var months = new Array();
			var i = 1;
			months["JAN"] = i++;
			months["FEB"] = i++;
			months["MAR"] = i++;
			months["APR"] = i++;
			months["MAY"] = i++;
			months["JUN"] = i++;
			months["JUL"] = i++;
			months["AUG"] = i++;
			months["SEP"] = i++;
			months["OCT"] = i++;
			months["NOV"] = i++;
			months["DEC"] = i++;
			
			var outDate = false;
			var REArr = this.regExp.exec(trimStr(str));
			
			if (REArr) {
				var day = (REArr[this.order["D"]]) ? REArr[this.order["D"]] : 1;
				var month = REArr[this.order["M"]];
				if (this.alphaMonth) {
					month = months[month.toUpperCase()];
					if (! month) {
						return false;
					}
				}
				month -= 1;
				
				var year = REArr[this.order["Y"]];
				outDate = new Date(year, month, day);

				if ((outDate.getDate() != day) || (outDate.getMonth() != month)) {
					return false;
				}
				
			}
			
			return outDate;
		}
		
		// Properties
		this.originalFormat = formatStr;
		this.regExp = _getRegExp(formatStr);
		this.alphaMonth = _alphaMonth(formatStr);
		this.order = _getOrder(formatStr);
		
		// Methods
		this.validFormat = _validate;
		this.parse = _parse;
		
	}
		

	//------------------------------
	// Properties
	if (! fieldObj) {
		alert("Developer message: field " + label + " not found.");
		return false;
	}
	
	this.obj = fieldObj;
	fieldObj.validationInfo = this;
	
	this.label = (label) ? label : "this field";

	this.required = false;
	this.type = "string";
	this.isArray = (fieldObj.length && fieldObj[0] && fieldObj[0].type) ? true : false;
	this.numericType = false;
	this.stringType = true;
	this.dateType = false;
	this.min = null;
	this.max = null;
	this.positive = false;
	this.negative = false;
	this.len = null;
	this.minlen = null;
	this.maxlen = null;
	this.dateFormat = null;
	this.dateFormatDescr = null;
	this.valFunction = null;
	this.valFuncParams = null;
	this.relatedTo = null;
	this.relationship = null;
	this.compareTo = null;
	this.compareHow = null;	
	this.validateIf = true;
	
	// Parse parameters string and define appropriate properties
	if (parameters) {
		var paramArr = parameters.split(/;/);
		for (var i = 0; i < paramArr.length; i++) {
			var equalsSignLoc = paramArr[i].indexOf("=");
			if (equalsSignLoc != -1) {
				var currParam = trimStr(paramArr[i].substr(0, equalsSignLoc).toLowerCase());
				var currValue = trimStr(paramArr[i].substr(equalsSignLoc + 1));
			}
			else {
				var currParam = trimStr(paramArr[i].toLowerCase());
			}
			
			switch (currParam) {
				case "required":
					this.required = true;
					break;
					
				case "type":
					currValue = currValue.toLowerCase();
					if (_validTypes.indexOf("|" + currValue + "|") != -1) {
						this.type = currValue;
						this.numericType = (_numericTypes.indexOf("|" + currValue + "|") != -1);
						this.stringType = (_stringTypes.indexOf("|" + currValue + "|") != -1);
						this.dateType = (_dateTypes.indexOf("|" + currValue + "|") != -1);
					}
					else { // invalid data type
						alert("Developer message: unknown field type (" + currValue + ") supplied.\nField: " + this.label);
						this.numericType = false;
						this.stringType = true;
						this.dateType = false;
					}
					break;
					
				case "min":
				case "max":
				case "len":
				case "minlen":
				case "maxlen":
					eval("this." + currParam + " = currValue");
					break;
					
				case "positive":
					this.positive = true;
					break;

				case "negative":
					this.negative = true;
					break;

				case "dateformat":
					if (! this.dateFormat) {
						this.dateFormat = new Array();
					}
					this.dateFormat[this.dateFormat.length] = new _dateFormat(currValue);
					break;
					
				case "dateformatdescr":
					this.dateFormatDescr = currValue;
					break;
					
				case "function":
					this.valFunction = currValue;
					break;
					
				case "funcparams":
					this.valFuncParams = currValue;
					break;
					
				case "relatedto":
					this.relatedTo = currValue;
					break;
					
				case "relationship":
					this.relationship = currValue.toLowerCase();
					break;
									
				case "compareto":
					this.compareTo = currValue;
					break;
					
				case "comparehow":
					this.compareHow = currValue.toLowerCase();
					break;

				case "validateif":
					this.validateIf = currValue;
					break;

				default:
					if (trimStr(currParam)) {
						alert("Developer message: unknown field attribute (" + currParam + ") supplied.\nField: " + this.label);
					}
					break;
			}
		}
	
		// Set up default date format and description
		if (this.dateType) {
			if (! this.dateFormat) {
				this.dateFormat = new Array();
				this.dateFormat[0] = new _dateFormat("M/D/YYYY");
				this.dateFormatDescr = "M/D/YYYY";
			}
			
			if (! this.dateFormatDescr) {
				var dateFormatDescrArr = new Array();
				for (var i = 0; i < this.dateFormat.length; i++) {
					dateFormatDescrArr[i] = this.dateFormat[i].originalFormat;
				}
				this.dateFormatDescr = dateFormatDescrArr.join(", ");
				
				switch (dateFormatDescrArr.length) {
					case 1:
						break;
						
					case 2:	
						this.dateFormatDescr = this.dateFormatDescr.replace(/, ([^,]+)$/, " or $1");
						break;
					
					default:
						this.dateFormatDescr = this.dateFormatDescr.replace(/, ([^,]+)$/, ", or $1");
						break;
				}
			}
		}
		
		// Check consistency of parameters
		if (! this.numericType && ! this.dateType && (this.min != null || this.max != null)) {
			alert("Developer message: 'min' and 'max' field attributes are only applicable to numeric and date field types.\nField: " + this.label);
			this.min = null;
			this.max = null;
		}

		if (! this.numericType && (this.positive || this.negative)) {
			alert("Developer message: 'positive' and 'negative' field attributes are only applicable to numeric types.\nField: " + this.label);
			this.positive = false;
			this.negative = false;		
		}
		
		if (! this.stringType && (this.len != null || this.minlen != null || this.maxlen != null)) {
			alert("Developer message: 'len', 'minlen', and 'maxlen' field attributes are only applicable to string field types.\nField: " + this.label);
			this.minlen = null;
			this.maxlen = null;
		}
		else {
			if (this.len != null && isNaN(this.len)) {
				alert("Developer message: supplied 'len' attribute is not a number.\nField: " + this.label);
				this.len = null;
			}
			else {
				this.len = (this.len != null) ? Number(this.len) : null;
			}
			
			if (this.minlen != null && isNaN(this.minlen)) {
				alert("Developer message: supplied 'minlen' attribute is not a number.\nField: " + this.label);
				this.minlen = null;
			}
			else {
				this.minlen = (this.minlen != null) ? Number(this.minlen) : null;
			}
			
			if (this.maxlen != null && isNaN(this.maxlen)) {
				alert("Developer message: supplied 'maxlen' attribute is not a number.\nField: " + this.label);
				this.maxlen = null;
			}
			else {
				this.maxlen = (this.maxlen != null) ? Number(this.maxlen) : null;
			}
		}
	
		if (this.numericType) {
			if (this.min != null && isNaN(this.min)) {
				alert("Developer message: supplied 'min' attribute is not a number.\nField: " + this.label);
				this.min = null;
			}
			else {
				this.min = (this.min != null) ? Number(this.min) : null;
			}
			
			if (this.max != null && isNaN(this.max)) {
				alert("Developer message: supplied 'max' attribute is not a number.\nField: " + this.label);
				this.max = null;
			}
			else {
				this.max = (this.max != null) ? Number(this.max) : null;
			}
		}
	
		if (this.dateType) {		
			if (this.min != null) {
				this.min = _parseDate(this, this.min);
				if (! this.min) {
					alert("Developer message: supplied 'min' attribute is not a valid date.\nField: " + this.label);
					this.min = null;
				}
			}

			if (this.max != null) {
				this.max = _parseDate(this, this.max);
				if (! this.max) {
					alert("Developer message: supplied 'max' attribute is not a valid date.\nField: " + this.label);
					this.max = null;
				}
			}
		}

		if ((this.min != null) && (this.max != null) && (this.min > this.max)) {
			alert("Developer message: supplied 'min' attribute is greater than supplied 'max' attribute.\nField: " + this.label);
			var tmp = this.min;
			this.min = this.max;
			this.max = tmp;
		}
	
		if (this.positive && this.negative) {
			alert("Developer message: 'positive' attribute is incompatible with 'negative' attribute.\nField: " + this.label);
			this.negative = false;			
		}
		
		if (this.positive && this.min != null) {
			alert("Developer message: 'positive' attribute is incompatible with 'min' attribute.\nField: " + this.label);
			this.min = null;
		}
		
		if (this.positive && this.max != null && this.max <= 0) {
			alert("Developer message: 'positive' attribute is incompatible with 'max' attribute with value less than zero.\nField: " + this.label);
			this.max = null;
		}

		if (this.negative && this.max != null) {
			alert("Developer message: 'negative' attribute is incompatible with 'max' attribute.\nField: " + this.label);
			this.min = null;
		}
		
		if (this.negative && this.min != null && this.min >= 0) {
			alert("Developer message: 'negative' attribute is incompatible with 'min' attribute with value greater than zero.\nField: " + this.label);
			this.max = null;
		}

		if ((this.minlen != null) && (this.maxlen != null) && (this.minlen > this.maxlen)) {
			alert("Developer message: supplied 'minlen' attribute is greater than supplied 'maxlen' attribute.\nField: " + this.label);
			var tmp = this.minlen;
			this.minlen = this.maxlen;
			this.maxlen = tmp;
		}

		if (this.len) {
			if (this.len < 0) {
				alert("Developer message: supplied 'len' attribute is negative.\nField: " + this.label);
				this.len = null;
			}
			else {
				this.minlen = this.len;
				this.maxlen = this.len;
			}
		}
		
		if (this.minlen < 0) {
			alert("Developer message: supplied 'minlen' attribute is negative.\nField: " + this.label);
			this.minlen = null;
		}
		
		if (this.maxlen < 0) {
			alert("Developer message: supplied 'maxlen' attribute is negative.\nField: " + this.label);
			this.maxlen = null;
		}
	}
	
	//------------------------------
	// Method functions
	function validate() {
	
		var _field = this;
	
		//------------------------------
		// Private validation functions
		
		// Returns true if value is empty, false otherwise.
		function _isEmpty(theValue) {
			theValue = trimStr(theValue);
			return !((theValue) || (! theValue && theValue == 0 && theValue.length > 0));
		}

		// Returns true if value is numeric, false otherwise.
		function _isNumeric() {
			return ! isNaN(_value);
		}

		// Returns true if value is an integer, false otherwise.
		function _isInteger() {
			return (_isNumeric(_value) && (_value == Math.floor(_value)));
		}

		// Returns true if value is a valid currency value, false otherwise.
		function _isCurrency() {
			return (_isNumeric(_value) && (_checkNumberFormat(null, 2)));
		}

		// Returns true if value is alphabetical (letters, space, comma, and period only), false otherwise.
		function _isAlpha() {
		 	var re = new RegExp("[^a-z ,.]", "i");	// Using this syntax to protect space from compressor.
		 	return (! re.test(trimStr(_value)));
		}

		// Returns true if value is strict alphabetical (letters only), false otherwise.
		function _isStrictAlpha() {
		 	var re = /[^a-z]/i;
		 	return (! re.test(trimStr(_value)));
		}

		// Returns true if value is strict alphanumerical (letters and digits only), false otherwise.
		function _isStrictAlphaNum() {
		 	var re = /[^a-z0-9]/i;
		 	return (! re.test(trimStr(_value)));
		}

		// Returns true if value is a string of digits only, false otherwise.
		function _isNumString() {
		 	var re = /[^0-9]/i;
		 	return (! re.test(trimStr(_value)));
		}

		// Returns true if value appears to be a valid email address, false otherwise.
		function _isEmail() {
		 	var re = /^(\w|\.)+@(\w|\.)+\.(com|net|org)$/i;
		 	return (re.test(trimStr(_value)));
		}

		// Returns true if value appears to be a valid URL, false otherwise.
		function _isUrl() {
		 	var re = /^https?:\/\/[^ ]+$/i;
		 	return (re.test(trimStr(_value)));
		}

		// Returns true if value is a valid date, false otherwise.
		function _isDate() {
			var valid = false;
			for (var i = 0; ! valid && i < _field.dateFormat.length; i++) {
				valid = (_field.dateFormat[i].parse(_value)) ? true : false;
			}
			return valid;
		}

		// Checks a number to ensure it has no more than integerDigits in the 
		// integer portion and no more than fractionalDigits in the fractional
		// portion. Returns true or false. Assumes a number is provided.
		// If integerDigits or fractionalDigits is null, it is ignored.
		function _checkNumberFormat(integerDigits, fractionalDigits) {		
			var integerPartOK = false;
			var fractionalPartOK = false;
			
			theValue = trimStr(_value);

			if (theValue.indexOf(".") != -1) {
				var valueParts = theValue.split(".", 2);
				var integerPart = valueParts[0];
				var fractionalPart = valueParts[1];
			}
			else {
				var integerPart = theValue;
				fractionalDigits = null;
			}

			integerPartOK = (integerDigits == null) || (integerPart == integerPart.substr(0, integerDigits));
			fractionalPartOK = (fractionalDigits == null) || (fractionalPart == fractionalPart.substr(0, fractionalDigits));

			return (integerPartOK && fractionalPartOK);
		}
		
		// Returns true if value's length is between min and max.
		// If either min or max is null, that boundary is ignored.
		function _checkLen(min, max) {
			var valueLen = _value.length;
			
			if (min == null && max == null) {
				return true;
			}
			
			if (min == max) { // exact length
				if (valueLen != min) {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be exactly " + min + " characters long. Current length is " + valueLen + " characters.");
				}
				else {
					return true;
				}
			}
			
			if (min == null) {
				if (valueLen > max) {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be no more than " + max + " characters long. Current length is " + valueLen + " characters.");
				}
				else {
					return true;
				}
			}
			
			if (max == null) {
				if (valueLen < min) {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be at least " + min + " characters long. Current length is " + valueLen + " characters.");
				}
				else {
					return true;
				}
			}
			
			if (valueLen < min || valueLen > max) {
				return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be between " + min + " and " + max + " characters long. Current length is " + valueLen + " characters.");
			}
			return true;
		}
				
		// Returns true if value is between min and max.
		// If either min or max is null, that boundary is ignored.
		function _checkRange(min, max) {			
			if (min == null && max == null) {
				return true;
			}
	
			var theValue = _value;
					
			if (min == null) {
				if (theValue > max) {
					if (_field.dateType) {
						return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be on or before " + _formatDate(max) + ".");
					}
					else {
						return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be less than or equal to " + max + ".");
					}
				}
				else {
					return true;
				}
			}
			
			if (max == null) {
				if (theValue < min) {
					if (_field.dateType) {
						return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be on or after " + _formatDate(min) + ".");
					}
					else {
						return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be greater than or equal to " + min + ".");
					}
				}
				else {
					return true;
				}
			}
			
			if (theValue < min || theValue > max) {
				if (_field.dateType) {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be between " + _formatDate(min) + " and " + _formatDate(max) + ".");
				}
				else {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be between " + min + " and " + max + ".");
				}
			}
			return true;
		}
		
		// Relate this field to another
		function _relationship(index) {
			var otherField = eval(_field.relatedTo);
			if (otherField && otherField.validationInfo) {
				var otherFieldDef = otherField.validationInfo;
				if (otherFieldDef.isArray && index != null) {
					if (otherField[index]) {
						otherField = otherField[index];
					}
					else {
						return true;
					}
				}
				var otherValue = _getValue(otherField);

				// Check requested relationship
				switch (_field.relationship) {
					case "dependent": // If the field is entered, the other field must be too.
						if (_isEmpty(otherValue) && ! _isEmpty(_value)) {
							return errorAlert(otherField, "If the " + _field.label + " field is entered, the " + otherFieldDef.label + " field is required also.");
						}
						break;
					
					case "mutuallydependent": // If either field is entered, the other one must be too.
						if (_isEmpty(otherValue) ^ _isEmpty(_value)) {
							if (_isEmpty(_value)) {
								var errorField = _fieldObj;
							}
							else {
								var errorField = otherField;
							}
							return errorAlert(errorField, "If either the " + _field.label + " or the " + otherFieldDef.label + " field is entered, the other is required also.");
						}
						break;
					
					case "mutuallyexclusive": // None or one may be entered, but not both.
						if (! (_isEmpty(otherValue) || _isEmpty(_value))) {
							return errorAlert(_fieldObj, "Either the " + _field.label + " field or the " + otherFieldDef.label + " field can be entered, but not both.");
						}
						break;

					case "exactlyone": // One of the two values must be entered, but not neither or both.
						if (! (_isEmpty(otherValue) ^ _isEmpty(_value))) {
							return errorAlert(otherField, "One of the " + _field.label + " field or the " + otherFieldDef.label + " field must be entered, but not both.");
						}
						break;

					case "atleastonerequired": // One or both may be entered, but not zero.
						if (_isEmpty(otherValue) && _isEmpty(_value)) {
							return errorAlert(otherField, "The " + _field.label + " field and/or the " + otherFieldDef.label + " field is required.");
						}
						break;
				}
			}
			return true;
		}

		// Compare this field to another
		function _compare(index) {
			var otherField = eval(_field.compareTo);
			if (otherField && otherField.validationInfo) {
				var otherFieldDef = otherField.validationInfo;
				if (otherFieldDef.isArray && index != null) {
					if (otherField[index]) {
						otherField = otherField[index];
					}
					else {
						return true;
					}
				}
				var otherValue = _getValue(otherField);
				
				if (otherValue == "" || _value == "") {
					return true;
				}
				
 				var thisValue = _value;

				// Perform requested comparison
				switch (_field.compareHow) {
					case "greaterthan":
						var comparisonText = "greater than";
												
						if (_field.numericType && otherFieldDef.numericType) {
							thisValue = Number(thisValue);
							otherValue = Number(otherValue);
						}
						if (_field.dateType && otherFieldDef.dateType) {
							otherValue = _parseDate(otherFieldDef, otherValue);
							comparisonText = "after";
						}
						if (thisValue <= otherValue) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be " + comparisonText + " " + otherFieldDef.label + ".");							
						}
						break;
					
					case "lessthan":
						var comparisonText = "less than";
						
						if (_field.numericType && otherFieldDef.numericType) {
							thisValue = Number(thisValue);
							otherValue = Number(otherValue);
						}
						if (_field.dateType && otherFieldDef.dateType) {
							otherValue = _parseDate(otherFieldDef, otherValue);
							comparisonText = "before";
						}
						if (thisValue >= otherValue) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be " + comparisonText + " " + otherFieldDef.label + ".");							
						}
						break;
					
					case "greaterthanorequal":
						var comparisonText = "greater than or equal to";
													
						if (_field.numericType && otherFieldDef.numericType) {
							thisValue = Number(thisValue);
							otherValue = Number(otherValue);
						}
						if (_field.dateType && otherFieldDef.dateType) {
							otherValue = _parseDate(otherFieldDef, otherValue);
							comparisonText = "equal to or after";
						}
						if (thisValue < otherValue) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be " + comparisonText + " " + otherFieldDef.label + ".");							
						}
						break;
					
					case "lessthanorequal":
						var comparisonText = "less than or equal to";
						
						if (_field.numericType && otherFieldDef.numericType) {
							thisValue = Number(thisValue);
							otherValue = Number(otherValue);
						}
						if (_field.dateType && otherFieldDef.dateType) {
							otherValue = _parseDate(otherFieldDef, otherValue);
							comparisonText = "equal to or before";
						}
						if (thisValue > otherValue) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be " + comparisonText + " " + otherFieldDef.label + ".");							
						}
						break;
				}
			}
			return true;
		}
		
		// Main validation function
		function _validate(index) {
			if (index == null) {
				_fieldObj = _field.obj;
			}
			else {
				_fieldObj = _field.obj[index];
			}
			_value = _getValue(_fieldObj);

			// Check required
			if (_isEmpty(_value)) {
				if (_field.required) {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " is a required field.");
				}
			}
			else {
				// Check based on type
				switch (_field.type) {
					case "numeric":
						if (! _isNumeric(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a valid number.");
						}
						break;
						
					case "integer":
						if (! _isInteger(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a valid integer.");
						}
						break;
						
					case "currency":
						if (! _isCurrency(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a valid dollar amount.");
						}
						break;
						
					case "string":
						break;
						
					case "alpha":
						if (! _isAlpha(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must contain only letters, spaces, commas, and periods.");
						}
						break;
						
					case "strictalpha":
						if (! _isStrictAlpha(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must contain only letters.");
						}
						break;
						
					case "strictalphanum":
						if (! _isStrictAlphaNum(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must contain only letters and digits.");
						}
						break;
						
					case "numstring":
						if (! _isNumString(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must contain only digits.");
						}
						break;
						
					case "email":
						if (! _isEmail(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a valid email address in the format 'user@host'.");
						}
						break;
						
					case "url":
						if (! _isUrl(_value)) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a valid URL beginning with 'http://' or 'https://'.");
						}
						break;
						
					case "date":
						_value = _parseDate(_field, _value);
						if (! _value) {
							return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a valid date in the format " + _field.dateFormatDescr + ".");
						}
						break;
				}
				// Check length (function sets the error message, if any)
				if (! _checkLen(_field.minlen, _field.maxlen)) {
					return false;
				}
	
				// Check positive/negative
				if (_field.positive && _value <= 0) {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a positive number.");
				}
				
				if (_field.negative && _value >= 0) {
					return errorAlert(_fieldObj, sentenceCase(_field.label) + " must be a negative number.");
				}
				
				// Check range (function sets the error message, if any)
				if (! _checkRange(_field.min, _field.max)) {
					return false;
				}
			}
	
			// Check relationship to other field, if appropriate.
			// Function must display error alerts.
			if (_field.relatedTo && _field.relationship && ! _relationship(index)) {
				return false;
			}
			
			// Check comparison to other field, if appropriate.
			// Function must display error alerts.
			if (_field.compareTo && _field.compareHow && ! _compare(index)) {
				return false;
			}
	
			// Call the additional validation function, if any.
			// Additional function must return true if validation passes,
			// and false if it fails. It must also handle error alerts.
			if (_field.valFunction) {
				var theFunction = eval("self." + _field.valFunction);
				var theParams = "";
				if (_field.valFuncParams) {
					theParams = "," + _field.valFuncParams;
				}
				if (theFunction) {
					if (! eval("theFunction(_fieldObj" + theParams + ")")) {
						return false;
					}
				}
			}
			
			// Passes validation
			return true;
			
		} // end function _validate()
		
		//------------------------------
		// Main code of validate method
		
		if (eval(this.validateIf)) {
			if (this.isArray) {
				for (i = 0; i < this.obj.length; i++) {
					if ( ! _validate(i)) {
						return false;
					}
				}
				return true;
			}
			else {
				return _validate();
			}
		}
		else {
			return true;
		}
	} // end function validate()

	//------------------------------
	// Method assignments
	this.validate = validate;
}


// Return value for form elements, including those that don't have a value property
// (e.g. drop-downs under Netscape)
function _getValue(element) {
	var outVal;
	if (element.value || element.value == 0) {
		outVal = element.value;
	}
	else {
		switch (element.type) {
			case "select-one":
				outVal = element.options[element.selectedIndex].value;
				break;
			case "select-multiple":
				outVal = element.options[element.selectedIndex].value;
				break;
			default:
				alert("Can't get value of " + element.type);
		}
	}
	return outVal;
}

	
// Validate a numeric range.
// Either min or max parameter can be null, in which case that boundary is ignored.
// Assumes the field has already passed numeric validation.
function _validateRange(_field, min, max) {
	if (min == null && max == null) {
		return true;
	}
	
	var _value = _getValue(_field);
	
	if (min == null) {
		if (_value > max) {
			return errorAlert(_fieldObj, "The value of " + _field.label + " must be less than or equal to " + max + ".");
		}
		else {
			return true;
		}
	}
	
	if (max == null) {
		if (_value < min) {
			return errorAlert(_fieldObj, "The value of " + _field.label + " must be greater than or equal to " + min + ".");
		}
		else {
			return true;
		}
	}
	
	if (_value < min || _value > max) {
		return errorAlert(_fieldObj, "The value of " + _field.label + " must be between " + min + " and " + max + ".");
	}
	return true;
}

function _formatDate(dateObj) {
	var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
	return dateObj.getDate() + " " + months[dateObj.getMonth()] + " " + dateObj.getFullYear();
}

function _parseDate(field, str) {
	var valid = false;
	for (var i = 0; (! valid) && i < field.dateFormat.length; i++) {
		valid = field.dateFormat[i].parse(str);
	}
	return valid;
}
	

//---------------------------------------------------------------------------------------------
// "Public" functions
//---------------------------------------------------------------------------------------------

// Validate all the defined fields.
function validateFields() {
	for (var i = 0; i < _fieldArray.length; i++) {
		if (! _fieldArray[i].validate()) {
			return false;
		}
	}
	return true;
}

// Define a single field
function defineField (fieldObj, description, parameters) {
	_fieldArray[_fieldArray.length] = new _Field(fieldObj, description, parameters);
}

// Delete all field definitions
function deleteFields() {
	for (var i = 0; i < _fieldArray.length; i++) {
		_fieldArray[i] = null;
	}
	_fieldArray = new Array();
}

//---------------------------------------------------------------------------------------------
// Miscellaneous functions
//---------------------------------------------------------------------------------------------
function errorConfirm(theField, Msg) {
	var choice = confirm(Msg);
	
	if (! choice) {
			
		if (theField.select) {
			theField.select();
		}
		
		if (theField.focus) {
			theField.focus();
		}
	}
	
	return choice;
}

function errorAlert(theField, msg) {
	
	alert(msg);
			
	if (theField.select) {
		theField.select();
	}
	
	if (theField.focus) {
		theField.focus();
	}
	
	return false;
}

// Compare two values to see if they are the same.
// Takes into account the type of the values (assumes both are the same type).
// Does trimmed, case-insensitive comparison for strings.
function identical(valueOne, valueTwo) {
	if (isNaN(Number(valueOne))) {	// values are not numeric
		return (trimStr(valueOne.toUpperCase()) == trimStr(valueTwo.toUpperCase()));
	}
	else {							// values are numeric
		return (parseFloat(valueOne) == parseFloat(valueTwo));
	}
}

// Trim a string (remove spaces at beginning and end).
// Returns the trimmed string.
function trimStr(str) {
	if (! str) {
		return str;
	}
	var tmpStr = str.replace(/\s+$/,"");
	return tmpStr.replace(/^\s+/,"");
}

// Make first character of string uppercase (remainder of string is unchanged)
function sentenceCase (string) {
	return string.substr(0,1).toUpperCase() + string.substr(1);
}
