import * as Ap from 'fp-ts/lib/Apply'
import * as A from 'fp-ts/lib/Array'
import * as E from 'fp-ts/lib/Either'
import * as Eq from 'fp-ts/lib/Eq'
import {
constFalse,
constTrue,
identity,
Predicate,
tuple,
} from 'fp-ts/lib/function'
import * as Mn from 'fp-ts/lib/Monoid'
import * as NEA from 'fp-ts/lib/NonEmptyArray'
import * as O from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'
import * as R from 'fp-ts/lib/Record'
import * as OExt from '@monorail/sharedHelpers/fp-ts-ext/Option'
import { isNotNil } from '@monorail/sharedHelpers/typeGuards'
export * from 'fp-ts/lib/Array'
// TODO: we should copy all these functions to ReadonlyArray (and change the types to ReadonlyArray), then alias those functions here.
// There is also likely some redundancy with some of these and what's available now that we are on fp-ts 2.
/**
* Curried, pipeable version of concat. Note that the arguments are in order for use with `pipe`, so the argument order might seem backwards at first glance.
* The first argument is passed as an object with the key of `suffix` to avoid misuse.
*
* The result is `prefix.concat(suffix)`
*
* @example
* ```typescript
* A.concat({ suffix: [1, 2] })([3, 4]) // [3,4,2,1]
*
* pipe(
* [1,2],
* A.concat({ suffix: [3, 4] })
* ) // [1,2,3,4]
* ```
*/
export const concat = ({ suffix }: { suffix: Array }) => (
prefix: Array,
): Array => prefix.concat(suffix)
/**
* Curried, pipeable version of concat with arguments flipped for concatenating at the start of another Array. The result is prefix.concat(suffix).
* The first argument is passed as an object with the key of `prefix` to avoid misuse.
*
* @example
* ```typescript
* A.precat({ prefix: [1, 2] })([3, 4]) // [1,2,3,4]
*
* pipe(
* [1,2],
* A.precat({ prefix: [3, 4] })
* ) // [3,4,1,2]
* ```
*/
export const precat = ({ prefix }: { prefix: Array }) => (
suffix: Array,
): Array => prefix.concat(suffix)
/**
* Curried, pipeable version of `snoc` (aka append), for adding an item to the end of an Array
*/
export const append = (x: A) => (xs: Array): NEA.NonEmptyArray =>
A.snoc(xs, x)
/**
* Curried, pipeable version of `cons`, for adding an item to the beginning of an Array
*/
export const prepend = (x: A) => (xs: Array): NEA.NonEmptyArray =>
A.cons(x, xs)
/**
* Pipeable forEach. Runs an effect on each element of the input Array, then returns the input Array unchanged.
*/
export const forEach = (f: (x: A) => void) => (xs: Array): Array => {
xs.forEach(f)
return xs
}
/**
* Pipeable forEach with index. Runs an effect on each element of the input Array, then returns the Array unchanged.
*/
export const forEachWithIndex = (f: (x: A, i: number) => void) => (
xs: Array,
): Array => {
xs.forEach(f)
return xs
}
/**
* Tests whether or not something is a member of an array via strict (===) equality
*/
export const elemWithEqStrict = (x: A) => (xs: Array): boolean =>
A.elem(Eq.eqStrict)(x, xs)
/**
* Tests whether or not something is a member of an array based on the supplied Eq instance
*
* Same as base A.elem, but a more pipeable signature. Named `elemP` to disambiguate from base `elem`.
*/
export const elemP = (eq: Eq.Eq) => (x: A) => (xs: Array): boolean =>
A.elem(eq)(x, xs)
/**
* Returns true if any values from xs exist in ys
* @param E the Eq insance that's used to compare
*/
export const elemAny = (eq: Eq.Eq) => (xs: Array) => (
ys: Array,
): boolean => A.array.foldMap(Mn.monoidAny)(xs, x => elemP(eq)(x)(ys))
/**
* Gets the length of an ArrayLike or string
*/
export const len = (xs: ArrayLike | string): number => xs.length
/**
* Lift a function of two arguments to a function which accepts and returns
* those same values in the context of Options
*/
export const liftOption2 = (f: (a: A) => (b: B) => C) => (
oa: O.Option,
) => (ob: O.Option): O.Option =>
pipe(
Ap.sequenceT(O.option)(oa, ob),
O.map(([a, b]) => f(a)(b)),
)
/**
* Like `intersperse`, but takes a map function that returns the item to be
* "interspersed" instead of directly taking the item itself
*/
export const intersperseMap = (f: (a: A) => A) => (as: Array) => {
if (len(as) < 2) {
return as
} else {
const initAs = A.init(as)
const lastA = A.last(as)
const result = liftOption2((init_: Array) => (last_: A) => {
const interspersedInit = A.array.chain(init_, x => tuple(x, f(x)))
return A.snoc(interspersedInit, last_)
})(initAs)(lastA)
return O.getOrElse(() => as)(result)
}
}
/**
* An indexed version of `intersperseMap`-- adds an index to the map function
*/
export const intersperseMapWithIndex = (f: (a: A, i: number) => A) => (
as: Array,
) => {
if (len(as) < 2) {
return as
} else {
const initAs = A.init(as)
const lastA = A.last(as)
const result = liftOption2((init_: Array) => (last_: A) => {
const pairs = A.array.mapWithIndex(init_, (i, x) => tuple(x, f(x, i)))
const interspersedInit = A.flatten(pairs)
return A.snoc(interspersedInit, last_)
})(initAs)(lastA)
return OExt.getOrElse(() => as)(result)
}
}
/**
* Variant of separate that returns the resulting arrays in a tuple
*/
export const separateT = (
eithers: Array>,
): [Array, Array] => {
const { left, right } = A.separate(eithers)
return [left, right]
}
/**
* Returns a boolean indicating whether the specified predicate function
* holds true for any element of an array.
*/
export const any = (as: Array, p: Predicate) => {
return as.some(p)
}
/**
* Returns a boolean indicating whether the specified predicate function
* holds true for all elements of an array.
*/
export const all = (as: Array, p: Predicate) => {
return as.every(p)
}
/**
* Returns a boolean indicating whether the specified predicate function
* holds true for no elements of an array.
*/
export const notAny = (as: Array, p: Predicate) => {
return !as.some(p)
}
/**
* Returns an array of elements which are in both input arrays but not in their
* intersection. Also known as symmetric difference or disjunctive union.
*/
export const xor = (eq: Eq.Eq) => (xs: Array, ys: Array) => [
...A.difference(eq)(xs, ys),
...A.difference(eq)(ys, xs),
]
/**
* Returns an object made up of a keys from the result the accessor function
*/
export const arrayToRecord = (
keyAccessor: (curr: T) => string,
mapValue?: (curr: T) => V,
) => (arr: Array): Record => {
return arr.reduce((acc, curr) => {
const key = keyAccessor(curr)
const value = isNotNil(mapValue) ? mapValue(curr) : curr
return pipe(
R.lookup(key, acc),
O.fold(
() => ({ ...acc, [key]: value }),
() => acc,
),
)
}, {})
}
/**
* Checks if an array is *not* empty
*/
export const isNotEmpty = (arr: Array) => !A.isEmpty(arr)
/**
* removes all occurences of an element from an Array
* @param E equals instance for comapring elements in the array
* @param t the value to remove
*/
export const without = (eq: Eq.Eq, t: T) => (xs: Array): Array =>
xs.filter(x => !eq.equals(x, t))
/**
* Converts an Option into an Array, if the Option is none,
* an empty array will be returned,
* if the option is some, and array with the value will be returned
* @param o the Option to convert
*/
export const fromOption = (o: O.Option): Array =>
pipe(
o,
O.fold(
() => [],
v => [v],
),
)
/**
* Converts an Either into an Array, returning an empty array if the either
* is Left, and an array of length one with the right value if the either
* is Right
* @param e the Either to convert
*/
export const fromEither = (e: E.Either): Array =>
pipe(
e,
E.fold(
() => [],
r => [r],
),
)
/**
* Adds or removes an item from an Array, depending on whether it's already in the Array
*/
export const toggle = (eq: Eq.Eq) => (a: A) => (as: Array) =>
(A.elem(eq)(a, as) ? A.difference : A.union)(eq)(as, [a])
/**
* Calculates the run length encoding of an array. Given a sorted array, this is
* equivalent to finding the counts of each entry.
*
* @example
*
* expect(rle(eqString)('aaabba'.split(''))).toEqual([
* ['a', 3], ['b', 2], ['a', 1]
* ])
*/
export const rle = (eq: Eq.Eq) => (as: Array): Array<[A, number]> =>
pipe(
as,
A.reduce([] as Array<[A, number]>, (runLengths, next) =>
pipe(
A.last(runLengths),
O.fold(
() => [[next, 1]],
([prev, n]) =>
eq.equals(prev, next)
? runLengths.slice(0, -1).concat([[prev, n + 1]])
: runLengths.concat([[next, 1]]),
),
),
),
)
type ExtractValues>> = {
[K in keyof T]: T[K] extends Array ? S : never
}
/**
* Variadic zip with type inference.
*
* Note: fp-ts Array has a more naive zip function, but because this is better-typed,
* we'll override the base `zip` function with this. If you want the base `zip`, you can import
* it directly from `fp-ts/lib/Array`.
*
* @example
* declare const ns: Array
* declare const ss: Array
* declare const bs: Array
* zip(ns, ns, ns) // :: Array<[number, number, number]>
* zip(ss, ns) // :: Array<[string, number]>
* zip(bs, ns, ss, ss) // :: Array<[boolean, number, string, string]>
*/
export const zip = >>(
...as: As
): Array> => {
const res: Array> = []
const l = as.length === 0 ? 0 : Math.min(...as.map(a => a.length))
for (let i = 0; i < l; i++) {
res[i] = as.map(a => a[i]) as ExtractValues
}
return res
}
/**
* Immutable, predicate-based splice
*/
export const spliceWhere = (predicate: Predicate) => (
mapMatch: (a: A) => Array,
mapNotMatch: (a: A) => A = identity,
) => (arr: Array): Array =>
pipe(
arr,
A.chain(a => (predicate(a) ? mapMatch(a) : [mapNotMatch(a)])),
)
/**
* Finds first element in an array for which `f` returns a `some`
*/
export const findFirstMapWithIndex = (
f: (i: number, a: A) => O.Option,
) => (as: Array): O.Option => {
const l = as.length
for (let i = 0; i < l; i++) {
const v = f(i, as[i])
if (O.isSome(v)) {
return v
}
}
return O.none
}
/**
* Array.compact that works on Array as opposed to Array