User:Gary/subjects age from year.js

/*

* decaffeinate suggestions:

* DS101: Remove unnecessary use of Array.from

* DS102: Remove unnecessary code created because of implicit returns

* DS202: Simplify dynamic range loops

* DS205: Consider reworking code to avoid use of IIFEs

* DS206: Consider reworking classes to avoid initClass

* DS207: Consider shorter variations of null checks

* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md

*/

/*

SUBJECT AGE FROM YEAR

Description: In an article about a person or a company, when the mouse hovers

over a year in the article, the age of the article's subject by that year

appears in a tooltip.

  • /

var SubjectAgeFromYear = (function() {

let now = undefined;

SubjectAgeFromYear = class SubjectAgeFromYear {

static initClass() {

now = new Date();

}

static extractYearFromText({

yearIndex,

patternIndex,

$newNode,

nodeText,

subjectYear,

years,

}) {

let $abbr;

const abbrText = years[yearIndex];

let currentYear = years[yearIndex];

const birthYearIndex = nodeText.indexOf(currentYear);

let workThisYear = true;

// don't work on this year-for AD years

if (

patternIndex === 0 &&

// 'year' is followed by a ' BC'; wait for next pattern to work on this

(nodeText.substr(birthYearIndex + currentYear.length, 3).indexOf('BC') >

-1 ||

// 'year' is preceded by a ','; this is probably a unit such as 1,000 km

nodeText.substr(birthYearIndex - 1, 1).indexOf(',') > -1 ||

// 'year' is preceded by a month; this is probably part of a day,

// like "January 1"

((currentYear.length <= 2 &&

(this.nearAMonth(nodeText, birthYearIndex, -1, years, yearIndex) &&

currentYear.indexOf('AD') === -1)) ||

// 'year' is followed by a month; this is probably part of a day,

// like "January 1"

this.nearAMonth(

nodeText,

birthYearIndex + currentYear.length,

1

)) ||

// 'year' is followed by "?year", such as "-year", " years"

nodeText

.substr(birthYearIndex + currentYear.length, 5)

.indexOf('year') > -1)

) {

workThisYear = false;

}

// After the following conditionals, currentYear will be converted from a

// STRING (which possibly holds BC/AD) to an INTEGER

// currentYear contains "BC" somewhere

currentYear =

currentYear.indexOf('BC') > -1 ||

((subjectYear.birthYear() < 0 || subjectYear.deathYear() < 0) &&

nodeText

.substr(birthYearIndex + currentYear.length + ' BC'.length, 10)

.indexOf('BC') > -1)

? -1 * parseInt(currentYear)

: // currentYear contains "AD" somewhere

currentYear.indexOf('AD') > -1 || currentYear.indexOf('CE') > -1

? parseInt(currentYear.replace(/AD/, ).replace(/CE/, ))

: // currentYear does not contain "BC" or "AD"

parseInt(currentYear);

const firstPart = nodeText.substring(0, birthYearIndex);

// Subtract one year from difference if it spans year zero

const difference =

(subjectYear.birthYear() < 0 && 0 < currentYear) ||

(subjectYear.birthYear() > 0 && 0 > currentYear)

? currentYear - subjectYear.birthYear() - 1

: currentYear - subjectYear.birthYear();

// find a year to act on; work on AD years first, then BC years

const condition =

workThisYear &&

(currentYear >= subjectYear.birthYear() ||

currentYear >=

subjectYear.birthYear() - subjectYear.birthYearBuffer()) &&

(currentYear <= subjectYear.deathYear() ||

currentYear <=

subjectYear.deathYear() + subjectYear.birthYearBuffer());

//#

// Create the hover with an ABBR tag.

if (condition) {

$abbr = $('');

const currentYearYearsAgo = now.getFullYear() - currentYear;

const currentYearYearsAgoText =

currentYearYearsAgo > 0

? `${this.pluralize('year', currentYearYearsAgo, true)} ago`

: currentYearYearsAgo < 0

? `${this.pluralize('year', currentYearYearsAgo, true)} from now`

: 'this year';

// after death year but before the buffer

if (

currentYear > subjectYear.deathYear() &&

currentYear <= subjectYear.deathYear() + subjectYear.birthYearBuffer()

) {

const yearsLater = currentYear - subjectYear.deathYear();

$abbr.attr(

'title',

`${this.pluralize('year', yearsLater, true)} after \

${subjectYear.phrase('death')}`

);

// was alive at currentYear

} else if (difference >= 0) {

// age at currentYear

$abbr.attr(

'title',

`${this.pluralize('year', difference, true)} old`

);

// birth year

if (difference === 0) {

const currentAge =

subjectYear.type() === 'biography' && subjectYear.isAlive()

? `; now ${now.getFullYear() -

subjectYear.birthYear()} years old`

: '';

// Add the person's current age.

$abbr.attr(

'title',

`${$abbr.attr('title')} \

(${subjectYear.phrase('birth')}${currentAge})`

);

// death year

} else if (currentYear === subjectYear.deathYear()) {

$abbr.attr(

'title',

`${$abbr.attr('title')} \

(${subjectYear.phrase('death')})`

);

}

// currentYear is before birth year

} else {

const absoluteDifference = Math.abs(difference);

$abbr.attr(

'title',

`${this.pluralize('year', absoluteDifference, true)} \

before ${subjectYear.phrase('birth')}`

);

}

// Add a note indicating how far away from now is the year.

if ($abbr.attr('title').indexOf(' now ') === -1) {

$abbr.attr(

'title',

`${$abbr.attr('title')} \

(${currentYearYearsAgoText})`

);

}

// Add the existing number from the page's text as the ABBR's text.

$abbr.append(abbrText);

} else {

$abbr = '';

}

// Append the new ABBR if we found a year we could work with; otherwise,

// just add the old text content back in.

$newNode.append(firstPart).append($abbr.length ? $abbr : abbrText);

// after the year, only for the last occurrence of a year in a node

if (yearIndex + 1 === years.length) {

const secondPart = nodeText.substring(birthYearIndex + abbrText.length);

$newNode.append(secondPart);

}

// This is used for when the loop rolls around again.

nodeText = nodeText.substring(birthYearIndex + abbrText.length);

return {

yearIndex,

patternIndex,

$newNode,

nodeText,

subjectYear,

years,

};

}

static findYearsInText({

patternIndex,

$node,

patterns,

spansToRemove,

subjectYear,

}) {

if ($node[0].nodeType !== 3) {

return true;

}

let nodeText = $node[0].nodeValue;

let years = nodeText.match(patterns[patternIndex]);

if (years == null) {

return true;

}

const minBirthYearBuffer = 100;

const age = subjectYear.deathYear() - subjectYear.birthYear();

subjectYear.birthYearBuffer(

age >= minBirthYearBuffer && subjectYear.type() === 'biography'

? age

: minBirthYearBuffer

);

let $newNode = $('');

// loop through each year in the same text node

for (

let i = 0, yearIndex = i, end = years.length, asc = 0 <= end;

asc ? i < end : i > end;

asc ? i++ : i--, yearIndex = i

) {

({

yearIndex,

patternIndex,

$newNode,

nodeText,

subjectYear,

years,

} = this.extractYearFromText({

yearIndex,

patternIndex,

$newNode,

nodeText,

subjectYear,

years,

}));

}

if ($newNode.contents().length > 0) {

$node.replaceWith($newNode);

return spansToRemove.push($newNode);

}

}

static findMatchesinCategory({

allBirthYears,

allDeathYears,

birthYear,

deathYear,

matches,

type,

}) {

// Set ordered match results to actual variable names.

let categoryYear = matches[0];

const categoryType = matches[1];

// Set the category's year to be negative if it's a BC year.

categoryYear =

categoryYear.indexOf('BC') > -1

? -1 * parseInt(categoryYear)

: parseInt(categoryYear);

// If type hasn't already been set to "biography", then check to see if it

// should. "Biography" type takes precendence over "establishment" type. We

// have to check for every category if it indicates that the type is actually

// a biography.

if (type !== 'biography') {

type = (categoryType != null

? categoryType.match(/(births|deaths)/)

: undefined)

? 'biography'

: 'establishment';

}

// Birth years

if (

!(categoryType != null

? categoryType.match(/(disestablishments|deaths|disestablished)/)

: undefined) &&

((type === 'biography' && categoryType === 'births') ||

type !== 'biography')

) {

birthYear = categoryYear;

allBirthYears.push(birthYear);

// Death years

} else {

// Only continue if type is "biography" and category is a "death year", or

// type is "establishment".

if (

(type === 'biography' && categoryType === 'deaths') ||

type === 'establishment'

) {

deathYear = categoryYear;

allDeathYears.push(deathYear);

}

}

return {

allBirthYears,

allDeathYears,

birthYear,

deathYear,

matches,

type,

};

}

static findYearFromCategory({

allBirthYears,

allDeathYears,

allMatches,

birthYear,

category,

deathYear,

type,

}) {

// Format: [pattern, order].

// The order should always be: [, ].

const patterns = [

// Special cases: a four-digit year, followed by a capitalized term

// E.g. 1980 Oscar winners

[/^([0-9]{4,4})\s([\w\s]+)$/, [1, 2]],

// E.g. 950 BC

[/^([0-9]{1,4}(\sBC)?)$/, [1]],

// Match a year at the start, with optionally the word "BC" at the end.

// E.g. 123 BC births; 1950 establishments

[/^([0-9]{1,4}(\sBC)?)\s([A-Za-z\s]+)$/, [1, 3]],

// E.g. Establishments in 1925

[/^(.*?)\s(in|for)\s([0-9]{1,4}(\sBC)?)$/, [3, 1]],

];

// Match the patterns to the category.

let matches = [];

for (let pattern of Array.from(patterns)) {

const matched = category.match(pattern[0]);

if (matched) {

for (let order of Array.from(pattern[1])) {

matches.push(matched[order]);

}

break;

}

}

// There is a match

if (matches.length > 0) {

allMatches.push(category);

({

allBirthYears,

allDeathYears,

birthYear,

deathYear,

matches,

type,

} = this.findMatchesinCategory({

allBirthYears,

allDeathYears,

birthYear,

deathYear,

matches,

type,

}));

}

return {

allBirthYears,

allDeathYears,

allMatches,

birthYear,

category,

deathYear,

type,

};

}

static findYearsFromCategories() {

let birthYear, deathYear, type;

let category;

let allBirthYears = [];

let allDeathYears = [];

let allMatches = [];

const categories = (() => {

const result = [];

for (category of Array.from(window.mw.config.get('wgCategories'))) {

result.push(category.replace(/_/g, ' '));

}

return result;

})();

for (category of Array.from(categories)) {

({

allBirthYears,

allDeathYears,

allMatches,

birthYear,

category,

deathYear,

type,

} = this.findYearFromCategory({

allBirthYears,

allDeathYears,

allMatches,

birthYear,

category,

deathYear,

type,

}));

}

// Show which category was matched for birth/death dates. Use a special

// object for this so I can set defaults without changing the original

// variable.

const catText = { type, birthYear, deathYear, allMatches };

if (!catText['type']) {

catText['type'] = 'establishment';

}

if (!catText['birthYear']) {

catText['birthYear'] = '(none)';

}

if (!catText['deathYear']) {

catText['deathYear'] = '(none)';

}

if (!catText['allMatches']) {

catText['allMatches'] = '(none)';

}

catText.allMatches = catText.allMatches.map((value) => `- ${value}`);

$('#catlinks').attr(

'title',

`Type: ${catText.type}\nBirth year: \

${catText.birthYear}\nDeath year: ${catText.deathYear}\n\nMatched \

categories:\n\n${catText.allMatches.join('\n')}`

);

return { allBirthYears, allDeathYears, birthYear, deathYear, type };

}

static init() {

const wgCNamespace = window.mw.config.get('wgCanonicalNamespace');

const wgAction = window.mw.config.get('wgAction');

const wgPageName = window.mw.config.get('wgPageName');

if (

(wgCNamespace !== '' ||

window.mw.util.getParamValue('disable') === 'age' ||

wgAction !== 'view') &&

!(

wgPageName === 'User:Gary/Sandbox' &&

(wgAction === 'view' || wgAction === 'submit')

)

) {

return false;

}

// Check if there are any categories.

if (window.mw.config.get('wgCategories') === null) {

return false;

}

let {

allBirthYears,

allDeathYears,

birthYear,

deathYear,

type,

} = this.findYearsFromCategories();

// We can't continue without a birth year

if (birthYear == null) {

return false;

}

// Sort birth years. They will be sorted again, with some removed, later as

// well.

allBirthYears.sort(function(a, b) {

if (a < b) {

return -1;

} else if (a > b) {

return 1;

} else {

return 0;

}

});

// Do death year first, so we can ensure the birth year comes before the

// death year

//

// Return the death year that is closest to today's year, without going past

// it

if (allDeathYears.length > 1) {

allDeathYears.sort(function(a, b) {

const aYearsAgo = now.getFullYear() - a;

const bYearsAgo = now.getFullYear() - b;

if (aYearsAgo < 0) {

return 1;

} else if (bYearsAgo < 0) {

return -1;

} else {

return aYearsAgo - bYearsAgo;

}

});

deathYear = allDeathYears[0];

// There are no death years, but there are at least two birth years, so one

// of them could possibly be a death year. Do this only for BC years because

// they are particularly problematic, since they only use categories like:

// "15 BC" and then "10s BC deaths".

} else if (

allDeathYears.length === 0 &&

allBirthYears.length >= 2 &&

allBirthYears[0] < 0 &&

allBirthYears[1] < 0

) {

// Set the birth year as the first year.

birthYear = allBirthYears[0];

// Remove the second birth year and set it as the death year.

deathYear = allBirthYears.splice(1, 1)[0];

// Set the type as a biography, because we got at least two years that

// are BC.

type = 'biography';

}

// Do birth years

//

// Return a birth year that is before the death year, and also closest

// to today's year.

if (allBirthYears.length > 1) {

allBirthYears.sort(function(a, b) {

if (deathYear != null) {

const aDeathDiff = deathYear - a;

const bDeathDiff = deathYear - b;

if (aDeathDiff < 0) {

return 1;

} else if (bDeathDiff < 0) {

return -1;

} else {

return aDeathDiff - bDeathDiff;

}

} else {

const aYearsAgo = now.getFullYear() - a;

const bYearsAgo = now.getFullYear() - b;

if (aYearsAgo < 0) {

return 1;

} else if (bYearsAgo < 0) {

return -1;

} else {

return aYearsAgo - bYearsAgo;

}

}

});

birthYear = allBirthYears[0];

}

// "isAlive" is only used for people, not establishments

const subjectYear = new SubjectYear();

subjectYear.type(type);

subjectYear.isAlive(false);

// The maximum possible age for each type.

const maxPossibleAge = (() => {

if (subjectYear.type() === 'biography') {

return 125;

} else if (subjectYear.type() === 'establishment') {

return 1000;

}

})();

// No death year is available, so logically determine if the person

// could possibly be alive right now

if (deathYear == null) {

deathYear = birthYear + maxPossibleAge;

if (deathYear >= now.getFullYear()) {

subjectYear.isAlive(true);

}

}

const spansToRemove = [];

const patterns = [];

const birthYearLength = Math.abs(birthYear).toString().length;

const deathYearLength = Math.abs(deathYear).toString().length;

const todayLength = now.getFullYear().toString().length;

const yearLength =

birthYear < 0 && deathYear > 0

? 1

: birthYearLength < deathYearLength

? birthYearLength

: deathYearLength;

patterns.push(

new RegExp(

`(AD |AD\u00A0)?\\b[0-9]{${yearLength},` +

todayLength +

'}\\b( AD|\u00A0AD| CE|\u00A0CE)?',

'g'

)

); // AD years

if (birthYear < 0) {

// BC years

patterns.push(

new RegExp(

`\\b[0-9]{${yearLength},${todayLength}` + '}( |\u00A0)?BC[E]?\\b',

'g'

)

);

}

const $allParagraphs = $(

wgAction === 'submit' ? '#wikiPreview' : '#bodyContent'

).find('> div > p, > div > div > p');

// Set the subject's birth and death years

subjectYear.birthYear(birthYear);

subjectYear.deathYear(deathYear);

// loop through each pattern to find

return (() => {

const result = [];

for (

var patternIndex = 0, end = patterns.length, asc = 0 <= end;

asc ? patternIndex < end : patternIndex > end;

asc ? patternIndex++ : patternIndex--

) {

// loop through each paragraph

// then loop through each text node in each paragraph

$allParagraphs.each((index, element) => {

return $(element)

.contents()

.each((index, element) => {

return this.findYearsInText({

patternIndex,

$node: $(element),

patterns,

spansToRemove,

subjectYear,

});

});

});

// remove SPANs from spansToRemove, and merge children with parent

result.push(

(() => {

const result1 = [];

for (var span of Array.from(spansToRemove)) {

const children = span.contents();

const parent = span.parent();

if (!parent.length) {

continue;

}

children.each(function(index, element) {

const $child = $(element);

return span.before($child.clone());

});

span.remove();

result1.push(parent[0].normalize());

}

return result1;

})()

);

}

return result;

})();

}

static nearAMonth(text, startIndex, beforeOrAfter, years, yearIndex) {

let match;

if (beforeOrAfter == null) {

beforeOrAfter = 1;

}

const monthsArray = [

'January',

'February',

'March',

'April',

'May',

'June',

'July',

'August',

'September',

'October',

'November',

'December',

];

const pattern = new RegExp(monthsArray.join('|'));

if (beforeOrAfter === 1) {

// find the word immediately following the startIndex

text = text.substring(startIndex, text.length);

match = text.match(pattern);

// is this match only a few characters ahead of startIndex?

if (match && text.indexOf(match[0]) === ' '.length) {

return true;

} else {

return false;

}

} else if (beforeOrAfter === -1) {

// first check if after the current year,

// there is NO ", nextYearIteration"

if (

years[yearIndex + 1] &&

startIndex + years[yearIndex].length + ', '.length !==

text.indexOf(years[yearIndex + 1])

) {

return false;

}

text = text.substring(0, startIndex);

match = text.match(pattern);

if (

match &&

text.indexOf(match[0]) === startIndex - ' '.length - match[0].length

) {

return true;

} else {

return false;

}

}

}

static pluralize(word, count, includeCount) {

if (includeCount == null) {

includeCount = false;

}

const includedCount = includeCount ? `${count} ` : '';

if (count === 1) {

return includedCount + word;

} else {

return includedCount + word + 's';

}

}

};

SubjectAgeFromYear.initClass();

return SubjectAgeFromYear;

})();

class SubjectYear {

birthYear(birthYearValue) {

if (birthYearValue == null) {

({ birthYearValue } = this);

}

this.birthYearValue = birthYearValue;

return this.birthYearValue;

}

birthYearBuffer(birthYearBufferValue) {

if (birthYearBufferValue == null) {

({ birthYearBufferValue } = this);

}

this.birthYearBufferValue = birthYearBufferValue;

return this.birthYearBufferValue;

}

deathYear(deathYearValue) {

if (deathYearValue == null) {

({ deathYearValue } = this);

}

this.deathYearValue = deathYearValue;

return this.deathYearValue;

}

isAlive(isAliveValue) {

if (isAliveValue == null) {

({ isAliveValue } = this);

}

this.isAliveValue = isAliveValue;

return this.isAliveValue;

}

phrase(phrase) {

phrase = phrase.toLowerCase();

const phrases = {

biography: {

birth: 'birth',

death: 'death',

alive: 'alive',

dead: 'dead',

},

establishment: {

birth: 'established',

death: 'disestablished',

alive: 'established',

dead: 'disestablished',

},

};

if (

this.typeValue == null ||

phrases[this.typeValue] == null ||

phrases[this.typeValue][phrase] == null

) {

return false;

}

return phrases[this.typeValue][phrase];

}

type(typeValue) {

if (typeValue == null) {

({ typeValue } = this);

}

this.typeValue = typeValue;

return (this.typeValue = this.typeValue.toLowerCase());

}

}

$(() => SubjectAgeFromYear.init());