vikzh/js-lists

View on GitHub
src/index.js

Summary

Maintainability
A
1 hr
Test Coverage
import * as pairs from 'pairs-js';

/**
 * Make a list
 * @param args
 * @returns {list}
 * @example
 * cons(1, 2, 3); // (1, 2, 3)
 * cons(); // ()
 */
const cons = (...args) => {
  const iter = (elements) => {
    if (!elements.length) {
      return null;
    }

    return pairs.cons(elements[0], iter(elements.slice(1)));
  };
  return iter(args);
};

/**
 * Check if arguments is list
 * @param list
 * @returns {boolean}
 * @example
 * isList(cons()); // true
 * isList(cons(1, 2)); // true
 * isList('text'); // false
 */
const isList = (list) => {
  if (list === null) {
    return true;
  }

  if (pairs.isPair(list)) {
    return isList(pairs.cdr(list));
  }

  return false;
};

const checkList = (list) => {
  if (!isList(list)) {
    let value;
    if (typeof list === 'object') {
      value = JSON.stringify(list, null, 2);
    } else {
      value = String(list);
    }
    throw new Error(`Argument must be list, but it was '${value}'`);
  }
};

/**
 * Get head element
 * @param list
 * @returns {*}
 * @example
 * head(cons(1, 2)); // 1
 */
const head = (list) => {
  checkList(list);
  return pairs.car(list);
};

/**
 * Get tail element
 * @param list
 * @returns {*}
 * @example
 * tail(cons(1, 2, 3)); // (2, 3)
 */
const tail = (list) => {
  checkList(list);
  return pairs.cdr(list);
};

/**
 * Check if list is empty
 * @param list
 * @returns {boolean}
 * @example
 * isEmpty(cons(1, 2)); // false
 * isEmpty(cons()); //true
 */
const isEmpty = (list) => {
  checkList(list);
  return list === null;
};

/**
 * Add element to list
 * @param element
 * @param list
 * @returns {list}
 * @example
 * push(3, cons(2, 1)); // (3, 2, 1)
 */
const push = (element, list) => {
  checkList(list);
  return pairs.cons(element, list);
};

/**
 * Convert list to string
 * @param list
 * @returns {string}
 * @example
 * toString(cons(1, 2, 3)); // (1, 2, 3);
 * toString(cons(1, cons(2, 3))); // (1, (2, 3));
 */
const toString = (list) => {
  if (!isList(list)) {
    if (pairs.isPair(list)) {
      return `pair: ${pairs.toString(list)}`;
    }
    if (typeof list === 'object') {
      return JSON.stringify(list, null, 2);
    }
    return list;
  }

  if (isEmpty(list)) {
    return '()';
  }

  const rec = (p) => {
    const first = head(p);
    const rest = tail(p);
    if (isEmpty(rest)) {
      return toString(first);
    }

    return `${toString(first)}, ${rec(rest)}`;
  };

  return `(${rec(list)})`;
};

/**
 * Check if list has an element
 * @param value
 * @param list
 * @returns {boolean}
 * @example
 * has(1, cons(1, 2)); // true
 * has(5, cons(1, 2)); // false
 */
const has = (value, list) => {
  checkList(list);
  if (isEmpty(list)) {
    return false;
  }
  if (head(list) === value) {
    return true;
  }
  return has(value, tail(list));
};

/**
 * Get number of elements in a list
 * @param list
 * @returns {int}
 * @example
 * count(cons(1, 2, 3)); // 3
 * count(cons()); // 0
 */
const count = (list) => {
  checkList(list);
  if (isEmpty(list)) {
    return 0;
  }

  const iter = (elements, n) => (isEmpty(elements) ? n : iter(tail(elements), n + 1));
  return iter(list, 0);
};

/**
 * Reverse list
 * @param list
 * @returns {list}
 * @example
 * reverse(cons(1, 2, 3)); // (3, 2, 1)
 */
const reverse = (list) => {
  checkList(list);
  const iter = (curElements, acc) => (isEmpty(curElements)
    ? acc
    : iter(tail(curElements), pairs.cons(head(curElements), acc)));
  return iter(list, cons());
};

/**
 * Join 2 lists
 * @param list1
 * @param list2
 * @returns {list}
 * @example
 * concat(cons(1, 2), cons(3, 4)); // (1, 2, 3, 4)
 */
const concat = (list1, list2) => {
  if (isEmpty(list1)) {
    return list2;
  }
  return pairs.cons(head(list1), concat(tail(list1), list2));
};

/**
 * Map list
 * @param f
 * @param list
 * @returns {list}
 * @example
 * const list = cons(1, 2, 3);
 * map(el => el ** 2, list); // (1, 4, 9)
 */
const map = (f, list) => {
  checkList(list);
  if (isEmpty(list)) {
    return cons();
  }
  return push(f(head(list)), map(f, tail(list)));
};

/**
 * Filter list
 * @param f
 * @param list
 * @returns {list}
 * @example
 * const list = cons(-1, 2, -5, 0, 9);
 * filter(el => el > 0, list); // (2, 9)
 */
const filter = (f, list) => {
  checkList(list);
  if (isEmpty(list)) {
    return cons();
  }
  return f(head(list)) ? push(head(list), filter(f, tail(list))) : filter(f, tail(list));
};

/**
 * Reduce list
 * @param f
 * @param acc
 * @param elements
 * @returns {*}
 * @example
 * reduce((el, acc) => acc + 1, 0, cons(1, 2, 3)); // 3
 */
const reduce = (f, acc, elements) => {
  if (isEmpty(elements)) {
    return acc;
  }

  return reduce(f, f(head(elements), acc), tail(elements));
};

const take = (countNum, list) => {
  const iter = (curNum, curList) => {
    if (curNum === countNum || isEmpty(curList)) {
      return cons();
    }

    return push(head(curList), iter(curNum + 1, tail(curList)));
  };

  return iter(0, list);
};

export {
  cons, head, tail, isEmpty, toString, has, count, reverse, isList, push,
  concat, map, filter, reduce, take,
};