Source: lib/sort.js

var stringSimilarity = require('string-similarity')
var _ = require('lodash')

/**
 * Stable sort, preserving the original order when possible.
 * @param {Array} arr - The array to sort.
 * @param {function} [fn] - A comparison function that returns
 * `-1` if the first argument scores less than the second argument,
 * `1` if the first argument scores more than the second argument,
 * and `0` if the scores are equal.
 * @return {Array} - The array, sorted.
 */
function sort (arr, fn) {
  fn = fn || sort.ascending()
  var i = 0
  var pairs = arr.map(function (x) {
    return {
      idx: i++,
      val: x
    }
  })
  pairs = pairs.sort(function (a, b) {
    var x = fn(a.val, b.val)
    if (x) { return x }
    return (a.idx < b.idx) ? -1 : ((a.idx > b.idx) ? 1 : 0)
  })
  for (i = 0; i < arr.length; i++) {
    arr[i] = pairs[i].val
  }
  return arr
}

/**
 * Create an ascending comparison function.
 * @param {function} fn - A scoring function.
 * @return {function} - A comparison function that returns
 * `-1` if the first argument scores less than the second argument,
 * `1` if the first argument scores more than the second argument,
 * and `0` if the scores are equal.
 */
sort.ascending = function (fn) {
  fn = fn || _.identity
  return function (a, b) {
    var x = fn(a)
    var y = fn(b)
    return (x < y) ? -1 : ((x > y) ? 1 : 0)
  }
}

/**
 * Create a descending comparison function.
 * @param {function} fn - A scoring function.
 * @return {function} - A comparison function that returns
 * `-1` if the first argument scores more than the second argument,
 * `1` if the first argument scores less than the second argument,
 * and `0` if the scores are equal.
 */
sort.descending = function (fn) {
  fn = fn || _.identity
  return function (a, b) {
    var x = fn(a)
    var y = fn(b)
    return (x < y) ? 1 : ((x > y) ? -1 : 0)
  }
}

/**
 * Combine comparison functions.
 * @param {...function} fn - A comparison function.
 * @return {function} - A combined comparison function that returns
 * the first comparison value unless the comparands are equal,
 * in which case it returns the next value.
 */
sort.combine = function () {
  var args = Array.prototype.slice.call(arguments)
  return args.reduce(function (fn1, fn2) {
    return function (a, b) {
      var val = fn1(a, b)
      return (val === 0) ? fn2(a, b) : val
    }
  })
}

/**
 * Compare tracks by Spotify popularity.
 * @param {Track} a - A track.
 * @param {Track} b - A track.
 * @return {integer} - `1` if `a` is less than `b`,
 * `-1` if `a` is greater than `b`,
 * and `0` if `a` is equal to `b`.
 */
sort.popularity = sort.descending(function (x) {
  return x.popularity || -1
})

/**
 * Compare tracks by Last.fm rating.
 * @param {Track} a - A track.
 * @param {Track} b - A track.
 * @return {integer} - `1` if `a` is less than `b`,
 * `-1` if `a` is greater than `b`,
 * and `0` if `a` is equal to `b`.
 */
sort.lastfm = sort.combine(sort.descending(function (x) {
  return x.lastfmPersonal
}), sort.descending(function (x) {
  return x.lastfmGlobal
}), sort.popularity)

/**
 * Compare albums by type. Proper albums are ranked highest,
 * followed by singles, guest albums, and compilation albums.
 * @param {JSON} a - An album.
 * @param {JSON} b - An album.
 * @return {integer} - `-1` if `a` is less than `b`,
 * `1` if `a` is greater than `b`,
 * and `0` if `a` is equal to `b`.
 */
sort.type = sort.descending(function (album) {
  var rankings = {
    'album': 4,
    'single': 3,
    'appears_on': 2,
    'compilation': 1
  }
  var type = album.album_type
  return rankings[type] || 0
})

/**
 * Compare albums by type and popularity.
 * @param {JSON} a - An album.
 * @param {JSON} b - An album.
 * @return {integer} - `-1` if `a` is less than `b`,
 * `1` if `a` is greater than `b`,
 * and `0` if `a` is equal to `b`.
 */
sort.album = sort.combine(sort.type, sort.popularity)

/**
 * Sort objects by similarity to a string.
 * @param {function} fn - A function returning the object's name
 * as a string.
 * @param {string} str - The string to compare against.
 * @return {function} - A comparison function.
 */
sort.similarity = function (fn, str) {
  return sort.descending(function (x) {
    return stringSimilarity.compareTwoStrings(fn(x), str)
  })
}

/**
 * Sort album objects by similarity to a string.
 * @param {string} album - The string to compare against.
 * @return {function} - A comparison function.
 */
sort.similarAlbum = function (album) {
  return sort.similarity(function (x) {
    return x.name + ' - ' + (x.artists[0].name || '')
  }, album)
}

/**
 * Sort artist objects by similarity to a string.
 * @param {string} artist - The string to compare against.
 * @return {function} - A comparison function.
 */
sort.similarArtist = function (artist) {
  return sort.similarity(function (x) {
    return x.name
  }, artist)
}

/**
 * Sort track objects by similarity to a string.
 * @param {string} track - The string to compare against.
 * @return {function} - A comparison function.
 */
sort.similarTrack = function (track) {
  return sort.similarity(function (x) {
    return x.name + ' - ' + (x.artists[0].name || '')
  }, track)
}

/**
 * Sort track objects by censorship.
 * Explicit tracks are preferred over censored ones.
 * @param {Track} a - A track.
 * @param {Track} b - A track.
 * @return {integer} - `1` if `a` is less than `b`,
 * `-1` if `a` is greater than `b`,
 * and `0` if `a` is equal to `b`.
 */
sort.censorship = sort.descending(function (x) {
  return x.explicit ? 1 : 0
})

/**
 * Sort track objects by similarity to a track,
 * popularity, and censorship.
 * @param {string} track - The track to compare against.
 * @return {function} - A comparison function.
 */
sort.track = function (track) {
  return sort.combine(sort.similarTrack(track),
                      sort.popularity,
                      sort.censorship)
}

module.exports = sort