/*
 * Copyright © Atomic Inc 2007-2008
 * http://jsorm.com
 *
 * This file contains work that is copyrighted and is distributed under one of several licenses. 
 * You may not use, modify or distribute this work, except under an approved license. 
 * Please visit the Web site listed above to obtain the original work and a license.
 */

//set up the base namespace object
jsorm = {};

/**
 * @fileoverview
 * Utilities necessary for jsorm i18n. These are used by the various other libraries.
 * 
 */

/**
 * Copy all of the elements of source to target. 
 * 
 * @param {object} target What to apply the source to
 * @param {object} source What to use to apply
 */
jsorm.apply = function(target,source) {
	for (var prp in source) {
		target[prp] = source[prp];
	}
}
/**
 * Shallow-clone an object. If a member is itself an object, it will not be copied by value but by reference.
 * 
 * @param {object} obj The object to shallow-clone
 * @returns {Object} The cloned object
 */
jsorm.clone = function(obj) {
	var newObj = new obj.constructor();
	for (var prp in obj) {
	        newObj[prp] = obj[prp];
	}
	return(newObj);
}
/**
 * Take a number, return it as a string, padded to the left, if necessary, with a fixed number of zeros. For example,
 * zeropad(10,5) returns 00010, while zeropad(10,1) returns 10.
 * 
 * @param {int} n Number to zeropad
 * @param {int} l Length to pad it out to
 * @returns {String} String with number padded
 */
jsorm.zeropad = function( n,l ){ 
	var ret = n+'';
	// do we need to pad or truncate?
	var d = l - ret.length;
	if (d>0) {
		for (var i=0;i<d; i++) {
			ret = '0'+ret;
		}
	}
	return(ret);
}


/**
 * Retrieve a file from the server via Ajax. Note that Ajax means asynchronous.
 * 
 * @param {String} fileName File name to retrieve, relative to the existing URL.
 * @param {Function} callback Function to call when retrieve is complete, success or failure. 
 * 	Signature is callback(fileName,xmlHttpObject,success,options)
 * @param {Object} options Options to pass as is to callback in options parameter
 */
jsorm.i18n_ajax = function(fileName,callback,options) {
	// get the file via Ajax, and callback that it is ready
	var xmlHttp;
	try {
	    // Firefox, Opera 8.0+, Safari
	    xmlHttp=new XMLHttpRequest();
    } catch (e) {
	    // Internet Explorer
	    try {
	      xmlHttp=new ActiveXObject("Msxml2.XMLHTTP");
	    } catch (e) {
	      try {
	        xmlHttp=new ActiveXObject("Microsoft.XMLHTTP");
	      } catch (e) {
			setTimeout(function(){callback(fileName,xmlHttp,false,options,"Your browser does not support AJAX!");},1);
	      }
	    }
	 }
	// we need this to handle scope within onreadystatechange
	 var h = xmlHttp;
	 xmlHttp.onreadystatechange=function() {
		var url = fileName;
		var a = h;
     	if(h.readyState==4) {
			// HTTP will give a status of 200, while file if it returns here is always 200
			if (h.status == 200 || (document.location.protocol == 'file:')) {
				callback(fileName,h,true,options);
			} else {
				callback(fileName,h,false,options);
			}
	    }
	 }
	 try {
		 xmlHttp.open("GET",fileName,true);
		 xmlHttp.send(null);
	} catch (e) {
		if (!options)
			options = {};
		options.e = e;
		setTimeout(function(){callback(fileName,xmlHttp,false,options);},1);
	}
}
// to hold the array of requests
jsorm.i18n_ajax.reqs = [];

/**
 * You should <b>never</b> use the constructor directly. Rather, use ResourceBundle.getBundle()
 * 
 * @class ResourceBundle represents a local representation of a locale-aware bundle of resources. Resource bundles are key/value pairs that
 * are aware of their locale. For example, if the key is SUNDAY, the value in locale English US (en_US) would be "Sunday" while in
 * French France (fr_FR) it would be "Dimanche". ResourceBundle allows you to abstract the details of locale changes from your code,
 * thus making for significantly easier to maintain and more portable code.
 * <p/>
 * To use ResourceBundle, you would do the following:
 * <ol>
 * <li>Prepare a resource bundle with the appropriate key-value pairs. See below.</li>
 * <li>Load the bundle using ResourceBundle.getBundle(config). The config includes the locale you wish to use.</li>
 * <li>In your code, instead of using actual strings, use rb.get(key)</li>
 * </ol>
 * 
 * <p/>
 * A resource bundle file is just a Java-compatible properties file. As such, the file can have any arbitrary name you desire, in 
 * any accessible directory, with the suffix '.properties'. Example names could be myfile.properties or yourfile.properties. 
 * Additionally, following the filename but before the suffix, you can have a language or a language plus a country. For example,
 * you can have English, English US, or English UK. The language is always two letters, lowercase, according to the official IANA 
 * subtag language registry http://www.iana.org/assignments/language-subtag-registry. The country is always two letters, uppercase,
 * according to the official ISO 3166 country codes 
 * http://www.iso.org/iso/country_codes/iso_3166_code_lists/english_country_names_and_code_elements.htm. The filename and optional
 * language are separated by an underscore '_'; the language and optional country are separate by an underscore '_'.
 * There are thus three forms for a localized properties file. The following table describes the three forms.
 * <table>
 * <tr><th>Sample Filename</th><th>Format</th></tr>
 * <tr><td>myfile_en_US.properties</td><td>myfile in US English. Used when asked for a US English bundle.</td></tr>
 * <tr><td>myfile_en.properties</td><td>myfile in English. Used when asked for an English bundle, or for a country-specific English 
 * 	bundle, e.g. UK English, but that bundle is not available.</td></tr>
 * <tr><td>myfile.properties</td><td>myfile as a default. Used when asked for a bundle that is not available.</td></tr>
 * </table>
 * <br/>
 * The search order when asked for a specific bundle and locale is as follows. The example assumes we have asked for lib/myfile bundle in
 * locale 'fr_FR' with a defaultLocale of 'en_US'.
 * <table>
 * <tr><th></th><th>Rule</th><th>Example</th></tr>
 * <tr><td>1</td><td>Search for file and locale</td><td>lib/myfile_fr_FR.properties</td></tr>
 * <tr><td>2</td><td>Search for file and language part of locale</td><td>lib/myfile_fr.properties</td></tr>
 * <tr><td>3</td><td>Search for file and default locale</td><td>lib/myfile_en_US.properties</td></tr>
 * <tr><td>4</td><td>Search for file and language part of default locale</td><td>lib/myfile_en.properties</td></tr>
 * <tr><td>5</td><td>Search for just the file as a default</td><td>lib/myfile.properties</td></tr>
 * <tr><td>6</td><td>Fail</td><td>Fail</td></tr>
 * </table>
 * <br/>
 * The system ensures that each entry is unique and no repeats occur. Thus, if the locale requests is only a language, it will not
 * search for that, then the language again, then the default, etc. Similarly, if the defaultLocale is the same as the requested locale,
 * there will be no repetition.
 * <p/>
 * The properties file is a straight text file. Each line represents another property, and is indicated by a key and a value. The key is 
 * the string before the first equals (=) sign, trimmed of whitespace. The value is everything after the equals (=) sign. Thus, a 
 * properties file for fr_FR might contain the following:
 * <pre>
 * Hello = Bonjour
 * Goodbye = Au revoir
 * </pre>
 * <br/>
 * If this properties file is loaded, rb.get('Hello') would return the string 'Bonjour' (without the quotes), while rb.get('Goodbye') 
 * would return the string 'Au revoir'.
 * 
 * @param {Object} config Configuration information
 * @config {String} name Name of the bundle, including path
 * @config {String} [locale] Locale to use, following the conventions in the introduction
 * @config {Object} data Data passed to allow determining timezone information
 * @constructor
 */
