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