Chalarangelo/30-seconds-of-code

View on GitHub
src/presenters/recommendationPresenter.js

Summary

Maintainability
C
7 hrs
Test Coverage
import Snippet from '#src/models/snippet.js';
import settings from '#src/config/settings.js';

export default class RecommendationPresenter {
  static candidates = null;
  static candidatesByLang = {};
  static candidatesByLangAndTag = {};

  static prepareRecommendableSnippets() {
    this.candidates = Snippet.scope('published', 'byRanking');

    const groupedCandidates = this.candidates.reduce((acc, snippet) => {
      if (!acc[snippet.languageId]) acc[snippet.languageId] = [];
      acc[snippet.languageId].push(snippet);
      return acc;
    }, {});

    Object.entries(groupedCandidates).forEach(
      ([language, languageSnippets]) => {
        const candidatesExceptLanguage = this.candidates.filter(
          snippet => snippet.languageId !== language
        );
        this.candidatesByLang[language] = languageSnippets.concat(
          candidatesExceptLanguage
        );

        const tagGroupedSnippets = languageSnippets.reduce((acc, snippet) => {
          if (!acc[snippet.primaryTag]) acc[snippet.primaryTag] = [];
          acc[snippet.primaryTag].push(snippet);
          return acc;
        }, {});

        this.candidatesByLangAndTag[language] = {};

        Object.entries(tagGroupedSnippets).forEach(([tag, tagSnippets]) => {
          const languageSnippetsExceptTag = languageSnippets.filter(
            snippet => snippet.primaryTag !== tag
          );
          this.candidatesByLangAndTag[language][tag] = tagSnippets
            .concat(languageSnippetsExceptTag)
            .concat(candidatesExceptLanguage);
        });
      }
    );
  }

  recommendableSnippets(language = null, tag = null) {
    if (RecommendationPresenter.candidates === null)
      RecommendationPresenter.prepareRecommendableSnippets();

    if (language === null) return RecommendationPresenter.candidates;

    const tagExists = Boolean(
      RecommendationPresenter.candidatesByLangAndTag[language][tag]
    );

    if (tag === null || !tagExists)
      return RecommendationPresenter.candidatesByLang[language];

    return RecommendationPresenter.candidatesByLangAndTag[language][tag];
  }

  constructor(object, options = {}) {
    this.object = object;
    this.options = options;

    this.id = this.object.id;
    this.slugId = this.object.slugId;
    this.languageId = this.object.languageId;
    this.primaryTag = this.object.primaryTag;
    this.searchTokensArray = this.object.searchTokensArray;
    this.isListed = this.object.isListed;
    this.searchTokensLength = this.object.searchTokensArray.length;
    this.recommendationRankings = new Map();
    this.minRankings = [];
  }

  recommendSnippets() {
    const recommendableSnippets = this.recommendableSnippets(
      this.object.languageId,
      this.object.primaryTag
    );

    recommendableSnippets.forEach(snippet => {
      // Skip if the snippet is the same as the current snippet
      if (snippet.id === this.id) return;
      // Skip if the snippet is the same in another language
      if (snippet.slugId === this.slugId) return;
      // Skip unless this snippet is listed or the object is unlisted
      if (!snippet.isListed && this.isListed) return;

      // Store the minimum ranking so far
      const minRanking = this.minRankings[0] || 0;

      // Performance optimization - if language score is 0 and the minimum
      // recommendation score is greater than the score limit without language,
      // then we can skip the rest of the calculations.
      const isSameLanguage = this.languageId === snippet.languageId;
      if (
        !isSameLanguage &&
        minRanking > settings.recommendations.scoreLimitWithoutLanguage
      )
        return;

      // Determine score for language:
      //  * Same language, as language = 100% of language score
      //  * Not same language = 0% of language score
      const languageScore = isSameLanguage
        ? settings.recommendations.fullLanguageScore
        : 0;

      // Performance optimization - if both language and primary tag scores are
      // 0 and the minimum recommendation score is greater than the score limit
      // without language and primary tag, then we can skip the rest of the
      // calculations.
      const primaryTagIndex = isSameLanguage
        ? snippet.tags.indexOf(this.primaryTag)
        : -1;

      if (
        !isSameLanguage &&
        primaryTagIndex === -1 &&
        minRanking >
          settings.recommendations.scoreLimitWithoutLanguageAndPrimaryTag
      )
        return;

      // Determine primary tag score:
      //  * Different language = 0% of tag score
      //  * Same primary tag = 100% of tag score
      //  * Contains primary tag, but not primary = 50% of tag score
      //  * Doesn't contain tag = 0% of language score
      const primaryTagScore =
        primaryTagIndex === -1
          ? 0
          : primaryTagIndex === 0
            ? settings.recommendations.fullPrimaryTagScore
            : settings.recommendations.halfPrimaryTagScore;

      // Determine search token score:
      //  * Count found tokens and divide by total number of tokens
      const searchTokenScore =
        (this.searchTokensArray.reduce(
          (a, t) => (snippet.searchTokensArray.includes(t) ? a + 1 : a),
          0
        ) /
          this.searchTokensLength) *
        settings.recommendations.searchTokenScoreLimit;

      // Divide by the limit to get a value between 0 and 1
      const recommendationRanking =
        (languageScore + primaryTagScore + searchTokenScore) /
        settings.recommendations.totalScoreLimit;

      // Performance optimization to minimize the number of times we have to
      // sort afterwards. As soon as the minimum amount of snippets has been
      // considered, we can start trimming off any snippets below the lowest
      // snippet's recommendation ranking.
      if (recommendationRanking > 0) {
        if (
          this.minRankings.length < settings.recommendations.recommendationCount
        ) {
          // First 4 snippets are always added
          this.minRankings.push(recommendationRanking);
        } else {
          // If the new ranking is lower than the lowest ranking, ignore it
          if (recommendationRanking < this.minRankings[0]) {
            return;
          } else {
            // Otherwise, replace the lowest ranking with the new ranking
            this.minRankings[0] = recommendationRanking;
          }
        }
        this.minRankings.sort((a, b) => a - b);

        this.recommendationRankings.set(snippet.id, [
          recommendationRanking,
          snippet.ranking,
          snippet,
        ]);
      }
    });

    return Array.from(this.recommendationRankings.values())
      .sort((a, b) => (a[0] === b[0] ? b[1] - a[1] : b[0] - a[0]))
      .slice(0, settings.recommendations.recommendationCount)
      .map(r => r[2]);
  }
}