User:Ohconfucius/test/MOSNUM utils.js

This is an old revision of this page, as edited by Pathoschild (talk | contribs) at 05:50, 3 April 2015 (use 'token' terminology consistently). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * TemplateScript adds configurable templates and scripts to the sidebar, and adds an example regex editor.
 * @see https://meta.wikimedia.org/wiki/TemplateScript
 * @update-token [[File:pathoschild/templatescript.js]]
 */
mw.loader.load('//tools-static.wmflabs.org/meta/scripts/pathoschild.templatescript.js');

var ohc = ohc || {};

/**
 * Provides internal utilities for manipulating dates in wikitext.
 */
ohc.dateutil = {
	/**
	 * Get the raw pattern from a regular expression object. For example, this
	 * function will convert /x+/ig into "x+".
	 * @param {object} s The regular expression to convert.
	 */
	regex_to_string: function(s) {
		return s.toString()
			.replace(/^\//, "")
			.replace(/\/[^\/]*$/, "");
	},
	
	/**
	 * Show an alert containing an error message.
	 * @param {string} s The message to display.
	 * @param {object} reg The regular expression that caused the error.
	 */
	alert_error: function(s, reg) { //reg can be undefined
		var message = "DATES SCRIPT:\n" + s;
		if (reg !== undefined)
			message += "\n\nRegex" + reg;
		message += "\n\nPlease report the name of the article to [????]";
		alert(message);
	},
	
	/**
	 * Replace regular expression patterns in the given text. The given regex
	 * and substitution strings can contain tokens that specify the date format
	 * to accept and the date format to convert the date to. Leading and
	 * trailing slashes are not needed for the regex.
	 * 
	 * The tokens can be either capturing or non-capturing. The capturing
	 * tokens make their output available for later processing. Most of the
	 * capturing tokens can be used in the output with equivalent meaning.
	 * 
	 * Internally, the routine maintains information about several dates, so
	 * the regex can contain more than one token specifying day, month or year.
	 * The first date will be assigned the first occurrences of tokens from
	 * each group, the second date will be assigned the second occurrences and
	 * so on. For example, if the regex is '@MM @DD @YYYY, @Month @ZM @Day',
	 * the first date will be assigned @MM as months, @DD as days and @YYYY as
	 * years. The second date will be assigned @Month as months and @Day as
	 * days. The third date will be assigned @ZM as months.
	 * 
	 * If used in the replacement string, all tokens in the format @XX will
	 * output the data of the first date. To access the subsequent dates,
	 * tokens in the format @XX2, @XX3, and so on must be used. @XX1 is
	 * provided as alias to @XX for convenience.
	 * 
	 * AVAILABLE REGEX TOKENS
	 * ======================
	 * Note: The capturing tokens start with an uppercase letter whereas the
	 * equivalent non-capturing tokens start with a lowercase letter.
	 * 
	 * Days:
	 *   • @SD  : Matches a day in numeric format without leading zero (1..31).
	 *   • @ZD  : Matches a day in numeric format with leading zero (01..31).
	 *   • @DD  : Matches a day in numeric format with optional leading zero (@SD or @ZD).
	 *   • @Day : Matches a day in numeric format with optional leading zero, with optional st, nd or th suffix. Equivalent to @DD@th?
	 *   • @sd, @zd, ... : noncapturing equivalents.
	 *   • @th  : Matches st, nd, rd or th.
	 *  
	 * Months:
	 *   • @SM  : Matches a month in numeric format without leading zero (1,2..12).
	 *   • @ZM  : Matches a month in numeric format with leading zero (01,02..12).
	 *   • @MM  : Matches a month in numeric format with optional leading zero (@SM or @ZM).
	 *   • @FullMonth : Matches a full month name (January, February).
	 *   • @Mon : Matches a short month name (Jan, Feb, ..). Also optionally matches dot (Jan., Feb., ..) and 'Sept', 'Sept.'.
	 *   • @Month : Matches a full or short month name (@FullMonth or @Mon).
	 *   • @sm, @zm, ... : noncapturing equivalents.
	 *
	 * Years:
	 *   • @YYYY : Matches a 4-digit year.
	 *   • @YY   : Matches a 2-digit year. 50-99 are interpreted as 1950..1999, 0-49 are interpreted as 2000-2049.
	 *   • @YYNN : Matches a 4 or 2-digit year (@YYYY or @YY).
	 *   • @Year : Matches a 1 to 4 digit year.
	 *   • @yyyy, @yy, ... : noncapturing equivalents.
	 *
	 * Special tokens:
	 *   • @@  : Matches a literal @.
	 *
	 * AVAILABLE REPLACEMENT STRING TOKENS
	 * ===================================
	 * Days:
	 *   • @SD   : Outputs a day in numeric format without leading zero (1-31).
	 *   • @ZD   : Outputs a day in numeric format with leading zero (01-31).
	 *   • @DD   : Equivalent to @ZD.
	 *   • @Day  : Equivalent to @SD.
	 *   • @LDay : Outputs the matched day string without any transformations.
	 *   • @SDn, @ZDn, ... : Where n is integer, outputs the day of the nth date in the specified format.
	 *
	 * Months:
	 *   • @SM  : Outputs a month in numeric format without leading zero (1-12).
	 *   • @ZM  : Outputs a month in numeric format with leading zero (01-12).
	 *   • @MM  : Equivalent to @ZM.
	 *   • @FullMonth : Outputs a full name of a month (January, February).
	 *   • @Mon    : Outputs a short name of a month (Jan, Feb).
	 *   • @Month  : Equivalent to @FullMonth.
	 *   • @LMonth : Outputs the matched month string without any transformations.
	 *   • @SMn, @ZMn, ... : Where n is integer, outputs the month of the nth date in the specified format.
	 *
	 * Years:
	 *   • @YYYY   Outputs 4-digit year. Valid only if the year was not captured by @Year.
	 *   • @YY     Outputs 2-digit year. Outputs the last two digits of a year. Valid only if the year was not captured by @Year.
	 *   • @YYNN   Equivalent to @YYYY
	 *   • @Year   Outputs 1 to 4 digit number identifying a year. Equivalent to @YYYY if the year is between 1000 and 9999.
	 *   • @LYear  Outputs the matched year string without any transformations.
	 *   • @YYYYn, @YYn, ... : Where n is integer, outputs the year of the nth date in the specified format.
	 * 
	 * Special tokens:
	 *   • @@ : Outputs a literal @.
	 * 
	 * All tokens in the replacement string which refer to data which isn't
	 * defined will be replaced with '@ERROR@'.
	 * 
	 * An optional function defining whether to make the replacement in particular
	 * cases can be provided. The function is supplied a number of parameters, each
	 * of which is an object defining the nth date as parsed by the routine. Each
	 * object contains numeric values of days, months and years as 'd', 'm' and 'y'
	 * properties respectively. Each value can be -1 if a token for that date
	 * value was not specified in the regex, or an error occurs. The function
	 * should return true if the replacement should be done, false otherwise.
	 * 	
	 * @param {string} text The text to change.
	 * @param {object|string} rg The regular expression to search in the text.
	 * @param {string} sub The pattern to replace matches with.
	 * @param {function} func A callback passed the matching date information
	 *        which returns whether to continue with the replacement.
	 */
	regex: function(text, rg, sub, func) {
		var reg = ohc.dateutil.regex_to_string(rg);
		var debug_reg = reg;
	
		var month_names = new Array("January", "February", "March", "April", "May",
			"June", "July", "August", "September", "October", "November", "December");
		var month_names_short = new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun",
			"Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
	
		var MAX_DATE = 4;
	
		var ParamType = {
			REGULAR : 1,
			
			SD : 10,
			ZD : 11,
			DD : 12,
			DAY : 13,
			
			SM : 20,
			ZM : 21,
			MM : 22,
			FMONTH : 23,
			MONTH : 24,
			MON : 25,
			
			YYYY : 30,
			YYNN : 31,
			YY : 32,
			YEAR : 33
		};
	
		var Group = {
			DAY : 0,
			MONTH : 1,
			YEAR : 2
		};
	
		var Formats = [
			{ type : ParamType.SD, group : Group.DAY,  token : "@SD", match : /([1-9]|[1-2][0-9]|30|31)/ },
			{ type : ParamType.ZD, group : Group.DAY,  token : "@ZD", match : /(0[1-9]|[1-2][0-9]|30|31)/ },
			{ type : ParamType.DD, group : Group.DAY,  token : "@DD", match : /(0?[1-9]|[1-2][0-9]|30|31)/ },
			{ type : ParamType.DAY,group : Group.DAY,  token : "@Day",match : /((?:[012]?[1-9]|10|20|30|31)(?:st|nd|rd|th|)?)/ },
			{ type : ParamType.SM, group : Group.MONTH,token : "@SM", match : /([1-9]|10|11|12)/ },
			{ type : ParamType.ZM, group : Group.MONTH,token : "@ZM", match : /(0[1-9]|10|11|12)/ },
			{ type : ParamType.MM, group : Group.MONTH,token : "@MM", match : /(0?[1-9]|10|11|12)/ },
			{
				type : ParamType.FMONTH, group : Group.MONTH, token : "@FullMonth",
				match : /(January|February|March|April|May|June|July|August|September|October|November|December)/
			},
			{
				type : ParamType.MONTH, group : Group.MONTH, token : "@Month",
				match : /(January|February|March|April|May|June|July|August|September|October|November|December|Jan\.|Jan|Feb\.|Feb|Mar\.|Mar|Apr\.|Apr|May|Jun\.|Jun|Jul\.|Jul|Aug\.|Aug|Sep\.|Sept\.|Sept|Sep|Oct\.|Oct|Nov\.|Nov|Dec\.|Dec)/
			},
			{ //must be after month entry
				type : ParamType.MON, group : Group.MONTH, token : "@Mon",
				match : /(Jan\.|Jan|Feb\.|Feb|Mar\.|Mar|Apr\.|Apr|May|Jun\.|Jun|Jul\.|Jul|Aug\.|Aug|Sep\.|Sept\.|Sept|Sep|Oct\.|Oct|Nov\.|Nov|Dec\.|Dec)/
			},
			{ type : ParamType.YYYY, group : Group.YEAR, token : "@YYYY", match : "([1-2][0-9]{3})" },
			{ type : ParamType.YYNN, group : Group.YEAR, token : "@YYNN", match : "([1-2][0-9]{3}|[0-9]{2})" },
			{ type : ParamType.YY,   group : Group.YEAR, token : "@YY", match : "([0-9]{2})" }, //must be after yyyy and yy24 entries
			{ type : ParamType.YEAR, group : Group.YEAR, token : "@Year", match : "([1-2][0-9]{3}|[1-9][0-9]{0,2})" }
		];
	
		var NCFormats = [
			{ token : "@sd", match : /(?:[1-9]|[1-2][0-9]|30|31)/ },
			{ token : "@zd", match : /(?:0[1-9]|[1-2][0-9]|30|31)/ },
			{ token : "@dd", match : /(?:0?[1-9]|[1-2][0-9]|30|31)/ },
			{ token : "@day",match : /(?:(?:[012]?[1-9]|10|20|30|31)(?:st|nd|rd|th|)?)/ },
			{ token : "@sm", match : /(?:[1-9]|10|11|12)/ },
			{ token : "@zm", match : /(?:0[1-9]|10|11|12)/ },
			{ token : "@mm", match : /(?:0?[1-9]|10|11|12)/ },
			{
				token : "@fullmonth",
				match : /(?:January|February|March|April|May|June|July|August|September|October|November|December)/
			},
			{
				token : "@month",
				match : /(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan\.|Jan|Feb\.|Feb|Mar\.|Mar|Apr\.|Apr|May|Jun\.|Jun|Jul\.|Jul|Aug\.|Aug|Sep\.|Sept\.|Sept|Sep|Oct\.|Oct|Nov\.|Nov|Dec\.|Dec)/
			},
			{ //must be after month entry
				token : "@mon",
				match : /(?:Jan\.|Jan|Feb\.|Feb|Mar\.|Mar|Apr\.|Apr|May|Jun\.|Jun|Jul\.|Jul|Aug\.|Aug|Sep\.|Sept\.|Sept|Sep|Oct\.|Oct|Nov\.|Nov|Dec\.|Dec)/
			},
			{ token : "@yyyy", match : /(?:[1-2][0-9]{3})/ },
			{ token : "@yynn", match : "(?:[1-2][0-9]{3}|[0-9]{2})" },
			{ token : "@yy", match : "(?:[0-9]{2})" }, //must be after yyyy and yy24 entries
			{ token : "@year", match : "(?:[1-2][0-9]{3}|[1-9][0-9]{0,2})" },
			// misc
			{ token : "@th", match : "(?:th|st|nd|rd)" }
		];
	
		// get positions of all capturing matches in the regex
		var params_by_index = {};
		var capt_regex = /(?:^|[^\\])\((?!(?:\?:|\?=|\?!))/g;
	
		var match;
		var pi = 0;
		while (match = capt_regex.exec(reg)) {
			var param = {};
			param.index = match.index;
			param.type = ParamType.REGULAR;
			param.num = pi;
			params_by_index[match.index] = param;
			pi++;
		}
	
		// get positions of all capturing tokens in the regex
		var token_per_group = [0,0,0];
	
		for (var i = 0; i < Formats.length; i++) {
			var index = -1;
			while (1) {
				index = reg.indexOf(Formats[i].token, index+1);
				if (index == -1)
					break;
				if (params_by_index[index] === undefined) {
						if (token_per_group[index] > MAX_DATE) {
							alert("DATE SCRIPT: unsupported number of dates from the same group");
						return;
					}
					var param = {};
					param.index = index;
					param.type = Formats[i].type;
					param.num = token_per_group[Formats[i].group];
					params_by_index[index] = param;
					token_per_group[Formats[i].group]++;
				}
			}
		}
	
		// pack the resulting array and sort by index
		var param_desc = [];
		for (var i in params_by_index) {
			param_desc.push(params_by_index[i]);
		}
		param_desc.sort(function(a,b) {return a.index - b.index;});
	
		//replace tokens with proper matches
		for (var i = 0; i < Formats.length; i++) {
			reg = reg.split(Formats[i].token).join(ohc.dateutil.regex_to_string(Formats[i].match));
		}
		for (var i = 0; i < NCFormats.length; i++) {
			reg = reg.split(NCFormats[i].token).join(ohc.dateutil.regex_to_string(NCFormats[i].match));
		}
		reg = reg.split("@@").join("@");
	
		//inline function for month parsing
		function parse_month(str) {
			var imonth = str.toLowerCase();
			for (var i = 0; i < month_names_short.length; i++) {
				if (imonth.substr(0,3) == month_names_short[i].toLowerCase()) {
					return i+1;
				}
			}
			return -1;
		}
	
		//inline function for 2-digit year parsing
		function parse_yy(str) {
			var y = parseInt(str, 10);
			if (y > 99) {
				return -1;
			}
			else if (y >= 20) {
				return y + 1900;
			}
			else {
				return y + 2000;
			}
		}
	
		//inline function which will do the replacement
		function regex_worker(str, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10,
		                      m11, m12, m13, m14, m15, m16, m17, m18, m19) {
			var MAX_REGEX = 20;
	
			// computed numeric values
			var day = [];
			var month = [];
			var year = [];
	
			// string values to output
			var raw_day = [];
			var raw_month = [];
			var raw_year = [];
			var sday = [];
			var zday = [];
			var smonth = [];
			var zmonth = [];
			var full_month = [];
			var short_month = [];
			var year_yy = [];
			var year_yyyy = [];
	
			var regex_param = [];
	
			//fill in the initial values
			for (var i = 0; i < MAX_DATE; i++) {
				raw_day[i] = raw_month[i] = raw_year[i] = "@ERROR@";
				sday[i] = zday[i] = smonth[i] = zmonth[i] = full_month[i] = short_month[i] = "@ERROR@";
				year_yy[i] = year_yyyy[i] = "@ERROR@";
				day[i] = month[i] = year[i] = -1;
			}
	
			// save all arguments as an array
			var params = [];
			params.push(m0); params.push(m1); params.push(m2); params.push(m3);
			params.push(m4); params.push(m5); params.push(m6); params.push(m7);
			params.push(m8); params.push(m9); params.push(m10); params.push(m11);
			params.push(m12); params.push(m13); params.push(m14); params.push(m15);
			params.push(m16); params.push(m17); params.push(m18); params.push(m19);
	
			// parse the arguments according to the specification given as param_desc
			for (var i = 0; i < param_desc.length; i++) {
				if (i > 19) {
					alert("DATE SCRIPT: param id out of bounds");
					return str;
				}
	
				var c_param = params[i];    //current param
				var c_desc = param_desc[i]; //current param description
	
				switch (c_desc.type) {
					case ParamType.REGULAR:
						regex_param[c_desc.num] = c_param;
						break;
	
					case ParamType.SD:
					case ParamType.ZD:
					case ParamType.DD:
						day[c_desc.num] = parseInt(c_param, 10);
						raw_day[c_desc.num] = c_param;
						break;
	
					case ParamType.DAY:
						day[c_desc.num] = parseInt(c_param.replace(/[^0-9]/g, ''), 10);
						raw_day[c_desc.num] = c_param;
						break;
	
					case ParamType.SM:
					case ParamType.ZM:
					case ParamType.MM:
						month[c_desc.num] = parseInt(c_param, 10);
						raw_month[c_desc.num] = c_param;
						break;
	
					case ParamType.FMONTH:
					case ParamType.MONTH:
					case ParamType.MON:
						month[c_desc.num] = parse_month(c_param);
						raw_month[c_desc.num] = c_param;
						break;
	
					case ParamType.YY:
						year[c_desc.num] = parse_yy(c_param);
						raw_year[c_desc.num] = c_param;
						break;
	
					case ParamType.YEAR:
					case ParamType.YYYY:
						year[c_desc.num] = parseInt(c_param, 10);
						raw_year[c_desc.num] = c_param;
						break;
	
					case ParamType.YYNN:
						var yy = parse_yy(c_param, 10);
						if (yy == -1) {
							yy = parseInt(c_param, 10);
						}
						year[c_desc.num] = yy;
						raw_year[c_desc.num] = c_param;
						break;
				}
			}
	
			//catch errors, if any
			for (var i = 0; i < MAX_DATE; i++) {
				if (day[i] == 0 || day[i] < -1 || day[i] > 31) {
					ohc.dateutil.alert_error("Invalid day [" + i + "]=" + day[i], debug_reg);
					day[i] = -1;
				}
	
				if (month[i] == 0 || month[i] < -1 || month[i] > 12) {
					ohc.dateutil.alert_error("Invalid month [" + i + "]=" + month[i], debug_reg);
					month[i] = -1;
				}
	
				if (year[i] == 0 || year[i] < -1 || year[i] > 9999) {
					ohc.dateutil.alert_error("Invalid year [" + i + "]=" + year[i], debug_reg);
					year[i] = -1;
				}
			}
	
			//check whether to make the replacement
			if (func !== undefined) {
				var d = [];
				for (var i = 0; i < MAX_DATE; i++) {
					var di = {};
					di.d = day[i]; di.m = month[i]; di.y = year[i];
					d.push(di);
				}
				if (func(d[0], d[1], d[2], d[3]) === false) {
					return str;
				}
			}
	
			//compute all needed formats
			for (var i = 0; i < MAX_DATE; i++) {
				if (day[i] != -1) {
					zday[i] = sday[i] = day[i].toString();
					if (day[i] < 10) {
						zday[i] = "0" + zday[i];
					}
				}
	
				if (month[i] != -1) {
					zmonth[i] = smonth[i] = month[i].toString();
					if (month[i] < 10) {
						zmonth[i] = "0" + zmonth[i];
					}
	
					full_month[i] = month_names[month[i]-1];
					short_month[i] = month_names_short[month[i]-1];
				}
	
				if (year[i] != -1) {
					year_yyyy[i] = year[i].toString();
					if (year[i] >= 1950 && year[i] < 2050) {
						year_yy[i] = year_yyyy[i].charAt(2) + year_yyyy[i].charAt(3);
					}
				}
			}
	
			//replace
			var csub = sub;
			csub = csub.split("$1").join(regex_param[0]);
			csub = csub.split("$2").join(regex_param[1]);
			csub = csub.split("$3").join(regex_param[2]);
			csub = csub.split("$4").join(regex_param[3]);
			csub = csub.split("$5").join(regex_param[4]);
			csub = csub.split("$6").join(regex_param[5]);
			csub = csub.split("$7").join(regex_param[6]);
			csub = csub.split("$8").join(regex_param[7]);
			csub = csub.split("$9").join(regex_param[8]);
			csub = csub.split("$10").join(regex_param[9]);
			csub = csub.split("$11").join(regex_param[10]);
			csub = csub.split("$12").join(regex_param[11]);
			csub = csub.split("$13").join(regex_param[12]);
			csub = csub.split("$14").join(regex_param[13]);
			csub = csub.split("$15").join(regex_param[14]);
			csub = csub.split("$16").join(regex_param[15]);
			csub = csub.split("$17").join(regex_param[16]);
			csub = csub.split("$18").join(regex_param[17]);
			csub = csub.split("$19").join(regex_param[18]);
			csub = csub.split("$20").join(regex_param[19]);
	
			csub = csub.split("@SD4").join(sday[3]);
			csub = csub.split("@ZD4").join(zday[3]);
			csub = csub.split("@DD4").join(zday[3]);
			csub = csub.split("@Day4").join(sday[3]);
			csub = csub.split("@LDay4").join(raw_day[3]);
			csub = csub.split("@SM4").join(smonth[3]);
			csub = csub.split("@ZM4").join(zmonth[3]);
			csub = csub.split("@MM4").join(zmonth[3]);
			csub = csub.split("@FullMonth4").join(full_month[3]);
			csub = csub.split("@Month4").join(full_month[3]);
			csub = csub.split("@Mon4").join(short_month[3]);
			csub = csub.split("@LMonth4").join(raw_month[3]);
			csub = csub.split("@YYYY4").join(year_yyyy[3]);
			csub = csub.split("@YYNN4").join(year_yyyy[3]);
			csub = csub.split("@Year4").join(year_yyyy[3]);
			csub = csub.split("@YY4").join(year_yy[3]);
			csub = csub.split("@LYear4").join(raw_year[3]);
	
			csub = csub.split("@SD3").join(sday[2]);
			csub = csub.split("@ZD3").join(zday[2]);
			csub = csub.split("@DD3").join(zday[2]);
			csub = csub.split("@Day3").join(sday[2]);
			csub = csub.split("@LDay3").join(raw_day[2]);
			csub = csub.split("@SM3").join(smonth[2]);
			csub = csub.split("@ZM3").join(zmonth[2]);
			csub = csub.split("@MM3").join(zmonth[2]);
			csub = csub.split("@FullMonth3").join(full_month[2]);
			csub = csub.split("@Month3").join(full_month[2]);
			csub = csub.split("@Mon3").join(short_month[2]);
			csub = csub.split("@LMonth3").join(raw_month[2]);
			csub = csub.split("@YYYY3").join(year_yyyy[2]);
			csub = csub.split("@YYNN3").join(year_yyyy[2]);
			csub = csub.split("@Year3").join(year_yyyy[2]);
			csub = csub.split("@YY3").join(year_yy[2]);
			csub = csub.split("@LYear3").join(raw_year[2]);
	
			csub = csub.split("@SD2").join(sday[1]);
			csub = csub.split("@ZD2").join(zday[1]);
			csub = csub.split("@DD2").join(zday[1]);
			csub = csub.split("@Day2").join(sday[1]);
			csub = csub.split("@LDay2").join(raw_day[1]);
			csub = csub.split("@SM2").join(smonth[1]);
			csub = csub.split("@ZM2").join(zmonth[1]);
			csub = csub.split("@MM2").join(zmonth[1]);
			csub = csub.split("@FullMonth2").join(full_month[1]);
			csub = csub.split("@Month2").join(full_month[1]);
			csub = csub.split("@Mon2").join(short_month[1]);
			csub = csub.split("@LMonth2").join(raw_month[1]);
			csub = csub.split("@YYYY2").join(year_yyyy[1]);
			csub = csub.split("@YYNN2").join(year_yyyy[1]);
			csub = csub.split("@Year2").join(year_yyyy[1]);
			csub = csub.split("@YY2").join(year_yy[1]);
			csub = csub.split("@LYear2").join(raw_year[1]);
	
			csub = csub.split("@SD1").join(sday[0]);
			csub = csub.split("@ZD1").join(zday[0]);
			csub = csub.split("@DD1").join(zday[0]);
			csub = csub.split("@Day1").join(sday[0]);
			csub = csub.split("@LDay1").join(raw_day[0]);
			csub = csub.split("@SM1").join(smonth[0]);
			csub = csub.split("@ZM1").join(zmonth[0]);
			csub = csub.split("@MM1").join(zmonth[0]);
			csub = csub.split("@FullMonth1").join(full_month[0]);
			csub = csub.split("@Month1").join(full_month[0]);
			csub = csub.split("@Mon1").join(short_month[0]);
			csub = csub.split("@LMonth1").join(raw_month[0]);
			csub = csub.split("@YYYY1").join(year_yyyy[0]);
			csub = csub.split("@YYNN1").join(year_yyyy[0]);
			csub = csub.split("@Year1").join(year_yyyy[0]);
			csub = csub.split("@YY1").join(year_yy[0]);
			csub = csub.split("@LYear1").join(raw_year[0]);
	
			csub = csub.split("@SD").join(sday[0]);
			csub = csub.split("@ZD").join(zday[0]);
			csub = csub.split("@DD").join(zday[0]);
			csub = csub.split("@Day").join(sday[0]);
			csub = csub.split("@LDay").join(raw_day[0]);
			csub = csub.split("@SM").join(smonth[0]);
			csub = csub.split("@ZM").join(zmonth[0]);
			csub = csub.split("@MM").join(zmonth[0]);
			csub = csub.split("@FullMonth").join(full_month[0]);
			csub = csub.split("@Month").join(full_month[0]); //this must be executed before the @Mon rule
			csub = csub.split("@Mon").join(short_month[0]);
			csub = csub.split("@LMonth").join(raw_month[0]);
			csub = csub.split("@YYYY").join(year_yyyy[0]); //this must be executed before the @YY rule
			csub = csub.split("@YYNN").join(year_yyyy[0]);
			csub = csub.split("@Year").join(year_yyyy[0]);
			csub = csub.split("@YY").join(year_yy[0]);
			csub = csub.split("@LYear").join(raw_year[0]);
	
			csub = csub.split("@@").join("@");
			return csub;
		}
	
		//check whether a simple regex (i.e. without using regex_worker) would suffice
		var simple_regex = true;
		for (var i = 0; i < param_desc.length; i++) {
			if (param_desc[i].type != ParamType.REGULAR) {
				simple_regex = false;
				break;
			}
		}
	
		//do the replacement
		try {
			var rg = new RegExp(reg,'gi');
	
			if (simple_regex == true)
				text = text.replace(rg, sub);
			else
				text = text.replace(rg, regex_worker);
	
			var aa_reg = debug_reg; //place for a breakpoint for debugging
		}
		catch(err) {
			var message = "Error in regex execution\n" + "ERROR: " + err.message + "\n";
			ohc.dateutil.alert_error(message, debug_reg);
		}
		return text;
	}
};

/**
 * Extends string objects with a chainable method to replace patterns containing
 * special date tokens.
 * @see ohc.dateutils.regex
 */
String.prototype.ohc_regex = function(rg, sub, func) {
	return ohc.dateutil.regex(this.toString(), rg, sub, func);
};