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 = $('<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());