User:Gary/comments in local time.js

/**

* COMMENTS IN LOCAL TIME

*

* Description:

* Changes 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) {

return `0${numberArg}`;

}

return numberArg;

}

function convertMonthToNumber(month) {

return new Date(`${month} 1, 2001`).getMonth();

}

function getDates(time) {

const [, oldHour, oldMinute, oldDay, oldMonth, oldYear] = time;

// Today

const today = new Date();

// Yesterday

const yesterday = new Date();

yesterday.setDate(yesterday.getDate() - 1);

// Tomorrow

const tomorrow = new Date();

tomorrow.setDate(tomorrow.getDate() + 1);

// Set the date entered.

const newTime = new Date();

newTime.setUTCFullYear(oldYear, convertMonthToNumber(oldMonth), oldDay);

newTime.setUTCHours(oldHour);

newTime.setUTCMinutes(oldMinute);

return { time: newTime, today, tomorrow, yesterday };

}

/**

* Determine whether to use the singular or plural word, and use that.

*

* @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 (!pluralArg) {

pluralArg = `${term}s`;

}

// There's only one item, so just use the singular word.

if (count === 1) {

return term;

}

// There are multiple items, so use the plural word.

return pluralArg;

}

class CommentsInLocalTime {

constructor() {

this.language = '';

this.LocalComments = {};

/**

* Settings

*/

this.settings();

this.language = this.setDefaultSetting(

'language',

this.LocalComments.language

);

// These values are also reflected in the documentation:

// https://en.wikipedia.org/wiki/Wikipedia:Comments_in_Local_Time#Default_settings

this.setDefaultSetting({

dateDifference: true,

dateFormat: 'dmy',

dayOfWeek: true,

dropDays: 0,

dropMonths: 0,

timeFirst: true,

twentyFourHours: false,

});

}

adjustTime(originalTimestamp, search) {

const { time, today, tomorrow, yesterday } = getDates(

originalTimestamp.match(search)

);

// A string matching the date pattern was found, but it cannot be

// converted to a Date object. Return it with no changes made.

if (Number.isNaN(time)) {

return [originalTimestamp, ''];

}

const date = this.determineDateText({

time,

today,

tomorrow,

yesterday,

});

const { ampm, hour } = this.getHour(time);

const minute = addLeadingZero(time.getMinutes());

const finalTime = `${hour}:${minute}${ampm}`;

// Determine the time offset.

const utcValue = (-1 * time.getTimezoneOffset()) / 60;

const utcOffset =

utcValue >= 0 ? `+${utcValue}` : `−${Math.abs(utcValue.toFixed(1))}`;

const utcPart = `(UTC${utcOffset})`;

const returnDate = this.LocalComments.timeFirst

? `${finalTime}, ${date} ${utcPart}`

: `${date}, ${finalTime} ${utcPart}`;

return returnDate;

}

convertNumberToMonth(number) {

return [

this.language.January,

this.language.February,

this.language.March,

this.language.April,

this.language.May,

this.language.June,

this.language.July,

this.language.August,

this.language.September,

this.language.October,

this.language.November,

this.language.December,

][number];

}

createDateText({ day, month, time, today, year }) {

// Calculate day of week

const dayNames = [

this.language.Sunday,

this.language.Monday,

this.language.Tuesday,

this.language.Wednesday,

this.language.Thursday,

this.language.Friday,

this.language.Saturday,

];

const dayOfTheWeek = dayNames[time.getDay()];

let descriptiveDifference = '';

let last = '';

// Create a relative descriptive difference

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.} 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 = $(

`${returnDate}`

);

// 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(

`${beforeMatch}`

);

}

// Replace the text content that appears after the timestamp.

if (afterMatch) {

$span.after(

`${afterMatch}`

);

}

}

} 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();

});