User:Gary/comments in local time.js: Difference between revisions

Content deleted Content added
add fixme
fixes
 
(23 intermediate revisions by 3 users not shown)
Line 1:
/**
* COMMENTS IN LOCAL TIME
*
Description: Changes [[Coordinated Universal Time|UTC]]-based times and dates, such as those used in signatures, to be relative to local time.
* Description:
Documentation: [[Wikipedia:Comments in Local Time]]
* Changes [[Coordinated Universal Time|UTC]]-based times and dates,
* such as those used in signatures, to be relative to local time.
*
* Documentation:
* [[Wikipedia:Comments in Local Time]]
*/
$(() => {
/**
* Given a number, add a leading zero if necessary, so that the final number
* has two characters.
*
* @param {number} number Number
* @returns {string} The number with a leading zero, if necessary.
*/
function addLeadingZero(number) {
const numberArg = number;
 
if (numberArg < 10) {
FIXME: Tables are shrunk, like the one found here: https://en.wikipedia.org/wiki/Wikipedia:Featured_list_candidates/List_of_currencies_in_North_America/archive3
return `0${numberArg}`;
*/
}
 
return numberArg;
var language;
}
function commentsInLocalTime()
{
/*
Language
LOCALIZING THIS SCRIPT
To localize this script, change the terms below, to the RIGHT of the colons,
to the correct term used in that language.
For example, in the French language,
'Today' : 'Today',
would be
'Today' : 'Aujourd’hui',
*/
language =
{
// relative terms
'Today' : 'Today',
'Yesterday' : 'Yesterday',
'Tomorrow' : 'Tomorrow',
'last' : 'last',
'this' : 'this',
// days of the week
'Sunday' : 'Sunday',
'Monday' : 'Monday',
'Tuesday' : 'Tuesday',
'Wednesday' : 'Wednesday',
'Thursday' : 'Thursday',
'Friday' : 'Friday',
'Saturday' : 'Saturday',
// months of the year
'January' : 'January',
'February' : 'February',
'March' : 'March',
'April' : 'April',
'May' : 'May',
'June' : 'June',
'July' : 'July',
'August' : 'August',
'September' : 'September',
'October' : 'October',
'November' : 'November',
'December' : 'December',
// difference words
'ago' : 'ago',
'from now' : 'from now',
// date phrases
'year' : 'year',
'years' : 'years',
'month' : 'month',
'months' : 'months',
'day' : 'day',
'days' : 'days'
};
 
function convertMonthToNumber(month) {
/*
return new Date(`${month} 1, 2001`).getMonth();
Settings
}
*/
if (typeof(LocalComments) == 'undefined') LocalComments = {};
if (typeof(LocalComments.dateDifference) == 'undefined') LocalComments.dateDifference = true;
if (typeof(LocalComments.dropDays) == 'undefined') LocalComments.dropDays = 0;
if (typeof(LocalComments.dropMonths) == 'undefined') LocalComments.dropMonths = 0;
if (typeof(LocalComments.dateFormat) == 'undefined')
{
// Deprecated: LocalizeConfig
if (typeof(LocalizeConfig) != 'undefined' && typeof(LocalizeConfig.dateFormat) != 'undefined' && LocalizeConfig.dateFormat != '') LocalComments.dateFormat = LocalizeConfig.dateFormat;
else LocalComments.dateFormat = 'dmy';
}
if (typeof(LocalComments.dayOfWeek) == 'undefined') LocalComments.dayOfWeek = true;
if (typeof(LocalComments.timeFirst) == 'undefined') LocalComments.timeFirst = true;
if (typeof(LocalComments.twentyFourHours) == 'undefined') LocalComments.twentyFourHours = false;
/*
End Settings
*/
 
function getDates(time) {
if (mw.config.get('wgCanonicalNamespace') == '' || mw.config.get('wgCanonicalNamespace') == 'MediaWiki' || mw.config.get('wgCanonicalNamespace') == 'Special' || mw.config.get('wgAction') == 'history') return false;
const [, oldHour, oldMinute, oldDay, oldMonth, oldYear] = time;
var topContainer;
if ($('#wikiPreview').length) topContainer = $('#wikiPreview');
else if ($('#mw-content-text').length && mw.config.get('wgAction') == 'view') topContainer = $('#mw-content-text');
else if ($('#bodyContent').length && mw.config.get('wgAction') == 'view') topContainer = $('#bodyContent');
else topContainer = false;
if (topContainer && topContainer.length) replaceText(topContainer, /(\d{1,2}):(\d{2}), (\d{1,2}) ([A-Z][a-z]+) (\d{4}) \(UTC\)/);
}
 
// Today
function addLeadingZero(number)
const today = new Date();
{
if (number < 10) number = '0' + number;
return number;
}
 
// Yesterday
function adjustTime(originalTimestamp, search)
const yesterday = new Date();
{
var time = originalTimestamp.match(search);
var oldHour = time[1], oldMinute = time[2], oldDay = time[3], oldMonth = time[4], oldYear = time[5];
var today = new Date(), yesterday = new Date(), tomorrow = new Date();
yesterday.setDate(yesterday.getDate() - 1);
tomorrow.setDate(tomorrow.getDate() + 1);
 
yesterday.setDate(yesterday.getDate() - 1);
// set the date entered
var time = new Date();
time.setUTCFullYear(oldYear, convertMonthToNumber(oldMonth), oldDay);
time.setUTCHours(oldHour);
time.setUTCMinutes(oldMinute);
 
// Tomorrow
// A string matching the date pattern was found, but it cannot be converted to a Date object. Return it with no changes made.
const tomorrow = new Date();
if (isNaN(time)) return [originalTimestamp, ''];
 
tomorrow.setDate(tomorrow.getDate() + 1);
// determine the time offset
var utcOffset = -1 * time.getTimezoneOffset() / 60;
if (utcOffset >= 0) utcOffset = '+' + utcOffset;
else utcOffset = '−' + Math.abs(utcOffset);
 
// setSet the date bits to outputentered.
const newTime = new Date();
var year = time.getFullYear();
var month = addLeadingZero(time.getMonth() + 1);
var day = time.getDate();
var hour = parseInt(time.getHours());
var minute = addLeadingZero(time.getMinutes());
 
newTime.setUTCFullYear(oldYear, convertMonthToNumber(oldMonth), oldDay);
// output am or pm depending on the date
newTime.setUTCHours(oldHour);
var ampm = '';
newTime.setUTCMinutes(oldMinute);
if (LocalComments.twentyFourHours) hour = addLeadingZero(hour);
else
{
ampm = ' am';
if (hour > 11) ampm = ' pm';
if (hour > 12) hour -= 12;
if (hour == '00') hour = 12;
}
 
return { time: newTime, today, tomorrow, yesterday };
// return 'today' or 'yesterday' if that is the case
}
if (year == today.getFullYear() && month == addLeadingZero(today.getMonth() + 1) && day == today.getDate()) var date = language['Today'];
else if (year == yesterday.getFullYear() && month == addLeadingZero(yesterday.getMonth() + 1) && day == yesterday.getDate()) var date = language['Yesterday'];
else if (year == tomorrow.getFullYear() && month == addLeadingZero(tomorrow.getMonth() + 1) && day == tomorrow.getDate()) var date = language['Tomorrow'];
else
{
// calculate day of week
var dayNames = new Array(language['Sunday'], language['Monday'], language['Tuesday'], language['Wednesday'], language['Thursday'], language['Friday'], language['Saturday']);
var dayOfTheWeek = dayNames[time.getDay()];
 
/**
var descriptiveDifference = '';
* Determine whether to use the singular or plural word, and use that.
var last = '';
*
* @param {string} term Original term
* @param {number} count Count of items
* @param {string} plural Pluralized term
* @returns {string} The word to use
*/
function pluralize(term, count, plural = null) {
let pluralArg = plural;
 
// No unique pluralized word is found, so just use a general one.
if (LocalComments.dateDifference)
if (!pluralArg) {
{
pluralArg = `${term}s`;
// calculate time difference from today
}
var millisecondsAgo = today.getTime() - time.getTime();
var daysAgo = Math.abs(Math.round(millisecondsAgo / 1000 / 60 / 60 / 24));
 
// There's only one item, so just use the singular word.
var differenceWord = '', last = '';
if (millisecondsAgocount >=== 01) {
return term;
{
}
differenceWord = language['ago'];
if (daysAgo <= 7) last = language['last'] + ' ';
}
else
{
differenceWord = language['from now'];
if (daysAgo <= 7) last = language['this'] + ' ';
}
 
// There are multiple items, so use the plural word.
// This method of computing the years & months is not exact.
return pluralArg;
// However, it's better than the previous method that used
}
// 1 January + delta days. That was usually quite off because
// it mapped the second delta month to February, which has only 28 days.
// This method is usually not more than one day off.
 
class CommentsInLocalTime {
var monthsAgo = Math.floor(daysAgo / 365 * 12); // close enough
constructor() {
var totalMonthsAgo = monthsAgo;
this.language = '';
var yearsAgo = Math.floor(monthsAgo / 12);
this.LocalComments = {};
 
/**
if (monthsAgo < LocalComments.dropMonths) yearsAgo = 0;
* Settings
else if (LocalComments.dropMonths > 0) monthsAgo = 0;
*/
else monthsAgo = monthsAgo - yearsAgo * 12;
this.settings();
 
this.language = this.setDefaultSetting(
if (daysAgo < LocalComments.dropDays)
'language',
{
this.LocalComments.language
monthsAgo = 0;
);
yearsAgo = 0;
}
else if (LocalComments.dropDays > 0) daysAgo = 0;
else daysAgo = daysAgo - Math.floor(totalMonthsAgo * 365 / 12);
 
// These values are also reflected in the documentation:
var descriptiveParts = [];
// https://en.wikipedia.org/wiki/Wikipedia:Comments_in_Local_Time#Default_settings
if (yearsAgo > 0)
this.setDefaultSetting({
{
dateDifference: true,
var fmtYears = yearsAgo + ' ' + pluralize(language['year'], yearsAgo, language['years']);
dateFormat: 'dmy',
descriptiveParts.push(fmtYears);
dayOfWeek: true,
}
dropDays: 0,
if (monthsAgo > 0)
dropMonths: 0,
{
timeFirst: true,
var fmtMonths = monthsAgo + ' ' + pluralize(language['month'], monthsAgo, language['months']);
twentyFourHours: false,
descriptiveParts.push(fmtMonths);
});
}
}
if (daysAgo > 0)
{
var fmtDays = daysAgo + ' ' + pluralize(language['day'], daysAgo, language['days']);
descriptiveParts.push(fmtDays);
}
 
adjustTime(originalTimestamp, search) {
descriptiveDifference = ' (' + descriptiveParts.join(', ') + ' ' + differenceWord + ')';
const { time, today, tomorrow, yesterday } = getDates(
}
originalTimestamp.match(search)
);
 
// A string matching the date pattern was found, but it cannot be
// format the date according to user preferences
// converted to a Date object. Return it with no changes made.
var formattedDate = '', monthName = convertNumberToMonth(time.getMonth());
if (Number.isNaN(time)) {
return [originalTimestamp, ''];
}
 
const date = this.determineDateText({
switch (LocalComments.dateFormat.toLowerCase())
time,
{
today,
case 'dmy':
tomorrow,
formattedDate = day + ' ' + monthName + ' ' + year;
yesterday,
break;
});
case 'mdy':
formattedDate = monthName + ' ' + day + ', ' + year;
break;
default:
formattedDate = year + '-' + month + '-' + addLeadingZero(day);
}
 
const { ampm, hour } = this.getHour(time);
var formattedDayOfTheWeek = '';
const minute = addLeadingZero(time.getMinutes());
if (LocalComments.dayOfWeek) formattedDayOfTheWeek = ', ' + last + dayOfTheWeek;
const finalTime = `${hour}:${minute}${ampm}`;
var date = formattedDate + formattedDayOfTheWeek + descriptiveDifference;
}
 
// Determine the time offset.
var finalTime = hour + ':' + minute + ampm;
const utcValue = (-1 * time.getTimezoneOffset()) / 60;
if (LocalComments.timeFirst) var returnDate = finalTime + ', ' + date + ' (UTC' + utcOffset + ')';
else var returnDate = date + ', ' + finalTime + ' (UTC' +const utcOffset + ')';=
utcValue >= 0 ? `+${utcValue}` : `−${Math.abs(utcValue.toFixed(1))}`;
return [returnDate, time];
}
 
const utcPart = `(UTC${utcOffset})`;
function convertMonthToNumber(month)
{
var output = new Date(month + ' 1, 2001');
return output.getMonth();
}
 
const returnDate = this.LocalComments.timeFirst
function convertNumberToMonth(number)
? `${finalTime}, ${date} ${utcPart}`
{
: `${date}, ${finalTime} ${utcPart}`;
var month = new Array(language['January'], language['February'], language['March'], language['April'], language['May'], language['June'], language['July'], language['August'], language['September'], language['October'], language['November'], language['December']);
return month[number];
}
 
return returnDate;
function pluralize(term, count, plural)
}
{
if (plural == null) plural = term + 's';
return (count == 1 ? term : plural);
}
 
convertNumberToMonth(number) {
function replaceText(node, search)
return [
{
this.language.January,
if (!node.length) return false;
this.language.February,
this.language.March,
// Check if this is a text node.
this.language.April,
if (node[0].nodeType == 3)
this.language.May,
{
this.language.June,
var value = node[0].nodeValue;
this.language.July,
var matches = value.match(search);
this.language.August,
this.language.September,
if (matches != null)
this.language.October,
{
this.language.November,
for (match = 0; match < matches.length; match++)
this.language.December,
{
][number];
if (afterMatch != null && length != null) var position = afterMatch.search(search) + beforeMatch.length + length;
}
else var position = value.search(search);
var length = matches[match].toString().length;
var beforeMatch = value.substring(0, position);
var afterMatch = value.substring(position + length);
var timeArray = adjustTime(matches[match].toString(), search);
// Is the "timestamp" attribute used for microformats?
var span = $('<span class="localcomments" style="font-size: 95%; white-space: nowrap;" timestamp="' + (timeArray[1] ? timeArray[1].getTime() : '') + '" title="' + matches[match] + '">' + timeArray[0] + '</span>');
node.replaceWith(span);
span.before($('<span class="before-localcomments"></span>').text(beforeMatch));
span.after($('<span class="after-localcomments"></span>').text(afterMatch));
 
createDateText({ day, month, time, today, year }) {
break;
// Calculate day of week
}
const dayNames = [
}
this.language.Sunday,
}
this.language.Monday,
else
this.language.Tuesday,
{
this.language.Wednesday,
var children = [];
this.language.Thursday,
var child = node.contents().eq(0);
this.language.Friday,
this.language.Saturday,
while (child.length)
];
{
const dayOfTheWeek = dayNames[time.getDay()];
children.push(child);
let descriptiveDifference = '';
child = $(child[0].nextSibling);
let last = '';
}
for (var child = 0; child < children.length; child++) replaceText(children[child], search);
}
}
 
// Create a relative descriptive difference
$(commentsInLocalTime);
if (this.LocalComments.dateDifference) {
({ descriptiveDifference, last } = this.createRelativeDate(
today,
time
));
}
 
const monthName = this.convertNumberToMonth(time.getMonth());
 
// Format the date according to user preferences
let formattedDate = '';
 
switch (this.LocalComments.dateFormat.toLowerCase()) {
case 'dmy':
formattedDate = `${day} ${monthName} ${year}`;
 
break;
case 'mdy':
formattedDate = `${monthName} ${day}, ${year}`;
 
break;
default:
formattedDate = `${year}-${month}-${addLeadingZero(day)}`;
}
 
const formattedDayOfTheWeek = this.LocalComments.dayOfWeek
? `, ${last}${dayOfTheWeek}`
: '';
 
return `${formattedDate}${formattedDayOfTheWeek}${descriptiveDifference}`;
}
 
/**
* Create relative date data.
*
* @param {Date} today Today
* @param {Date} time The timestamp from a comment
* @returns {Object.<string, *>} Relative date data
*/
createRelativeDate(today, time) {
/**
* The time difference from today, in milliseconds.
*
* @type {number}
*/
const millisecondsAgo = today.getTime() - time.getTime();
 
/**
* The number of days ago, that we will display. It's not necessarily the
* total days ago.
*
* @type {number}
*/
let daysAgo = Math.abs(Math.round(millisecondsAgo / 1000 / 60 / 60 / 24));
const { differenceWord, last } = this.relativeText({
daysAgo,
millisecondsAgo,
});
 
// This method of computing the years and months is not exact. However,
// it's better than the previous method that used 1 January + delta days.
// That was usually quite off because it mapped the second delta month to
// February, which has only 28 days. This method is usually not more than
// one day off, except perhaps over very distant dates.
 
/**
* The number of months ago, that we will display. It's not necessarily
* the total months ago.
*
* @type {number}
*/
let monthsAgo = Math.floor((daysAgo / 365) * 12);
 
/**
* The total amount of time ago, in months.
*
* @type {number}
*/
const totalMonthsAgo = monthsAgo;
 
/**
* The number of years ago that we will display. It's not necessarily the
* total years ago.
*
* @type {number}
*/
let yearsAgo = Math.floor(totalMonthsAgo / 12);
 
if (totalMonthsAgo < this.LocalComments.dropMonths) {
yearsAgo = 0;
} else if (this.LocalComments.dropMonths > 0) {
monthsAgo = 0;
} else {
monthsAgo -= yearsAgo * 12;
}
 
if (daysAgo < this.LocalComments.dropDays) {
monthsAgo = 0;
yearsAgo = 0;
} else if (this.LocalComments.dropDays > 0 && totalMonthsAgo >= 1) {
daysAgo = 0;
} else {
daysAgo -= Math.floor((totalMonthsAgo * 365) / 12);
}
 
const descriptiveParts = [];
 
// There is years text to add.
if (yearsAgo > 0) {
descriptiveParts.push(
`${yearsAgo} ${pluralize(
this.language.year,
yearsAgo,
this.language.years
)}`
);
}
 
// There is months text to add.
if (monthsAgo > 0) {
descriptiveParts.push(
`${monthsAgo} ${pluralize(
this.language.month,
monthsAgo,
this.language.months
)}`
);
}
 
// There is days text to add.
if (daysAgo > 0) {
descriptiveParts.push(
`${daysAgo} ${pluralize(
this.language.day,
daysAgo,
this.language.days
)}`
);
}
 
return {
descriptiveDifference: ` (${descriptiveParts.join(
', '
)} ${differenceWord})`,
last,
};
}
 
determineDateText({ time, today, tomorrow, yesterday }) {
// Set the date bits to output.
const year = time.getFullYear();
const month = addLeadingZero(time.getMonth() + 1);
const day = time.getDate();
 
// Return 'today' or 'yesterday' if that is the case
if (
year === today.getFullYear() &&
month === addLeadingZero(today.getMonth() + 1) &&
day === today.getDate()
) {
return this.language.Today;
}
 
if (
year === yesterday.getFullYear() &&
month === addLeadingZero(yesterday.getMonth() + 1) &&
day === yesterday.getDate()
) {
return this.language.Yesterday;
}
 
if (
year === tomorrow.getFullYear() &&
month === addLeadingZero(tomorrow.getMonth() + 1) &&
day === tomorrow.getDate()
) {
return this.language.Tomorrow;
}
 
return this.createDateText({ day, month, time, today, year });
}
 
getHour(time) {
let ampm;
let hour = Number.parseInt(time.getHours(), 10);
 
if (this.LocalComments.twentyFourHours) {
ampm = '';
hour = addLeadingZero(hour);
} else {
// Output am or pm depending on the date.
ampm = hour <= 11 ? ' am' : ' pm';
 
if (hour > 12) {
hour -= 12;
} else if (hour === 0) {
hour = 12;
}
}
 
return { ampm, hour };
}
 
relativeText({ daysAgo, millisecondsAgo }) {
let differenceWord = '';
let last = '';
 
// The date is in the past.
if (millisecondsAgo >= 0) {
differenceWord = this.language.ago;
 
if (daysAgo <= 7) {
last = `${this.language.last} `;
}
 
// The date is in the future.
} else {
differenceWord = this.language['from now'];
 
if (daysAgo <= 7) {
last = `${this.language.this} `;
}
}
 
return { differenceWord, last };
}
 
replaceText(node, search) {
if (!node) {
return;
}
 
// Check if this is a text node.
if (node.nodeType === 3) {
// Don't continue if this text node's parent tag is one of these.
if (['CODE', 'PRE'].includes(node.parentNode.nodeName)) {
return;
}
 
const value = node.nodeValue;
const matches = value.match(search);
 
if (matches) {
// Only act on the first timestamp we found in this node. This is for
// the rare occassion that there is more than one timestamp in the
// same text node.
const [match] = matches;
const position = value.search(search);
const stringLength = match.toString().length;
 
// Grab the text content before and after the matching timestamp,
// which we'll then wrap in their own SPAN nodes.
const beforeMatch = value.slice(0, position);
const afterMatch = value.slice(position + stringLength);
const returnDate = this.adjustTime(match.toString(), search);
 
// Create the code to display the new local comments content.
const $span = $(
`<span class="localcomments" style="font-size: 95%;" title="${match}">${returnDate}</span>`
);
 
// Replace the existing text node in the page with our new local
// comments node.
$(node).replaceWith($span);
 
// Replace the text content that appears before the timestamp.
if (beforeMatch) {
$span.before(
`<span class="before-localcomments">${beforeMatch}</span>`
);
}
 
// Replace the text content that appears after the timestamp.
if (afterMatch) {
$span.after(
`<span class="after-localcomments">${afterMatch}</span>`
);
}
}
} else {
const children = [];
let child;
 
[child] = node.childNodes;
 
while (child) {
children.push(child);
child = child.nextSibling;
}
 
// Loop through children and run this func on it again, recursively.
children.forEach((child2) => {
this.replaceText(child2, search);
});
}
}
 
run() {
if (
['', 'MediaWiki', 'Special'].includes(
mw.config.get('wgCanonicalNamespace')
)
) {
return;
}
 
// Check for disabled URLs.
const isDisabledUrl = ['action=history'].some((disabledUrl) =>
document.___location.href.includes(disabledUrl)
);
 
if (isDisabledUrl) {
return;
}
 
this.replaceText(
document.querySelector('.mw-body-content .mw-parser-output'),
/(\d{1,2}):(\d{2}), (\d{1,2}) ([A-Z][a-z]+) (\d{4}) \(UTC\)/
);
}
 
setDefaultSetting(...args) {
// There are no arguments.
if (args.length === 0) {
return false;
}
 
// The first arg is an object, so just set that data directly onto the
// settings object. like {setting 1: true, setting 2: false}
if (typeof args[0] === 'object') {
const [settings] = args;
 
// Loop through each setting.
Object.keys(settings).forEach((name) => {
const value = settings[name];
 
if (typeof this.LocalComments[name] === 'undefined') {
this.LocalComments[name] = value;
}
});
 
return settings;
}
 
// The first arg is a string, so use the first arg as the settings key,
// and the second arg as the value to set it to.
const [name, setting] = args;
 
if (typeof this.LocalComments[name] === 'undefined') {
this.LocalComments[name] = setting;
}
 
return this.LocalComments[name];
}
 
/**
* Set the script's settings.
*
* @returns {undefined}
*/
settings() {
// The user has set custom settings, so use those.
if (window.LocalComments) {
this.LocalComments = window.LocalComments;
}
 
/**
* Language
*
* LOCALIZING THIS SCRIPT
* To localize this script, change the terms below,
* to the RIGHT of the colons, to the correct term used in that language.
*
* For example, in the French language,
*
* 'Today' : 'Today',
*
* would be
*
* 'Today' : "Aujourd'hui",
*/
this.LocalComments.language = {
// Relative terms
Today: 'Today',
Yesterday: 'Yesterday',
Tomorrow: 'Tomorrow',
last: 'last',
this: 'this',
 
// Days of the week
Sunday: 'Sunday',
Monday: 'Monday',
Tuesday: 'Tuesday',
Wednesday: 'Wednesday',
Thursday: 'Thursday',
Friday: 'Friday',
Saturday: 'Saturday',
 
// Months of the year
January: 'January',
February: 'February',
March: 'March',
April: 'April',
May: 'May',
June: 'June',
July: 'July',
August: 'August',
September: 'September',
October: 'October',
November: 'November',
December: 'December',
 
// Difference words
ago: 'ago',
'from now': 'from now',
 
// Date phrases
year: 'year',
years: 'years',
month: 'month',
months: 'months',
day: 'day',
days: 'days',
};
}
}
 
// Check if we've already ran this script.
if (window.commentsInLocalTimeWasRun) {
return;
}
 
window.commentsInLocalTimeWasRun = true;
 
const commentsInLocalTime = new CommentsInLocalTime();
 
commentsInLocalTime.run();
});