jsorm.ResourceBundle = function(config){
	// keep the name and locale for reference
	this.name = config.name;
	/**
	 * Which locale was actually found. This can differ from the requested locale due to the search path.
	 */
	this.locale = config.locale;
	/**
	 * Which locale was requested.
	 */
	this.givenLocale = config.givenLocale;
	var data = config.data;
	
	/*
	 * process the data - it is in the format where each line 
	 * key = value
	 * 
	 * # this is a comment
	 *  our regex is as follows:
	 *  match any line that has any blank space followed by no # symbol and some real chars
	 *  followed by one = and then the results
	 */
	var entry = /^\s*([^#\s][^=]*)\b\s*=\s*(.*)?\s*$/;
	var lines = data.split('\n');
	var entries = {};
	for (var i=0;i<lines.length;i++) {
		// check for comment and key = value match
		var m = entry.exec(lines[i]);
		if (m) {
			entries[m[1]] = m[2];
		}
	}
	/**
	 * get the value of a key
	 * 
	 * @param {String} key The key to look up
	 * @returns {String} The localized value
	 */
	this.get = function(key) {
		var a = entries[key];
		return(a);
	}
};
jsorm.apply(jsorm.ResourceBundle,/** @scope jsorm.ResourceBundle */{
	bundles : {},
	/**
	 * Default locale to use. Change this to override.
	 * @memberOf jsorm.ResourceBundle
	 */
	defaultLocale : 'en_US',
	
	/**
	 * Default path to use. Change this to change the default, or override on each {@link #getBundle}. 
	 * To be relative to the current page, use './'
	 * @memberOf jsorm.ResourceBundle
	 */
	path : '/lib/locale/',
	
	/**
	 * Get a bundle. This will return an object which is a loaded bundle.
	 * This function is intelligent enough to know when the bundle is already loaded and not go back to the server.
	 * See the overview of this section to understand what a resource bundle properties file should look like, what 
	 * the search path is and other salient elements.
	 * The name is appended to the path to create the search path.
	 * 
	 * @param {object} config The configuration information
	 * @config {String} name Name of the file to load, including path, e.g. lib/foo
	 * @config {String} [locale] Name of the locale, normally a language and country as described in the overview, e.g. en_US
	 * @config {String} [path] Path to use to search for bundle files, or undefined/null to use the default path
	 * @config {function} callback Function to call when bundle is loaded or has failed. The signature is callback(success,bundle,options)
	 * @config {Object} options Options to pass through to the callback
	 * @memberOf jsorm.ResourceBundle
	 */
	getBundle : function(config) {
		var name = config.name;
		var locale = config.locale;
		var callback = config.callback;
		var ptopts = config.options;
		// list of all those that would satisfy this list
		var list = jsorm.ResourceBundle.baseLocale(locale);
		
		// the paths we use
		var path = config.path ? config.path : jsorm.ResourceBundle.path;
		var bundle = null;
		
		// keep track of all the files we need to get
		var checker = [];
		for (var i=0;i<list.length;i++) {
			var url = path+name+(list[i]?'_'+list[i]:'') + '.properties';
			checker[i] = {complete: false, success: false, url: url,locale:list[i]};
		}
		// get each of the files
		var count = {complete: 0, required: list.length};
		for (var i=0;i<list.length;i++) {
			var url = checker[i].url;
			var rbopts = {url: url, checker: checker, index: i, callback: callback, count: count,
							name: name, givenLocale: locale, locale:list[i], options: ptopts};
			var rbCallback = function(success,options) {
				return(function(){
					// so we have finished with this fileName
					var ch = options.checker[options.index];
					ch.complete = true;
					options.count.complete++;
					// did we have success?
					ch.success = success;
					// did we finish them all? if so, we can find the actual bundle and do the callback
					if (options.count.complete == options.count.required && options.callback && typeof(options.callback) == 'function') {
						var allSuccess = false, bundle = null;
						for (var j=0;j<options.checker.length;j++) {
							if (options.checker[j].success) {
								allSuccess = true;
								bundle = jsorm.ResourceBundle.bundles[options.checker[j].url];
								break;
							}
						}
						// get the actual bundle we would use and callback
						options.callback(allSuccess,bundle,options.options);
					}				
				})
			}
			
			// did we already get this one?
			if (jsorm.ResourceBundle.bundles[url] == false) {
				var rbWrap = rbCallback(false,rbopts);
				setTimeout(rbWrap,1);
			} else if (jsorm.ResourceBundle.bundles[url]) {
				var rbWrap = rbCallback(true,rbopts);
				setTimeout(rbWrap,1);
			} else {
				// make the ajax call with the async callback
				rbopts.rbc = rbCallback;
				jsorm.i18n_ajax(url,function(url,xmlHttp,success,options){
					// if we were successful, read in the bundle - read every bundle we get, even if 
					//   we have a specific match
					jsorm.ResourceBundle.bundles[url] = success ? new jsorm.ResourceBundle({name:options.name,locale:options.locale,data:xmlHttp.responseText}) : false;	
					options.rbc(success,options)();
				},rbopts);
			}
		}
		// do we need to do the callback?
	},

	/**
	 * Construct the list of filenames we should look for. This implements the rule described in the overview of this class wherein
	 * first we take the name+locale, then name+language, then name+defaultLocale, then name+countryOfDefaultLocale, then name. It
	 * uses {@link #baseLocale} to generate the list of extensions.
	 * 
	 * @param {String} name The name of the bundle, including full path
	 * @param {String} locale The locale to use
	 * @returns {Array} Array with list of full names, excluding the .properties suffix
	 * @memberOf jsorm.ResourceBundle
	 * @private
	 */
	baseName : function(name,locale) {
		var list = [];
		var listL = jsorm.ResourceBundle.baseLocale(locale);
		for (var i=0; i<listL.length; i++) {
			list.push(name+(listL[i]?'_'+listL[i]:''));
		}
		// now we take them all and construct the full list with bundle
		return(list);	
	},
	/**
	 * Construct the list of locales we should look for. This implements the rule described in the overview of this class wherein
	 * first we take the locale, then language, then defaultLocale, then countryOfDefaultLocale, then blank. This is a list of 
	 * characters to append to the filename. It properly handles underscores (_), as well as ensuring the same entry does not appear
	 * twice.
	 * 
	 * @param {String} locale The locale to use
	 * @returns {Array} Array with list of locales in order, with each entry guaranteed to be unique
	 * @memberOf jsorm.ResourceBundle
	 * @private
	 */
	baseLocale : function(locale) {
		// the locale is split by a _ e.g, en_US en_GB fr_CA or just en, fr, de
		// list of the ones we are going to return
		var list = [];
		// to keep track of ones we have done already
		var suffixes = {};
		var index;
	
		// if locale is blank, we are just using the default
		if (locale) {
			// first with the whole thing
			list.push(locale);
			suffixes[locale] = 1;
			// next just language locale
			index = locale.indexOf('_');
			if (index > -1) {
				var lname = locale.substring(0,index);
				list.push(lname);
				suffixes[lname] = 1;
			}
		}
		// now use the default locale, if different from already used
		var defL = jsorm.ResourceBundle.defaultLocale;
	
		// first the entire locale
		if (!suffixes[defL]) {
			list.push(defL);
		}
		// now just the language part
		index = defL.indexOf('_');
		if (index > -1) {
			var lname = defL.substring(0,index);
			if (!suffixes[lname]) {
				list.push(lname);
			}
		}
	
		// finally the basic name
		list.push('');
	
		// now we take them all and construct the full list with bundle
		return(list);		
	}
});

/**
 * Create a new currency object. 
 * 
 * @class Currency formatting library. Each object represents a currency. It is responsible for converting an absolute amount, e.g. 67.85, 
 * into a formatted string that is appropriate for the given currency. One first creates a currency object by passing it the 
 * international standard three-letter abbreviation. One can then use that object to format any amount of funds.
 * <p/>
 * Example usage is as follows:
 * <ol>
 * <li>Create a new currency object as <code>var cur = new Currency('USD');</code></li>
 * <li>Format an amount as <code>var f = cur.format(22.57); // returns a string with value $22.57</code></li>
 * <li>Get information about the currency abbreviation (e.g. USD), name (e.g. Dollars), and country (e.g. United States of America)
 *   using <code>cur.getAbbreviation(); cur.getName(); cur.getCountry();</code></li>
 * </ol>
 * 
 * @param {string} abbr The three-letter ISO standard abbreviation for a currency. If the given standard is not found, the defaultCurrency
 * 					will be used.
 * @constructor
 */
jsorm.Currency = function(abbr){
	this.abbr = jsorm.Currency.currencies[abbr] ? abbr : jsorm.Currency.defaultCurrency;
	this.data = jsorm.Currency.currencies[this.abbr];
}

jsorm.apply(jsorm.Currency.prototype,/** @scope jsorm.Currency.prototype */{
	/**
	 * Format a value of funds in a the given currency. This will correctly handle place separators, currency symbols, decimal separators, 
	 * and location of symbol (before vs. after the amount).
	 * 
	 * @param {float} value The amount to format
	 * @return {String} The formatted value
	 */
	format : function(value) {
		var str = '';
		value = value ? value : 0;
		// now we determine how to render it from the currency
		str = jsorm.Currency.doMoney(value,this.data.dec,this.data.symbol,this.data.group,this.data.after);
		// return the string
		return(str);
	},
	/**
	 * Get the abbreviation for the currency. This is normally the amount passed to the constructor. However, if that amount was not
	 * found, the default currency was used and will be returned.
	 * 
	 * @return {String} Three-letter ISO standard for the currency represented by this object
	 */
	getAbbreviation : function() {
		return(this.abbr);
	},
	/**
	 * Get the name of this currency. 
	 * 
	 * @return {String} Name of the currency
	 */
	getName : function() {
		return(this.data.currency);
	},
	/**
	 * Get the name of the country where this currency is used.
	 * 
	 * @return {String} The country name
	 */
	getCountry : function() {
		return(this.data.country);
	}
});
jsorm.apply(jsorm.Currency, /** @scope jsorm.Currency */{
	/**
	 * Currency configurators. They are shown as follows;
	 * dec - decimal separator e.g. between dollars and cents
	 * symbol - the currency symbol, e.g. $
	 * group - grouping (thousands/millions) separator e.g. ,
	 * after - whether to put the currency symbol after the value or before e.g. $2.00 vs 4.00 NIS
	 * currency
	 * country
	 * 
	 * @private
	 */
	currencies : {
		ALL : {dec : '.', symbol : '\u004c\u0065\u006b', group : ',', after : false, currency : 'Leke', country : 'Albania'},
		USD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'America (United States of America)'},
		AFN : {dec : '.', symbol : '\u060b', group : ',', after : false, currency : 'Afghanis', country : 'Afghanistan'},
		ARS : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Pesos', country : 'Argentina'},
		AWG : {dec : '.', symbol : '\u0192', group : ',', after : false, currency : 'Guilders (also called Florins)', country : 'Aruba'},
		AUD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Australia'},
		AZN : {dec : '.', symbol : '\u043c\u0430\u043d', group : ',', after : false, currency : 'New Manats', country : 'Azerbaijan'},
		BSD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Bahamas'},
		BBD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Barbados'},
		BYR : {dec : '.', symbol : '\u0070\u002e', group : ',', after : false, currency : 'Rubles', country : 'Belarus'},
		BEF : {dec : '.', symbol : '\u20a3', group : ',', after : false, currency : 'Francs (obsolete)', country : 'Belgium'},
		BZD : {dec : '.', symbol : '\u0042\u005a\u0024', group : ',', after : false, currency : 'Dollars', country : 'Belize'},
		BMD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Bermuda'},
		BOB : {dec : '.', symbol : '\u0024\u0062', group : ',', after : false, currency : 'Bolivianos', country : 'Bolivia'},
		BAM : {dec : '.', symbol : '\u004b\u004d', group : ',', after : false, currency : 'Convertible Marka', country : 'Bosnia and Herzegovina'},
		BWP : {dec : '.', symbol : '\u0050', group : ',', after : false, currency : 'Pulas', country : 'Botswana'},
		BGN : {dec : '.', symbol : '\u043b\u0432', group : ',', after : false, currency : 'Leva', country : 'Bulgaria'},
		BRL : {dec : '.', symbol : '\u0052\u0024', group : ',', after : false, currency : 'Reais', country : 'Brazil'},
		BRC : {dec : '.', symbol : '\u20a2', group : ',', after : false, currency : 'Cruzeiros (obsolete)', country : 'Brazil'},
		GBP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Britain (United Kingdom)'},
		BND : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Brunei Darussalam'},
		KHR : {dec : '.', symbol : '\u17db', group : ',', after : false, currency : 'Riels', country : 'Cambodia'},
		CAD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Canada'},
		KYD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Cayman Islands'},
		CLP : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Pesos', country : 'Chile'},
		CNY : {dec : '.', symbol : '\u5143', group : ',', after : false, currency : 'Yuan Renminbi', country : 'China'},
		COP : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Pesos', country : 'Colombia'},
		CRC : {dec : '.', symbol : '\u20a1', group : ',', after : false, currency : 'Colón', country : 'Costa Rica'},
		HRK : {dec : '.', symbol : '\u006b\u006e', group : ',', after : false, currency : 'Kuna', country : 'Croatia'},
		CUP : {dec : '.', symbol : '\u20b1', group : ',', after : false, currency : 'Pesos', country : 'Cuba'},
		CYP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Cyprus'},
		CZK : {dec : '.', symbol : '\u004b\u010d', group : ',', after : false, currency : 'Koruny', country : 'Czech Republic'},
		DKK : {dec : '.', symbol : '\u006b\u0072', group : ',', after : false, currency : 'Kroner', country : 'Denmark'},
		DOP : {dec : '.', symbol : '\u0052\u0044\u0024', group : ',', after : false, currency : 'Pesos', country : 'Dominican Republic'},
		XCD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'East Caribbean'},
		EGP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Egypt'},
		SVC : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Colones', country : 'El Salvador'},
		GBP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'England (United Kingdom)'},
		EEK : {dec : '.', symbol : '\u006b\u0072', group : ',', after : false, currency : 'Krooni', country : 'Estonia'},
		EUR : {dec : '.', symbol : '\u20ac', group : ',', after : false, currency : '', country : 'Euro'},
		XEU : {dec : '.', symbol : '\u20a0', group : ',', after : false, currency : '', country : 'European Currency Unit (obsolete)'},
		FKP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Falkland Islands'},
		FJD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Fiji'},
		FRF : {dec : '.', symbol : '\u20a3', group : ',', after : false, currency : 'Francs (obsolete)', country : 'France'},
		GHC : {dec : '.', symbol : '\u00a2', group : ',', after : false, currency : 'Cedis', country : 'Ghana'},
		GIP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Gibraltar'},
		GRD : {dec : '.', symbol : '\u20af', group : ',', after : false, currency : 'Drachmae (obsolete)', country : 'Greece'},
		GTQ : {dec : '.', symbol : '\u0051', group : ',', after : false, currency : 'Quetzales', country : 'Guatemala'},
		GGP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Guernsey'},
		GYD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Guyana'},
		NLG : {dec : '.', symbol : '\u0192', group : ',', after : false, currency : 'Guilders (also called Florins) (obsolete)', country : 'Holland (Netherlands)'},
		HNL : {dec : '.', symbol : '\u004c', group : ',', after : false, currency : 'Lempiras', country : 'Honduras'},
		HKD : {dec : '.', symbol : '\u0048\u004b\u0024', group : ',', after : false, currency : 'Dollars (General written use)', country : 'Hong Kong'},
		HKD : {dec : '.', symbol : '\u5713', group : ',', after : false, currency : 'Dollars (BOC notes)', country : 'Hong Kong'},
		HKD : {dec : '.', symbol : '\u5713', group : ',', after : false, currency : 'Dollars (SCB notes)', country : 'Hong Kong'},
		HKD : {dec : '.', symbol : '\u5143', group : ',', after : false, currency : 'Dollars (HSBC notes)', country : 'Hong Kong'},
		HUF : {dec : '.', symbol : '\u0046\u0074', group : ',', after : false, currency : 'Forint', country : 'Hungary'},
		ISK : {dec : '.', symbol : '\u006b\u0072', group : ',', after : false, currency : 'Kronur', country : 'Iceland'},
		INR : {dec : '.', symbol : '\u20a8', group : ',', after : false, currency : 'Rupees (Rs or Rs. are commonly used instead of the symbol)', country : 'India'},
		IDR : {dec : '.', symbol : '\u0052\u0070', group : ',', after : false, currency : 'Rupiahs', country : 'Indonesia'},
		IRR : {dec : '.', symbol : '\ufdfc', group : ',', after : false, currency : 'Rials', country : 'Iran'},
		IEP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Punt (obsolete)', country : 'Ireland'},
		IMP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Isle of Man'},
		ILS : {dec : '.', symbol : '\u20aa', group : ',', after : false, currency : 'New Shekels', country : 'Israel'},
		ITL : {dec : '.', symbol : '\u20a4', group : ',', after : false, currency : 'Lire (obsolete)', country : 'Italy'},
		JMD : {dec : '.', symbol : '\u004a\u0024', group : ',', after : false, currency : 'Dollars', country : 'Jamaica'},
		JPY : {dec : '.', symbol : '\u00a5', group : ',', after : false, currency : 'Yen', country : 'Japan'},
		JEP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Jersey'},
		KZT : {dec : '.', symbol : '\u043b\u0432', group : ',', after : false, currency : 'Tenge', country : 'Kazakhstan'},
		KPW : {dec : '.', symbol : '\u20a9', group : ',', after : false, currency : 'Won', country : 'Korea (North)'},
		KRW : {dec : '.', symbol : '\u20a9', group : ',', after : false, currency : 'Won', country : 'Korea (South)'},
		KGS : {dec : '.', symbol : '\u043b\u0432', group : ',', after : false, currency : 'Soms', country : 'Kyrgyzstan'},
		LAK : {dec : '.', symbol : '\u20ad', group : ',', after : false, currency : 'Kips', country : 'Laos'},
		LVL : {dec : '.', symbol : '\u004c\u0073', group : ',', after : false, currency : 'Lati', country : 'Latvia'},
		LBP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Lebanon'},
		LRD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Liberia'},
		CHF : {dec : '.', symbol : '\u0043\u0048\u0046', group : ',', after : false, currency : 'Switzerland Francs', country : 'Liechtenstein'},
		LTL : {dec : '.', symbol : '\u004c\u0074', group : ',', after : false, currency : 'Litai', country : 'Lithuania'},
		LUF : {dec : '.', symbol : '\u20a3', group : ',', after : false, currency : 'Francs (obsolete)', country : 'Luxembourg'},
		MKD : {dec : '.', symbol : '\u0434\u0435\u043d', group : ',', after : false, currency : 'Denars', country : 'Macedonia'},
		MYR : {dec : '.', symbol : '\u0052\u004d', group : ',', after : false, currency : 'Ringgits', country : 'Malaysia'},
		MTL : {dec : '.', symbol : '\u004c\u006d', group : ',', after : false, currency : 'Liri', country : 'Malta'},
		MUR : {dec : '.', symbol : '\u20a8', group : ',', after : false, currency : 'Rupees', country : 'Mauritius'},
		MXN : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Pesos', country : 'Mexico'},
		MNT : {dec : '.', symbol : '\u20ae', group : ',', after : false, currency : 'Tugriks', country : 'Mongolia'},
		MZN : {dec : '.', symbol : '\u004d\u0054', group : ',', after : false, currency : 'Meticais', country : 'Mozambique'},
		NAD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Namibia'},
		NPR : {dec : '.', symbol : '\u20a8', group : ',', after : false, currency : 'Rupees', country : 'Nepal'},
		ANG : {dec : '.', symbol : '\u0192', group : ',', after : false, currency : 'Guilders (also called Florins)', country : 'Netherlands Antilles'},
		NLG : {dec : '.', symbol : '\u0192', group : ',', after : false, currency : 'Guilders (obsolete)', country : 'Netherlands'},
		NZD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'New Zealand'},
		NIO : {dec : '.', symbol : '\u0043\u0024', group : ',', after : false, currency : 'Cordobas', country : 'Nicaragua'},
		NGN : {dec : '.', symbol : '\u20a6', group : ',', after : false, currency : 'Nairas', country : 'Nigeria'},
		KPW : {dec : '.', symbol : '\u20a9', group : ',', after : false, currency : 'Won', country : 'North Korea'},
		NOK : {dec : '.', symbol : '\u006b\u0072', group : ',', after : false, currency : 'Krone', country : 'Norway'},
		OMR : {dec : '.', symbol : '\ufdfc', group : ',', after : false, currency : 'Rials', country : 'Oman'},
		PKR : {dec : '.', symbol : '\u20a8', group : ',', after : false, currency : 'Rupees', country : 'Pakistan'},
		PAB : {dec : '.', symbol : '\u0042\u002f\u002e', group : ',', after : false, currency : 'Balboa', country : 'Panama'},
		PYG : {dec : '.', symbol : '\u0047\u0073', group : ',', after : false, currency : 'Guarani', country : 'Paraguay'},
		PEN : {dec : '.', symbol : '\u0053\u002f\u002e', group : ',', after : false, currency : 'Nuevos Soles', country : 'Peru'},
		PHP : {dec : '.', symbol : '\u0050\u0068\u0070', group : ',', after : false, currency : 'Pesos', country : 'Philippines'},
		PLN : {dec : '.', symbol : '\u007a\u0142', group : ',', after : false, currency : 'Zlotych', country : 'Poland'},
		QAR : {dec : '.', symbol : '\ufdfc', group : ',', after : false, currency : 'Rials', country : 'Qatar'},
		RON : {dec : '.', symbol : '\u006c\u0065\u0069', group : ',', after : false, currency : 'New Lei', country : 'Romania'},
		RUB : {dec : '.', symbol : '\u0440\u0443\u0431', group : ',', after : false, currency : 'Rubles', country : 'Russia'},
		SHP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Saint Helena'},
		SAR : {dec : '.', symbol : '\ufdfc', group : ',', after : false, currency : 'Riyals', country : 'Saudi Arabia'},
		RSD : {dec : '.', symbol : '\u0414\u0438\u043d\u002e', group : ',', after : false, currency : 'Dinars', country : 'Serbia'},
		SCR : {dec : '.', symbol : '\u20a8', group : ',', after : false, currency : 'Rupees', country : 'Seychelles'},
		SGD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Singapore'},
		SKK : {dec : '.', symbol : '\u0053\u0049\u0054', group : ',', after : false, currency : 'Koruny', country : 'Slovakia'},
		EUR : {dec : '.', symbol : '\u20ac', group : ',', after : false, currency : 'Euro', country : 'Slovenia'},
		SBD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Solomon Islands'},
		SOS : {dec : '.', symbol : '\u0053', group : ',', after : false, currency : 'Shillings', country : 'Somalia'},
		ZAR : {dec : '.', symbol : '\u0052', group : ',', after : false, currency : 'Rand', country : 'South Africa'},
		KRW : {dec : '.', symbol : '\u20a9', group : ',', after : false, currency : 'Won', country : 'South Korea'},
		ESP : {dec : '.', symbol : '\u20a7', group : ',', after : false, currency : 'Pesetas (obsolete)', country : 'Spain'},
		LKR : {dec : '.', symbol : '\u20a8', group : ',', after : false, currency : 'Rupees', country : 'Sri Lanka'},
		SEK : {dec : '.', symbol : '\u006b\u0072', group : ',', after : false, currency : 'Kronor', country : 'Sweden'},
		CHF : {dec : '.', symbol : '\u0043\u0048\u0046', group : ',', after : false, currency : 'Francs', country : 'Switzerland'},
		SRD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Suriname'},
		SYP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'Syria'},
		TWD : {dec : '.', symbol : '\u004e\u0054\u0024', group : ',', after : false, currency : 'New Dollars', country : 'Taiwan'},
		THB : {dec : '.', symbol : '\u0e3f', group : ',', after : false, currency : 'Baht', country : 'Thailand'},
		TTD : {dec : '.', symbol : '\u0054\u0054\u0024', group : ',', after : false, currency : 'Dollars', country : 'Trinidad and Tobago'},
		TRY : {dec : '.', symbol : '\u0059\u0054\u004c', group : ',', after : false, currency : 'New Lira', country : 'Turkey'},
		TRL : {dec : '.', symbol : '\u20a4', group : ',', after : false, currency : 'Liras', country : 'Turkey'},
		TVD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'Tuvalu'},
		UAH : {dec : '.', symbol : '\u20b4', group : ',', after : false, currency : 'Hryvnia', country : 'Ukraine'},
		GBP : {dec : '.', symbol : '\u00a3', group : ',', after : false, currency : 'Pounds', country : 'United Kingdom'},
		USD : {dec : '.', symbol : '\u0024', group : ',', after : false, currency : 'Dollars', country : 'United States of America'},
		UYU : {dec : '.', symbol : '\u0024\u0055', group : ',', after : false, currency : 'Pesos', country : 'Uruguay'},
		UZS : {dec : '.', symbol : '\u043b\u0432', group : ',', after : false, currency : 'Sums', country : 'Uzbekistan'},
		VAL : {dec : '.', symbol : '\u20a4', group : ',', after : false, currency : 'Lire (obsolete)', country : 'Vatican City'},
		VEB : {dec : '.', symbol : '\u0042\u0073', group : ',', after : false, currency : 'Bolivares', country : 'Venezuela'},
		VND : {dec : '.', symbol : '\u20ab', group : ',', after : false, currency : 'Dong', country : 'Vietnam'},
		YER : {dec : '.', symbol : '\ufdfc', group : ',', after : false, currency : 'Rials', country : 'Yemen'},
		ZWD : {dec : '.', symbol : '\u005a\u0024', group : ',', after : false, currency : 'Zimbabwe Dollars', country : 'Zimbabwe'}
	},

	/**
	 * Default currency to use. This may be changed to use another currency. However, if you change it to an unknown one, 
	 * the behaviour is unpredictable and strange errors may occur. The default currency <b>must</b> exist in the table.
	 * 
	 * @memberOf jsorm.Currency
	 */
	defaultCurrency: 'USD',

	/**
     * Format a number as a currency.
     * 
     * @param {float} value The numeric value to format
     * @param {String} sep The separator between whole currency and partial (e.g. dollars and cents)
     * @param {String} symbol The currency symbol
     * @param {String} comma The separator between thousands, millions, etc.
     * @param {boolean} after If the currency symbol should go after the currency, default is before
     * @return {String} The formatted currency string
	 * @memberOf jsorm.Currency
     * @private
     */
    doMoney : function(v,dec,symbol,group,after){
        v = (Math.round((v-0)*100))/100;
        v = (v == Math.floor(v)) ? v + dec+"00" : ((v*10 == Math.floor(v*10)) ? v + "0" : v);
        v = String(v);
        var ps = v.split('.');
        var whole = ps[0];
        var sub = ps[1] ? dec + ps[1] : dec+'00';
		if (!group) {
			group = '';
		}
        var r = /(\d+)(\d{3})/;
        while (r.test(whole)) {
            whole = whole.replace(r, '$1' + group + '$2');
        }
        v = whole + sub;
		// we have now processed the comma and the separator
	
		// now we need to do the symbol and before vs after
		if (!symbol) {
			symbol = '';
		}
		if (after) {
			v = v + symbol;
		} else {
	        if(v.charAt(0) == '-'){
	            v = '-' +symbol+ v.substr(1);
	        } else {
				v = symbol + v;
			}			
		}
		return(v);
    },
	/**
	 * Get the list of acceptable currency symbols in an array
	 * 
	 * @memberOf jsorm.Currency
	 * @return {@String} List of acceptable currency symbols
	 */
	getCurrencies : function() {
		var list = [];
		for (var i in jsorm.Currency.currencies) {
			list.push(i);
		}
		return(list);
	}
});

