  /* global */
  var initHasRun = false;
  
  if (typeof Object.prototype.validate === 'undefined') {
    Object.prototype.validate = function () {};
  }

  if (typeof String.prototype.trim === 'undefined') {
    String.prototype.trim = function () {
      return this.replace(/^\s*([\w]+.*[\w]+)\s*$/, "$1");
    }
  }

  if (typeof String.prototype.normalize === 'undefined') {
    String.prototype.normalize = function () {
      temp = this.replace(/_/g, " ");
      return temp.replace(/\b\w+\b/g, function(word) {return word.substring(0,1).toUpperCase() + word.substring(1);});
    }
  }

  if (typeof String.prototype.stripNonInt === 'undefined') {
    String.prototype.stripNonInt = function () {
      return this.replace(/\D/g, "");
    }
  }

  if (typeof String.prototype.stripNonFloat === 'undefined') {
    String.prototype.stripNonFloat = function () {
      var i=0, parts=this.split('.');
      for (i; i<parts.length; i++) {
        if (/\D/.test(parts[i]) && /[^\d^\$^,)]/.test(parts[i])) {
          return NaN;
        }
        parts[i] = parts[i].replace(/[\D\.]/g, "");
      }
      return parts.join('.');
    }
  }

  function validCard(card) {
  // based on Hiroto Sekine; additions by Chris Hill
    card.stripNonInt();
    var is_valid = false;       // Assume invalid card
    var ln = card.length;	 // Get input value length
    if ((16 <= ln) && (ln <= 19)) {
      --ln;
      checksum = 0;	// start with 0 checksum
      dbl = false;	// Start with a non-doubling

      //------------------------------------------------------------
      //	Beginning backward loop through string
      //-------------------------------------------------------------

      for (idx = ln; idx >= 0; idx--) {
        digit = card.substr(idx, 1);	  // Isolate character
        if (("0" <= digit && digit <= "9") || digit == " " || digit == "-") {
  	  if (digit != " " && digit != "-") { // Skip connector
  	    digit -= "0";	  // Remove ASCII bias
            if (dbl) {		  // If in the "double-add" phase
              digit += digit; // Then double first
              if (digit > 9) { // Cast nines
                digit -= 9;
  	      }
  	    }
            dbl = !dbl; // Flip doubling flag
            checksum += digit;  // Add to running sum
            if (checksum > 9) {  // Cast tens
              checksum -= 10; // Same as MOD 10, but faster
  	    }
          }
  	} else {
  	  return(is_valid);	// Invalid
        }
      }
      is_valid = (checksum == 0) ? true : false; // Must sum to 0
    }
    return(is_valid);
  }

  function isValidPhone(v,re) {
    var vv = v.stripNonInt();
    if (false!==re.test(vv)) {
      return true;
    }
    return false;
  }

  function isValidNumber(v,re) {
    var vv = v.stripNonFloat();
    if (false!==re.test(vv)) {
      return true;
    }
    return false;
  }
  
  function isValidTxtLength(v,ml) {
    // ml: minimum length (integer)
    var re, s;
    if (ml=='') { ml = 1; }
    s = vBindings["txtN"].regex.toString();
    s = s.replace("{N}", ml); 
    s = s.substr(1, s.length-2);  // remove regexp boundary marks
    re = new RegExp(s);
    return isValid(v,re);
  }
  
  function isValidCard(v,re) {
    var vv = v.stripNonInt();
    if (false!==re.test(vv) && false!==validCard(vv)) {
      return true;
    }
    return false;
  }

  function isValid(v,re) {
    return re.test(v);
  }

  var vBindings = {
   "alphnm": {"valmethod":"isValid", "regex":/^[\w\x20'"\-]+$/, "prompt":"Only letters, numbers, hyphens, underscores, spaces, and apostrophes are allowed"},
   "card":   {"valmethod":"isValidCard", "regex":/^[45]\d{15}$/, "prompt":"Card number entries must have 16 digits. If yours does, check for a typo"},
   "cuid":   {"valmethod":"isValid", "regex":/^\d{5,8}$/},
   "email":  {"valmethod":"isValid", "regex":/^[a-z][\w\.'\-]*[a-z0-9]@[a-z][\w\.\'\-]*[a-z0-9]\.[a-z][a-z\.]*[a-z]$/i, "prompt":"The local part (username) of the address must begin with a letter and end with a letter or number.\nThere must be an @ separator.\nThe domain must begin with a letter or number and end with a letter.\nIn between, letters, numbers, apostrophes, dots, and hyphens are permitted."},
   "expdate":{"valmethod":"isValid", "regex":/^\d?\d\/\d{2}$/, "prompt":"Enter the expiration date using digits in the format MM/YY or M/YY"},
   "expdat4":{"valmethod":"isValid", "regex":/^\d?\d\/\d{4}$/, "prompt":"Enter the expiration date using digits in the format MM/YYYY.\nEx.: March 2010 ==> 03/2010"},
   "number": {"valmethod":"isValidNumber", "regex":/^\d+\.?\d*$/, "prompt":"Numbers may begin with a $ sign.\nNumbers may have comma separators and decimal points.\nNo other nonnumeric characters are allowed."},
   "phone":  {"valmethod":"isValid", "regex":/^[\(]*\d{3}[\)]*?[\-\s\.]\d{3}?[\-\s\.]\d{4}$/, "prompt":"Phone numbers should have 10 digits, optionally grouped with a dot, hyphen, or space"},
   "proper": {"valmethod":"isValid", "regex":/^[a-z '\-]+$/i, "prompt":"Only letters, spaces, hyphens, and apostrophes are allowed"},
   "txt2":   {"valmethod":"isValid", "regex":/^\s*([-+#"']*\w+\b[-\+!@#%\,\.\?"']*\s*){2,}$/, "prompt":"Your entry is invalid or shorter than two words."},
   "txt3":   {"valmethod":"isValid", "regex":/^\s*([-+#"']*[\w']+\b[-\+!@#%\,\.\?"']*\s*){2,}[-+#"']*[\w']+[\+!%\.\?"'\x20]*$/, "prompt":"Your entry is invalid or shorter than three words."},
   "txtN":   {"valmethod":"isValidTxtLength", "regex":/(\b\S+\b[\.,!?:;]*\s*){{N},}/, "prompt":"Your entry is invalid or not long enough."},
   "uri":    {"valmethod":"isValid", "regex": "^http://.*"},
   "noval":  {"valmethod":"", "regex":/none/},
   "val":    {"valmethod":"isValid", "regex":/^\.$/},
   "zip":    {"valmethod":"isValid", "regex":/^\d{5}$|^\d{5}[-\s]{1}\d{4}$/, "prompt":"ZIP codes should have 5 digits, optionally followed by a hyphen and 4 more digits"}
  };

  var vMethods = {
    vcheckbox: function () {
      var str = new String(this.name);
      if (false===this.checked) {
        alert('PLEASE CHECK ' + str.normalize());
        this.focus();
      }
      return this.checked;
    },
    vmulti: function () {
      var itself, n, prend, vb;
      this.bind = function (el,t) {
        n = new String(el.name);
        vb = vBindings[t];
        itself = el;
      };
      this.test = function () {
        var txt = (typeof itself.value !== 'undefined') ? itself.value : '';
        txt = txt.trim();
        var vf = eval(vb.valmethod);
        var r = (typeof itself.attributes["min"] !== 'undefined') ? vf(txt,itself.attributes["min"].value) : vf(txt,vb.regex);
        if (false === r) {
          prend = (typeof itself.attributes["min"] !== 'undefined') ? " It should have at least " + itself.attributes["min"].value + " words." : "";
          alert('MISSING OR INVALID VALUE FOR ' + n.normalize() + ((typeof vb.prompt!=='undefined') ? ('\n' + vb.prompt + prend) : ''));
          itself.focus();
          return false;
        }
        return true;
      };
    },
    vradio: function () {
      var i; // vector
      var str = new String(this.name);
      var found; // boolean
      for (i=0; i<this.form[this.name].length; i++) {
        found = this.form[this.name][i].checked;
        if (found) { return true; }
      }
      alert('PLEASE SELECT A VALUE FOR ' + str.normalize());
      this.focus();
      return false;
    },
    vselectOne: function () {
      var str = new String(this.name);
      if (0 < this.options[this.selectedIndex].value.length) {
        return true;
      }
      alert('PLEASE SELECT A VALUE FOR ' + str.normalize());
      this.focus();
      return false;
    }
  };

  function initializeValidation(f) {
    var attr, e, estr, esub, newa, vt;
    /* loop through form elements */
    for (var i=0; i<f.elements.length; i++) {
      e = f.elements[i];
      vt = (typeof e.attributes['valtype'] !== 'undefined') ? e.attributes.getNamedItem('valtype').value : '';
//alert('FOR ' + e.name + ' WITH TYPE ' + e.type + ' VT = |' + vt + '|');      
      if (0 < vt.length) {
        switch (e.type) {
          case "checkbox":
            e.validate = vMethods.vcheckbox;
          break;
          case "radio":
            e.validate = vMethods.vradio;
          break;
          case "select-one":
            e.validate = vMethods.vselectOne;
            break;
          case "text": case "textarea": case "password":
            estr = new String(e.value);
            estr = estr.trim();
            e.valObj = new vMethods.vmulti;
            e.valObj.bind(e,vt);
            e.validate = e.valObj.test;
          break;
          default:
            // "button": "hidden":
            attr = e.attributes;
            newa = document.createAttribute('valtype');
            newa.value = 'noval';
            attr.setNamedItem(newa);
            e.validate = null;
        } // switch
      } else {
        attr = e.attributes;
        newa = document.createAttribute('valtype');
        newa.value = 'noval';
        attr.setNamedItem(newa);
        e.validate = null;
      } // if 0
//if (e.validate) { alert('FOR ' + e.name + ' OF TYPE ' + e.type + ' VALIDATE = ' + e.validate.toString()); }
    } // for
    initHasRun = true;
  }

  function check(f) {
    var loop = true;
    var result;
    if (false === initHasRun) { initializeValidation(f); }
    /* loop through form elements */
    for (var i=0; loop && i<f.elements.length; i++) {
      var e = f.elements[i];
      if (null !== e.validate) {
        if (typeof e.validate === 'function') {
          if (false === (result=e.validate())) {
            loop = false;
          }
        }
      } // if null
    } // for
    if (false!==result && confirm("Are you sure you want to submit this form now?")) {
      return true;
    } else {
      return false;
    }
  }
