www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

date.js (24698B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 Zotero.Date = new function(){
     27 	this.isMultipart = isMultipart;
     28 	this.multipartToSQL = multipartToSQL;
     29 	this.multipartToStr = multipartToStr;
     30 	this.isSQLDate = isSQLDate;
     31 	this.isSQLDateTime = isSQLDateTime;
     32 	this.sqlHasYear = sqlHasYear;
     33 	this.sqlHasMonth = sqlHasMonth;
     34 	this.sqlHasDay = sqlHasDay;
     35 	this.getUnixTimestamp = getUnixTimestamp;
     36 	this.toUnixTimestamp = toUnixTimestamp;
     37 	this.getFileDateString = getFileDateString;
     38 	this.getFileTimeString = getFileTimeString;
     39 	this.getLocaleDateOrder = getLocaleDateOrder;
     40 	
     41 	var _localeDateOrder = null;
     42 	var _months;
     43 	var _monthsWithEnglish;
     44 	
     45 	this.init = function () {
     46 		if (!Zotero.isFx || Zotero.isBookmarklet) {
     47 			throw new Error("Unimplemented");
     48 		}
     49 		
     50 		return Zotero.HTTP.request(
     51 			'GET', 'resource://zotero/schema/dateFormats.json', { responseType: 'json' }
     52 		).then(function(xmlhttp) {
     53 			var json = xmlhttp.response;
     54 			
     55 			var locale = Zotero.locale;
     56 			var english = locale.startsWith('en');
     57 			// If no exact match, try first two characters ('de')
     58 			if (!json[locale]) {
     59 				locale = locale.substr(0, 2);
     60 			}
     61 			// Try first two characters repeated ('de-DE')
     62 			if (!json[locale]) {
     63 				locale = locale + "-" + locale.toUpperCase();
     64 			}
     65 			// Look for another locale with same first two characters
     66 			if (!json[locale]) {
     67 				let sameLang = Object.keys(json).filter(l => l.startsWith(locale.substr(0, 2)));
     68 				if (sameLang.length) {
     69 					locale = sameLang[0];
     70 				}
     71 			}
     72 			// If all else fails, use English
     73 			if (!json[locale]) {
     74 				locale = 'en-US';
     75 				english = true;
     76 			}
     77 			_months = json[locale];
     78 
     79 			// Add English versions if not already added
     80 			if (english) {
     81 				_monthsWithEnglish = _months;
     82 			}
     83 			else {
     84 				_monthsWithEnglish = {};
     85 				for (let key in _months) {
     86 					_monthsWithEnglish[key] = _months[key].concat(json['en-US'][key]);
     87 				}
     88 			}
     89 		});
     90 	};
     91 	
     92 	
     93 	/**
     94 	 * @param {Boolean} [withEnglish = false] - Include English months
     95 	 * @return {Object} - Object with 'short' and 'long' arrays
     96 	 */
     97 	this.getMonths = function (withEnglish) {
     98 		if (withEnglish) {
     99 			if (_monthsWithEnglish) return _monthsWithEnglish;
    100 		}
    101 		else {
    102 			if (_months) return _months;
    103 		}
    104 		
    105 		if (Zotero.isFx && !Zotero.isBookmarklet) {
    106 			throw new Error("Months not cached");
    107 		}
    108 		
    109 		// TODO: Use JSON file for connectors
    110 		return _months = _monthsWithEnglish = {
    111 			short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
    112 			long: ["January", "February", "March", "April", "May", "June", "July", "August",
    113 				"September", "October", "November", "December"]};
    114 	}
    115 	
    116 	/**
    117 	* Convert an SQL date in the form '2006-06-13 11:03:05' into a JS Date object
    118 	*
    119 	* Can also accept just the date part (e.g. '2006-06-13')
    120 	**/
    121 	this.sqlToDate = function (sqldate, isUTC) {
    122 		try {
    123 			if (!this.isSQLDate(sqldate) && !this.isSQLDateTime(sqldate)) {
    124 				throw new Error("Invalid date");
    125 			}
    126 			
    127 			var datetime = sqldate.split(' ');
    128 			var dateparts = datetime[0].split('-');
    129 			if (datetime[1]){
    130 				var timeparts = datetime[1].split(':');
    131 			}
    132 			else {
    133 				timeparts = [false, false, false];
    134 			}
    135 			
    136 			// Invalid date part
    137 			if (dateparts.length==1){
    138 				throw new Error("Invalid date part");
    139 			}
    140 			
    141 			if (isUTC){
    142 				return new Date(Date.UTC(dateparts[0], dateparts[1]-1, dateparts[2],
    143 					timeparts[0], timeparts[1], timeparts[2]));
    144 			}
    145 			
    146 			return new Date(dateparts[0], dateparts[1]-1, dateparts[2],
    147 				timeparts[0], timeparts[1], timeparts[2]);
    148 		}
    149 		catch (e){
    150 			Zotero.debug(sqldate + ' is not a valid SQL date', 2)
    151 			return false;
    152 		}
    153 	}
    154 	
    155 	
    156 	/**
    157 	* Convert a JS Date object to an SQL date in the form '2006-06-13 11:03:05'
    158 	*
    159 	* If _toUTC_ is true, creates a UTC date
    160 	**/
    161 	this.dateToSQL = function (date, toUTC) {
    162 		try {
    163 			if (toUTC){
    164 				var year = date.getUTCFullYear();
    165 				var month = date.getUTCMonth();
    166 				var day = date.getUTCDate();
    167 				var hours = date.getUTCHours();
    168 				var minutes = date.getUTCMinutes();
    169 				var seconds = date.getUTCSeconds();
    170 			}
    171 			else {
    172 				var year = date.getFullYear();
    173 				var month = date.getMonth();
    174 				var day = date.getDate();
    175 				var hours = date.getHours();
    176 				var minutes = date.getMinutes();
    177 				var seconds = date.getSeconds();
    178 			}
    179 			
    180 			year = Zotero.Utilities.lpad(year, '0', 4);
    181 			month = Zotero.Utilities.lpad(month + 1, '0', 2);
    182 			day = Zotero.Utilities.lpad(day, '0', 2);
    183 			hours = Zotero.Utilities.lpad(hours, '0', 2);
    184 			minutes = Zotero.Utilities.lpad(minutes, '0', 2);
    185 			seconds = Zotero.Utilities.lpad(seconds, '0', 2);
    186 			
    187 			return year + '-' + month + '-' + day + ' '
    188 				+ hours + ':' + minutes + ':' + seconds;
    189 		}
    190 		catch (e){
    191 			Zotero.debug(date + ' is not a valid JS date', 2);
    192 			return '';
    193 		}
    194 	}
    195 	
    196 	
    197 	/**
    198 	 * Convert a JS Date object to an ISO 8601 UTC date/time
    199 	 *
    200 	 * @param	{Date}		date		JS Date object
    201 	 * @return	{String}				ISO 8601 UTC date/time
    202 	 *									e.g. 2008-08-15T20:00:00Z
    203 	 */
    204 	this.dateToISO = function (date) {
    205 		var year = date.getUTCFullYear();
    206 		var month = date.getUTCMonth();
    207 		var day = date.getUTCDate();
    208 		var hours = date.getUTCHours();
    209 		var minutes = date.getUTCMinutes();
    210 		var seconds = date.getUTCSeconds();
    211 		
    212 		year = Zotero.Utilities.lpad(year, '0', 4);
    213 		month = Zotero.Utilities.lpad(month + 1, '0', 2);
    214 		day = Zotero.Utilities.lpad(day, '0', 2);
    215 		hours = Zotero.Utilities.lpad(hours, '0', 2);
    216 		minutes = Zotero.Utilities.lpad(minutes, '0', 2);
    217 		seconds = Zotero.Utilities.lpad(seconds, '0', 2);
    218 		
    219 		return year + '-' + month + '-' + day + 'T'
    220 			+ hours + ':' + minutes + ':' + seconds + 'Z';
    221 	}
    222 	
    223 	
    224 	var _re8601 = /^([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/;
    225 	
    226 	/**
    227 	 * @return {Boolean} - True if string is an ISO 8601 date, false if not
    228 	 */
    229 	this.isISODate = function (str) {
    230 		return _re8601.test(str);
    231 	}
    232 	
    233 	/**
    234 	 * Convert an ISO 8601–formatted date/time to a JS Date
    235 	 *
    236 	 * Adapted from http://delete.me.uk/2005/03/iso8601.html (AFL-licensed)
    237 	 *
    238 	 * @param	{String}		isoDate		ISO 8601 date
    239 	 * @return {Date|False} - JS Date, or false if not a valid date
    240 	 */
    241 	this.isoToDate = function (isoDate) {
    242 		var d = isoDate.match(_re8601);
    243 		if (!d) return false;
    244 		
    245 		var offset = 0;
    246 		var date = new Date(d[1], 0, 1);
    247 		
    248 		if (d[3]) { date.setMonth(d[3] - 1); }
    249 		if (d[5]) { date.setDate(d[5]); }
    250 		if (d[7]) { date.setHours(d[7]); }
    251 		if (d[8]) { date.setMinutes(d[8]); }
    252 		if (d[10]) { date.setSeconds(d[10]); }
    253 		if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
    254 		if (d[14]) {
    255 			offset = (Number(d[16]) * 60) + Number(d[17]);
    256 			offset *= ((d[15] == '-') ? 1 : -1);
    257 		}
    258 		
    259 		offset -= date.getTimezoneOffset();
    260 		var time = (Number(date) + (offset * 60 * 1000));
    261 		return new Date(time);
    262 	}
    263 	
    264 	
    265 	this.isoToSQL = function (isoDate) {
    266 		return Zotero.Date.dateToSQL(Zotero.Date.isoToDate(isoDate), true); // no 'this' for translator sandbox
    267 	}
    268 	
    269 	
    270 	/*
    271 	 * converts a string to an object containing:
    272 	 *    day: integer form of the day
    273 	 *    month: integer form of the month (indexed from 0, not 1)
    274 	 *    year: 4 digit year (or, year + BC/AD/etc.)
    275 	 *    part: anything that does not fall under any of the above categories
    276 	 *          (e.g., "Summer," etc.)
    277 	 *
    278 	 * Note: the returned object is *not* a JS Date object
    279 	 */
    280 	var _slashRe = /^(.*?)\b([0-9]{1,4})(?:([\-\/\.\u5e74])([0-9]{1,2}))?(?:([\-\/\.\u6708])([0-9]{1,4}))?((?:\b|[^0-9]).*?)$/
    281 	var _yearRe = /^(.*?)\b((?:circa |around |about |c\.? ?)?[0-9]{1,4}(?: ?B\.? ?C\.?(?: ?E\.?)?| ?C\.? ?E\.?| ?A\.? ?D\.?)|[0-9]{3,4})\b(.*?)$/i;
    282 	var _monthRe = null;
    283 	var _dayRe = null;
    284 	
    285 	this.strToDate = function (string) {
    286 		var date = {
    287 			order: ''
    288 		};
    289 		
    290 		// skip empty things
    291 		if(!string) {
    292 			return date;
    293 		}
    294 		
    295 		var parts = [];
    296 		
    297 		// Parse 'yesterday'/'today'/'tomorrow'
    298 		var lc = (string + '').toLowerCase();
    299 		if (lc == 'yesterday' || (Zotero.isClient && lc === Zotero.getString('date.yesterday'))) {
    300 			string = Zotero.Date.dateToSQL(new Date(Date.now() - 1000*60*60*24)).substr(0, 10); // no 'this' for translator sandbox
    301 		}
    302 		else if (lc == 'today' || (Zotero.isClient && lc == Zotero.getString('date.today'))) {
    303 			string = Zotero.Date.dateToSQL(new Date()).substr(0, 10);
    304 		}
    305 		else if (lc == 'tomorrow' || (Zotero.isClient && lc == Zotero.getString('date.tomorrow'))) {
    306 			string = Zotero.Date.dateToSQL(new Date(Date.now() + 1000*60*60*24)).substr(0, 10);
    307 		}
    308 		else {
    309 			string = string.toString().replace(/^\s+|\s+$/g, "").replace(/\s+/, " ");
    310 		}
    311 		
    312 		// first, directly inspect the string
    313 		var m = _slashRe.exec(string);
    314 		if(m &&
    315 		  ((!m[5] || !m[3]) || m[3] == m[5] || (m[3] == "\u5e74" && m[5] == "\u6708")) &&	// require sane separators
    316 		  ((m[2] && m[4] && m[6]) || (!m[1] && !m[7]))) {						// require that either all parts are found,
    317 		  																		// or else this is the entire date field
    318 			// figure out date based on parts
    319 			if(m[2].length == 3 || m[2].length == 4 || m[3] == "\u5e74") {
    320 				// ISO 8601 style date (big endian)
    321 				date.year = m[2];
    322 				date.month = m[4];
    323 				date.day = m[6];
    324 				date.order += m[2] ? 'y' : '';
    325 				date.order += m[4] ? 'm' : '';
    326 				date.order += m[6] ? 'd' : '';
    327 			} else if(m[2] && !m[4] && m[6]) {
    328 				date.month = m[2];
    329 				date.year = m[6];
    330 				date.order += m[2] ? 'm' : '';
    331 				date.order += m[6] ? 'y' : '';
    332 			} else {
    333 				// local style date (middle or little endian)
    334 				var country = Zotero.locale ? Zotero.locale.substr(3) : "US";
    335 				if(country == "US" ||	// The United States
    336 				   country == "FM" ||	// The Federated States of Micronesia
    337 				   country == "PW" ||	// Palau
    338 				   country == "PH") {	// The Philippines
    339 					date.month = m[2];
    340 					date.day = m[4];
    341 					date.order += m[2] ? 'm' : '';
    342 					date.order += m[4] ? 'd' : '';
    343 				} else {
    344 					date.month = m[4];
    345 					date.day = m[2];
    346 					date.order += m[2] ? 'd' : '';
    347 					date.order += m[4] ? 'm' : '';
    348 				}
    349 				date.year = m[6];
    350 				date.order += 'y';
    351 			}
    352 			
    353 			if(date.year) date.year = parseInt(date.year, 10);
    354 			if(date.day) date.day = parseInt(date.day, 10);
    355 			if(date.month) {
    356 				date.month = parseInt(date.month, 10);
    357 				
    358 				if(date.month > 12) {
    359 					// swap day and month
    360 					var tmp = date.day;
    361 					date.day = date.month
    362 					date.month = tmp;
    363 					date.order = date.order.replace('m', 'D')
    364 						.replace('d', 'M')
    365 						.replace('D', 'd')
    366 						.replace('M', 'm');
    367 				}
    368 			}
    369 			
    370 			if((!date.month || date.month <= 12) && (!date.day || date.day <= 31)) {
    371 				if(date.year && date.year < 100) {	// for two digit years, determine proper
    372 													// four digit year
    373 					var today = new Date();
    374 					var year = today.getFullYear();
    375 					var twoDigitYear = year % 100;
    376 					var century = year - twoDigitYear;
    377 					
    378 					if(date.year <= twoDigitYear) {
    379 						// assume this date is from our century
    380 						date.year = century + date.year;
    381 					} else {
    382 						// assume this date is from the previous century
    383 						date.year = century - 100 + date.year;
    384 					}
    385 				}
    386 				
    387 				if(date.month) date.month--;		// subtract one for JS style
    388 				else delete date.month;
    389 				
    390 				//Zotero.debug("DATE: retrieved with algorithms: "+JSON.stringify(date));
    391 				
    392 				parts.push(
    393 					{ part: m[1], before: true },
    394 					{ part: m[7] }
    395 				);
    396 			} else {
    397 				// give up; we failed the sanity check
    398 				Zotero.debug("DATE: algorithms failed sanity check");
    399 				var date = {
    400 					order: ''
    401 				};
    402 				parts.push({ part: string });
    403 			}
    404 		} else {
    405 			//Zotero.debug("DATE: could not apply algorithms");
    406 			parts.push({ part: string });
    407 		}
    408 		
    409 		// couldn't find something with the algorithms; use regexp
    410 		// YEAR
    411 		if(!date.year) {
    412 			for (var i in parts) {
    413 				var m = _yearRe.exec(parts[i].part);
    414 				if (m) {
    415 					date.year = m[2];
    416 					date.order = _insertDateOrderPart(date.order, 'y', parts[i]);
    417 					parts.splice(
    418 						i, 1,
    419 						{ part: m[1], before: true },
    420 						{ part: m[3] }
    421 					);
    422 					//Zotero.debug("DATE: got year (" + date.year + ", " + JSON.stringify(parts) + ")");
    423 					break;
    424 				}
    425 			}
    426 		}
    427 		
    428 		// MONTH
    429 		if(date.month === undefined) {
    430 			// compile month regular expression
    431 			let months = Zotero.Date.getMonths(true); // no 'this' for translator sandbox
    432 			months = months.short.map(m => m.toLowerCase())
    433 				.concat(months.long.map(m => m.toLowerCase()));
    434 			
    435 			if(!_monthRe) {
    436 				_monthRe = new RegExp("^(.*)\\b("+months.join("|")+")[^ ]*(?: (.*)$|$)", "i");
    437 			}
    438 			
    439 			for (var i in parts) {
    440 				var m = _monthRe.exec(parts[i].part);
    441 				if (m) {
    442 					// Modulo 12 in case we have multiple languages
    443 					date.month = months.indexOf(m[2].toLowerCase()) % 12;
    444 					date.order = _insertDateOrderPart(date.order, 'm', parts[i]);
    445 					parts.splice(
    446 						i, 1,
    447 						{ part: m[1], before: "m" },
    448 						{ part: m[3], after: "m" }
    449 					);
    450 					//Zotero.debug("DATE: got month (" + date.month + ", " + JSON.stringify(parts) + ")");
    451 					break;
    452 				}
    453 			}
    454 		}
    455 		
    456 		// DAY
    457 		if(!date.day) {
    458 			// compile day regular expression
    459 			if(!_dayRe) {
    460 				var daySuffixes = Zotero.getString ? Zotero.getString("date.daySuffixes").replace(/, ?/g, "|") : "";
    461 				_dayRe = new RegExp("\\b([0-9]{1,2})(?:"+daySuffixes+")?\\b(.*)", "i");
    462 			}
    463 			
    464 			for (var i in parts) {
    465 				var m = _dayRe.exec(parts[i].part);
    466 				if (m) {
    467 					var day = parseInt(m[1], 10);
    468 					// Sanity check
    469 					if (day <= 31) {
    470 						date.day = day;
    471 						date.order = _insertDateOrderPart(date.order, 'd', parts[i]);
    472 						if(m.index > 0) {
    473 							var part = parts[i].part.substr(0, m.index);
    474 							if(m[2]) {
    475 								part += " " + m[2];;
    476 							}
    477 						} else {
    478 							var part = m[2];
    479 						}
    480 						parts.splice(
    481 							i, 1,
    482 							{ part: part }
    483 						);
    484 						//Zotero.debug("DATE: got day (" + date.day + ", " + JSON.stringify(parts) + ")");
    485 						break;
    486 					}
    487 				}
    488 			}
    489 		}
    490 		
    491 		// Concatenate date parts
    492 		date.part = '';
    493 		for (var i in parts) {
    494 			date.part += parts[i].part + ' ';
    495 		}
    496 		
    497 		// clean up date part
    498 		if(date.part) {
    499 			date.part = date.part.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, "");
    500 		}
    501 		
    502 		if(date.part === "" || date.part == undefined) {
    503 			delete date.part;
    504 		}
    505 		
    506 		//make sure year is always a string
    507 		if(date.year || date.year === 0) date.year += '';
    508 		
    509 		return date;
    510 	}
    511 	
    512 	this.isHTTPDate = function(str) {
    513 		var dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
    514 		var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
    515 					"Oct", "Nov", "Dec"];
    516 		str = str.trim();
    517 		var temp = str.split(',');
    518 		if (temp.length > 1) {
    519 			var dayOfWeek = temp[0];
    520 			if(dayNames.indexOf(dayOfWeek) == -1) {
    521 				return false;
    522 			}
    523 
    524 			str = temp[1].trim();
    525 		}
    526 		temp = str.split(' ');
    527 		temp = temp.filter((t) => ! t.match(/^\s*$/));
    528 		if (temp.length < 5) {
    529 			return false;
    530 		}
    531 		if (!temp[0].trim().match(/[0-3]\d/)) {
    532 			return false;
    533 		}
    534 		if (monthNames.indexOf(temp[1].trim()) == -1) {
    535 			return false;
    536 		}
    537 		if (!temp[2].trim().match(/\d\d\d\d/)) {
    538 			return false;
    539 		}
    540 		temp.splice(0, 3);
    541 		var time = temp[0].trim().split(':');
    542 		if (time.length < 2) {
    543 			return false;
    544 		}
    545 		for (let t of time) {
    546 			if (!t.match(/\d\d/)) {
    547 				return false;
    548 			}
    549 		}
    550 		temp.splice(0, 1);
    551 		var zone = temp.join(' ').trim();
    552 		return !!zone.match(/([+-]\d\d\d\d|UTC?|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT)/)
    553 	};
    554 	
    555 	
    556 	function _insertDateOrderPart(dateOrder, part, partOrder) {
    557 		if (!dateOrder) {
    558 			return part;
    559 		}
    560 		if (partOrder.before === true) {
    561 			return part + dateOrder;
    562 		}
    563 		if (partOrder.after === true) {
    564 			return dateOrder + part;
    565 		}
    566 		if (partOrder.before) {
    567 			var pos = dateOrder.indexOf(partOrder.before);
    568 			if (pos == -1) {
    569 				return dateOrder;
    570 			}
    571 			return dateOrder.replace(new RegExp("(" + partOrder.before + ")"), part + '$1');
    572 		}
    573 		if (partOrder.after) {
    574 			var pos = dateOrder.indexOf(partOrder.after);
    575 			if (pos == -1) {
    576 				return dateOrder + part;
    577 			}
    578 			return dateOrder.replace(new RegExp("(" + partOrder.after + ")"), '$1' + part);
    579 		}
    580 		return dateOrder + part;
    581 	}
    582 	
    583 	
    584 	
    585 	/**
    586 	 * does pretty formatting of a date object returned by strToDate()
    587 	 *
    588 	 * @param {Object} date A date object, as returned from strToDate()
    589 	 * @param {Boolean} shortFormat Whether to return a short (12/1/95) date
    590 	 * @return A formatted date string
    591 	 * @type String
    592 	 **/
    593 	this.formatDate = function (date, shortFormat) {
    594 		if(shortFormat) {
    595 			var localeDateOrder = getLocaleDateOrder();
    596 			var string = localeDateOrder[0]+"/"+localeDateOrder[1]+"/"+localeDateOrder[2];
    597 			return string.replace("y", (date.year !== undefined ? date.year : "00"))
    598 			             .replace("m", (date.month !== undefined ? 1+date.month : "0"))
    599 			             .replace("d", (date.day !== undefined ? date.day : "0"));
    600 		} else {
    601 			var string = "";
    602 			
    603 			if(date.part) {
    604 				string += date.part+" ";
    605 			}
    606 			
    607 			var months = Zotero.Date.getMonths().long; // no 'this' for translator sandbox
    608 			if(date.month != undefined && months[date.month]) {
    609 				// get short month strings from CSL interpreter
    610 				string += months[date.month];
    611 				if(date.day) {
    612 					string += " "+date.day+", ";
    613 				} else {
    614 					string += " ";
    615 				}
    616 			}
    617 			
    618 			if(date.year) {
    619 				string += date.year;
    620 			}
    621 		}
    622 		
    623 		return string;
    624 	}
    625 	
    626 	this.strToISO = function (str) {
    627 		var date = this.strToDate(str);
    628 		
    629 		if(date.year) {
    630 			var dateString = Zotero.Utilities.lpad(date.year, "0", 4);
    631 			if (parseInt(date.month) == date.month) {
    632 				dateString += "-"+Zotero.Utilities.lpad(date.month+1, "0", 2);
    633 				if(date.day) {
    634 					dateString += "-"+Zotero.Utilities.lpad(date.day, "0", 2);
    635 				}
    636 			}
    637 			return dateString;
    638 		}
    639 		return false;
    640 	}
    641 	
    642 	
    643 	this.sqlToISO8601 = function (sqlDate) {
    644 		var date = sqlDate.substr(0, 10);
    645 		var matches = date.match(/^([0-9]{4})\-([0-9]{2})\-([0-9]{2})/);
    646 		if (!matches) {
    647 			return false;
    648 		}
    649 		date = matches[1];
    650 		// Drop parts for reduced precision
    651 		if (matches[2] !== "00") {
    652 			date += "-" + matches[2];
    653 			if (matches[3] !== "00") {
    654 				date += "-" + matches[3];
    655 			}
    656 		}
    657 		var time = sqlDate.substr(11);
    658 		// TODO: validate times
    659 		if (time) {
    660 			date += "T" + time + "Z";
    661 		}
    662 		return date;
    663 	}
    664 	
    665 	this.strToMultipart = function (str) {
    666 		if (!str){
    667 			return '';
    668 		}
    669 		
    670 		var parts = this.strToDate(str);
    671 		
    672 		// FIXME: Until we have a better BCE date solution,
    673 		// remove year value if not between 1 and 9999
    674 		if (parts.year) {
    675 			var year = parts.year + '';
    676 			if (!year.match(/^[0-9]{1,4}$/)) {
    677 				delete parts.year;
    678 			}
    679 		}
    680 		
    681 		parts.month = typeof parts.month != "undefined" ? parts.month + 1 : '';
    682 		
    683 		var multi = (parts.year ? Zotero.Utilities.lpad(parts.year, '0', 4) : '0000') + '-'
    684 			+ Zotero.Utilities.lpad(parts.month, '0', 2) + '-'
    685 			+ (parts.day ? Zotero.Utilities.lpad(parts.day, '0', 2) : '00')
    686 			+ ' '
    687 			+ str;
    688 		return multi;
    689 	}
    690 	
    691 	// Regexes for multipart and SQL dates
    692 	// Allow zeroes in multipart dates
    693 	// TODO: Allow negative multipart in DB and here with \-?
    694 	var _multipartRE = /^[0-9]{4}\-(0[0-9]|10|11|12)\-(0[0-9]|[1-2][0-9]|30|31) /;
    695 	var _sqldateRE = /^\-?[0-9]{4}\-(0[1-9]|10|11|12)\-(0[1-9]|[1-2][0-9]|30|31)$/;
    696 	var _sqldateWithZeroesRE = /^\-?[0-9]{4}\-(0[0-9]|10|11|12)\-(0[0-9]|[1-2][0-9]|30|31)$/;
    697 	var _sqldatetimeRE = /^\-?[0-9]{4}\-(0[1-9]|10|11|12)\-(0[1-9]|[1-2][0-9]|30|31) ([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-5][0-9])$/;
    698 	
    699 	/**
    700 	 * Tests if a string is a multipart date string
    701 	 * e.g. '2006-11-03 November 3rd, 2006'
    702 	 */
    703 	function isMultipart(str){
    704 		if (isSQLDateTime(str)) {
    705 			return false;
    706 		}
    707 		return _multipartRE.test(str);
    708 	}
    709 	
    710 	
    711 	/**
    712 	 * Returns the SQL part of a multipart date string
    713 	 * (e.g. '2006-11-03 November 3rd, 2006' returns '2006-11-03')
    714 	 */
    715 	function multipartToSQL(multi){
    716 		if (!multi){
    717 			return '';
    718 		}
    719 		
    720 		if (!isMultipart(multi)){
    721 			return '0000-00-00';
    722 		}
    723 		
    724 		return multi.substr(0, 10);
    725 	}
    726 	
    727 	
    728 	/**
    729 	 * Returns the user part of a multipart date string
    730 	 * (e.g. '2006-11-03 November 3rd, 2006' returns 'November 3rd, 2006')
    731 	 */
    732 	function multipartToStr(multi){
    733 		if (!multi){
    734 			return '';
    735 		}
    736 		
    737 		if (!isMultipart(multi)){
    738 			return multi;
    739 		}
    740 		
    741 		return multi.substr(11);
    742 	}
    743 	
    744 	
    745 	function isSQLDate(str, allowZeroes) {
    746 		if (allowZeroes) {
    747 			return _sqldateWithZeroesRE.test(str);
    748 		}
    749 		return _sqldateRE.test(str);
    750 	}
    751 	
    752 	
    753 	function isSQLDateTime(str){
    754 		return _sqldatetimeRE.test(str);
    755 	}
    756 	
    757 	
    758 	function sqlHasYear(sqldate){
    759 		return isSQLDate(sqldate, true) && sqldate.substr(0,4)!='0000';
    760 	}
    761 	
    762 	
    763 	function sqlHasMonth(sqldate){
    764 		return isSQLDate(sqldate, true) && sqldate.substr(5,2)!='00';
    765 	}
    766 	
    767 	
    768 	function sqlHasDay(sqldate){
    769 		return isSQLDate(sqldate, true) && sqldate.substr(8,2)!='00';
    770 	}
    771 	
    772 	
    773 	function getUnixTimestamp() {
    774 		return Math.round(Date.now() / 1000);
    775 	}
    776 	
    777 	
    778 	function toUnixTimestamp(date) {
    779 		if (date === null || typeof date != 'object' ||
    780 				date.constructor.name != 'Date') {
    781 			throw new Error(`'${date}' is not a valid date`);
    782 		}
    783 		return Math.round(date.getTime() / 1000);
    784 	}
    785 	
    786 	
    787 	/**
    788 	 * Convert a JS Date to a relative date (e.g., "5 minutes ago")
    789 	 *
    790 	 * Adapted from http://snipplr.com/view/10290/javascript-parse-relative-date/
    791 	 *
    792 	 * @param	{Date}	date
    793 	 * @return	{String}
    794 	 */
    795 	this.toRelativeDate = function (date) {
    796 		var str;
    797 		var now = new Date();
    798 		var timeSince = now.getTime() - date;
    799 		var inSeconds = timeSince / 1000;
    800 		var inMinutes = timeSince / 1000 / 60;
    801 		var inHours = timeSince / 1000 / 60 / 60;
    802 		var inDays = timeSince / 1000 / 60 / 60 / 24;
    803 		var inYears = timeSince / 1000 / 60 / 60 / 24 / 365;
    804 		
    805 		var n;
    806 		
    807 		// in seconds
    808 		if (Math.round(inSeconds) == 1) {
    809 			var key = "secondsAgo";
    810 		}
    811 		else if (inMinutes < 1.01) {
    812 			var key = "secondsAgo";
    813 			n = Math.round(inSeconds);
    814 		}
    815 		
    816 		// in minutes
    817 		else if (Math.round(inMinutes) == 1) {
    818 			var key = "minutesAgo";
    819 		}
    820 		else if (inHours < 1.01) {
    821 			var key = "minutesAgo";
    822 			n = Math.round(inMinutes);
    823 		}
    824 		
    825 		// in hours
    826 		else if (Math.round(inHours) == 1) {
    827 			var key = "hoursAgo";
    828 		}
    829 		else if (inDays < 1.01) {
    830 			var key = "hoursAgo";
    831 			n = Math.round(inHours);
    832 		}
    833 		
    834 		// in days
    835 		else if (Math.round(inDays) == 1) {
    836 			var key = "daysAgo";
    837 		}
    838 		else if (inYears < 1.01) {
    839 			var key = "daysAgo";
    840 			n = Math.round(inDays);
    841 		}
    842 		
    843 		// in years
    844 		else if (Math.round(inYears) == 1) {
    845 			var key = "yearsAgo";
    846 		}
    847 		else {
    848 			var key = "yearsAgo";
    849 			var n = Math.round(inYears);
    850 		}
    851 		
    852 		return Zotero.getString("date.relative." + key + "." + (n ? "multiple" : "one"), n);
    853 	}
    854 	
    855 	
    856 	function getFileDateString(file){
    857 		var date = new Date();
    858 		date.setTime(file.lastModifiedTime);
    859 		return date.toLocaleDateString();
    860 	}
    861 	
    862 	
    863 	function getFileTimeString(file){
    864 		var date = new Date();
    865 		date.setTime(file.lastModifiedTime);
    866 		return date.toLocaleTimeString();
    867 	}
    868 	
    869 	/**
    870 	 * Get the order of the date components based on the current locale
    871 	 *
    872 	 * Returns a string with y, m, and d (e.g. 'ymd', 'mdy')
    873 	 */
    874 	function getLocaleDateOrder(){
    875 		if (!_localeDateOrder) {
    876 			switch (Zotero.locale ? Zotero.locale.substr(3) : "US") {
    877 				// middle-endian
    878 				case 'US': // The United States
    879 				case 'BZ': // Belize
    880 				case 'FM': // The Federated States of Micronesia
    881 				case 'PA': // Panama
    882 				case 'PH':	// The Philippines
    883 				case 'PW':	// Palau
    884 				case 'ZW': // Zimbabwe
    885 					_localeDateOrder = 'mdy';
    886 					break;
    887 				
    888 				// big-endian
    889 				case 'fa': // Persian
    890 				case 'AL': // Albania
    891 				case 'CA': // Canada
    892 				case 'CN': // China
    893 				case 'HU': // Hungary
    894 				case 'JP': // Japan
    895 				case 'KE': // Kenya
    896 				case 'KR': // Korea
    897 				case 'LT': // Lithuania
    898 				case 'LV': // Latvia
    899 				case 'MN': // Mongolia
    900 				case 'SE': // Sweden
    901 				case 'TW': // Taiwan
    902 				case 'ZA': // South Africa
    903 					_localeDateOrder = 'ymd';
    904 					break;
    905 				
    906 				// little-endian
    907 				default:
    908 					_localeDateOrder = 'dmy';
    909 			}
    910 		}
    911 		return _localeDateOrder;
    912 	}
    913 }