/**
 * Construct a new TimeZone. In general, you should <b>never</b> construct a new TimeZone directly, but rather use TimeZone.getZone()
 * 
 * @class Represents a timezone. A timezone is responsible for reporting what the zoneoffset is between UTC and 
 * and the zone for a particular moment in time, i.e. accounting for standard time, DST and all other changes, when it can.
 * There are time when it cannot, for example when it is only created with an offset from UTC/GMT. 
 * <p/>
 * You use a TimeZone as follows:
 * <ol>
 * <li>Create a TimeZone by TimeZone.getZone()</li>
 * <li>Get the information (offset, isDst, abbreviation) for a particular moment in time using either 
 * getZoneInfo() or getZoneInfoUTC()</li>
 * <li>Get the name of the zone using getName()</li>
 * </ol>
 * <p/>
 * TimeZone retrieves its data from one of three places, depending on the zone name passed
 * <ul>
 * <li>Blank: it will use the current local timezone. It will have no knowledge of DST or other historical oddities</li>
 * <li>GMT[-|+]nnnn: it will assume that the offset from GMT is as given. It will have no knowledge of DST or other historical oddities</li>
 * <li>Name: if the name is one recognized as a standard elsie zoneinfo database, and the specialized i18n format of the database
 *     is available in an appropriate directory, then the data will be used. In this case, the full zone will be known, and
 *     DST and other historical oddities will be available.</li>
 * </ul>
 * <p/>
 * The full database from Elsie, specially compiled, is included as part of this distribution. It may need to be updated regularly. 
 * For more information about the Elsie database, see http://www.twinsun.com/tz/tz-link.htm
 * 
 * @param {String} name Name of the timezone for reference
 * @param {Object} zoneData Object with all our zone data, or null if we should just use the local machine default
 * @constructor
 */
jsorm.TimeZone = function(name,zoneData) {
	this.zoneData = zoneData;
	this.name = name;	
};

jsorm.apply(jsorm.TimeZone.prototype, /** @scope jsorm.TimeZone.prototype */{
	/**
	 * Find the last transaction in the data that applies before a given moment in time.
	 * 
	 * @param {long} secs Seconds since system Epoch (midnight at start of 1 January 1970 UTC) we are looking for
	 * @param {Object} zoneData Zone data as provided by the specially compiled zoneinfo files
	 * @param {boolean} zoned If the seconds passed were passed as seconds since system Epoch or local seconds
	 * @return {Array} Array containing the most recent entry. The elements of the array are [offset,isDst,abbreviation]
	 * @private
	 */
	findTx : function(secs,zoneData,zoned) {
		// this is easy - look through until the last transition - however, we may need to take into account the offsets if zoned==true
		var tx = zoneData.transitions;
		var ty = zoneData.types;
		var len = tx.length-1;
		var i, offset;
		var zi = ty[0];
		for (i=-1; i<len; i++) {
			offset = zoned ? zi[0] : 0;
			if (tx[i+1][0] > secs-offset) {
				break;
			} else {
				zi = ty[tx[i+1][1]];
			}
		}
		return(zi);
	},

	/**
	 * Get the name of the zone. This is either the standardized GMT[-|+]nnnn name or the actual name of the zoneinfo file.
	 * 
	 * @return {String} The name
	 */
	getName : function() {
		return(this.name);
	},
	/**
	 * Get the offset, whether or not in DST, and abbreviation to use for a particular moment in time. The moment
	 * in time is defined by passing the <u>local</u> date and time in the zone. If you wish to pass UTC time, use
	 * getZoneInfoUTC(). The following two examples are for New York City. The first shows standard time, the second
	 * daylight savings time.
	 * <br/>
	 * <table>
	 * <tr><th>Inputs</th><th>Output</th><th>Description</th></tr>
	 * <tr><td>getZoneInfo(1995,5,2,21,10,10)</td><td>{offset: -14400, isDst: 1, abbr: 'EDT'}</td><td>On 3 June 1995 at 21:10:10, 
	 * 	the offset was 4 hours behind UTC, it was in DST, and the abbreviation is EDT</td></tr>
	 * <tr><td>getZoneInfo(1995,0,2,21,10,10)</td><td>{offset: -18000, isDst: 0, abbr: 'EST'}</td><td>On 3 January 1995 at 21:10:10, 
	 * 	the offset was 5 hours behind UTC, it was not in DST, and the abbreviation is EST</td></tr>
	 * </table>
	 * 
	 * @param {int} year The year whose offset we are looking for
	 * @param {int} month The month in the year whose offset we are looking for, from 0-11
	 * @param {int} day The day in the month whose offset we are looking for, from 0-max
	 * @param {int} hour The hour in the day whose offset we are looking for, from 0-23
	 * @param {int} minute The minute in the hour whose offset we are looking for, from 0-59
	 * @param {int} second The second in the minute whose offset we are looking for, from 0-59
	 * @return {Object} Object with three elements: {offset: the_offset, isDst: 0|1, abbr: abbreviation}
	 */
	getZoneInfo : function(year,month,day,hour,minute,second) {
		var ret = {};
		// seconds only, not milliseconds
		var secs = Math.abs(Date.UTC(year,month,day+1,hour,minute,second)/1000);
		// do we have zone data or not?
		if (!this.zoneData) {
			var d = new Date();
			d.setTime(secs*1000);
			ret = {offset: d.getTimezoneOffset()*-1, isDst: 0, abbr: ''};			
		} else {
			// now find the most recent transition
			var zi = this.findTx(secs,this.zoneData,true);
			ret = {offset: zi[0], isDst: zi[1], abbr: zi[2]};
		}
		return(ret);
	},
	/**
	 * Get the offset, whether or not in DST, and abbreviation to use for a particular moment in time. The moment
	 * in time is defined by passing the <u>UTC</u> time in seconds since the system Epoch (midnight at the start of 1 January 1970 UTC). 
	 * If you wish to pass locale time, use getZoneInfo(). The following two examples are for New York City. The first shows 
	 * standard time, the second daylight savings time.
	 * <br/>
	 * <table>
	 * <tr><th>Inputs</th><th>Output</th><th>Description</th></tr>
	 * <tr><td>getZoneInfoUTC(802228210)</td><td>{offset: -14400, isDst: 1, abbr: 'EDT'}</td><td>On 3 June 1995 at 21:10:10, 
	 * 	the offset was 4 hours behind UTC, it was in DST, and the abbreviation is EDT</td></tr>
	 * <tr><td>getZoneInfoUTC(789185410)</td><td>{offset: -18000, isDst: 0, abbr: 'EST'}</td><td>On 3 January 1995 at 21:10:10, 
	 * 	the offset was 5 hours behind UTC, it was not in DST, and the abbreviation is EST</td></tr>
	 * </table>
	 * 
	 * @param {int} secs The seconds since the system Epoch whose offset we are looking for
	 * @return {Object} An object with three elements: {offset: the_offset, isDst: 0|1, abbr: abbreviation}
	 */
	getZoneInfoUTC : function(secs) {
		var ret = {};
		// do we have zone data or not?
		if (!this.zoneData) {
			var offset = new Date(year,month,day,hour,minute,second).getTimezoneOffset()*-1;
			var abbr = '';
			var isDst = 0;
			ret = {offset: offset, isDst: isDst, abbr: abbr};
		} else {
			// get the transaction
			var zi = this.findTx(secs,this.zoneData,false);
			ret = {offset: zi[0], isDst: zi[1], abbr: zi[2]};
		}
		return(ret);
	}
});

jsorm.apply(jsorm.TimeZone, /** @scope jsorm.TimeZone */{
	zones : {},
	/**
	 * Base path to use for searching for zoneinfo data files. Change this to change the default. It can be overridden on each 
	 * TimeZone.getZone() call.
	 * @memberOf jsorm.TimeZone
	 */
	basepath : '/lib/',
	/**
	 * Search path to use for searching for zoneinfo data files, relative to basepath. 
	 * Change this to change the default. It can be overridden on each 
	 * TimeZone.getZone() call.
	 * @memberOf jsorm.TimeZone
	 */
	path : 'i18n/zoneinfo/',
	version : null,
	
	/**
	 * Report the version of zoneinfo information we are using. This will return a single string in the format "nnnna" where "nnnn" is the 
	 * year, e.g. 2007, and "a" is a letter. The first release of the year is a, the second is b, etc.
	 * <p/>
	 * This function is intelligent enough to know when the version is loaded and not go back to the server.<br/>
	 * Configuration information is an object with the following elements:
	 * <ul>
	 * <li>basepath: override the default basepath and use a different basepath</li>
	 * <li>path: relative path to use to search for zoneinfo files</li>
	 * <li>callback: function to call when zone is loaded or has failed. The signature is callback(success,version,options)</li>
	 * <li>options: options to pass through to the callback</li>
	 * </ul>
	 * 
	 * @param {object} config The configuration information. See above.
	 * @memberOf jsorm.TimeZone
	 */
	getVersion : function(config) {
		var callback = config.callback;
		var basepath = config.basepath ? config.basepath : jsorm.TimeZone.basepath;
		var path = config.path ? config.path : jsorm.TimeZone.path;
		// do we already have the list?
		if (jsorm.TimeZone.version) {
			setTimeout(function(){callback(true,jsorm.TimeZone.version)},1);
		} else {
			jsorm.i18n_ajax(basepath+path+'+VERSION',function(url,xmlHttp,success,options) {
				if (success) {
					jsorm.TimeZone.version = xmlHttp.responseText;
				}
				options.callback(success,jsorm.TimeZone.version,options);
			},{callback: callback, options: config.options})
		}		
	},
	
	/**
	 * Create a standardized GMT name
	 * 
	 * @param {String} sign The sign to use, normally + or -
	 * @param {String} hr The hour to use
	 * @param {String} min The minute to use
	 * @return {String} Standardized GMT name in format GMT[+|-]nnnn
	 * @memberOf jsorm.TimeZone
	 * @private
	 */
	createGmtName : function(sign,hr,min) {
		var name = 'GMT'+sign+(hr.length == 2 ? hr : '0'+hr) + ':';
		switch(min.length) {
			case 0:
				name+='00';
				break;
			case 1:
				name+='0'+min;
				break;
			case 2:
				name+=min;
				break;
		}
		return(name);
	},
	
	/**
	 * Get a list of all valid zoneinfo zone names. This will return an object containing all the known timezones. The object is as follows:
	 * {zones: {zoneName: {comments: comments, country: ISO standard 2-letter country code, coordinates: coordinates}}}
	 * <p/>
	 * This function is intelligent enough to know when the list is loaded and not go back to the server.<br/>
	 * 
	 * @param {Object} config The configuration information. See above.
	 * @config {String} basepath Override the default basepath and use a different basepath
	 * @config {String} path Relative path to use to search for zoneinfo files
	 * @config {Function} callback Function to call when list is loaded or has failed. The signature is callback(success,zonelist,options)
	 * @config {Object} options Options to pass through to the callback
	 * @memberOf jsorm.TimeZone
	 */
	getZoneList : function(config) {
		var callback = config.callback;
		var basepath = config.basepath ? config.basepath : jsorm.TimeZone.basepath;
		var path = config.path ? config.path : jsorm.TimeZone.path;
		// do we already have the list?
		if (jsorm.TimeZone.zonelist) {
			setTimeout(function(){callback(true,jsorm.TimeZone.zonelist)},1);
		} else {
			jsorm.i18n_ajax(basepath+path+'zones',function(url,xmlHttp,success,options) {
				if (success) {
					jsorm.TimeZone.zonelist = eval("("+xmlHttp.responseText+")");
				}
				options.callback(success,jsorm.TimeZone.zonelist, options);
			},{callback: callback, options: config.options});
		}
	},
	
	/**
	 * Get a zone. This will return an object which is a TimeZone.
	 * This function is intelligent enough to know when the zone is already loaded and not go back to the server.
	 * 
	 * @param {Object} config The configuration information. See above.
	 * @config {String} name The name of the zone to load, or undefined/null if you want to use the local time
	 * @config {String} basepath Override the default basepath and use a different basepath</li>
	 * @config {String} path Relative path to use to search for zoneinfo files</li>
	 * @config {Function} callback Function to call when list is loaded or has failed. The signature is callback(success,zonelist,options)</li>
	 * @config {Object} options Options to pass through to the callback</li>
	 * @memberOf jsorm.TimeZone
	 */
	getZone : function(config) {
		var name = config.name;
		var callback = config.callback;
		var options = config.options;
		if (!name) {
			name = "local";

			// do we have it? if not, generate it
			var zone = jsorm.TimeZone.zones[name];
			if (!zone) {
				// null zoneData means just calculate directly
				zone = new jsorm.TimeZone(name,null);
				jsorm.TimeZone.zones[name] = zone;				
			}
			setTimeout(function(){callback(true,zone,options)},1);
		} else {
			// check if it already exists
			var zone = jsorm.TimeZone.zones[name];
			if (zone) {
				setTimeout(function(){callback(true,zone,options);},1);
			} else {
				// we need to know if it is a GMT+/-HH:mm timezone
				// is it a kind that we can process using zoneinfo?
				var pattern = /^\s*GMT([\+\-])(\d\d?):?(\d?\d?)$/;
				var m = pattern.exec(name);
				if (m) {
					// we can find the standard offset, but not the DST, since GMT+/-hh:mm gives us no
					//    DST info
					var offset = (parseInt(m[2])*60+parseInt(m[3]))*60*(m[1] == '+' ? 1 : -1);
					var dstIncr = 0;
					var zoneName = this.createGmtName(m[1],m[2],m[3]);
					// do we have it? if not, generate it
					var zone = jsorm.TimeZone.zones[zoneName];
					if (!zone) {
						zone = new jsorm.TimeZone(zoneName,{leaps:[],transitions:[],types:[[offset,0,zoneName]]})
						jsorm.TimeZone.zones[zoneName] = zone;
					}
					setTimeout(function(){callback(true,zone,options);},1);
				} else {
					// we will try to use zoneinfo
					var basepath = config.basepath ? config.basepath : jsorm.TimeZone.basepath;
					var path = config.path ? config.path : jsorm.TimeZone.path;
					// we need to set up the correct path
					jsorm.i18n_ajax(basepath+path+name,function(url,xmlHttp,success,options) {
						/*
						 * Format of the JSON is:
						 *     {
						 * 		leaps: [[sec1,cum_leap_seconds],[sec2,cum_leap_seconds],..,[secN,cum_leap_seconds],
						 * 		transitions: [[sec1,type1],[sec2,type2],..,[secN,typeN]],
						 * 		types: [[offset,isdst,name],[offset,isdst,name],..,[offset,isdst,name]]
						 * 	   }
						 * 
						 * Where any secN is the number of seconds since the Unix epoch (midnight at the beginning of Jan 1, 1970 UTC)
						 * Where cum_leap_seconds is the total leap seconds (not the addition at that moment) as of that moment
						 * Where typeN is the index into the types array for the zone settings that begin at that sec
						 * Where the offset is offset from UTC, isdst is 0/1 for being DST, and name is the short form name for the zone
						 */
						if (success) {
							var tzinfo = eval("("+xmlHttp.responseText+")");
							zone = new jsorm.TimeZone(options.name,tzinfo);
							jsorm.TimeZone.zones[options.name] = zone;								
						}
						options.callback(success,zone,options.options);
					},{name: name, callback: callback,options:options});
				}
			}
		}
	}
	
});

