index.js

import {
  compose,
  curry,
  forEach,
  identity,
  into,
  invoker,
  join,
  lensIndex,
  map,
  over,
  path,
  pathEq,
  pathOr,
  reduce,
  split,
  toUpper,
  useWith,
} from "ramda";

const dotSplitter = split(".");

/**
 * Retrieve the value at a given nested dotted path.
 *
 * @func
 * @param {String} path The dotted path to use.
 * @param {Object} obj The object to retrieve the nested property from.
 * @return {*} The data at `path`.
 * @example
 *
 * dp("a.b", {a: {b: 2}}); //=> 2
 * dp("a.b.", {c: {b: 2}}); //=> undefined
 * dp("a.b.0", {a: {b: [1, 2, 3]}}); //=> 1
 * dp("a.b.-2", {a: {b: [1, 2, 3]}}); //=> 2
 */
export const dp = useWith(path, [dotSplitter]);

/**
 * Retrieve the value at a given nested dotted path if it has a value otherwise return the default value.
 *
 * @func
 * @param {*} default The default value
 * @param {String} path The dotted path to use.
 * @param {Object} obj The object to retrieve the nested property from.
 * @return {*} The data at `path` if it has a value otherwise the default value.
 * @example
 *
 * dpOr(1, "a.b", {a: {b: 2}}); //=> 2
 * dpOr(1, "a.b.", {c: {b: 2}}); //=> undefined
 * dpOr(1, "a.b.1", {a: {b: [1, 2, 3]}}); //=> 2
 * dpOr(1, "a.b.-2", {a: {b: [1, 2, 3]}}); //=> 2
 */
export const dpOr = useWith(pathOr, [identity, dotSplitter]);

/**
 * Determines whether a dotted path on an object has a specific value, in
 * [`R.equals`](#equals) terms. Most likely used to filter a list.
 *
 * @func
 * @param {*} val The value to compare the nested property with
 * @param {String} path The dotted path of the nested property to use
 * @param {Object} obj The object to check the nested property in
 * @return {Boolean} `true` if the value equals the nested object property,
 *         `false` otherwise.
 * @example
 *
 * const user1 = { address: { zipCode: 90210 } };
 * const user2 = { address: { zipCode: 55555 } };
 * const user3 = { name: 'Bob' };
 * const users = [ user1, user2, user3 ];
 * const isFamous = dpEq(90210, 'address.zipCode');
 * R.filter(isFamous, users); //=> [ user1 ]
 *
 */
export const dpEq = useWith(pathEq, [identity, dotSplitter]);

/**
 * Returns a new list by plucking the same dotted path off all objects in
 * the list supplied.
 *
 * `pluck` will work on
 * any [functor](https://github.com/fantasyland/fantasy-land#functor) in
 * addition to arrays, as it is equivalent to `R.map(dp(k), f)`.
 *
 * @func
 * @param {String} key The dotted path to pluck off of each object.
 * @param {Array} f The array or functor to consider.
 * @return {Array} The list of values for the given key.
 * @example
 *
 * dpPluck('name.first', [{name: { first: 'fred' }}, {name: { first: 'wilma'} }]); //=> ['fred', 'wilma']
 *
 */
export const dpPluck = useWith(map, [dp]);

/**
 * Returns a list of tuples where the first item is the original item from the leftCollection
 * and the second is is the object from the rightCollection that has a rightComparedPath value equal to the value at leftComparedPath
 *
 * @func
 * @param {String} rightComparedPath The dotted path to compare to leftComparedPath on each rightCollection object.
 * @param {Array} rightCollection The right side of the join
 * @param {String} leftComparedPath The dotted path to compare on each leftCollection object.
 * @param {Array} leftCollection The left side of the join
 * @return {Array} A list of tuples where the first item is the original item from the leftCollection and the second is the joined item
 * @example
 *
 * const leftCollection = [{ a: "a", b: 2 }];
 * const rightCollection = [{ id: "a", c: 2 }];
 * const result = leftJoin("id", rightCollection, "a", leftCollection); //=> [[{ a: "a", b: 2 }, { id: "a", c: 2 }]]
 *
 */
export const leftJoin = curry(
  (rightComparedPath, rightCollection, leftComparedPath, leftCollection) => {
    const hashMap = new Map();

    forEach((item) => {
      const key = dp(rightComparedPath, item);
      if (key) {
        hashMap.set(String(key), item);
      }
    }, rightCollection);

    return map((item) => [
      item,
      hashMap.get(String(dp(leftComparedPath, item))) ?? null,
    ])(leftCollection);
  },
);

/**
 * Returns the largest value for a function applied to a collection of objects
 *
 * @func
 * @param {Function} func The function to apply to each object in the collection.
 * @param {Array} collection The collection to check
 * @return {Number} The largest value returned by applying func to each object in collection.
 * @example
 *
 * maxOf(prop("a"), [{ a: 2 }, { a: 1 }, { a: null }]); //=> 2
 *
 */
export const maxOf = curry((func, collection) =>
  reduce(
    (largest, curr) => {
      const val = func(curr);

      if (val != null && (largest == null || val > largest)) {
        return val;
      } else {
        return largest;
      }
    },
    null,
    collection,
  ),
);

/**
 * Returns the smallest value for a function applied to a collection of objects
 *
 * @func
 * @param {Function} func The function to apply to each object in the collection.
 * @param {Array} collection The collection to check
 * @return {Number} The smallest value returned by applying func to each object in collection.
 * @example
 *
 * minOf(prop("a"), [{ a: 2 }, { a: 1 }, { a: null }]); //=> 1
 *
 */
export const minOf = curry((func, collection) =>
  reduce(
    (smallest, curr) => {
      const val = func(curr);

      if (val != null && (smallest == null || val < smallest)) {
        return val;
      } else {
        return smallest;
      }
    },
    null,
    collection,
  ),
);

/**
 * Invokes a function on each object in a collection and returns each result
 *
 * @func
 * @param {Function} func The function to invoke on each object in the collection.
 * @param {Array} collection The collection to invoke
 * @return {*} An array of each result of invoking func on each item in collection
 * @example
 *
 * const objs = [{ test() { return "a" } }, { test() { return "b" } }];
 * invokeMap("test", objs); //=> ["a", "b"]
 *
 */
export const invokeMap = useWith(map, [invoker(0)]);

/**
 * Uppercases the first letter of a string
 *
 * @func
 * @param {String} str The string to uppercase the first letter of
 * @return {String} The input string with its first letter uppercased
 * @example
 *
 * upperFirst("test"); //=> "Test"
 *
 */
export const upperFirst = compose(join(""), over(lensIndex(0), toUpper));

/**
 * Is a shorthand for into([], compose(), arr ?? [])
 *
 * @func
 * @param {Array} transformedArray The array to transform
 * @param {Function[]} transformers A list of functions to transform the input array
 * @return {Array} The transformed input array
 * @example
 *
 * intoArray(map(i => i + 1), map(i => i * 2), [1, 2])
 *
 */
export const intoArray = (...args) => into([], compose(...args));