ilios/frontend

View on GitHub
packages/ilios-common/addon/helpers/sort-by.js

Summary

Maintainability
C
7 hrs
Test Coverage
// taken from Ember Composable Helpers (https://github.com/DockYard/ember-composable-helpers), then modified.
import { get } from '@ember/object';
import { isEmpty } from '@ember/utils';
import { helper } from '@ember/component/helper';
import asArray from 'ilios-common/utils/as-array';

const collator = new Intl.Collator(undefined, {
  sensitivity: 'base',
});

function normalizeToBoolean(val) {
  if (typeof val === 'boolean') {
    return val;
  }

  if (typeof val === 'number') {
    if (val > 0) {
      return false;
    } else if (val < 0) {
      return true;
    }
  }

  return val;
}

function safeValueForKey(ctx, key) {
  if (ctx === null || ctx === undefined) {
    return ctx;
  }
  return get(ctx, key);
}

function sortDesc(key, a, b) {
  if (isEmpty(key)) {
    return 0;
  }

  const aValue = safeValueForKey(a, key);
  const bValue = safeValueForKey(b, key);

  const isANullable = typeof aValue == 'undefined' || aValue === null;
  const isBNullable = typeof bValue == 'undefined' || bValue === null;

  if (isANullable && isBNullable) {
    //both values are nullable
    return 0;
  }

  if (isBNullable) {
    // keep bValue last
    return -1;
  }
  if (isANullable) {
    // put aValue last
    return 1;
  }

  if (aValue.toLowerCase && bValue.toLowerCase) {
    return collator.compare(bValue, aValue);
  }

  if (aValue < bValue) {
    return 1;
  } else if (aValue > bValue) {
    return -1;
  }

  return 0;
}

function sortAsc(key, a, b) {
  if (isEmpty(key)) {
    return 0;
  }

  const aValue = safeValueForKey(a, key);
  const bValue = safeValueForKey(b, key);

  const isANullable = typeof aValue == 'undefined' || aValue === null;
  const isBNullable = typeof bValue == 'undefined' || bValue === null;

  if (isANullable && isBNullable) {
    //both values are nullable
    return 0;
  }

  if (isBNullable) {
    // keep bValue last
    return -1;
  }
  if (isANullable) {
    // put aValue last
    return 1;
  }

  if (aValue.toLowerCase && bValue.toLowerCase) {
    return collator.compare(aValue, bValue);
  }

  if (aValue < bValue) {
    return -1;
  } else if (aValue > bValue) {
    return 1;
  }

  return 0;
}

class SortBy {
  constructor(...args) {
    let [array] = args;
    if (typeof array.toArray === 'function') {
      if (typeof array.slice === 'function') {
        //for ember data toArray still exists but is deprecated, we want slice if it's there
        array = array.slice();
      } else {
        array = array.toArray();
      }
    }

    this.array = [...array];
  }

  comparator(key) {
    return typeof key === 'function' ? key : this.defaultSort(key);
  }

  defaultSort(sortKey) {
    let func = sortAsc;
    if (sortKey.match(':desc')) {
      func = sortDesc;
    }

    return (a, b) => func(sortKey.replace(/:desc|:asc/, ''), a, b);
  }
}

/**
 * best O(n); worst O(n^2)
 * If we feel like swapping with something more performant like QuickSort or MergeSort
 * then it should be easy
 *
 * @class BubbleSort
 * @extends SortBy
 */
class BubbleSort extends SortBy {
  perform(keys = []) {
    let swapped = false;

    let compFuncs = keys.map((key) => this.comparator(key));
    let compFunc = (a, b) => {
      for (let i = 0; i < compFuncs.length; i += 1) {
        let result = compFuncs[i](a, b);
        if (result === 0) {
          continue;
        }
        return result;
      }
      return 0;
    };
    for (let i = 1; i < this.array.length; i += 1) {
      for (let j = 0; j < this.array.length - i; j += 1) {
        let shouldSwap = normalizeToBoolean(compFunc(this.array[j + 1], this.array[j]));
        if (shouldSwap) {
          [this.array[j], this.array[j + 1]] = [this.array[j + 1], this.array[j]];

          swapped = true;
        }
      }

      // no need to continue sort if not swapped in any inner iteration
      if (!swapped) {
        return this.array;
      }
    }
  }
}

export default helper(function sortBy(params) {
  // slice params to avoid mutating the provided params
  let sortParams = params.slice();
  let array = asArray(sortParams.pop());
  let sortKeys = sortParams;

  if (!array || !sortKeys || sortKeys.length === 0) {
    return [];
  }

  if (sortKeys.length === 1 && Array.isArray(sortKeys[0])) {
    sortKeys = sortKeys[0];
  }

  const sortKlass = new BubbleSort(array);
  sortKlass.perform(sortKeys);
  return sortKlass.array;
});