/**
 * You should <b>not</b> use the constructor for Calendar. Use Calendar.getCalendar() instead.
 * 
 * @class A Calendar is an object that manages and gives information for a single moment in time under a single timezone,
 * for a single locale and under a single calendaring system. Unlike the javascript Date object, Calendar can use multiple calendars,
 * any locale, and any timezone. In general, one uses a Calendar object as follows:
 * <ol>
 * <li>Determine the specific moment in time you want, and create a javascript date object around it</li>
 * <li>Determine the desired locale, and save its name</li>
 * <li>Determine the desired timezone, and either save its name or load its object TimeZone</li>
 * <li>Determine the specifica calendar implementation you want, e.g. Gregorian, Julian, Hebrew, Muslim, etc.</li>
 * <li>Call Calendar.getCalendar(config) #getCalendar, where the config contains all the configuration information just determined, 
 * 		as well as the correct callback</li>
 * <li>Get the fields whose information you want.</li>
 * <li>Set any fields and then get more fields.</li>
 * <li>Change the locale or timezone, then set and get more fields</li>
 * </ol>
 * <p/>
 * 
 * <table>
 * <tr><th>Field</th><th>Range</th><th>Meaning</th></tr>
 * <tr><td>YEAR</td><td>Any</td><td>year in the calendar implementation</td></tr>
 * <tr><td>MONTH</td><td>0 to 11 or 12, depending on calendar implementation</td><td>month in the given year</td></tr>
 * <tr><td>DATE</td><td>0 to max, depending on the month and calendar implementation</td><td>date in the given month</td></tr>
 * <tr><td>DAY_OF_YEAR</td><td>0 to max, depending on calendar implementation</td><td>day of the given year this is</td></tr>
 * <tr><td>LEAP</td><td>either 1 or 0</td><td>number of leap days in the given year</td></tr>
 * <tr><td>ERA</td><td>0 or 1</td><td>1 for AD/CE, 0 for BC/BCE</td></tr>
 * <tr><td>DAY_OF_WEEK</td><td>0 (Sunday) through 6 (Saturday)</td><td>day of week of the given date</td></tr>
 * <tr><td>DAY_OF_WEEK_IN_MONTH</td><td>1 to 5</td><td>incidence of the day of week in the given month</td></tr>
 * <tr><td>WEEK_OF_YEAR</td><td>0 through 53</td><td>week of the year for the given date</td></tr>
 * <tr><td>WEEK_OF_MONTH</td><td>0 through 5</td><td>week of the given month for the given date</td></tr>
 * <tr><td>FIRST_DAY_OF_WEEK</td><td>0 (Sunday) through 6 (Saturday)</td><td>first day of the week, as passed in the parameters</td></tr>
 * <tr><td>MIN_DAYS_IN_WEEK</td><td>1 through 7</td><td>minimum days in the week, as passed in the parameters</td></tr>
 * <tr><td>ZONE_OFFSET</td><td>-86400 through 86400</td><td>actual offset from UTC/GMT for the given moment in time for this zone, in seconds</td></tr>
 * <tr><td>DST</td><td>0 through 1</td><td>1 if the zone is currently known to be in DST, 0 otherwise</td></tr>
 * <tr><td>ZONENAME</td>N/A<td></td><td>name of the zone as provided by the zone parameter</td></tr>
 * <tr><td>RFC822</td><td>-2400 through +2400</td><td>rfc822-compliant form of the name of the zone at the given moment in time</td></tr>
 * <tr><td>ISO_YEAR</td><td>Any</td><td>year according to the ISO commercial calendar</td></tr>
 * <tr><td>ISO_WEEK_OF_YEAR</td><td>0 through 53</td><td>week of the year according to the ISO commercial calendar</td></tr>
 * <tr><td>HOUR_OF_DAY</td><td>0 (midnight) to 23</td><td>24-hour format hour of the day from </td></tr>
 * <tr><td>HOUR</td><td>1 through 12</td><td>12-hour format hour of the day</td></tr>
 * <tr><td>AM_PM</td><td>0 or 1</td><td>0 from midnight until just before noon, 1 from noon until just before midnight</td></tr>
 * <tr><td>MINUTE</td><td>0 through 59</td><td>minute of the hour</td></tr>
 * <tr><td>SECOND</td><td>0 through 59</td><td>second of the minute</td></tr>
 * <tr><td>MILLISECOND</td><td>0 through 999</td><td>millisecond of the second</td></tr>
 * </table>
 * NOTE: field numbers are always 0 to (max-1) rather than 1 to max unless it makes no sense that way (e.g. year) 
 *       or is explicitly specified. Known exceptions are: HOUR (1-12)
 * <p/>
 * Note that when using set for a field, values that are too large or too small are acceptable. On the next recomputation,
 * the field will be properly normalized. For example, if the month is January and you set the date to 35, the next recomputation
 * will show a date of 5 February.
 * 
 * Calendar also handles formatting of the Calendar object. It supports three strings for formatting:
 * <ul>
 * <li>Java DateFormat</li>
 * <li>PHP date()</li>
 * <li>strftime()</li>
 * </ul>
 * <p/>
 * In order to implement a specific calendar implementation, e.g. Gregorian, the implementation must follow the contract set forth
 * in CalendarImpl. This is normally provided by core developers, rather than Calendar users.
 * In general, you should <b>never</b> directly instantiate a Calendar. Rather you should use #getCalendar.
 * @constructor
 * @param {Object} config A configuration object with the necessary parameters to construct the calendar object. They are:
 * @config {Date} date JavaScript date object with the moment in time to be managed by this Calendar instance. If empty, will use right now.
 * @config {TimeZone} zone TimeZone object representing the timezone for this Calendar
 * @config {String} locale Name of the locale to use for this Calendar
 * @config {String} calendar Name of the calendar implementation to use for this Calendar, in lower-case, e.g. 'gregorian'
 * @config {String} basepath Base path to use for getting to locales, relative to the page URL
 * @config {String} localePath Path to use for getting to locales, relative to the page URL
 * @config {function} callback Callback function to use when the Calendar object is fully ready or has failed. The signature of the 
 * callback is as in getCalendar().
 * @constructor
 */
jsorm.Calendar = function(config) {
	// apply parasitic inheritance
	
	// if we have no date, use right now
	var date = config.date ? config.date : new Date();
	// instantiate our timezone
	var zone = config.zone;
	var locale = config.locale;
	// save our zone and locale
	this.zone = zone;
	// this is the absolute time in milliseconds since the Unix Epoch (Jan 1, 1970 Gregorian UTC)
	this.time = date.getTime();
	// save the specific Calendar instance name and type
	this.calendar = config.calendar;

	// setLocale loads the bundle via ajax, so we need to use a callback
	var callback = function(success,that,options,e) {
		// did we get the locale or not?
		if (success) {
			options.callback(true,that,options.options,null);
		} else {
			options.callback(false,null,options.options,e);
		}
	}
	this.setLocale({locale:config.locale, basepath: config.basepath, localePath: config.localePath, callback: callback, options: config});
	
	return 
}

