User:Gary/subjects age from year.js
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
* 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 = $('<abbr class="subject-age-from-year"></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 = $('<span></span>');
// 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<RegExp>, order<Array>].
// The order should always be: [<year>, <type>].
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());