Greasy Fork

eBay Local Time

Converts times on ebay.com and ebay.co.uk to the user's local time

// ==UserScript==
//
// @name          eBay Local Time
// @description   Converts times on ebay.com and ebay.co.uk to the user's local time
// @author        nice_bow_tie
//
// @homepage      http://userscripts.org/scripts/show/153187
//
// @include       http://*.ebay.*
//
// @grant         GM_getValue
// @grant         GM_setValue
//
// @version       1.2
//
// @history       1.2  Add option to toggle between 24 hour and AM/PM display
// @history       1.1  Convert some additional date-time fields
// @history       1.0  Initial release
//
// @namespace https://greasyfork.org/users/2542
// ==/UserScript==


(function(){

	//RFC-2822 month names
	var standard_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
	                       "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

	var eng_long_months = ["January", "February", "March", "April", "May",
	                       "June", "July", "August", "September", "October",
	                       "November", "December"];

	var eng_weekdays    = ["Sunday", "Monday", "Tuesday", "Wednesday",
	                       "Thursday", "Friday", "Saturday"];

	var site_data = {};

	//=========================================================================
	// site_data contains the data used by the script for each supported site
	//=========================================================================
	//
	// site_data[site].abbr_month_names
	// ------------------------------------------------------------------------
	//    an array of short month names used on the site
	//
	// site_data[site].long_month_names
	// ------------------------------------------------------------------------
	//    an array of long month names used on the site
	//
	// site_data[site].weekday_names
	// ------------------------------------------------------------------------
	//    an array of weekday names used on the site
	//
	// site_data[site].timezones
	// ------------------------------------------------------------------------
	//    an array of timezones used on the site, such as PST or BST
	//    the timezone name is followed by the signed 4-digit offset from UTC,
	//    for example "PST:-0800"
	//
	// site_data[site].timezone_periods
	// ------------------------------------------------------------------------
	//    an object defining valid time periods for each timezone, used to find
	//    the correct timezone when it's not part of the displayed time;
	//    each entry is an array of time periods in the format
	//    YYYYMMDDHHMMSS-YYYYMMDDHHMMSS, which define the times when that
	//    timezone is used
	//
	// site_data[site].locations
	// ------------------------------------------------------------------------
	//    an array of {selector, format} objects that identify the location and
	//    format of the times the script should change:
	//    ---------------------------------------------------------------------
	//    'selector' = a CSS selector string that selects the nodes containing
	//    times in the format described by 'format'
	//    if the time is split across > 1 node (eg. the date in one node and
	//    the time in another), those nodes must be consecutive in the list of
	//    nodes matching the selector
	//    ---------------------------------------------------------------------
	//    'format' = a string describing the format of the date/time text in
	//    the nodes (eg. "m-d-y|h:i:s T")
	//    the following placeholders are used to indicate where in the text
	//    that component should appear:
	//            W     : weekday name
	//            d     : 2-digit day number (01-31)
	//            D     : 1 or 2-digit day number (1-31)
	//            m     : short month name
	//            M     : long month name
	//            n     : 2-digit month number (01-12)
	//            Y     : 4-digit year
	//            y     : 2-digit year
	//            h     : 2-digit hour (00-23)
	//            i     : 2-digit minute (00-59)
	//            s     : 2-digit second (00-59)
	//            T     : timezone
	//    other special characters are:
	//            space : matches one or more whitespace characters
	//            |     : indicates where the text is split across nodes
	//    all other characters in the format string are treated as literals
	//
	//=========================================================================

	site_data["ebay.com"] = {};

	site_data["ebay.com"].abbr_month_names = standard_months;
	site_data["ebay.com"].long_month_names = eng_long_months;
	site_data["ebay.com"].weekday_names    = eng_weekdays;
	site_data["ebay.com"].timezones        = ["PST:-0800", "PDT:-0700"];

	site_data["ebay.com"].timezone_periods =
	{
		PST: [ "20080101000000-20080309015959",
		       "20081102020000-20090308015959",
		       "20091101020000-20100314015959",
		       "20101107020000-20110313015959",
		       "20111106020000-20120311015959",
		       "20121104020000-20130310015959",
		       "20131103020000-20140309015959",
		       "20141102020000-20150308015959",
		       "20151101020000-20160313015959",
		       "20161106020000-20170312015959",
		       "20171105020000-20180311015959",
		       "20181104020000-20190310015959",
		       "20191103020000-20200308015959"
		     ],

		PDT: [ "20080309030000-20081102005959",
		       "20090308030000-20091101005959",
		       "20100314030000-20101107005959",
		       "20110313030000-20111106005959",
		       "20120311030000-20121104005959",
		       "20130310030000-20131103005959",
		       "20140309030000-20141102005959",
		       "20150308030000-20151101005959",
		       "20160313030000-20161106005959",
		       "20170312030000-20171105005959",
		       "20180311030000-20181104005959",
		       "20190310030000-20191103005959"
		     ]
	};

	site_data["ebay.com"].locations        =
	[
		//view item page
		{ selector : "div.lable~div span, .vi-is1-dt-eu>span, .vi-is1-dt>span",
		  format   : "m d, Y|h:i:s T"                },

		//revision summary page / bid history page
		{ selector : ".pagecontainer td:nth-child(-n+2), td>div>span, .titleValueFont",
		  format   : "m-d-y|h:i:s T"                 },

		//purchase history page / bid history page (ending time)
		{ selector : ".pagecontainer td, .titleValueFont",
		  format   : "m-d-y h:i:s T"                 },

		//'last revised' time
		{ selector : ".vi-desc-revHistory div, .vi-cmb>div",
		  format   : "m d, Y h:i:s T"                },

		//item page new design: active listings
		{ selector : ".vi-VR-enddate-sec, .rev-date",
		  format   : "m d, Y h:i:s T"                },

		//item page new design: active listings
		{ selector : ".vi-VR-enddate-no-sec",
		  format   : "m d h:i:s T"                   },

		//item new page design: completed listings
		{ selector : ".vi-ended, .vi-ended .endedDate span",
		  format   : "m d, Y|h:i:s T"                },

		//my ebay
		{ selector : ".my_itl-itR div>span",
		  format   : "n/d/y at h:i:s"                },

		//search results
		{ selector : "span.tme>span, div.timeLeftInfo>span",
		  format   : "m-d h:i"                       },

		//feedback
		{ selector : ".FbOuterYukon td:last-child",
		  format   : "m-d-y h:i"                     },
	];

	site_data["ebay.co.uk"] = {};

	site_data["ebay.co.uk"].abbr_month_names = standard_months;
	site_data["ebay.co.uk"].long_month_names = eng_long_months;
	site_data["ebay.co.uk"].weekday_names    = eng_weekdays;
	site_data["ebay.co.uk"].timezones        = ["GMT:+0000", "BST:+0100"];

	site_data["ebay.co.uk"].timezone_periods =
	{
		GMT: [ "20080101000000-20080330005959",
		       "20081026020000-20090329005959",
		       "20091025020000-20100328005959",
		       "20101031020000-20110327005959",
		       "20111030020000-20120325005959",
		       "20121028020000-20130331005959",
		       "20131027020000-20140330005959",
		       "20141026020000-20150329005959",
		       "20151025020000-20160327005959",
		       "20161030020000-20170326005959",
		       "20171029020000-20180325005959",
		       "20181028020000-20190331005959",
		       "20191027020000-20200329005959"
		     ],

		BST: [ "20080330020000-20081026005959",
		       "20090329020000-20091025005959",
		       "20100328020000-20101031005959",
		       "20110327020000-20111030005959",
		       "20120325020000-20121028005959",
		       "20130331020000-20131027005959",
		       "20140330020000-20141026005959",
		       "20150329020000-20151025005959",
		       "20160327020000-20161030005959",
		       "20170326020000-20171029005959",
		       "20180325020000-20181028005959",
		       "20190331020000-20191027005959"
		     ]
	};

	site_data["ebay.co.uk"].locations        =
	[
		//view item page
		{ selector : "div.lable~div span, .vi-is1-dt-eu>span, .vi-is1-dt>span",
		  format   : "d m, Y|h:i:s T"                },

		//revision summary page / bid history page
		{ selector : ".pagecontainer td:nth-child(-n+2), td>div>span, .titleValueFont",
		  format   : "d-m-y|h:i:s T"                 },

		//purchase history page / bid history page (ending time)
		{ selector : ".pagecontainer td, .titleValueFont",
		  format   : "d-m-y h:i:s T"                 },

		//'last revised' time
		{ selector : ".vi-desc-revHistory div, .vi-cmb>div",
		  format   : "d m, Y h:i:s T"                },

		//item page new design: active listings
		{ selector : ".vi-VR-enddate-sec, .rev-date",
		  format   : "d m, Y h:i:s T"                },

		//item page new design: active listings
		{ selector : ".vi-VR-enddate-no-sec",
		  format   : "d m h:i:s T"                   },

		//item new page design: completed listings
		{ selector : ".vi-ended, .vi-ended .endedDate span",
		  format   : "d m, Y|h:i:s T"                },

		//my ebay
		{ selector : ".my_itl-itR div>span",
		  format   : "d/n/y at h:i:s"                },

		//search results
		{ selector : "span.tme>span, div.timeLeftInfo>span",
		  format   : "d-m h:i"                       },

		//feedback
		{ selector : ".FbOuterYukon td:last-child",
		  format   : "d-m-y h:i"                     },
	];

	//=========================================================================

	//get the site data for the current page
	var hostname = window.location.hostname;
	while (hostname && !site_data[hostname])
		hostname = hostname.substr(hostname.indexOf(".")+1 || 9999);
	if (!(site_data = site_data[hostname]))
		return;

	var timezones = [];
	site_data.timezones.forEach(function(tz) {
		timezones = timezones.concat(tz.split(":"));});

	var tz_periods = [];
	timezones.forEach(function(tz)
	{
		if (site_data.timezone_periods && site_data.timezone_periods[tz])
		{
			site_data.timezone_periods[tz].forEach(function(tz_period)
			{
				if (/^\d{14}-\d{14}$/.test(tz_period))
				{
					var a = tz_period.split("-");
					tz_periods.push({start:a[0], end:a[1], tz:tz});
				}
			});
		}
	});

	//regexps that match the format string placeholders
	var regexp_strings =
	{
		W: site_data.weekday_names.join("|"),
		d: "0?[1-9]|[1-2][0-9]|3[0-1]",
		D: "[1-9]|[1-2][0-9]|3[0-1]",
		m: site_data.abbr_month_names.join("|"),
		M: site_data.long_month_names.join("|"),
		n: "0[1-9]|1[012]",
		Y: "20[0-2][0-9]",
		y: "[0-2][0-9]",
		h: "[0-1][0-9]|2[0-3]",
		i: "[0-5][0-9]",
		s: "[0-5][0-9]",
		T: timezones.join("|").replace(/[+-]/g, "\\$&"),
	};

	var use_12_hour_clock = false;
	if (typeof GM_getValue == "function")
		use_12_hour_clock = !!GM_getValue("Use12HourClock");
	
	//chrome workaround
	if (window.chrome)
		use_12_hour_clock = JSON.parse(localStorage.getItem("Use12HourClock"));

	(function ProcessPage()
	{
		var found_nodes = [];

		//find and process the nodes matching each location selector
		site_data.locations.forEach(function(location_data)
		{
			var nodes = [];
			var qsa = document.querySelectorAll(location_data.selector.replace(/\s*(,|$)/g, ":not([nbt-processed])$1"));
			for (var i = 0; i < qsa.length; i++)
			{
				found_nodes.push(qsa[i]);
				for (var j = 0; j < qsa[i].childNodes.length; j++)
				{
					var child = qsa[i].childNodes[j];
					if(child.nodeType == 3 && child.data.match(/\S/))
						nodes.push(child);
				}
			}
			var format_array = location_data.format.split("|");
			var node_count = format_array.length;
			while (nodes.length >= node_count)
			{
				if (!ProcessNode(nodes, format_array, node_count))
					nodes.shift();
			}
		});

		found_nodes.forEach(function(n){n.setAttribute("nbt-processed", 1);});
		window.setTimeout(ProcessPage, 2000);
	})();

	//process node_count text nodes from the node_list
	//if successful, remove the matched node(s) and return true
	function ProcessNode(node_list, format_array, node_count)
	{
		var node_data = [];
		for (var i = 0; i < node_count; i++)
		{
			var format_string = format_array[i];

			//create regexp from the format string and match against the text
			var re = "";
			for (var j = 0; j < format_string.length; j++)
			{
				var c = format_string[j];
				var str = regexp_strings[c] ||
				          c.replace(/[-.+\\{}?*()|[\]]/, "\\$&")
				           .replace(" ", "\\s+");
				re += "(" + str + ")";
			}
			re = "^([\\s\\S]*?)" + re + "([\\s\\S]*?)$";

			var match_result;
			if (match_result = node_list[i].data.match(re))
			{
				match_result.shift();
				match_result.prefix = match_result.shift();
				match_result.suffix = match_result.pop();
				//match_result[0] -> match_result[n] now contain the node text
				//corresponding to characters 0 -> n of the format string,
				//also set node_data[c] to the text corresponding to each
				//placeholder c in the format string
				for (var j = 0; j < format_string.length; j++)
				{
					var c = format_string[j];
					if (regexp_strings[c])
						node_data[c] = match_result[j];
				}
				node_data.push(match_result);
			}
			else return;
		}

		//check it's valid
		if (node_data.length != node_count
			|| !(node_data['m'] || node_data['M'] || node_data['n'])
			|| !(node_data['d'] || node_data['D'])
			|| !(node_data['h'])
		)
			return;

		//create array of strings to be used in the Date.parse argument
		var parse_array = [];
		parse_array[0] = node_data['d'] || node_data['D'];
		var month_number =
			site_data.abbr_month_names.indexOf(node_data['m'])+1 ||
			site_data.long_month_names.indexOf(node_data['M'])+1 ||
			node_data['n'];
		parse_array[1] = standard_months[month_number-1];
		var year = node_data['Y'] || ("20" + node_data['y']);
		if (year.length != 4) //if not found, assume it's the current year
			year = new Date().getFullYear() + "";
		parse_array[2] = year;
		parse_array[3] = node_data['h'];
		parse_array[4] = node_data['i'] || "00";
		parse_array[5] = node_data['s'] || "00";

		if (!node_data['T'])
		{
			//no timezone specified, so get it from tz_periods
			var date_str = year;
			date_str += ("0"+month_number).substr(-2);
			date_str += ("0"+parse_array[0]).substr(-2);
			date_str += parse_array[3] + parse_array[4] + parse_array[5];
			tz_periods.some(function(tz_period)
			{
				if (date_str >= tz_period.start && date_str <= tz_period.end)
					return (node_data['T'] = tz_period.tz);
			});

			if (!node_data['T'])
				return;
		}

		var next_tz = timezones[timezones.indexOf(node_data['T'])+1];
		if (!/^[+-]\d{4}$/.test(next_tz))
			return;
		parse_array[6] = next_tz;

		//parse the time using RFC-2822 format
		var ts = Date.parse("0 1 2 3:4:5 6".replace(/\d/g,
						function(i) {return parse_array[i];}));

		if (!isNaN(ts))
		{
			function fmt(n, pad) {return (n<10 && pad) ? "0"+n : ""+n;}

			//convert to local time
			var date = new Date(ts);
			var new_date =
			{
				W: site_data.weekday_names[date.getDay()],
				d: fmt(date.getDate(), true),
				D: fmt(date.getDate(), false),
				m: site_data.abbr_month_names[date.getMonth()],
				M: site_data.long_month_names[date.getMonth()],
				n: fmt(date.getMonth()+1, true),
				Y: fmt(date.getFullYear()),
				y: fmt(date.getFullYear()).substr(2),
				h: fmt(date.getHours(), true),
				i: fmt(date.getMinutes(), true),
				s: fmt(date.getSeconds(), true),
			};

			//save the original date/time for the tooltip
			var tooltip = [];
			for (var i = 0; i < node_count; i++)
				tooltip.push(node_data[i].join(""));
			tooltip = tooltip.join(" ").replace(/\s+/g, " ");

			//update the node data with the new local time values
			var changed = false;
			node_data.forEach(function(node_data, i)
			{
				var am_pm = "";
				var format_str = format_array[i];
				for (var j = 0; j < format_str.length; j++)
				{
					var c = format_str[j];
					if (c == "T" || (c == " " && format_str[j+1] == "T"))
						node_data[j] = ""; //remove the timezone
					else if (new_date[c] && new_date[c] != node_data[j])
					{
						node_data[j] = new_date[c];
						changed = true;
						if (c == "h" && use_12_hour_clock)
						{
							//change to 12 hour time
							var hours = +node_data[j];
							am_pm = (hours < 12) ? " AM" : " PM";
							node_data[j] = (hours + 11) % 12 + 1;
						}
					}
				}
				if (am_pm != "")
					node_data.push(am_pm);
			});

			for (var i = 0; i < node_count; i++)
			{
				var node = node_list.shift();

				if (changed)
				{
					//the time has changed, so update the DOM with the new time
					var new_node = document.createElement("span");
					new_node.className = "nbt-localtime";
					new_node.style.fontSize = "inherit";
					var prefix = node_data[i].prefix;
					if (prefix)
						new_node.appendChild(document.createTextNode(prefix));
					var span = document.createElement("span");
					span.title = tooltip;
					span.style.fontSize = "inherit";
					var t = document.createTextNode(node_data[i].join(""));
					span.appendChild(t);
					new_node.appendChild(span);
					var suffix = node_data[i].suffix;
					if (suffix)
						new_node.appendChild(document.createTextNode(suffix));

					if (i == node_count-1)
					{
						var star = document.createElement("b");
						star.style.color = "#00a000";
						star.style.fontSize = "inherit";
						star.style.fontWeight = "normal";
						star.style.cursor = "default";
						star.appendChild(document.createTextNode(" * "));

						var ref_node = new_node.lastChild;
						var index = suffix.search(/\s/);
						if (index == 0)
							ref_node = span;
						else if (index > 0)
							new_node.lastChild.splitText(index);

						new_node.insertBefore(star, ref_node.nextSibling);

						if (window.chrome)
						{
							//workaround for a Chrome (Webkit) bug that causes
							//the star to wrap onto a new line
							if (node.parentNode.className == "endedDate")
							{
								var bugfixer = document.createTextNode(" ");
								node.parentNode.insertBefore(bugfixer, node);
							}
						}
					}

					new_node.addEventListener("dblclick", ToggleTimeDisplay, false);
  					node.parentNode.replaceChild(new_node, node);
				}
			}

			return true;
		}
	}

	function ToggleTimeDisplay()
	{
		var success = false;
		if (typeof GM_setValue == "function")
		{
			GM_setValue("Use12HourClock", !use_12_hour_clock);
			success = (GM_getValue("Use12HourClock") === !use_12_hour_clock);
		}
		
		//chrome workaround
		if (window.chrome)
		{
			localStorage.setItem("Use12HourClock", !use_12_hour_clock);
			success = (JSON.parse(localStorage.getItem("Use12HourClock")) === !use_12_hour_clock);
		}

		if (success)
			alert("eBay Local Time display has been changed to: " +
				(use_12_hour_clock ? "24 hour clock" : "12 hour clock (AM/PM)") +
				"\n\n(Takes effect from next page load)");
		else
			alert("Error: eBay Local Time display setting could not be changed " +
				"(requires Greasemonkey or a Greasemonkey-compatible userscript manager)");
	}

})()