Source: services/text-formatters.js

/**
 * Commonly-used functions for formatting text in badge labels. Includes
 * ordinal numbers, currency codes, star ratings, versions, etc.
 *
 * @module
 */

import dayjs from 'dayjs'
import calendar from 'dayjs/plugin/calendar.js'
import relativeTime from 'dayjs/plugin/relativeTime.js'
dayjs.extend(calendar)
dayjs.extend(relativeTime)

/**
 * Creates a string of stars and empty stars based on the rating.
 * The number of stars is determined by the integer part of the rating.
 * An additional star or a three-quarter star or a half star or a quarter star is added based on the decimal part of the rating.
 * The remaining stars are empty stars until the maximum number of stars is reached.
 *
 * @param {number} rating - Current rating
 * @param {number} [max] - Maximum rating
 * @returns {string} A string of stars and empty stars
 */
function starRating(rating, max = 5) {
  const flooredRating = Math.floor(rating)
  let stars = ''
  while (stars.length < flooredRating) {
    stars += '★'
  }
  const decimal = rating - flooredRating
  if (decimal >= 0.875) {
    stars += '★'
  } else if (decimal >= 0.625) {
    stars += '¾'
  } else if (decimal >= 0.375) {
    stars += '½'
  } else if (decimal >= 0.125) {
    stars += '¼'
  }

  while (stars.length < max) {
    stars += '☆'
  }
  return stars
}

/**
 * Converts the ISO 4217 code to the corresponding currency symbol.
 * If the the symbol for the code is not found, then the code itself is returned.
 *
 * @param {string} code - ISO 4217 code
 * @returns {string} Currency symbol for the code
 */
function currencyFromCode(code) {
  return (
    {
      CNY: '¥',
      EUR: '€',
      GBP: '₤',
      USD: '$',
    }[code] || code
  )
}

/**
 * Calculates the ordinal number of the given number.
 * For example, if the input is 1, the output is “1ˢᵗ”.
 *
 * @param {number} n - Input number
 * @returns {string} Ordinal number of the input number
 */
function ordinalNumber(n) {
  const s = ['ᵗʰ', 'ˢᵗ', 'ⁿᵈ', 'ʳᵈ']
  const v = n % 100
  return n + (s[(v - 20) % 10] || s[v] || s[0])
}

const metricPrefix = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
const metricPower = metricPrefix.map((a, i) => Math.pow(1000, i + 1))

/**
 * Given a number (positive or negative), returns a string with appropriate unit in the metric system, SI.
 * Note: numbers beyond the peta- cannot be represented as integers in JS.
 * For example, if you call metric(1000), it will return "1k", which means one kilo or one thousand.
 *
 * @param {number} n - Input number
 * @returns {string} String with appropriate unit in the metric system, SI
 */
function metric(n) {
  for (let i = metricPrefix.length - 1; i >= 0; i--) {
    const limit = metricPower[i]
    const absN = Math.abs(n)
    if (absN >= limit) {
      const scaledN = absN / limit
      if (scaledN < 10) {
        // For "small" numbers, display one decimal digit unless it is 0.
        const oneDecimalN = scaledN.toFixed(1)
        if (oneDecimalN.charAt(oneDecimalN.length - 1) !== '0') {
          const res = `${oneDecimalN}${metricPrefix[i]}`
          return n > 0 ? res : `-${res}`
        }
      }
      const roundedN = Math.round(scaledN)
      if (roundedN < 1000) {
        const res = `${roundedN}${metricPrefix[i]}`
        return n > 0 ? res : `-${res}`
      } else {
        const res = `1${metricPrefix[i + 1]}`
        return n > 0 ? res : `-${res}`
      }
    }
  }
  return `${n}`
}

/**
 * Remove the starting v in a string if it exists.
 * For example, omitv("v1.2.3") returns "1.2.3", but omitv("hello") returns "hello".
 *
 * @param {string} version - Version string
 * @returns {string} Version string without the starting v
 */
function omitv(version) {
  if (version.charCodeAt(0) === 118) {
    return version.slice(1)
  }
  return version
}

const ignoredVersionPatterns = /^[^0-9]|[0-9]{4}-[0-9]{2}-[0-9]{2}/

/**
 * Add a starting v to the version unless it doesn't starts with a digit or is a date (yyyy-mm-dd)
 * For example, addv("1.2.3") returns "v1.2.3", but addv("hello") or addv("2021-10-31"), returns "hello" and "2021-10-31" respectively.
 *
 * @param {string} version - Version string
 * @returns {string} Version string with the starting v
 */
function addv(version) {
  version = `${version}`
  if (version.startsWith('v') || ignoredVersionPatterns.test(version)) {
    return version
  } else {
    return `v${version}`
  }
}

/**
 * Returns a string that is either the singular or the plural form of a word,
 * depending on the length of the countable parameter.
 *
 * @param {string} singular - Singular form of the word
 * @param {string[]} countable - Array of values you want to count
 * @param {string} plural - Plural form of the word
 * @returns {string} Singular or plural form of the word
 */
function maybePluralize(singular, countable, plural) {
  plural = plural || `${singular}s`

  if (countable && countable.length === 1) {
    return singular
  } else {
    return plural
  }
}

/**
 * Returns a formatted date string without the year based on the value of input date param d.
 *
 * @param {Date | string | number | object } d - Input date in dayjs compatible format, date object, datestring, Unix timestamp etc.
 * @returns {string} Formatted date string
 */
function formatDate(d) {
  const date = dayjs(d)
  const dateString = date.calendar(null, {
    lastDay: '[yesterday]',
    sameDay: '[today]',
    lastWeek: '[last] dddd',
    sameElse: 'MMMM YYYY',
  })
  // Trim current year from date string
  return dateString.replace(` ${dayjs().year()}`, '').toLowerCase()
}

/**
 * Returns a relative date from the input timestamp.
 * For example, day after tomorrow's timestamp will return 'in 2 days'.
 *
 * @param {number | string} timestamp - Unix timestamp
 * @returns {string} Relative date from the unix timestamp
 */
function formatRelativeDate(timestamp) {
  const parsedDate = dayjs.unix(parseInt(timestamp, 10))
  if (!parsedDate.isValid()) {
    return 'invalid date'
  }
  return dayjs().to(parsedDate).toLowerCase()
}

export {
  starRating,
  currencyFromCode,
  ordinalNumber,
  metric,
  omitv,
  addv,
  maybePluralize,
  formatDate,
  formatRelativeDate,
}