jsorm.apply(jsorm.Calendar.prototype,/** @scope jsorm.Calendar.prototype */{
	/**
	 * hash containing the list of changed (i.e. dirty) fields. The key is the name of the field; the value is the time in milliseconds
	 * since the system Epoch (midnight 1 January 1970) that the fields was last changed.
	 * 
	 * @private
	 */
	dirty : {},
	/**
	 * boolean value determining if anything is dirty. This is necessary because JavaScript does not support object.length on dirty{} 
	 * to determine if any fields have changed.
	 * @private
	 */
	dirtyCount : false,
	/**
	 * By default, Sunday is the first day of the week
	 * @private
	 */
	defaultFirstDayInWeek : 0,
	/**
	 * By default, the minimum days in the week is 1.
	 * @private
	 */
	defaultMinDaysInWeek : 1,
	/**
	 * Default style to use for Calendar output formatting is Java. Change this to change the default.
	 */
	defaultStyle : jsorm.Calendar.JAVA,

	/**
	 * Format this Calendar according to a formatting string and style. For more information, see the appropriate constants.
	 * JAVA
	 * PHP
	 * STRFTIME
	 * 
	 * @param {String} format A formatting string according to the style given
	 * @param {String} style One of jsorm.Calendar.JAVA, jsorm.Calendar.PHP, jsorm.Calendar.STRFTIME
	 * @return {String} The formatted date and time
	 */
	format : function(format,style) {
		// we format using the given style if available; if not, or none given, use the default
		var f = (style && jsorm.Calendar.formatters[style]) ? jsorm.Calendar.formatters[style] : jsorm.Calendar.formatters[this.defaultStyle];
		return(f.fn(this.fields,format,f.codes,this));
	},
	/**
	 * Format this Calendar to return the date alone, according to the default style for the given locale.
	 * 
	 * @return {String} Formatted date
	 */
	date : function() {
		return(this.format(this.dateFormat,this.defaultStyle));
	},
	/**
	 * Format this Calendar to return the time alone, according to the default style for the given locale.
	 * 
	 * @return {String} Formatted time
	 */
	time : function() {
		return(this.format(this.timeFormat,this.defaultStyle));
	},
	/**
	 * Format this Calendar to return the date and time, according to the default style for the given locale.
	 * 
	 * @return {String} Formatted date and time
	 */
	dateTime : function() {
		return(this.format(this.dateTimeFormat,this.defaultStyle));		
	},

	/*
	 * BEGIN SPECIFIC FIELD GET FUNCTIONS
	 */
	/**
	 * Get the year. Synonym for get('YEAR')
	 * 
	 * @return {int} Year
	 */
	getYear : function() {
		return this.get('YEAR');
	},
	/**
	 * Get the month. Synonym for get('MONTH')
	 * 
	 * @return {int} Month from 0 to 11 (12 if the Calendar Implementation supports 13 months)
	 */
	getMonth : function() {
		return this.get('MONTH');
	},
	/**
	 * Get the date. Synonym for get('DATE')
	 * 
	 * @return {int} Date from 0 to maximum date in month
	 */
	getDate : function() {
		return this.get('DATE');
	},
	/**
	 * Get the week of year. Synonym for get('WEEK_OF_YEAR')
	 * 
	 * @return {int} Week of year from 0 to maximum. This is affected by #setFirstDayOfWeek and #setMinDaysInWeek
	 */
	getWeekOfYear : function() {
		return this.get('WEEK_OF_YEAR');
	},
	/**
	 * Get the week of month. Synonym for get('WEEK_OF_MONTH')
	 * 
	 * @return {int} Week of month from 0 to maximum. This is affected by #setFirstDayOfWeek and #setMinDaysInWeek
	 */
	getWeekOfMonth : function() {
		return this.get('WEEK_OF_MONTH');
	},
	/**
	 * Get the day of the year. Synonym for get('DAY_OF_YEAR')
	 * 
	 * @return {int} Day of year from 0 to maximum for year. For Gregorian, this is 364 (365 for a leap year)
	 */
	getDayOfYear : function() {
		return this.get('DAY_OF_YEAR');
	},
	/**
	 * Get the day of week. Synonym for get('DAY_OF_WEEK')
	 * 
	 * @return {int} Day of week from 0 (Sunday) to 6 (Saturday)
	 */
	getDayOfWeek : function() {
		return this.get('DAY_OF_WEEK');
	},
	/**
	 * Get the occurrence of this day of week in the month. Synonym for get('DAY_OF_WEEK_IN_MONTH')
	 * 
	 * @return {int} Date from 0 to 4
	 */
	getDayOfWeekInMonth : function() {
		return this.get('DAY_OF_WEEK_IN_MONTH');
	},
	/**
	 * Get the hour. Synonym for get('HOUR')
	 * 
	 * @return {int} Hour of day from 1 to 12
	 */
	getHour : function() {
		return this.get('HOUR');
	},
	/**
	 * Get the hour. Synonym for get('HOUR_OF_DAY')
	 * 
	 * @return {int} Hour of day from 0 to 23
	 */
	getHourOfDay : function() {
		return this.get('HOUR_OF_DAY');
	},
	/**
	 * Get the era. Synonym for get('ERA')
	 * 
	 * @return {int} Era, normally 0 before some significant change (e.g. BC/BCE) and 1 after (e.g. AD/CE)
	 */
	getEra : function() {
		return this.get('ERA');
	},
	/**
	 * Get AM versus PM. Synonym for get('AM_PM')
	 * 
	 * @return {int} 0 if AM and 1 if PM
	 */
	getAmPm : function() {
		return this.get('AM_PM');
	},
	/**
	 * Get the minute in the hour. Synonym for get('MINUTE')
	 * 
	 * @return {int} Minute from 0 to 59
	 */	
	getMinute : function() {
		return this.get('MINUTE');
	},
	/**
	 * Get the second in the minute. Synonym for get('SECOND')
	 * 
	 * @return {int} Second from 0 to 59
	 */	
	getSecond : function() {
		return this.get('SECOND');
	},
	/**
	 * Get the millisecond in the second. Synonym for get('MILLISECOND')
	 * 
	 * @return {int} Millisecond from 0 to 999
	 */	
	getMillisecond : function() {
		return this.get('MILLISECOND');
	},
	/**
	 * Get the maximum value for a given field. This can change for different moments in time. For example, the
	 * maximum DATE in the month of January is 30, while in June it is 29.
	 * 
	 * @param {String} field The field whose maximum we want
	 * @return {int} The maximum as an integer
	 */	
	getMax : function(field) {
		return this.calendar.getMax(field,this.fields);
	},
	/**
	 * Get the minimum value for a given field. 
	 * 
	 * @param {String} field The field whose minimum we want
	 * @return {int} The minimum as an integer
	 */	
	getMin : function(field) {
		return this.calendar.getMin(field,this.fields);
	},
	/**
	 * Get what the first day of the week is, as set by the user or default.
	 * 
	 * @return {int} The first day of the week from 0 (Sunday) to 6 (Saturday)
	 */
	getFirstDayOfWeek : function() {
		return this.firstDayOfWeek;
	},
	/**
	 * Get what the minimum days in week is, as set by the user or default.
	 * 
	 * @return {int} Minimum days in week, from 1 to 7
	 */
	getMinimumDaysInWeek : function() {
		return this.minDaysInWeek;
	},
	/**
	 * Get the jsorm.TimeZone object that is active for this Calendar.
	 * 
	 * @return {TimeZone} TimeZone object
	 */
	getZone : function() {
		return this.zone;
	},
	/*
	 * END SPECIFIC FIELD GET FUNCTIONS
	 */

	/*
	 * BEGIN SPECIFIC FIELD SET FUNCTIONS
	 */
	/**
	 * Set the year. Synonym for set('YEAR',val)
	 * 
	 * @param {int} val The year
	 */
	setYear : function(val) {
		this.set('YEAR',val);
	},
	/**
	 * Set the month. Synonym for set('MONTH',val)
	 * 
	 * @param {int} val The month
	 */
	setMonth : function(val) {
		this.set('MONTH',val);
	},
	/**
	 * Set the month. Synonym for set('MONTH',val)
	 * 
	 * @param {int} val The month
	 */	
	setDate : function(val) {
		this.set('DATE',val);
	},
	/**
	 * Set the week of year. Synonym for set('WEEK_OF_YEAR',val)
	 * NOTE: At this point, this has no impact.
	 * 
	 * @param {int} val The week of year
	 */
	setWeekOfYear : function(val) {
		this.set('WEEK_OF_YEAR',val);
	},
	/**
	 * Set the week of month. Synonym for set('WEEK_OF_MONTH',val)
	 * NOTE: At this point, this has no impact.
	 * 
	 * @param {int} val The week of month
	 */
	setWeekOfMonth : function(val) {
		this.set('WEEK_OF_MONTH',val);
	},
	/**
	 * Set the day of year. Synonym for set('DAY_OF_YEAR',val)
	 * NOTE: At this point, this has no impact.
	 * 
	 * @param {int} val The day of year
	 */
	setDayOfYear : function(val) {
		this.set('DAY_OF_YEAR',val);
	},
	/**
	 * Set the day of week. Synonym for set('DAY_OF_WEEK',val)
	 * NOTE: At this point, this has no impact.
	 * 
	 * @param {int} val The day of week
	 */
	setDayOfWeek : function(val) {
		this.set('DAY_OF_WEEK',val);		
	},
	/**
	 * Set the day of week in month. Synonym for set('DAY_OF_WEEK_IN_MONTH',val)
	 * NOTE: At this point, this has no impact.
	 * 
	 * @param {int} val The occurrence of the day of week in month
	 */
	setDayOfWeekInMonth : function(val) {
		this.set('DAY_OF_WEEK_IN_MONTH',val);		
	},
	/**
	 * Set the hour. Synonym for set('HOUR',val)
	 * 
	 * @param {int} val The hour from 1 to 12
	 */
	setHour : function(val) {
		this.set('HOUR',val);		
	},
	/**
	 * Set the hour of day. Synonym for set('HOUR_OF_DAY',val)
	 * 
	 * @param {int} val The hour of day from 0 to 23
	 */
	setHourOfDay : function(val) {
		this.set('HOUR_OF_DAY',val);		
	},
	/**
	 * Set the era. Synonym for set('ERA',val)
	 * 
	 * @param {int} val The era, normally as 0 or 1
	 */	
	setEra : function(val) {
		this.set('ERA',val);		
	},
	/**
	 * Set the am or pm. Synonym for set('AM_PM',val)
	 * 
	 * @param {int} val 0 for AM and 1 for PM
	 */	
	setAmPm : function(val) {
		this.set('AM_PM',val);		
	},
	/**
	 * Set the minute. Synonym for set('MINUTE',val)
	 * 
	 * @param {int} val The minute from 0 to 59
	 */
	setMinute : function(val) {
		this.set('MINUTE',val);		
	},
	/**
	 * Set the second. Synonym for set('SECOND',val)
	 * 
	 * @param {int} val The second from 0 to 59
	 */
	setSecond : function(val) {
		this.set('SECOND',val);		
	},
	/**
	 * Set the millisecond. Synonym for set('MILLISECOND',val)
	 * 
	 * @param {int} val The millisecond from 0 to 999
	 */
	setMillisecond : function(val) {
		this.set('MILLISECOND',val);		
	},
	/**
	 * Set the first day of the week. This is used to calculate the WEEK_OF_MONTH and WEEK_OF_YEAR fields.
	 * 
	 * @param {int} val The first day of the week, from 0 (Sunday) to 6 (Saturday)
	 */
	setFirstDayOfWeek : function(val) {
		this.firstDayOfWeek = val;
		jsorm.apply(this.fields,this.calendar.processWeekCount(this.firstDayOfWeek,this.minDaysInWeek,this.fields));
	},
	/**
	 * Set the minimum days in week. This is used to calculate the WEEK_OF_MONTH and WEEK_OF_YEAR fields.
	 * 
	 * @param {int} val The minimum number of days from 1 to 7
	 */
	setMinimumDaysInWeek : function(val) {
		this.minDaysInWeek = true;
		jsorm.apply(this.fields,this.calendar.processWeekCount(this.firstDayOfWeek,this.minDaysInWeek,this.fields));
	},
	/**
	 * Set the zone to use
	 * 
	 * @param {TimeZone} val The TimeZone object to use
	 */
	setZone : function(val) {
		this.zone = val;
		this.fields = this.calendar.processTime(this.time,this.zone,this.firstDayOfWeek,this.minDaysInWeek);
		this.dirty = {};
		this.dirtyCount = false;		
	},
	/*
	 * END SPECIFIC FIELD SET FUNCTIONS
	 */

	/*
	 * BEGIN LOCALIZATION FUNCTIONS
	 */
	/**
	 * Set the locale to a new locale. The locale must follow the standards set by ResourceBundle.
	 * 
	 * @param {Object} config Object containing the configuration parameters.
	 * @config {String} [basepath] Base path for the locale, overriding the default
	 * @config {String} [localePath] Path to the locale relative to the base path, overriding the default
	 * @config {function} callback Function to call when setting of locale is complete. The signature of the callback 
	 * should be callback(success,calendarObject,options,e)
	 * @config {Object} options Object containing options to pass to the callback
	 */
	setLocale : function(config) {
		this.locale = config.locale;
		// set our localePath
		var basepath = config.basepath ? config.basepath : jsorm.Calendar.basepath;
		var path = config.localePath ? config.localePath : jsorm.Calendar.localePath;
		this.localeBundlePath = basepath+path;
		this.localeBundleName = this.calendar.name;
		var options = {cal: this, callback: config.callback, config: config};
		jsorm.ResourceBundle.getBundle({path: this.localeBundlePath, name: this.localeBundleName,locale: this.locale,callback: function(success,bundle,options,message) {
			var that = options.cal, e;
			if (success && bundle) {
				// save the bundle
				that.bundle = bundle;
				// does the localized bundle have defined a first day in week and minimum days in first week?
				var day1wk = bundle.get('FIRSTDAYINWEEK');
				var minDaysWk = bundle.get('MINIMUMDAYSINWEEK');
				that.firstDayOfWeek = (!day1wk || isNaN(day1wk)) ? that.defaultFirstDayInWeek : parseInt(day1wk);
				that.minDaysFirstWeek = (!minDaysWk || isNaN(minDaysWk)) ? that.defaultMinDaysInWeek : parseInt(minDaysWk);

				// set up some defaults for formatting
				var f = jsorm.Calendar.formats[that.locale] ? jsorm.Calendar.formats[that.locale] : jsorm.Calendar.iso;
				that.dateFormat = f.date;
				that.timeFormat = f.time;
				that.dateTimeFormat = f.date + ' ' + f.time;
				that.fields = that.calendar.processTime(that.time,that.zone,that.firstDayOfWeek,that.minDaysInWeek);

				e = null;
			} else if (!options.config.locale) {
				// what if we do not have a locale given and we could not load? then we use the default built-in for the calendar

				// does the localized bundle have defined a first day in week and minimum days in first week?
				var day1wk = that._getLocalized('FIRSTDAYINWEEK');
				var minDaysWk = that._getLocalized('MINIMUMDAYSINWEEK');
				that.firstDayOfWeek = (!day1wk || isNaN(day1wk)) ? that.defaultFirstDayInWeek : parseInt(day1wk);
				that.minDaysFirstWeek = (!minDaysWk || isNaN(minDaysWk)) ? that.defaultMinDaysInWeek : parseInt(minDaysWk);

				// set up some defaults for formatting
				var f = jsorm.Calendar.formats[that.locale] ? jsorm.Calendar.formats[that.locale] : jsorm.Calendar.iso;
				that.dateFormat = f.date;
				that.timeFormat = f.time;
				that.dateTimeFormat = f.date + ' ' + f.time;
				that.fields = that.calendar.processTime(that.time,that.zone,that.firstDayOfWeek,that.minDaysInWeek);

				// success
				e = null;
				success = true;
			} else {
				// did not go blank, but did not successfully load either. Fail.
				e = "Unable to load bundle";
			}
			// do the relevant callback
			if (options.callback && typeof(options.callback) == 'function') {
				var opts = options.config ? options.config.options : null;
				options.callback(success,that,opts,e);
			}
		},options: options});				
	},
	
	/**
	 * Get the localized version of a particular string. If there is no bundle, or the bundle does not have the appropriate key,
	 * then the default is taken from the Calendar implementation defaultLocaleInfo.
	 * 
	 * @param {String} key The key to get localized
	 * @return {String} Localized string for given key
	 * @private
	 */
	_getLocalized : function(key) {
		var name = null;
		if (this.bundle && this.bundle.get) {
			name = this.bundle.get(key);
		}
		// if there was no localized version, use the default built-in
		if (!name)
			name = this.calendar.defaultLocaleInfo[key];
		return(name);		
	},
	/**
	 * Get the day of the week as a localized string in short form, e.g. Thu. 
	 * 
	 * @param {int} d The day of the week to get from 0 (Sunday) to 6 (Saturday)
	 * @return {String} Localized string for day of week
	 * @private
	 */
	_getTextDayOfWeekShort : function(d) {
		return(this._getTextDayOfWeek(d,true));
	},
	/**
	 * Get the day of the week as a localized string in long form, e.g. Thursday. 
	 * 
	 * @param {int} d The day of the week to get from 0 (Sunday) to 6 (Saturday)
	 * @return {String} Localized string for day of week
	 * @private
	 */
	_getTextDayOfWeekLong : function(d) {
		return(this._getTextDayOfWeek(d,false));
	},
	/**
	 * Get the day of the week as a localized string in long or short form, e.g. Thursday or Thu. 
	 * 
	 * @param {int} d The day of the week to get from 0 (Sunday) to 6 (Saturday)
	 * @param {boolean} shortName True if the name should be in short form, false if in long form
	 * @return {String} Localized string for day of week
	 * @private
	 */
	_getTextDayOfWeek : function(d,shortName) {
		// get the index name for the month
		var name = ['SUNDAY','MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY'][d];
		// get the localized version
		var l = this._getLocalized(name);
		// the short form is *after* the :
		return(l.split(':')[shortName?1:0]);
	},
	/**
	 * Get the name of the month as a localized string in long or short form, e.g. February or Feb 
	 * 
	 * @param {int} m The month to get from 0 to 11 or 12
	 * @param {boolean} shortName True if the name should be in short form, false if in long form
	 * @return {String} Localized string for month name
	 * @private
	 */
	_getTextMonth : function(m,shortName) {
		// get the index name for the month
		var name = ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER'][m];
		// get the localized version
		var l = this._getLocalized(name);
		// the short form is *after* the :
		return(l.split(':')[shortName?1:0]);
	},
	/**
	 * Get the name of the month as a localized string in short form, e.g. Feb 
	 * 
	 * @param {int} m The month to get from 0 to 11 or 12
	 * @return {String} Localized string for month name
	 * @private
	 */
	_getTextMonthShort : function(m) {
		return(this._getTextMonth(m,true));
	},
	/**
	 * Get the name of the month as a localized string in long form, e.g. February
	 * 
	 * @param {int} m The month to get from 0 to 11 or 12
	 * @return {String} Localized string for month name
	 * @private
	 */
	_getTextMonthLong : function(m) {
		return(this._getTextMonth(m,false));
	},
	/**
	 * Get AM or PM as a localized string
	 * 
	 * @param {int} d 0 for AM or 1 for PM
	 * @return {String} Localized string for AM/PM
	 * @private
	 */
	_getTextAmPm : function(d) {
		// get the index name for the month
		var name = ['AM','PM'][d];
		// get the localized version
		return(this._getLocalized(name));
	},
	/**
	 * Get the name of the era as a localized string
	 * 
	 * @param {int} e 0 for before an era shift (e.g. BC/BCE), 1 for after (e.g. AD/CE)
	 * @return {String} Localized string for era name
	 * @private
	 */
	_getTextEra : function(e) {
		var name = ['ERA0','ERA1'][e];
		return(this._getLocalized(name));
	},
	/**
	 * Get the suffix to a number for counting, e.g. 'st' to add to 1 to get 1st, 'nd' to add to 2 to get 2nd, etc.
	 * This number is also localized.
	 * 
	 * @param {int} n The number to which we want to add the suffix
	 * @return {String} Localized string for suffix
	 * @private
	 */
	_getTextCount : function(n) {
		// first we try 'COUNT' plus the number turned to a string
		var name = this._getLocalized('COUNT'+(n+1));
		// if that was empty, we try a generic form
		if (!name) {name = this._getLocalized('COUNTN');}
		// if that was empty, we use blank
		if (!name) {name = '';}
		return(name);		
	},
	/*
	 * END LOCALIZATION FUNCTIONS
	 */

	/*
	 * BEGIN get/set/add/roll FUNCTIONS
	 */
	/**
	 * Get the value of a specific field. See the overview for the names of fields. This function forces a recalculation of
	 * The time and fields, if any is dirty.
	 * 
	 * @param {string} field The name of the field to get.
	 */
	get : function(field) {
		if (this.dirtyCount) {
			this.time = this.calendar.processFields(this.fields,this.dirty,this.zone);
			this.fields = this.calendar.processTime(this.time,this.zone,this.firstDayOfWeek,this.minDaysInWeek);
			this.dirty = {};
			this.dirtyCount = false;
		}
		return(this.fields[field]);
	},
	/**
	 * Set a field, marking as dirty, but no immediate recomputation, in order to save. Recomputation will happen at the next get().
	 * 
	 * @param {String} field The name of the field to change. See the overview for the names of fields.
	 * @param {int} val The value to set the field to.
	 */
	set : function(field,val) {
		this.fields[field] = val;
		this.dirty[field] = new Date().getTime();
		this.dirtyCount = true;
	},

	/**
	 * Add a field with a delta and do immediate recomputation. Add does increment larger fields.
	 * E.g. if the current time is 09:50, add('MINUTE',20) gives 10:10 while roll('MINUTE',20) gives 09:10.
	 * 
	 * @param {String} field The name of the field that should be added. See the overview for the names of fields.
	 * @param {int} delta The amount to increment (positive) or decrement (negative)
	 */
	add : function(field, delta) {
		// process the date and then mark all as clean
		if (this.dirtyCount) {
			this.time = this.calendar.processFields(this.fields,this.dirty,this.zone);
			this.dirty = {};
			this.dirtyCount = false;
			this.fields = this.calendar.processTime(this.time,this.zone,this.firstDayOfWeek,this.minDaysInWeek);
		}

		// what do we do?
		var ms1d = jsorm.Calendar.ms1d, ms1m = jsorm.Calendar.ms1m, ms1h = jsorm.Calendar.ms1h, ms7d = jsorm.Calendar.ms7d;
		switch(field) {
			case 'DATE':
				this.time = this.time+delta*ms1d;
				break;
			case 'DAY_OF_YEAR':
				this.time = this.time+delta*ms1d;
				break;
			case 'DAY_OF_WEEK':
				this.time = this.time+delta*ms1d;
				break;
			case 'YEAR':
				this.fields.YEAR += delta;
				this.time = this.calendar.processFields(this.fields,this.dirty,this.zone);
				break;
			case 'MILLISECOND':
				this.time = this.time+delta;
				break;
			case 'SECOND':
				this.time = this.time+delta*1000;
				break;
			case 'MINUTE':
				this.time = this.time+delta*ms1m;
				break;
			case 'HOUR':
				this.time = this.time+delta*ms1h;
			case 'HOUR_OF_DAY':
				this.time = this.time+delta*ms1h;
				break;
			case 'MONTH':
				this.fields.MONTH += delta;
				this.time = this.calendar.processFields(this.fields,this.dirty,this.zone);
				break;
			case 'AM_PM':
				// every AM_PM is a 12 hour cycle
				this.time = this.time+delta*ms1d/2;
				break;
			case 'WEEK_OF_MONTH':
				this.time = this.time+delta*ms7d;
				break;
			case 'WEEK_OF_YEAR':
				this.time = this.time+delta*ms7d;
				break;
		}
		this.fields = this.calendar.processTime(this.time,this.zone,this.firstDayOfWeek,this.minDaysInWeek);
		this.dirty = {};
		this.dirtyCount = false;
	},

	/**
	 * Roll a field with a delta and do immediate recomputation. roll does not increment larger fields.
	 * E.g. if the current time is 09:50, add('MINUTE',20) gives 10:10 while
	 * roll('MINUTE',20) gives 09:10
	 * 
	 * @param {String} field The name of the field that should be rolled. See the overview for the names of fields.
	 * @param {int} delta The amount to increment (positive) or decrement (negative)
	 */
	roll : function(field, delta) {
		// use get() so we force a recomputation
		var cur = this.get(field);
		// what if the cur+delta has overflow, even multiple times? We need the modulo
		var max = this.calendar.getMax(field,this.fields);
		var min = this.calendar.getMin(field,this.fields);
		var range = max-min+1;
		var val = (cur-min+range+delta) % range + min;
		if (val < min) {
			val += range;
		}
		// set forces the dirty flag		
		this.set(field,val);
		// process all fields and then mark dirty as clean
		this.time = this.calendar.processFields(this.fields,this.dirty,this.zone);	
		this.dirty = {};
		this.dirtyCount = false;
		this.fields = this.calendar.processTime(this.time,this.zone,this.firstDayOfWeek,this.minDaysInWeek);
	},

	/**
	 * set the absolute time in milliseconds since system Epoch (midnight 1 January 1970)
	 * 
	 * @param {long} time Time in milliseconds
	 */
	setTime : function(time) {
		this.time = time;
		this.fields = this.calendar.processTime(this.time,this.zone,this.firstDayOfWeek,this.minDaysInWeek);
		this.dirty = {};
		this.dirtyCount = false;		
	},
	/**
	 * get the absolute time in milliseconds since system Epoch (midnight 1 January 1970)
	 * @return {long} time in milliseconds
	 */
	getTime : function() {
		// do we have to process dirty first?
		if (this.dirtyCount) {
			this.time = this.calendar.processFields(this.fields,this.dirty,this.zone);
			this.fields = this.calendar.processTime(this.time,this.zone,this.firstDayOfWeek,this.minDaysInWeek);
			this.dirty = {};
			this.dirtyCount = false;
		}
		return(this.time);
	}
	/*
	 * END get/set/add/roll FUNCTIONS
	 */

});

