User:Pathoschild/script/MOSNUM utils.js

From Wikipedia, the free encyclopedia
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.
	 * 
	 * Warning: These tokens should only be used in patterns that contain the
	 * expected date pattern (like /@YYYY-@ZM-@ZD/), because they're not restricted
	 * to valid matches. For example, /@ZD/ (zero-padded day) will match the
	 * '20' and '14' in 2014.
	 * 
	 * 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;
	},
	
	/**
	 * Run JavaScript unit tests and print the results to the console.
	 */
	runUnitTests: function() {
		$.ajax('//tools-static.wmflabs.org/meta/scripts/pathoschild.util.js', { dataType:'script', cache:true }).then(function() {
			pathoschild.util.RunTests(function() {
				var expect = chai.expect;
				describe('regex_to_string', function() {
					var regex_to_string = ohc.dateutil.regex_to_string;
		
					it('converts a simple /./ pattern', function() {
						expect(regex_to_string(/./))
							.to.equal('.');
					});
					it('converts a simple /.+/ pattern', function() {
						expect(regex_to_string(/.+/))
							.to.equal('.+');
					});
					it('converts a simple /\\/.+/ pattern with an escaped forward slash', function() {
						expect(regex_to_string(/\/.+/))
							.to.equal('\\/.+');
					});
					it('converts a simple /\\/.+/ig pattern with an escaped forward slash and regex modifiers', function() {
						expect(regex_to_string(/\/.+/ig))
							.to.equal('\\/.+');
					});
				});
				describe('regex', function() {
					var regex = ohc.dateutil.regex;
		
					describe('special tokens', function() {
						// literal @
						it('/@@/ matches a literal @', function() {
							expect(regex('2014-01-01 @ @@', /@@/, 'X'))
								.to.equal('2014-01-01 X XX');
						});
		
						// ordinal suffix
						it('/@th/ matches valid ordinal suffixes (st, nd, rd, th)', function() {
							expect(regex('st nd rd th', /\b@th\b/, 'X'))
								.to.equal('X X X X');
						});
		
						it('/@th/ ignores invalid ordinal suffixes', function() {
							expect(regex('first second third fourth raster 1 14', /\b@th\b/, 'X'))
								.to.equal('first second third fourth raster 1 14');
						});
					});
		
					describe('grouping day tokens', function() {
						// day without leading zero
						it('@SD (day without leading zero) captures valid values', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@SD\b/, '[@SD]'))
								.to.equal('[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27] [28] [29] [30] [31]');
						});
						it('@SD (day without leading zero) ignores invalid values', function() {
							expect(regex('0 01 010 2001 1st first', /\b@SD\b/, '[@SD]'))
								.to.equal('0 01 010 2001 1st first');
						});
		
						// day with leading zero
						it('@ZD (day with leading zero) captures valid values', function() {
							expect(regex('01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@ZD\b/, '[@ZD]'))
								.to.equal('[01] [02] [03] [04] [05] [06] [07] [08] [09] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27] [28] [29] [30] [31]');
						});
						it('@ZD (day with leading zero) ignores invalid values', function() {
							expect(regex('0 00 1 2 3 4 5 6 7 8 9 100 101 001 1st first', /\b@ZD\b/, '[@ZD]'))
								.to.equal('0 00 1 2 3 4 5 6 7 8 9 100 101 001 1st first');
						});
		
						// day with optional leading zero
						it('@DD (day with optional leading zero) captures valid values without leading zero', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@DD\b/, '[@DD]'))
								.to.equal('[01] [02] [03] [04] [05] [06] [07] [08] [09] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27] [28] [29] [30] [31]');
						});
						it('@DD (day with optional leading zero) captures valid values with leading zero', function() {
							expect(regex('01 02 03 04 05 06 07 08 09', /\b@DD\b/, '[@DD]'))
								.to.equal('[01] [02] [03] [04] [05] [06] [07] [08] [09]');
						});
						it('@DD (day with optional leading zero) ignores invalid values', function() {
							expect(regex('0 00 010 100 101 001 1st first', /\b@DD\b/, '[@DD]'))
								.to.equal('0 00 010 100 101 001 1st first');
						});
		
						// day with optional leading zero and optional ordinal suffix
						it('@Day (day with optional leading zero and optional ordinal suffix) captures valid values without leading zero or ordinal', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@Day\b/, '[@Day]'))
								.to.equal('[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27] [28] [29] [30] [31]');
						});
						it('@Day (day with optional leading zero and optional ordinal suffix) captures valid values without leading zero, but with ordinal', function() {
							expect(regex('1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th 30th 31st', /\b@Day\b/, '[@Day]'))
								.to.equal('[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27] [28] [29] [30] [31]');
						});
					});
		
					describe('non-grouping day tokens', function() {
						// day without leading zero
						it('@sd (day without leading zero) matches valid values', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@sd\b/, 'X'))
								.to.equal('X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X');
						});
						it('@sd (day without leading zero) ignores invalid values', function() {
							expect(regex('0 01 010 2001 1st first', /\b@sd\b/, 'X'))
								.to.equal('0 01 010 2001 1st first');
						});
		
						// day with leading zero
						it('@zd (day with leading zero) matches valid values', function() {
							expect(regex('01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@zd\b/, 'X'))
								.to.equal('X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X');
						});
						it('@zd (day with leading zero) ignores invalid values', function() {
							expect(regex('0 00 1 2 3 4 5 6 7 8 9 100 101 001 1st first', /\b@zd\b/, 'X'))
								.to.equal('0 00 1 2 3 4 5 6 7 8 9 100 101 001 1st first');
						});
		
						// day with optional leading zero
						it('@dd (day with optional leading zero) matches valid values without leading zero', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@dd\b/, 'X'))
								.to.equal('X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X');
						});
						it('@dd (day with optional leading zero) matches valid values with leading zero', function() {
							expect(regex('01 02 03 04 05 06 07 08 09', /\b@dd\b/, 'X'))
								.to.equal('X X X X X X X X X');
						});
						it('@dd (day with optional leading zero) ignores invalid values', function() {
							expect(regex('0 00 010 100 101 001 1st first', /\b@dd\b/, 'X'))
								.to.equal('0 00 010 100 101 001 1st first');
						});
		
						// day with optional leading zero and optional ordinal suffix
						it('@day (day with optional leading zero and optional ordinal suffix) matches valid values without leading zero or ordinal', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31', /\b@day\b/, 'X'))
								.to.equal('X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X');
						});
						it('@day (day with optional leading zero and optional ordinal suffix) matches valid values without leading zero, but with ordinal', function() {
							expect(regex('1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th 30th 31st', /\b@day\b/, 'X'))
								.to.equal('X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X');
						});
					});
		
					describe('grouping month tokens', function() {
						// month without leading zero
						it('@SM (month without leading zero) captures valid values', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12', /\b@SM\b/, '[@SM]'))
								.to.equal('[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12]');
						});
						it('@SM (month without leading zero) ignores invalid values', function() {
							expect(regex('0 01 02 03 04 05 06 07 08 09 010 13 21 2001', /\b@SM\b/, '[@SM]'))
								.to.equal('0 01 02 03 04 05 06 07 08 09 010 13 21 2001');
						});
		
						// month with leading zero
						it('@ZM (month with leading zero) captures valid values', function() {
							expect(regex('01 02 03 04 05 06 07 08 09 10 11 12', /\b@ZM\b/, '[@ZM]'))
								.to.equal('[01] [02] [03] [04] [05] [06] [07] [08] [09] [10] [11] [12]');
						});
						it('@ZM (month with leading zero) ignores invalid values', function() {
							expect(regex('0 1 2 3 4 5 6 7 8 9 010 13 21 2001', /\b@ZM\b/, '[@ZM]'))
								.to.equal('0 1 2 3 4 5 6 7 8 9 010 13 21 2001');
						});
		
						// month with optional leading zero
						it('@MM (month with optional leading zero) captures valid values without leading zero', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12', /\b@MM\b/, '[@MM]'))
								.to.equal('[01] [02] [03] [04] [05] [06] [07] [08] [09] [10] [11] [12]');
						});
						it('@MM (month with optional leading zero) captures valid values with leading zero', function() {
							expect(regex('01 02 03 04 05 06 07 08 09', /\b@MM\b/, '[@MM]'))
								.to.equal('[01] [02] [03] [04] [05] [06] [07] [08] [09]');
						});
						it('@MM (month with optional leading zero) ignores invalid values', function() {
							expect(regex('0 00 010 13 14 15 16 17 18 19 20 100 101 001', /\b@MM\b/, '[@MM]'))
								.to.equal('0 00 010 13 14 15 16 17 18 19 20 100 101 001');
						});
		
						// full month name
						it('@FullMonth (full month name) captures valid values', function() {
							expect(regex('January February March April May June July August September October November December', /\b@FullMonth\b/, '[@FullMonth]'))
								.to.equal('[January] [February] [March] [April] [May] [June] [July] [August] [September] [October] [November] [December]');
						});
						it('@FullMonth (full month name) ignores invalid values', function() {
							expect(regex('0 1 2 3 4 5 6 7 8 9 10 11 12 jan Jan feb Feb mar Mar apr Apr jun Jun jul Jul aug Aug sep Sep oct Oct nov Nov dec Dec test', /\b@FullMonth\b/, '[@FullMonth]'))
								.to.equal('0 1 2 3 4 5 6 7 8 9 10 11 12 jan Jan feb Feb mar Mar apr Apr jun Jun jul Jul aug Aug sep Sep oct Oct nov Nov dec Dec test');
						});
		
						// short month name
						it('@Mon (short month name) captures valid values', function() {
							expect(regex('Jan Jan. Feb Feb. Mar Mar. Apr Apr. May Jun Jun. Jul Jul. Aug Aug. Sep Sep. Oct Oct. Nov Nov. Dec Dec. ', /\b@Mon /, '[@Mon] '))
								.to.equal('[Jan] [Jan] [Feb] [Feb] [Mar] [Mar] [Apr] [Apr] [May] [Jun] [Jun] [Jul] [Jul] [Aug] [Aug] [Sep] [Sep] [Oct] [Oct] [Nov] [Nov] [Dec] [Dec] ');
						});
						it('@Mon (short month name) ignores invalid values', function() {
							expect(regex('0 1 2 3 4 5 6 7 8 9 10 11 12 january January february February march March april April june June july July august August september September october October november November december December test', /\b@Mon\b/, '[@Mon]'))
								.to.equal('0 1 2 3 4 5 6 7 8 9 10 11 12 january January february February march March april April june June july July august August september September october October november November december December test');
						});
		
						// full or short name
						it('@Month (full or short month name) captures valid long values', function() {
							expect(regex('January February March April May June July August September October November December', /\b@Month\b/, '[@Month]'))
								.to.equal('[January] [February] [March] [April] [May] [June] [July] [August] [September] [October] [November] [December]');
						});
						it('@Month (full or short month name) captures valid short values', function() {
							expect(regex('Jan Jan. Feb Feb. Mar Mar. Apr Apr. May Jun Jun. Jul Jul. Aug Aug. Sep Sep. Oct Oct. Nov Nov. Dec Dec. ', /\b@Month /, '[@Month] '))
								.to.equal('[January] [January] [February] [February] [March] [March] [April] [April] [May] [June] [June] [July] [July] [August] [August] [September] [September] [October] [October] [November] [November] [December] [December] ');
						});
					});
		
					describe('non-grouping month tokens', function() {
						// month without leading zero
						it('@sm (month without leading zero) matches valid values', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12', /\b@sm\b/, '[@sm]'))
								.to.equal('[@sm] [@sm] [@sm] [@sm] [@sm] [@sm] [@sm] [@sm] [@sm] [@sm] [@sm] [@sm]');
						});
						it('@sm (month without leading zero) ignores invalid values', function() {
							expect(regex('0 01 02 03 04 05 06 07 08 09 010 13 21 2001', /\b@sm\b/, 'X'))
								.to.equal('0 01 02 03 04 05 06 07 08 09 010 13 21 2001');
						});
		
						// month with leading zero
						it('@zm (month with leading zero) matches valid values', function() {
							expect(regex('01 02 03 04 05 06 07 08 09 10 11 12', /\b@zm\b/, '[@zm]'))
								.to.equal('[@zm] [@zm] [@zm] [@zm] [@zm] [@zm] [@zm] [@zm] [@zm] [@zm] [@zm] [@zm]');
						});
						it('@zm (month with leading zero) ignores invalid values', function() {
							expect(regex('0 1 2 3 4 5 6 7 8 9 010 13 21 2001', /\b@zm\b/, '[@zm]'))
								.to.equal('0 1 2 3 4 5 6 7 8 9 010 13 21 2001');
						});
		
						// month with optional leading zero
						it('@mm (month with optional leading zero) matches valid values without leading zero', function() {
							expect(regex('1 2 3 4 5 6 7 8 9 10 11 12', /\b@mm\b/, '[@mm]'))
								.to.equal('[@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm]');
						});
						it('@mm (month with optional leading zero) matches valid values with leading zero', function() {
							expect(regex('01 02 03 04 05 06 07 08 09', /\b@mm\b/, '[@mm]'))
								.to.equal('[@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm] [@mm]');
						});
						it('@mm (month with optional leading zero) ignores invalid values', function() {
							expect(regex('0 00 010 13 14 15 16 17 18 19 20 100 101 001', /\b@mm\b/, '[@mm]'))
								.to.equal('0 00 010 13 14 15 16 17 18 19 20 100 101 001');
						});
		
						// full month name
						it('@FullMonth (full month name) matches valid values', function() {
							expect(regex('January February March April May June July August September October November December', /\b@fullmonth\b/, '[@fullmonth]'))
								.to.equal('[@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth] [@fullmonth]');
						});
						it('@fullmonth (full month name) ignores invalid values', function() {
							expect(regex('0 1 2 3 4 5 6 7 8 9 10 11 12 jan Jan feb Feb mar Mar apr Apr jun Jun jul Jul aug Aug sep Sep oct Oct nov Nov dec Dec test', /\b@fullmonth\b/, '[@fullmonth]'))
								.to.equal('0 1 2 3 4 5 6 7 8 9 10 11 12 jan Jan feb Feb mar Mar apr Apr jun Jun jul Jul aug Aug sep Sep oct Oct nov Nov dec Dec test');
						});
		
						// short month name
						it('@mon (short month name) matches valid values', function() {
							expect(regex('Jan Jan. Feb Feb. Mar Mar. Apr Apr. May Jun Jun. Jul Jul. Aug Aug. Sep Sep. Oct Oct. Nov Nov. Dec Dec. ', /\b@mon /, '[@mon] '))
								.to.equal('[@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] [@mon] ');
						});
						it('@mon (short month name) ignores invalid values', function() {
							expect(regex('0 1 2 3 4 5 6 7 8 9 10 11 12 january January february February march March april April june June july July august August september September october October november November december December test', /\b@mon\b/, '[@mon]'))
								.to.equal('0 1 2 3 4 5 6 7 8 9 10 11 12 january January february February march March april April june June july July august August september September october October november November december December test');
						});
		
						// full or short name
						it('@month (full or short month name) matches valid long values', function() {
							expect(regex('January February March April May June July August September October November December', /\b@month\b/, '[@month]'))
								.to.equal('[@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month]');
						});
						it('@month (full or short month name) matches valid short values', function() {
							expect(regex('Jan Jan. Feb Feb. Mar Mar. Apr Apr. May Jun Jun. Jul Jul. Aug Aug. Sep Sep. Oct Oct. Nov Nov. Dec Dec. ', /\b@month /, '[@month] '))
								.to.equal('[@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] [@month] ');
						});
					});
				});
			});
		});
	}
};

/**
 * 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);
};

/*********
** legacy compatibility wrappers
********/
function ohc_regex_to_string(s) {
	return ohc.dateutil.regex_to_string(s);
}
function ohc_alert_error(s, reg) {
	return ohc.dateutil.alert_error(s, reg);
}
function ohc_regex(rg, sub, func) {
	var editbox = $('#wpTextbox1');

	var text = ohc.dateutil.regex(editbox.val(), rg, sub, func);

	editbox.val(text);
}