cint
Version:
A library of Javascript utility functions with an emphasis on Functional Programming.
818 lines (720 loc) • 18.9 kB
JavaScript
/***********************************
* String
***********************************/
/**
* Performs variable substitution on the string, replacing items in {curly braces}.
*
* Same as Lodash's _.template(str, o, { interpolate: /{([\s\S]+?)}/g }).
*
* Based on supplant by Douglas Crockford http://javascript.crockford.com/remedial.html
*
* @param str
* @param o
*/
export const supplant = (str, o) =>
str.replace(/{([^{}]*)}/g,
(a, b) => b in o ? o[b] : a
)
/**
* Returns true if the string starts with the given substring. Returns false if the substring is the empty string.
*
* @param str
* @param sub The substring.
*/
export const startsWith = (str, sub) =>
str.indexOf(sub) === 0 && sub !== ''
/**
* Returns the substring before the first instance of the given delimiter.
*
* @param str
* @param delim
*/
export const before = (str, delim) =>
str.split(delim)[0]
/**
* Returns the substring after the first instance of the given delimiter. Returns the whole string if the delimiter is not found.
*
* @param str
* @param delim
*/
export const after = (str, delim) => {
const delimIndex = str.indexOf(delim)
return delimIndex >= 0 ?
str.substring(delimIndex + delim.length) : str
}
/**
* Returns the substring between the given delimiters.
*
* @param str
* @param left
* @param right
*/
export const between = (str, left, right) =>
before(after(str, left), right)
/**
* Wraps a string with a left and right. If right omitted, wraps both ends in left.
*
* @param middle
* @param left
* @param right
*/
export const bookend = (middle, left, right) =>
(left || '') + middle + (right || left || '')
/**
* Returns a single string that repeats the string n times. Optionally joins it with the given delimeter
*
* @param str
* @param n
* @param delim Default: ''
*/
export const repeatString = (str, n, delim) => {
delim = delim || ''
return mapNumber(n, () => str).join(delim)
}
/**
* Capitalizes the first letter of each word in the given string.
*
* @param str
*/
export const toTitleCase = str => {
const capitalizeFirst = s =>
s.length ? s[0].toUpperCase() + s.substring(1).toLowerCase() : ''
return str.split(' ').map(capitalizeFirst).join(' ')
}
/***********************************
* Number
***********************************/
/**
* Returns the ordinal value (like '1st' or '2nd') of the given integer.
*
* @param n
*/
export const ordinal = n => {
const lastDigit = n % 10
return n + (
n >= 11 && n <= 13 ? 'th' :
lastDigit === 1 ? 'st' :
lastDigit === 2 ? 'nd' :
lastDigit === 3 ? 'rd' :
'th')
}
/**
* Invokes the given export const n times = , passing the index for each invocation, and returns an array of the results=> .
*
* @param n
* @param f
*/
export const mapNumber = (n, f) => {
const results = []
for (let i = 0; i < n; i++) {
results.push(f(i))
}
return results
}
/** Adds two numbers. */
export const addTwo = (x, y) => x + y
/** Adds all given arguments. */
export const add = (...args) =>
args.length ? args.reduce(addTwo) : 0
/***********************************
* Array
***********************************/
/**
* Group the array of objects by one of the object's properties or mappable function. Returns an array of { key: ___, items: ___ } objects which represent all the items in the original array grouped by the value of the specified grouping key.
*
* @param arr
* @param propOrFunc
*/
export const orderedGroup = (arr, propOrFunc) => {
if (!propOrFunc) {
throw new Error('You must specific a property name or mappable function.')
}
const getGroupKey = typeof propOrFunc === 'function'
? propOrFunc
: item => item[propOrFunc]
const results = []
const dict = {}
const len = arr.length
for (let i = 0; i < len; i++) {
const key = getGroupKey(arr[i])
if (!(key in dict)) {
dict[key] = []
results.push({ key: key, items: dict[key] })
}
dict[key].push(arr[i])
}
return results
}
/**
* Returns a dictionary whose keys are the values of the array and values are the number of instances of that value within the array.
*
* @param arr
*/
export const tally = arr => {
const dict = {}
const len = arr.length
for (let i = 0; i < len; i++) {
const count = dict[arr[i]] || 0
dict[arr[i]] = count + 1
}
return dict
}
/** Tally the property values of an array of object, grouping the counts for each property under its value.
e.g.
[
{
ideal: 4,
past: 3,
present: 7
},
{
ideal: 5,
past: 7,
present: 7
}
]
=>
{
"4": {
ideal: 1
},
"3": {
past: 1
}
"5": {
ideal: 1
}
"7": {
present: 2,
past: 1
}
}
*/
export const tallyProps = arr => {
const tallies = {}
for (let i = 0; i < arr.length; i++) {
const o = arr[i]
// loop through each property so the value can be tallied in the tallies object
for (const key in o) {
// cache the value
const value = o[key]
// create a new tally object if it doesn't exist for this value
if (!tallies[value]) {
tallies[value] = {}
}
// increment the count
tallies[value][key] = (tallies[value][key] || 0) + 1
}
}
return tallies
}
/**
* Returns the in-bounds index of the given index for the array, supports negative and out-of-bounds indices.
*
* @private
* @param arr
* @param i
*/
export const circ = (arr, i) => {
// return first index if i is null or undefined
if (i === undefined || i === null) return arr[0]
// one modulus to get in range, another to eliminate negative
return (i % arr.length + arr.length) % arr.length
}
/**
* Indexes into an array, supports negative indices.
*
* @param arr
* @param n
*/
export const index = (arr, i) => arr[circ(arr, i)]
/**
* Returns a new array containing the elements of the given array shifted n spaces to the left, wrapping around the end.
*
* @param arr
* @param n
*/
export const rotate = (arr, n) => {
const output = []
const len = arr.length
for (let i = 0; i < len; i++) {
output.push(index(arr, i + n))
}
return output
}
/**
* Creates an object with a property for each element of the given array, determined by a function that returns the property as a { key: value }=> .
*
* @param arr
* @param f
*/
export const toObject = (arr, f) => {
const keyValues = []
const len = arr.length
for (let i = 0; i < len; i++) {
keyValues.push(f(arr[i], i))
}
return merge.apply(arr, keyValues)
}
/**
* Functional, nondestructive version of Array.prototype.splice.
*
* @param arr
* @param index
* @param howMany
* @param elements
*/
export const spliced = (arr, index, howMany, ...elements) => {
const elementsLen = elements.length
const results = []
const len = arr.length
// add starting elements
for (let i = 0; i < index && i < len; i++) {
results.push(arr[i])
}
// add inserted elements
for (let i = 0; i < elementsLen; i++) {
results.push(elements[i])
}
// add ending elements
for (let i = index + howMany; i < len; i++) {
results.push(arr[i])
}
return results
}
/**
* Breaks up the array into n evenly-sized chunks.
*
* Solution from http://stackoverflow.com/questions/8188548/splitting-a-js-array-into-n-arrays
*
* @param arr
* @param n
*/
export const chunk = (a, n) => {
const len = a.length; const out = []; let i = 0
while (i < len) {
const size = Math.ceil((len - i) / n--)
out.push(a.slice(i, i += size))
}
return out
}
/***********************************
* Object
***********************************/
/**
* Returns a new object with the given key and value.
*
* @param key
* @param value
*/
export const keyValue = (key, value) => {
const o = {}
o[key] = value
return o
}
/** Gets the value of a key of the given object.
*/
export const getValue = (o, key) => o[key]
/** Sets the value of the given key and returns the object.
*/
export const setValue = (o, key, value) => {
o[key] = value
return o
}
/** Creates a mapping function that applies the given function to the value of the specified key=> .
*/
export const mapOverKey = (f, originalKey, newKey) =>
o => setValue(o, newKey || originalKey, f(o[originalKey]))
/**
* Join the object into a single string with the given separators separating properties from each other as well as values.
*
* @param obj
* @param propSeparator
* @param valueSeparator
*/
export const joinObject = (obj, propSeparator, valueSeparator) => {
const keyValuePairs = []
for (const prop in obj) {
keyValuePairs.push(prop + valueSeparator + obj[prop])
}
return keyValuePairs.join(propSeparator)
}
/**
* Returns a new object with the given objects merged onto it. Non-undefined properties on later arguments override identical properties on earlier arguments.
*
* @param objects
*/
export const merge = (...args) => {
const mothership = {}
// iterate through each given object
const len = args.length
for (let i = 0; i < len; i++) {
const outlier = args[i]
// add each property to the mothership
for (const prop in outlier) {
if (typeOf(outlier[prop]) === 'object' && outlier[prop].constructor === Object && outlier[prop] !== null && !(outlier[prop] instanceof Array)) {
mothership[prop] = merge(mothership[prop], outlier[prop]) // RECURSION
}
else if (outlier[prop] !== undefined) {
mothership[prop] = outlier[prop]
}
}
}
return mothership
}
/**
* Returns an array whose items are the result of calling f(key, value) on each property of the given object. If f is undefined, returns a list of { key: ___, value: ___ } objects.
*
* @param obj
* @param f
*/
export const toArray = (obj, f) => {
f = f || ((key, value) => ({ key: key, value: value }))
const result = []
for (const key in obj) {
result.push(f(key, obj[key]))
}
return result
}
/**
* Returns a new object that only includes the properties of the given obj for which f(key, value) is true.
*
* @param obj
* @param f
*/
export const filterObject = (obj, f) => {
const result = {}
for (const key in obj) {
if (f(key, obj[key])) {
result[key] = obj[key]
}
}
return result
}
/**
* Changes the specified keys in an object.
*
* @param obj
* @param changeKeys
* @example cint.changeKeys(
* { fname: 'Raine', lname: 'Lourie', specialty: 'Javascript' },
* { fname: 'first', lname: 'last' }
* )
*/
export const changeKeys = (obj, changedKeys) => {
const result = {}
for (const key in obj) {
result[key in changedKeys ? changedKeys[key] : key] = obj[key]
}
return result
}
/** Calls a function on an object and returns the object (for chaining purposes)=> .
*/
export const tap = (f, o) => {
f(o)
return o
}
/* console.log's the given object and returns the object (for chaining purposes).
*/
export const look = o => {
console.log(o)
return o
}
/***********************************
* Function
***********************************/
/**
* Returns a function that returns the inverse of the given boolean function=> .
*
* @param f The function to inverse=> .
*/
export const not = f => (...args) => !f(...args)
/**
* Returns a new function that inserts the given curried arguments to the inner function at the specified index of its runtime arguments=> .
*
* @example _.partial(f, args...) is equivalent to cint.partialAt(f, 0, args) and _.partialRight(f, args...) is equivalent to cint.partialAt(f, n, args) where n is the arity of the function.
* @param f
* @param index
* @param curriedArgs
*/
export const partialAt = (f, index, curriedArgs) => (...args) => {
// handle negative indices
// Note that we add 1 so that -1 points to the slot AFTER the last element, not before the last element (which is what the last index points to).
if (index < 0) {
index = circ(args, index) + 1
}
const spliceArgs = [args, index, 0].concat(curriedArgs)
const newArgs = spliced.apply(this, spliceArgs)
return f.apply(this, newArgs)
}
/**
* Returns a new function that calls the function with a limit on the number of arguments.
*
* @param f
* @param n
*/
export const aritize = (f, n) => (...args) => f(...args.slice(0, n))
/** Recursively invokes the given function with no parameters until it returns a non-functional value=> .
*
@param value
*/
export const callTillValue = value =>
typeof value === 'function' ? callTillValue(value()) : value
/**
* Returns a function that calls the given function as normal, then passes its inputs and output to the spier (defaults to console.log=> )
*
* @param f
* @param [spier=console.log]
*/
export const spy = (f, spier) => {
const that = this
spier = spier || console.log.bind(console)
return (...args) => {
const out = f.apply(that, args)
spier.call(that, f, args, out)
return out
}
}
/** Converts the given synchronous function into an asynchronous function that applies its arguments to the original function and invokes callback(error, result)=> .
*
@param f
*/
export const toAsync = f =>
(...args) => {
const that = this
const callback = index(args, -1)
setTimeout(() => {
try {
const result = f.apply(that, args)
callback(null, result)
}
catch (e) {
callback(e)
}
}, 0)
}
/***********************************
* Utility
***********************************/
/** Compares two items lexigraphically. Returns 1 if a>b, 0 if a==b, or -1 if a<b.
*
@param a
@param b
*/
export const compare = (a, b) =>
a > b ? 1 :
a < b ? -1 :
0
/**
* Compares the value of the given property between two objects.
*
* @param property
*/
export const compareProperty = (property, a, b) =>
compare(a[property], b[property])
/**
* Create a new instance of the given constructor with the given constructor arguments. Useful for higher order programmer where the new keyword won't work.
*
* @param C Constructor
* @param args
*/
export const createNew = (C, args) => new C(...args)
/**
* Returns a compare function that can be passed to Array.sort to sort in the order of the given array of properties. A property can also be appended with ' ASC' or ' DESC' to control the sort order=> .
*
* @param props
*/
export const dynamicCompare = props => {
if (!props || !(props instanceof Array)) {
throw new Error('props is falsey or not an Array')
}
return function(a, b) {
const len = props.length
for (let i = 0; i < len; i++) {
let aVal, bVal, sortDir
if (typeof props[i] === 'function') {
aVal = props[i](a)
bVal = props[i](b)
sortDir = 'asc'
}
else if (props[i].toLowerCase().indexOf(' ') >= 0) {
const splitVal = props[i].split(' ')
aVal = a[splitVal[0]]
bVal = b[splitVal[0]]
sortDir = splitVal[1].toLowerCase()
}
else {
aVal = a[props[i]]
bVal = b[props[i]]
sortDir = 'asc'
}
// this is important so that if the values are equal, it continues to the next sort property
if (aVal !== bVal) {
return sortDir === 'asc' ? compare(aVal, bVal) : compare(bVal, aVal)
}
}
return 0
}
}
/** Returns true if all the items in a are equal to all the items in b, recursively.
*
@param a
@param b
*/
export const equals = (a, b) => {
if (typeof a !== typeof b) {
return false
}
// compare arrays
if (a instanceof Array) {
// check if the arrays are the same length
if (a.length !== b.length) {
return false
}
// check the equality of each item
for (let i = 0, l = a.length; i < l; i++) {
if (!b || !b[i] || !equals(a[i], b[i])) { // RECURSION
return false
}
}
}
// compare primitives
else if (typeof a === 'number' || typeof a === 'string' || typeof a === 'boolean' || typeof a === 'undefined') {
if (a !== b) {
return false
}
}
// compare objects
else if (Object.keys(a).length === Object.keys(b)) {
for (const property in a) {
if (!(property in b && b[property] === a[property])) {
return false
}
}
}
else {
return a === b
}
return true
}
/** Converts the given value to a string by calling its toString method.
*/
export const intoString = value => value.toString()
/** Returns true if the given value is not undefined, null, or an empty string.
*
@param x
*/
export const isValue = x =>
x !== undefined && x !== null && x !== ''
/** Returns a string representation of the given scalar, array, or dictionary.
*
@param o
*/
export const hash = o => {
if (o === undefined) {
return 'undefined'
}
else if (o === null) {
return 'null'
}
else if (typeof o === 'string' || typeof o === 'number') {
return '' + o
}
else if (typeOf(o) === 'array') {
return '_[{0}]_'.format(o.map(hash).join(','))
}
else if (typeOf(o) === 'object') {
let objString = ''
for (const prop in o) {
objString += supplant('{0}_:_{1}', [prop, hash(o[prop])])
}
// escape for handlebars
return supplant('_\{{0}}_', [objString]) // eslint-disable-line no-useless-escape
}
else {
throw new Error('Unhashable value: ' + o)
}
}
/** Generates a pseudo-random string that can be assumed to be unique. */
const guid = (function() {
const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
return () => S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()
})()
/** Returns a string representing the type of the object, with special handling for null and arrays.
*
@author Douglas Crockford http://javascript.crockford.com/remedial.html
@param value
*/
export const typeOf = value => {
let s = typeof value
if (s === 'object') {
if (value) {
if (typeof value.length === 'number' &&
!value.propertyIsEnumerable('length') && // eslint-disable-line no-prototype-builtins
typeof value.splice === 'function') {
s = 'array'
}
}
else {
s = 'null'
}
}
return s
}
/***********************************
* Export Public Interface
***********************************/
export default {
// string
supplant,
startsWith,
before,
after,
between,
bookend,
repeatString,
toTitleCase,
// number
ordinal,
mapNumber,
addTwo,
add,
// array
orderedGroup,
tally,
tallyProps,
index,
rotate,
toObject,
spliced,
chunk,
// object
keyValue,
getValue,
setValue,
mapOverKey,
joinObject,
toArray,
filterObject,
changeKeys,
tap,
look,
// function
not,
partialAt,
aritize,
callTillValue,
spy,
toAsync,
// utility
compare,
compareProperty,
createNew,
dynamicCompare,
equals,
intoString,
isValue,
hash,
guid,
typeOf,
}