jsorm.apply(jsorm.Calendar,/** @scope jsorm.Calendar */{
	impls : {},
	/**
	 * Default path to locales. This may be changed at the class level (Calendar.localePath = '/new/path') to have all instances default
	 * to a new path. In either case, each getCalendar() call can override the default.
	 * @memberOf jsorm.Calendar
	 */
	localePath : 'i18n/locale/',
	/**
	 * Default path to calendar implementations. This may be changed at the class level (Calendar.calendarPath = '/new/path') to 
	 * have all instances default to a new path. In either case, each #getCalendar call can override the default.
	 * @memberOf jsorm.Calendar
	 */
	calendarPath : 'i18n/calendars/',
	/**
	 * Default base path to locales and calendars. This may be changed at the class level (Calendar.basePath = '/new/path') to 
	 * have all instances default to a new path. In either case, each getCalendar() call can override the default.
	 * @memberOf jsorm.Calendar
	 */
	basePath : '/lib/',
	/**
	 * Default calendar implementation to use. This is the only one that ships by default with i18n.
	 * @memberOf jsorm.Calendar
	 */
	defaultCalendar : 'Gregorian',
	/**
	 * Constant for Java-style formatting.
	 * See <a href="http://java.sun.com/javase/6/docs/api/java/text/SimpleDateFormat.html">
	 * http://java.sun.com/javase/6/docs/api/java/text/SimpleDateFormat.html</a>
	 * @memberOf jsorm.Calendar
	 */
	JAVA : 'J',
	/**
	 * Constant for PHP-style formatting.
	 * See <a href="http://www.php.net/date">http://www.php.net/date</a>
	 * @memberOf jsorm.Calendar
	 */
	PHP : 'P',
	/**
	 * Constant for strftime-style formatting.
	 * See <a href="http://opengroup.org/onlinepubs/007908799/xsh/strftime.html">http://opengroup.org/onlinepubs/007908799/xsh/strftime.html</a>
	 * @memberOf jsorm.Calendar
	 */
	STRFTIME : 'S',
	
	/**
	 * Constants for milliseconds in a minute, hour, day, week.
	 * 
	 * @memberOf jsorm.Calendar
	 * @private
	 */
	ms1m : 60*1000,
	ms1h : 3600*1000,
	ms1d : 3600*1000*24,
	ms7d : 7*3600*1000*24,
	
	
	/**
	 * Static function to get a new Calendar. This is the primary entry point into the Calendar class and only approved method of 
	 * acquiring a new Calendar object.
	 * 
 	 * @param {Object} config An object containing the necessary configuration parameters for the new Calendar.
	 * @config {TimeZone/String} [zone] TimeZone object or the name of a timezone. The name of the timezone must follow the rules for
	 *     TimeZone.getZone()
	 * @config {Date} [date] JavaScript Date object representing the moment in time to use. If undefined or null, will use right now
	 * @config {String} [locale] Name of a locale to use, following the ResourceBundle convention. If no bundle can be found,
	 *     The default of the Calendar implementation will be used
	 * @config {String} [calendar] Case-insensitive name of a Calendar Implementation to use. If undefined or null, will use
	 *     Calendar.defaultCalendar. By default, only Gregorian is provided. For
	 *     more information on creating and deploying new implementations, see CalendarImpl
	 * @config {String} [calendarPath] Path to use to load Calendar implementations via Ajax. If undefined or null, will use
	 * 		Calendar.calendarPath
	 * @config {String} [localePath] Path to use to load locale implementations via Ajax. If undefined or null, will use Calendar.localePath
	 * @config {Function} callback Function to call when the Calendar load is complete
	 * The signature of the callback should be callback(success,calendarObject,options,e) where success = boolean for success or failure,
	 * calendarObject = the Calendar object if successful or null if failed, options = the options passed as part of getCalendar under 
	 * config.options, e = the error message if failed
	 * @config {Object} [options] Options to pass to the callback as the options parameter
	 * @memberOf jsorm.Calendar
	 */
	getCalendar : function(config) {
		// do we have the zone or not?
		if (typeof(config.zone) == 'object') {
			var date = config.date, zone = config.zone, locale = config.locale, callback = config.callback, opts = config.options;
			// our default is Gregorian
			var calendar = config.calendar ? config.calendar : jsorm.Calendar.defaultCalendar;
			calendar = calendar.toLowerCase();
			var basepath = config.basePath ? config.basePath : jsorm.Calendar.basePath;
			var calpath = config.calendarPath ? config.calendarPath : jsorm.Calendar.calendarPath;
			var localepath = config.localePath ? config.localePath : jsorm.Calendar.localePath;
			
			// was this already loaded? we have the prototype in there, we need to make a new one
			if (jsorm.Calendar.impls[calendar]) {
				var calConf = {calendar : jsorm.Calendar.impls[calendar](), date: date, zone: zone, locale: locale,
							  basepath: basepath, localePath:localepath, callback: callback, options: opts};
				var calInst = new jsorm.Calendar(calConf);
			} else {
				// it was not, so load it and then run
				jsorm.i18n_ajax(basepath+calpath+calendar+'.js',function(url,xmlHttp,success,options) {
					var calInst = null;
					if (success) {
						// we have it loaded
						jsorm.Calendar.impls[options.calendar] = function() {return eval("("+xmlHttp.responseText+")");};
						try {
							var calConf = {calendar : jsorm.Calendar.impls[calendar](), date: options.date, zone: options.zone, 
											locale: options.locale, options: options.options,
										  basepath: options.basepath, localePath: options.localePath, callback: options.callback};
							var calInst = new jsorm.Calendar(calConf);
						} catch (e) {
							callback(false,null,options.options,e);							
						}
					} else {
						callback(false,null,options.options,"Unknown calendar");
					}
				},{calendar:calendar,date:date,zone:zone,locale:locale,basepath:basepath,localePath:localepath,callback:callback,options:opts});
			}						
		} else {
			jsorm.TimeZone.getZone({name: config.zone, options: config, callback: function(success,zone,options,e){
				// did we have success?
				if (success) {
					options.zone = zone;
					jsorm.Calendar.getCalendar(options);
				} else {
					// no success, so callback that we know of no such zone
					config.callback(false,null,options.options,"Unknown zone");
				}
			}});
		}
	},
	/**
	 * ISO standard date 
	 * @memberOf jsorm.Calendar
	 * @private
	 */
	iso : {date: 'yyyy-MM-DD', time: 'H:i:sP'},
	/**
	 * Standard date formats based on country codes
	 * @memberOf jsorm.Calendar
	 * @private
	 */
	formats : {
		AF : {name : 'AFGHANISTAN', date : ''},
		AX : {name : 'ÅLAND ISLANDS', date : ''},
		AL : {name : 'ALBANIA', date : 'dd/mm/yyyy'},
		DZ : {name : 'ALGERIA', date : ''},
		AS : {name : 'AMERICAN SAMOA', date : ''},
		AD : {name : 'ANDORRA', date : ''},
		AO : {name : 'ANGOLA', date : ''},
		AI : {name : 'ANGUILLA', date : ''},
		AQ : {name : 'ANTARCTICA', date : ''},
		AG : {name : 'ANTIGUA AND BARBUDA', date : ''},
		AR : {name : 'ARGENTINA', date : 'dd/mm/yyyy'},
		AM : {name : 'ARMENIA', date : 'dd.mm.yyyy'},
		AW : {name : 'ARUBA', date : ''},
		AU : {name : 'AUSTRALIA', date : 'dd/mm/yyyy'},
		AT : {name : 'AUSTRIA', date : 'yyyy-mm-dd'},
		AZ : {name : 'AZERBAIJAN', date : ''},
		BS : {name : 'BAHAMAS', date : ''},
		BH : {name : 'BAHRAIN', date : ''},
		BD : {name : 'BANGLADESH', date : 'dd/mm/yyyy'},
		BB : {name : 'BARBADOS', date : 'dd/mm/yyyy'},
		BY : {name : 'BELARUS', date : 'dd/mm/yyyy'},
		BE : {name : 'BELGIUM', date : 'dd/mm/yyyy'},
		BZ : {name : 'BELIZE', date : 'dd/mm/yyyy'},
		BJ : {name : 'BENIN', date : ''},
		BM : {name : 'BERMUDA', date : ''},
		BT : {name : 'BHUTAN', date : ''},
		BO : {name : 'BOLIVIA', date : 'dd/mm/yyyy'},
		BA : {name : 'BOSNIA AND HERZEGOVINA', date : ''},
		BW : {name : 'BOTSWANA', date : ''},
		BV : {name : 'BOUVET ISLAND', date : ''},
		BR : {name : 'BRAZIL', date : 'dd/mm/yyyy'},
		IO : {name : 'BRITISH INDIAN OCEAN TERRITORY', date : ''},
		BN : {name : 'BRUNEI DARUSSALAM', date : ''},
		BG : {name : 'BULGARIA', date : 'dd.mm.yyyy'},
		BF : {name : 'BURKINA FASO', date : ''},
		BI : {name : 'BURUNDI', date : ''},
		KH : {name : 'CAMBODIA', date : ''},
		CM : {name : 'CAMEROON', date : ''},
		CA : {name : 'CANADA', date : 'dd/mm/yyyy'},
		CV : {name : 'CAPE VERDE', date : ''},
		KY : {name : 'CAYMAN ISLANDS', date : ''},
		CF : {name : 'CENTRAL AFRICAN REPUBLIC', date : ''},
		TD : {name : 'CHAD', date : ''},
		CL : {name : 'CHILE', date : 'dd/mm/yyyy'},
		CN : {name : 'CHINA', date : 'yyyy\u5e74mm\u6708dd\u65e5'},
		CX : {name : 'CHRISTMAS ISLAND', date : ''},
		CC : {name : 'COCOS (KEELING) ISLANDS', date : ''},
		CO : {name : 'COLOMBIA', date : 'dd/mm/yyyy'},
		KM : {name : 'COMOROS', date : ''},
		CG : {name : 'CONGO', date : ''},
		CD : {name : 'CONGO, THE DEMOCRATIC REPUBLIC OF THE', date : ''},
		CK : {name : 'COOK ISLANDS', date : ''},
		CR : {name : 'COSTA RICA', date : ''},
		CI : {name : 'CÔTE D\'IVOIRE', date : ''},
		HR : {name : 'CROATIA', date : 'd.m.yyyy.'},
		CU : {name : 'CUBA', date : ''},
		CY : {name : 'CYPRUS', date : 'dd/mm/yyyy'},
		CZ : {name : 'CZECH REPUBLIC', date : 'd.m.yyyy'},
		DK : {name : 'DENMARK', date : 'dd-mm-yyyy'},
		DJ : {name : 'DJIBOUTI', date : ''},
		DM : {name : 'DOMINICA', date : 'dd/mm/yyyy'},
		DO : {name : 'DOMINICAN REPUBLIC', date : 'dd/mm/yyyy'},
		EC : {name : 'ECUADOR', date : 'dd/mm/yyyy'},
		EG : {name : 'EGYPT', date : 'dd/mm/yyyy'},
		SV : {name : 'EL SALVADOR', date : 'dd/mm/yyyy'},
		GQ : {name : 'EQUATORIAL GUINEA', date : ''},
		ER : {name : 'ERITREA', date : ''},
		EE : {name : 'ESTONIA', date : 'dd/mm/yyyy'},
		ET : {name : 'ETHIOPIA', date : ''},
		FK : {name : 'FALKLAND ISLANDS (MALVINAS)', date : ''},
		FO : {name : 'FAROE ISLANDS', date : ''},
		FJ : {name : 'FIJI', date : ''},
		FI : {name : 'FINLAND', date : 'd.m.yyyy'},
		FR : {name : 'FRANCE', date : 'dd/mm/yyyy'},
		GF : {name : 'FRENCH GUIANA', date : 'dd-mm-yyyy'},
		PF : {name : 'FRENCH POLYNESIA', date : ''},
		TF : {name : 'FRENCH SOUTHERN TERRITORIES', date : ''},
		GA : {name : 'GABON', date : ''},
		GM : {name : 'GAMBIA', date : ''},
		GE : {name : 'GEORGIA', date : ''},
		DE : {name : 'GERMANY', date : ''},
		GH : {name : 'GHANA', date : ''},
		GI : {name : 'GIBRALTAR', date : ''},
		GR : {name : 'GREECE', date : 'dd/mm/yyyy'},
		GL : {name : 'GREENLAND', date : ''},
		GD : {name : 'GRENADA', date : 'dd/mm/yyyy'},
		GP : {name : 'GUADELOUPE', date : ''},
		GU : {name : 'GUAM', date : ''},
		GT : {name : 'GUATEMALA', date : ''},
		GG : {name : 'GUERNSEY', date : ''},
		GN : {name : 'GUINEA', date : ''},
		GW : {name : 'GUINEA-BISSAU', date : ''},
		GY : {name : 'GUYANA', date : 'dd/mm/yyyy'},
		HT : {name : 'HAITI', date : ''},
		HM : {name : 'HEARD ISLAND AND MCDONALD ISLANDS', date : ''},
		VA : {name : 'HOLY SEE (VATICAN CITY STATE)', date : ''},
		HN : {name : 'HONDURAS', date : ''},
		HK : {name : 'HONG KONG', date : 'dd/mm/yyyy'},
		HU : {name : 'HUNGARY', date : 'yyyy.mm.dd'},
		IS : {name : 'ICELAND', date : 'dd.mm.yyyy'},
		IN : {name : 'INDIA', date : 'DD/MM/yyyy'},
		ID : {name : 'INDONESIA', date : 'dd-mm-yyyy'},
		IR : {name : 'IRAN, ISLAMIC REPUBLIC OF', date : 'dd/mm/yyyy'},
		IQ : {name : 'IRAQ', date : ''},
		IE : {name : 'IRELAND', date : 'dd/mm/yyyy'},
		IM : {name : 'ISLE OF MAN', date : ''},
		IL : {name : 'ISRAEL', date : 'dd/mm/yyyy'},
		IT : {name : 'ITALY', date : 'dd/mm/yyyy'},
		JM : {name : 'JAMAICA', date : 'dd/mm/yyyy'},
		JP : {name : 'JAPAN', date : 'yyyy\u5e74m\u6708d\u65e5'},
		JE : {name : 'JERSEY', date : ''},
		JO : {name : 'JORDAN', date : 'dd/mm/yyyy'},
		KZ : {name : 'KAZAKHSTAN', date : ''},
		KE : {name : 'KENYA', date : 'dd/mm/yyyy'},
		KI : {name : 'KIRIBATI', date : ''},
		KP : {name : 'KOREA, DEMOCRATIC PEOPLE\'S REPUBLIC OF', date : 'yyyy\ub144 MM\uc6d4 DD\uc77c'},
		KR : {name : 'KOREA, REPUBLIC OF', date : 'yyyy\ub144 MM\uc6d4 DD\uc77c'},
		KW : {name : 'KUWAIT', date : ''},
		KG : {name : 'KYRGYZSTAN', date : ''},
		LA : {name : 'LAO PEOPLE\'S DEMOCRATIC REPUBLIC', date : ''},
		LV : {name : 'LATVIA', date : 'dd.mm.yyyy'},
		LB : {name : 'LEBANON', date : ''},
		LS : {name : 'LESOTHO', date : ''},
		LR : {name : 'LIBERIA', date : ''},
		LY : {name : 'LIBYAN ARAB JAMAHIRIYA', date : ''},
		LI : {name : 'LIECHTENSTEIN', date : ''},
		LT : {name : 'LITHUANIA', date : 'yyyy-mm-dd'},
		LU : {name : 'LUXEMBOURG', date : ''},
		MO : {name : 'MACAO', date : 'dd/mm/yyyy'},
		MK : {name : 'MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF', date : ''},
		MG : {name : 'MADAGASCAR', date : ''},
		MW : {name : 'MALAWI', date : ''},
		MY : {name : 'MALAYSIA', date : 'dd/mm/yyyy'},
		MV : {name : 'MALDIVES', date : ''},
		ML : {name : 'MALI', date : ''},
		MT : {name : 'MALTA', date : ''},
		MH : {name : 'MARSHALL ISLANDS', date : ''},
		MQ : {name : 'MARTINIQUE', date : ''},
		MR : {name : 'MAURITANIA', date : ''},
		MU : {name : 'MAURITIUS', date : ''},
		YT : {name : 'MAYOTTE', date : ''},
		MX : {name : 'MEXICO', date : 'dd/mm/yyyy'},
		FM : {name : 'MICRONESIA, FEDERATED STATES OF', date : 'mm/dd/yyyy'},
		MD : {name : 'MOLDOVA, REPUBLIC OF', date : ''},
		MC : {name : 'MONACO', date : ''},
		MN : {name : 'MONGOLIA', date : 'yyyy-mm-dd'},
		ME : {name : 'MONTENEGRO', date : 'd.m.yyyy.'},
		MS : {name : 'MONTSERRAT', date : ''},
		MA : {name : 'MOROCCO', date : ''},
		MZ : {name : 'MOZAMBIQUE', date : ''},
		MM : {name : 'MYANMAR', date : ''},
		NA : {name : 'NAMIBIA', date : ''},
		NR : {name : 'NAURU', date : ''},
		NP : {name : 'NEPAL', date : 'yyyy-mm-dd'},
		NL : {name : 'NETHERLANDS', date : 'dd-mm-yyyy'},
		AN : {name : 'NETHERLANDS ANTILLES', date : ''},
		NC : {name : 'NEW CALEDONIA', date : ''},
		NZ : {name : 'NEW ZEALAND', date : 'dd/mm/yyyy'},
		NI : {name : 'NICARAGUA', date : ''},
		NE : {name : 'NIGER', date : ''},
		NG : {name : 'NIGERIA', date : ''},
		NU : {name : 'NIUE', date : ''},
		NF : {name : 'NORFOLK ISLAND', date : ''},
		MP : {name : 'NORTHERN MARIANA ISLANDS', date : ''},
		NO : {name : 'NORWAY', date : 'd.m.y'},
		OM : {name : 'OMAN', date : ''},
		PK : {name : 'PAKISTAN', date : 'dd/mm/yyyy'},
		PW : {name : 'PALAU', date : 'mm/dd/yyyy'},
		PS : {name : 'PALESTINIAN TERRITORY', date : ''},
		PA : {name : 'PANAMA', date : 'dd/mm/yyyy'},
		PG : {name : 'PAPUA NEW GUINEA', date : ''},
		PY : {name : 'PARAGUAY', date : 'dd/mm/yyyy'},
		PE : {name : 'PERU', date : 'dd/mm/yyyy'},
		PH : {name : 'PHILIPPINES', date : 'dd/mm/yyyy'},
		PN : {name : 'PITCAIRN', date : ''},
		PL : {name : 'POLAND', date : 'd.mm.yyyy'},
		PT : {name : 'PORTUGAL', date : 'dd/mm/yyyy'},
		PR : {name : 'PUERTO RICO', date : 'dd/mm/yyyy'},
		QA : {name : 'QATAR', date : ''},
		RE : {name : 'RÉUNION', date : ''},
		RO : {name : 'ROMANIA', date : 'dd/mm/yyyy'},
		RU : {name : 'RUSSIAN FEDERATION', date : 'dd.mm.yyyy'},
		RW : {name : 'RWANDA', date : ''},
		BL : {name : 'SAINT BARTHÉLEMY', date : ''},
		SH : {name : 'SAINT HELENA', date : ''},
		KN : {name : 'SAINT KITTS AND NEVIS', date : 'dd/mm/yyyy'},
		LC : {name : 'SAINT LUCIA', date : 'dd/mm/yyyy'},
		MF : {name : 'SAINT MARTIN', date : ''},
		PM : {name : 'SAINT PIERRE AND MIQUELON', date : ''},
		VC : {name : 'SAINT VINCENT AND THE GRENADINES', date : 'dd/mm/yyyy'},
		WS : {name : 'SAMOA', date : ''},
		SM : {name : 'SAN MARINO', date : ''},
		ST : {name : 'SAO TOME AND PRINCIPE', date : ''},
		SA : {name : 'SAUDI ARABIA', date : ''},
		SN : {name : 'SENEGAL', date : ''},
		RS : {name : 'SERBIA', date : 'd.m.yyyy'},
		SC : {name : 'SEYCHELLES', date : ''},
		SL : {name : 'SIERRA LEONE', date : ''},
		SG : {name : 'SINGAPORE', date : 'dd/mm/yyyy'},
		SK : {name : 'SLOVAKIA', date : 'd.m.yyyy'},
		SI : {name : 'SLOVENIA', date : 'dd/mm/yyyy'},
		SB : {name : 'SOLOMON ISLANDS', date : ''},
		SO : {name : 'SOMALIA', date : ''},
		ZA : {name : 'SOUTH AFRICA', date : 'yyyy-mm-dd'},
		GS : {name : 'SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS', date : ''},
		ES : {name : 'SPAIN', date : 'dd/mm/yyyy'},
		LK : {name : 'SRI LANKA', date : ''},
		SD : {name : 'SUDAN', date : ''},
		SR : {name : 'SURINAME', date : ''},
		SJ : {name : 'SVALBARD AND JAN MAYEN', date : ''},
		SZ : {name : 'SWAZILAND', date : ''},
		SE : {name : 'SWEDEN', date : 'yyyy-mm-dd'},
		CH : {name : 'SWITZERLAND', date : 'dd.mm.yyyy'},
		SY : {name : 'SYRIAN ARAB REPUBLIC', date : ''},
		TW : {name : 'TAIWAN, PROVINCE OF CHINA', date : 'yyyy-mm-dd'},
		TJ : {name : 'TAJIKISTAN', date : ''},
		TZ : {name : 'TANZANIA, UNITED REPUBLIC OF', date : ''},
		TH : {name : 'THAILAND', date : 'dd/mm/yyyy'},
		TL : {name : 'TIMOR-LESTE', date : ''},
		TG : {name : 'TOGO', date : ''},
		TK : {name : 'TOKELAU', date : ''},
		TO : {name : 'TONGA', date : ''},
		TT : {name : 'TRINIDAD AND TOBAGO', date : 'dd/mm/yyyy'},
		TN : {name : 'TUNISIA', date : ''},
		TR : {name : 'TURKEY', date : 'dd/mm/yyyy'},
		TM : {name : 'TURKMENISTAN', date : ''},
		TC : {name : 'TURKS AND CAICOS ISLANDS', date : ''},
		TV : {name : 'TUVALU', date : ''},
		UG : {name : 'UGANDA', date : ''},
		UA : {name : 'UKRAINE', date : 'dd.mm.yyyy'},
		AE : {name : 'UNITED ARAB EMIRATES', date : ''},
		GB : {name : 'UNITED KINGDOM', date : 'dd/mm/yyyy'},
		US : {name : 'UNITED STATES', date : 'mm/dd/yyyy'},
		UM : {name : 'UNITED STATES MINOR OUTLYING ISLANDS', date : 'mm/dd/yyyy'},
		UY : {name : 'URUGUAY', date : 'dd/mm/yyyy'},
		UZ : {name : 'UZBEKISTAN', date : ''},
		VU : {name : 'VANUATU', date : ''},
		VA : {name : 'VATICAN CITY STATE', date : ''},
		VE : {name : 'VENEZUELA', date : 'dd/mm/yyyy'},
		VN : {name : 'VIET NAM', date : 'dd/mm/yyyy'},
		VG : {name : 'VIRGIN ISLANDS, BRITISH', date : ''},
		VI : {name : 'VIRGIN ISLANDS, U.S.', date : ''},
		WF : {name : 'WALLIS AND FUTUNA', date : ''},
		EH : {name : 'WESTERN SAHARA', date : ''},
		YE : {name : 'YEMEN', date : ''},
		CG : {name : 'ZAIRE', date : ''},
		ZM : {name : 'ZAMBIA', date : ''},
		ZW : {name : 'ZIMBABWE', date : ''}		
	},
	
	/*
	 * Formatting functions
	 */	

	formatters : {
		/*
		 * Java-style - single or multiple characters, and '' for literals
		 *  See http://java.sun.com/javase/6/docs/api/java/text/SimpleDateFormat.html
		 * jsorm.Calendar.JAVA = 'J
		 * 
		 */
		'J' : {
			fn : function(fields,format,formatter,cal) {
				/*
				 * here is how we process it
				 * we go through each letter, which has one of several possibilities:
				 * 1) A new letter: we keep it in a buffer, process the last one, and check the next
				 * 2) The same as the previous: increment the count for the previous and check the next
				 * 3) Last letter: treat like 1 or 2, but then process
				 * 4) Non-letter: pass it on as literal
				 * 5) Single-quote (''): everything from the first to the next is treated as a literal
				 */
				var str = '', buf = '', l = '', count = 1, e = false;
				for (var i=0; i<format.length; i++) {
					var f = format[i];
					// Single-quote means literal
					if (f == '\'') {
						// did we have a previous one?
						if (e) {
							str += buf;
							buf = '';
							e = false;
						} else {
							buf = '';
							e = true;
						}
						// we do not record any last letter
						l = '';
					} else if (e) {
						// we are already escaped
						buf += f;
					} else if (f == l) {
						// same as last letter; just increment the count
						count++;
					} else {
						// not the same as the last letter - process the last letter
						if (formatter[l])
							str += formatter[l].fn(fields,count,cal);
						// do we have a formatter for the new letter?
						if (formatter[f]) {
							l = f;
							count = 1;
						} else {
							l = '';
							str += f;
							count = 0;
						}
					}
				}
				// after the last one, we need to process it
				if (formatter[l]) {
					str += formatter[l].fn(fields,count,cal);
				}
				return(str);
			},
			codes : {
				G : {fn: function(fields,len,cal) { return (cal._getTextEra(fields.ERA));}, desc: 'Era designator (AD/BC)'},
				y : {fn: function(fields,len,cal) { 
						return(jsorm.zeropad(fields.YEAR,len).slice(len*-1));
						return(m);
					}, desc: 'Year 07;2007'},
				M : {fn: function(fields,len,cal) {
						var m = fields.MONTH;
						switch (len) {
							case 1:
								m+=1;
								break;
							case 2:
								m = jsorm.zeropad(m+1,len).slice(len*-1);
								break;
							case 3:
								m = cal._getTextMonthShort(fields.MONTH);
								break;
							default:
								m = cal._getTextMonthLong(fields.MONTH);
								break;
						}
						return(m);
					}, desc: 'Month in year 07;Jul;July'},
				w : {fn: function(fields,len,cal) {
						return(jsorm.zeropad(fields.ISO_WEEK_OF_YEAR,len));
					}, desc: 'ISO week in year 1-53'},
				W : {fn: function(fields,len,cal) {
						return(fields.WEEK_OF_MONTH);
					}, desc: 'Week in month 1-5'},
				D : {fn: function(fields,len,cal) {
						return(jsorm.zeropad(fields.DAY_OF_YEAR+1,len));
					}, desc: 'Day in year 1-365'},
				d : {fn: function(fields,len,cal) {
						return(jsorm.zeropad(fields.DATE+1,len));
					}, desc: 'Day in month 1-31'},
				F : {fn: function(fields,len,cal) {
						return(fields.DAY_OF_WEEK_IN_MONTH);
					}, desc: 'Incidence of day of week in month - 2nd Tuesday in month: 2'},
				E : {fn: function(fields,len,cal) {
						var m = len > 3 ? cal._getTextDayOfWeekLong(fields.DAY_OF_WEEK) : cal._getTextDayOfWeekShort(fields.DAY_OF_WEEK);
						return(m);
					}, desc: 'Day in week Tue/Tuesday'},
				a : {fn: function(fields,len,cal) {
						return(cal._getTextAmPm(fields.AM_PM));
					}, desc: 'am/pm marker in capitals AM/PM'},
				H : {fn: function(fields,len,cal) {return(jsorm.zeropad(fields.HOUR_OF_DAY,len)); }, desc: 'Hour in day 0-23'},
				k : {fn: function(fields,len,cal) {return(jsorm.zeropad(fields.HOUR_OF_DAY==0?24:fields.HOUR_OF_DAY,len)); }, desc: 'Hour in day 1-24'},
				K : {fn: function(fields,len,cal) {return(jsorm.zeropad(fields.HOUR%12,len));}, desc: 'Hour in am/pm 0-11'},
				h : {fn: function(fields,len,cal) {return(jsorm.zeropad(fields.HOUR,len));}, desc: 'Hour in am/pm 1-12'},
				m : {fn: function(fields,len,cal) {return(jsorm.zeropad(fields.MINUTE,len))}, desc: 'Minute in hour'},
				s : {fn: function(fields,len,cal) {return(jsorm.zeropad(fields.SECOND,len))}, desc: 'Second in minute'},
				S : {fn: function(fields,len,cal) {return(jsorm.zeropad(fields.MILLISECOND,len))}, desc: 'Millisecond'},
				z : {fn: function(fields,len,cal) {return(fields.ZONENAME);}, desc: 'Time zone - Pacific Standard Time;PST;GMT-08:00'},
				Z : {fn: function(fields,len,cal) {return(fields.RFC822);}, desc: 'Time zone as RFC 822 format - -0800'}
			}
		},
		/*
		 * PHP-style - single characters for each and \ for literals
		 * see http://www.php.net/date
		 * jsorm.Calendar.PHP = 'P'
		 */
		'P' : {
			fn : function(fields,format,formatter,cal) {
				/*
				 * here is how we process it
				 * we go through each letter, which has one of several possibilities:
				 * 1) \ (backslash) - escape the next character to literal
				 * 2) Known letter - transform
				 * 3) Unknown letter - treat as literal
				 */
				var str = '', e = false;
				for (var i=0; i<format.length; i++) {
					var f = format[i];
					// Single-quote means literal
					if (f == '\\') {
						// did we have a previous one?
						if (e) {
							str += f;
							e = false;
						} else {
							e = true;
						}
					} else if (e) {
						// we are already escaped
						str += f;
					} else if (formatter[f]) {
						str += formatter[f].fn(fields,cal);						
					} else {
						str += f;
					}
				}
				return(str);
			},
			codes : {
				d : {fn: function(fields,cal) {return(jsorm.zeropad(fields.DATE+1,2));}, desc: 'Day of the month, 2 digits with leading zeros'},
				D : {fn: function(fields,cal) {return(cal._getTextDayOfWeekShort(fields.DAY_OF_WEEK).substring(0,3));}, desc: 'A textual representation of a day, three letters'},
				j : {fn: function(fields,cal) {return(fields.DATE+1);}, desc: 'Day of the month without leading zeros'},
				l : {fn: function(fields,cal) {return(cal._getTextDayOfWeekLong(fields.DAY_OF_WEEK));}, desc: 'A full textual representation of the day of the week'},
				N : {fn: function(fields,cal) {var dow = fields.DAY_OF_WEEK; return(dow == 0 ? 7 : dow);}, desc: 'ISO-8601 numeric representation of the day of the week 1-7 for Mon-Sun'},
				S : {fn: function(fields,cal) {return(cal._getTextCount(fields.DATE));}, desc: 'English ordinal suffix for the day of the month, 2 characters'},
				w : {fn: function(fields,cal) {return(fields.DAY_OF_WEEK);}, desc: 'Numeric representation of the day of the week 0-6 for Sun-Sat'},
				z : {fn: function(fields,cal) {return(fields.DAY_OF_YEAR);}, desc: 'Day of the year 0-364/5'},
				W : {fn: function(fields,cal) {return(fields.ISO_WEEK_OF_YEAR);}, desc: 'ISO-8601 week number of year, weeks starting on Monday'},
				F : {fn: function(fields,cal) {return(cal._getTextMonthLong(fields.MONTH));}, desc: 'A full textual representation of a month, such as January or March'},
				m : {fn: function(fields,cal) {return(jsorm.zeropad(fields.MONTH+1,2))}, desc: 'Numeric representation of a month, with leading zeros 01-12'},
				M : {fn: function(fields,cal) {return(cal._getTextMonthShort(fields.MONTH).substring(0,3));}, desc: 'A short textual representation of a month, three letters Jan-Dec'},
				n : {fn: function(fields,cal) {return(fields.MONTH+1)}, desc: 'Numeric representation of a month, without leading zeros 1-12'},
				t : {fn: function(fields,cal) {return(cal.getMax('DATE',fields)+1)}, desc: 'Number of days in the given month'},
				L : {fn: function(fields,cal) {return(fields.LEAP);}, desc: 'leap year 1 for yes, 0 for no'},
				o : {fn: function(fields,cal) {return(fields.ISO_YEAR)}, desc: 'ISO-8601 year number. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead.'},
				Y : {fn: function(fields,cal) {return(fields.YEAR);}, desc: 'Full numeric representation of the year'},
				y : {fn: function(fields,cal) {return(jsorm.zeropad(fields.YEAR,2).slice(-2));}, desc: '2-digit numeric representation of the year'},
				a : {fn: function(fields,cal) {return(cal._getTextAmPm(fields.AM_PM).toLowerCase());}, desc: 'Lowercase am and pm'},
				A : {fn: function(fields,cal) {return(cal._getTextAmPm(fields.AM_PM).toUpperCase());}, desc: 'Uppercase AM and PM'},
				g : {fn: function(fields,cal) {return(fields.HOUR)}, desc: '12-hour format of an hour without leading zeros 1-12'},
				G : {fn: function(fields,cal) {return(fields.HOUR_OF_DAY)}, desc: '24-hour format of an hour without leading zeros 0-23'},
				h : {fn: function(fields,cal) {return(jsorm.zeropad(fields.HOUR,2));}, desc: '12-hour format of an hour with leading zeros 01-12'},
				H : {fn: function(fields,cal) {return(jsorm.zeropad(fields.HOUR_OF_DAY,2));}, desc: '24-hour format of an hour with leading zeros 00-23'},
				i : {fn: function(fields,cal) {return(jsorm.zeropad(fields.MINUTE,2));}, desc: 'Minutes with leading zeros 00-59'},
				s : {fn: function(fields,cal) {return(jsorm.zeropad(fields.SECOND,2));}, desc: 'Seconds, with leading zeros 00-59'},
				u : {fn: function(fields,cal) {return(fields.MILLISECOND);}, desc: 'Milliseconds'},
				e : {fn: function(fields,cal) {return(cal.zone.getName());}, desc: 'Timezone'},
				I : {fn: function(fields,cal) {return(fields.DST);}, desc: 'If date is in daylight savings time: 1 for DST, 0 for not'},
				O : {fn: function(fields,cal) {return(fields.RFC822);}, desc: 'Difference to Greenwich time (GMT) in hours -0500'},
				P : {fn: function(fields,cal) {var v = fields.RFC822; var len = v.len; return(v.slice(0,v.length-2)+':'+v.slice(-2));}, desc: 'Difference to Greenwich time (GMT) with colon between hours and minutes -05:00'},
				T : {fn: function(fields,cal) {return(fields.ZONENAME);}, desc: 'Timezone abbreviation EST MDT PST'},
				c : {fn: function(fields,cal) {return(cal.format('Y-m-d H:i:sP',jsorm.Calendar.PHP));}, desc: 'ISO 8601 date 2007-11-27T14:35:50-05:00 equal to Y-m-d H:i:sP'},
				r : {fn: function(fields,cal) {return(cal.format('D, d-M-Y H:i:s O',jsorm.jsorm.Calendar.PHP));}, desc: 'RFC 822 date Thu, 21 Dec 2000 16:01:07 +0200 equal to D, d-M-Y H:i:s O'},
				U : {fn: function(fields,cal) {return(cal.time/1000);}, desc: 'Seconds since the Unix epoch January 1 1970 00:00:00 GMT'}
			}
		},
		/*
		 * strftime - each formatting character is preceded by %, everything else is passed through as literal
		 * see http://opengroup.org/onlinepubs/007908799/xsh/strftime.html	
		 * jsorm.Calendar.STRFTIME = 'S'
		 */
		'S' : {
			fn : function(fields,format,formatter,cal) {
				/*
				 * here is how we process it
				 * we go through each letter, which has one of several possibilities:
				 * 1) % - use the formatter for the next character
				 * 2) Known letter after % - convert
				 * 3) Anything else - literal
				 */
				var str = '', e = false;
				for (var i=0; i<format.length; i++) {
					var f = format[i];
					// Are we already in a formatter
					if (e) {
						// we looking for a formatter
						if (formatter[f]) {
							str += formatter[f].fn(fields,cal);							
						}
						e = false;						
					} else if (f == '%') {
						e = true;
					} else {
						str += f;
					}
				}
				return(str);
			},
			codes : {
				a: {fn: function(fields,cal) { return cal._getTextDayOfWeekShort(fields.DAY_OF_WEEK).substring(0,3) }, desc: 'abbreviated weekday name according to the current locale'},
				A: {fn: function(fields,cal) { return cal._getTextDayOfWeekLong(fields.DAY_OF_WEEK) }, desc: 'full weekday name according to the current locale'},
				b: {fn: function(fields,cal) { return cal._getTextMonthShort(fields.MONTH).substring(0.3) }, desc: 'abbreviated month name according to the current locale'},
				B: {fn: function(fields,cal) { return cal._getTextMonthLong(fields.MONTH) }, desc: 'full month name according to the current locale'},
				c: {fn: function(fields,cal) { return new Date(cal.time).toString() }, desc: 'preferred date and time representation for the current locale'},
				C: {fn: function(fields,cal) { return jsorm.zeropad(fields.YEAR,4).substring(0,2)}, desc: 'century number (the year divided by 100 and truncated to an integer) as a decimal number [00-99].'},
				d: {fn: function(fields,cal) { return jsorm.zeropad(fields.DATE+1,2) }, desc: 'day of the month as a decimal number (range 01 to 31)'},
				D: {fn: function(fields,cal) { return cal.format('%m/%d/%y',jsorm.Calendar.STRFTIME) }, desc: 'Same as %m/%d/%y'},
				e: {fn: function(fields,cal) { var date = fields.DATE+1; return date<10?' '+date:''+date }, desc: 'day of the month as a decimal number [1,31]; a single digit is preceded by a space'},
				h: {fn: function(fields,cal) { return cal.format('%b',jsorm.Calendar.STRFTIME) }, desc: 'Same as %b'},
				H: {fn: function(fields,cal) { return jsorm.zeropad(fields.HOUR_OF_DAY,2) }, desc: 'hour as a decimal number using a 24-hour clock (range 00 to 23)'},
				I: {fn: function(fields,cal) { return jsorm.zeropad(fields.HOUR,2) }, desc: 'hour as a decimal number using a 12-hour clock (range 01 to 12)'},
				j: {fn: function(fields,cal) { return jsorm.zeropad(fields.DAY_OF_YEAR+1,3) }, desc: 'day of the year as a decimal number [001,366]'},
				m: {fn: function(fields,cal) { return jsorm.zeropad(fields.MONTH+1,2) }, desc: 'month as a decimal number (range 01 to 12)'}, // month-1
				M: {fn: function(fields,cal) { return jsorm.zeropad(fields.MINUTE,2) }, desc: 'minute as a decimal number'},
				n: {fn: function(fields,cal) { return '\n' }, desc: 'Newline character'},
				p: {fn: function(fields,cal) { return cal._getTextAmPm(fields.AM_PM).toUpperCase() }, desc: 'either am or pm according to the given time value, or the corresponding strings for the current locale'},
				r: {fn: function(fields,cal) { return cal.format('%I:%M:%S %p',jsorm.Calendar.STRFTIME) }, desc: 'time in a.m. and p.m. notation; in the POSIX locale this is equivalent to %I:%M:%S %p'},
				R: {fn: function(fields,cal) { return cal.format('%H:%M',jsorm.Calendar.STRFTIME) }, desc: 'time in 24 hour notation (%H:%M)'},
				S: {fn: function(fields,cal) { return jsorm.zeropad(fields.SECOND,2) }, desc: 'second as a decimal number'},
				t: {fn: function(fields,cal) { return '\t' }, desc: 'tab character'},
				T: {fn: function(fields,cal) { return cal.format('%H:%M:%S',jsorm.Calendar.STRFTIME) }, desc: 'time (%H:%M:%S)'},
				u: {fn: function(fields,cal) { return fields.DAY_OF_WEEK==0?fields.DAY_OF_WEEK+7:fields.DAY_OF_WEEK }, desc: 'weekday as a decimal number [1,7], with 1 representing Monday'},
				U: {fn: function(fields,cal) { return jsorm.zeropad(fields.WEEK_OF_YEAR,2)}, desc: 'week number of the year (Sunday as the first day of the week) as a decimal number [00,53]'},
				V: {fn: function(fields,cal) { return jsorm.zeropad(fields.WEEK_OF_YEAR+1,2)}, desc: 'week number of the year (Monday as the first day of the week) as a decimal number [01,53]. If the week containing 1 January has four or more days in the new year, then it is considered week 1. Otherwise, it is the last week of the previous year, and the next week is week 1'},
				w: {fn: function(fields,cal) { return fields.DAY_OF_WEEK }, desc: 'day of the week as a decimal, Sunday being 0'}, // 0..6 == sun..sat
				W: {fn: function(fields,cal) { return jsorm.zeropad(fields.WEEK_OF_YEAR,2)}, desc: 'week number of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday are considered to be in week 0'},
				x: {fn: function(fields,cal) { return cal.date()}, desc: 'locale appropriate date representation'}, 
				X: {fn: function(fields,cal) { return cal.time()}, desc: 'locale appropriate time representation'}, 
				y: {fn: function(fields,cal) { return jsorm.zeropad(fields.YEAR,2).slice(-2); }, desc: 'year as a decimal number without a century (range 00 to 99)'},
				Y: {fn: function(fields,cal) { return jsorm.zeropad(fields.YEAR,4) }, desc: 'year as a decimal number including the century'},
				Z: {fn: function(fields,cal) { return fields.ZONENAME }, desc: 'Zone name'},
				'%': {fn: function(fields,cal) { return '%' }, desc: 'Literal %'}
			}
		}
	}	
	
});



