User:Gary/subjects age from year.js - Wikipedia


Article Images

Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.

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