import * as fc from 'fast-check'
import * as M from 'monocle-ts'
import { indexArray } from 'monocle-ts/lib/Index/Array'
import * as Foldable from 'fp-ts/Foldable'
import { constant, flow, identity, pipe, Predicate } from 'fp-ts/function'
import * as Arr from 'fp-ts/lib/Array'
import * as Eq from 'fp-ts/lib/Eq'
import * as NEA from 'fp-ts/lib/NonEmptyArray'
import * as O from 'fp-ts/lib/Option'
import { pipeable } from 'fp-ts/lib/pipeable'
import * as TE from 'fp-ts/lib/TaskEither'
import * as Traversable from 'fp-ts/Traversable'
import * as T from 'fp-ts/Tree'
import { findFirstMapWithIndex } from '@monorail/sharedHelpers/fp-ts-ext/Array'
import { coerce, iso, name, Named, the } from '@monorail/sharedHelpers/names'
export * from 'fp-ts/lib/Tree'
export { forestInstances as forest }
const ForestURI = 'Forest'
type ForestURI = typeof ForestURI
declare module 'fp-ts/HKT' {
interface URItoKind {
readonly [ForestURI]: T.Forest
}
}
const forestInstances: Foldable.Foldable1 &
Traversable.Traversable1 = {
URI: ForestURI,
...Foldable.getFoldableComposition(Arr.array, T.tree),
...Traversable.getTraversableComposition(Arr.array, T.tree),
}
export const {
map: mapForest,
reduce: reduceForest,
reduceRight: reduceRightForest,
foldMap: foldMapForest,
} = pipeable(forestInstances)
export const getForestTraversal = M.fromTraversable(forestInstances)
export const getForestFold = M.fromFoldable(forestInstances)
export function getForestOptionalFromPath(path: NEA.NonEmptyArray) {
return pipe(
getParent(path),
O.getOrElse>(() => []),
Arr.reduce(
new M.Optional, T.Forest>(O.some, constant),
(fo, i) =>
fo
.compose(indexArray>().index(i))
.composeLens(M.Lens.fromProp>()('forest')),
),
)
}
export function getTreeOptionalFromPath(
path: NEA.NonEmptyArray,
): M.Optional, T.Tree> {
return getForestOptionalFromPath(path).compose(
indexArray>().index(NEA.last(path)),
)
}
export const fromForestPath = (f: T.Forest) => (p: NodePath) => {
return getTreeOptionalFromPath(p).getOption(f)
}
/**
* Constructs an instance of `Abritrary>` given an `Arbitrary` and a `maxDepth`
*/
export function getArbitrary(
arb: fc.Arbitrary,
opts: { maxDepth?: number; maxWidth?: number } = { maxDepth: 5, maxWidth: 5 },
): fc.Arbitrary> {
const tree: fc.Memo> = fc.memo>(n => {
return n <= 1
? fc.record({
value: arb,
forest: fc.constant(Array>()),
})
: fc.record({
value: arb,
forest:
opts.maxWidth === undefined
? fc.array(tree())
: fc.array(tree(), opts.maxWidth),
})
})
return tree(opts.maxDepth)
}
/**
* Splices trees in a forest that match a predicate. `mapMatch` returns a
* `Forest` that will be flattened into the match's parent forest, so you can
* add or remove nodes at the matched level.
*/
export const spliceWhere = (predicate: Predicate>) => (
mapMatch: (a: T.Tree) => T.Forest,
mapNotMatch: (a: T.Tree) => T.Tree = identity,
) => (forest: T.Forest): T.Forest =>
pipe(
forest,
Arr.map(tree =>
T.make(
tree.value,
spliceWhere(predicate)(mapMatch, mapNotMatch)(tree.forest),
),
),
Arr.chain(t => (predicate(t) ? mapMatch(t) : [mapNotMatch(t)])),
)
/**
* Splices trees in a forest that match a predicate, asynchronously. `mapMatch`
* returns a `Task>` that will be flattened into the match's parent
* forest, so you can add or remove nodes at the matched level.
*/
export const spliceWhereAsync = (predicate: Predicate>) => (
mapMatch: (a: T.Tree) => TE.TaskEither>,
mapNotMatch: (a: T.Tree) => TE.TaskEither> = TE.of,
) => (forest: T.Forest): TE.TaskEither> =>
pipe(
forest,
Arr.map(
(tree): TE.TaskEither> =>
pipe(
tree.forest,
spliceWhereAsync(predicate)(mapMatch, mapNotMatch),
TE.chain(matched => {
const t = T.make(tree.value, matched)
const newF = predicate(t)
? mapMatch(t)
: pipe(
mapNotMatch(t),
TE.map(x => [x]),
)
return newF
}),
),
),
Arr.sequence(TE.taskEither),
TE.map(Arr.flatten),
)
/**
* Duplicates all trees in a forest that match `predicate`. Does _not_ copy
* children.
*/
export const duplicateWhere = (predicate: Predicate>) => (
modify: (a: A) => A = identity,
) => spliceWhere(predicate)(a => [a, T.make(modify(a.value))])
/**
* Duplicates all trees in a forest that match `predicate`, asynchronously. Does
* _not_ copy children.
*/
export const duplicateWhereAsync = (predicate: Predicate>) => (
modify: (a: A) => TE.TaskEither,
) =>
spliceWhereAsync(predicate)(a =>
pipe(
a.value,
modify,
TE.map(dupe => [a, T.make(dupe)]),
),
)
/**
* Removes all trees in a forest that match `predicate`.
*/
export const removeWhere = (predicate: Predicate>) =>
spliceWhere(predicate)(_a => [])
/**
* Adds a child to all trees in a forest that match `predicate`.
*/
export const addChildWhere = (predicate: Predicate>) => (
createChild: (a: T.Tree) => T.Tree,
) =>
spliceWhere(predicate)(a => [
T.make(a.value, Arr.snoc(a.forest, createChild(a))),
])
/**
* Adds a child to all trees in a forest that match `predicate`, asynchronously
*/
export const addChildWhereAsync = (predicate: Predicate>) => (
createChild: (a: T.Tree) => TE.TaskEither>,
): ((forest: T.Forest) => TE.TaskEither>) =>
spliceWhereAsync(predicate)(a =>
pipe(
a,
createChild,
TE.map(child => [T.make(a.value, Arr.snoc(a.forest, child))]),
),
)
export const addRoot = (root: A) => (forest: T.Forest): T.Forest => [
...forest,
T.make(root),
]
/**
* A NodePath represents a path from a forest to a particular tree.
*/
type NodePath = NEA.NonEmptyArray
/**
* Returns an index-path to the first node in a forest that matches `predicate`,
* or `none` if no such node exists.
*
* A named `NodePath` should always lead to a tree for the associated forest.
*/
export const getPath = (predicate: Predicate>) => (
forest: Named, Name>,
): O.Option> =>
pipe(
the(forest),
findFirstMapWithIndex((i, t) =>
predicate(t)
? O.some([i])
: pipe(
getPath(predicate)(coerce(t.forest)),
O.map(p => [i, ...the(p)]),
),
),
O.map(p => coerce(p)),
)
/**
* Returns the node pointed to by a named path
*/
export const getNode = (forest: Named, Name>) => (
path: Named,
): A =>
pipe(
the(path),
NEA.init,
Arr.reduce(the(forest), (f, i) => f[i].forest),
f => f[NEA.last(the(path))].value,
)
/**
* Changes a path to the position left of a node.
*/
export const getLeft = (path: NodePath): NodePath =>
NEA.snoc(NEA.init(path), NEA.last(path) - 1)
/**
* Changes a named path to the position left of a node, if one exists.
*
* Guaranteed to be either a valid path for the associated forest, or `none`.
*/
export const getValidatedLeft = (
path: Named,
): O.Option> =>
pipe(
the(path),
O.fromPredicate(p => NEA.last(p) > 0),
O.map(p => coerce(getLeft(p))),
)
/**
* Changes a path to the parent of a node, if one exists.
*/
export const getParent = (path: NodePath): O.Option =>
pipe(path, NEA.init, NEA.fromArray)
/**
* Changes a named path to the parent of a node, if one exists.
*/
export const getValidatedParent = (
path: Named,
): O.Option> =>
pipe(
the(path),
getParent,
O.map(p => coerce(p)),
)
/**
* Changes a path to the position right of a node
*/
export const getRight = (path: NodePath): NodePath =>
NEA.snoc(NEA.init(path), NEA.last(path) + 1)
/**
* Changes a path to the first child of a node
*/
export const getFirstChild = (path: NodePath): NodePath => NEA.snoc(path, 0)
/**
* Returns true if the node at path `b` is the same node as or a descendent of
* the node at path `a`
*/
export const isEqualOrDescendentOf = (a: Named) => (
b: Named,
): boolean => the(a).every((n, i) => n === the(b)[i])
export const isNodePathDescendantOf = (a: Named) => (
b: Named,
): boolean =>
the(b).length > the(a).length && the(a).every((n, i) => n === the(b)[i])
export const isNodePathEqual = (a: Named) => (
b: Named,
): boolean => NEA.getEq(Eq.eqNumber).equals(the(a), the(b))
const namedRootOptional = (): M.Optional<
Named, Name>,
T.Forest
> => iso, Name>().asOptional()
const pathToForestOptional = (
path: NodePath,
): M.Optional, Name>, T.Forest> =>
pipe(
getParent(path),
O.getOrElse>(() => []),
Arr.reduce(namedRootOptional(), (fo, i) =>
fo
.compose(indexArray>().index(i))
.composeLens(M.Lens.fromProp>()('forest')),
),
)
const pathToOptional = (
path: NodePath,
): M.Optional, Name>, T.Tree> =>
pathToForestOptional(path).compose(
indexArray>().index(NEA.last(path)),
)
/**
* Moves a node at `from` to `to`. The paths must be created from a named forest.
*
* @example
* const forest = [
* T.make('a', [
* T.make('b', [
* T.make('d'),
* ]),
* T.make('c'),
* ]),
* ]
* name(forest)(nf => {
* const from = getPath(n => n.value === 'b')(nf)
* const to = getPath(n => n.value === 'a')(nf)
* expect(moveNode(from, to)(nf)).toEqual(
* O.some([
* T.make('b', [
* T.make('d'),
* ]),
* T.make('a', [
* T.make('c'),
* ]),
* ]),
* )
* })
*/
export const moveNode = (
from: Named,
to: Named,
) => (forest: Named, Name>): O.Option> => {
if (isEqualOrDescendentOf(from)(to)) {
return O.none
}
const toIndex = NEA.last(the(to))
const fromIndex = NEA.last(the(from))
const adjustedTo: NodePath = pipe(
the(to),
NEA.mapWithIndex((i, p) =>
i === the(from).length - 1 && the(from)[i] < p ? p - 1 : p,
),
)
const toForestOptional = pathToForestOptional(adjustedTo)
const fromForestOptional = pathToForestOptional(the(from))
const fromOptional = pathToOptional(the(from))
const fromTree = pipe(forest, fromOptional.getOption)
const removeFrom = fromForestOptional.modify(f =>
pipe(
f,
Arr.deleteAt(fromIndex),
O.getOrElse(() => f),
),
)
const injectTo = pipe(
fromTree,
O.fold(
() => identity,
ft =>
toForestOptional.modify(f => [
...f.slice(0, toIndex),
ft,
...f.slice(toIndex),
]),
),
)
return O.some(the(pipe(forest, removeFrom, injectTo)))
}
/**
* Moves a node to the left if possible.
*
* @example
* const forest = [
* T.make('a', [
* T.make('b', [
* T.make('d'),
* ]),
* T.make('c'),
* ]),
* ]
* expect(moveLeft(n => n.value === 'c')(forest)).toEqual(O.some([
* T.make('a', [
* T.make('c'),
* T.make('b', [
* T.make('d'),
* ]),
* ]),
* ]))
*/
export const moveLeft = (predicate: Predicate>) => (
forest: T.Forest,
): O.Option> =>
name(forest)(nf =>
pipe(
nf,
getPath(predicate),
O.bindTo('from'),
O.bind('to', ({ from }) => pipe(getValidatedLeft(from))),
O.chain(({ from, to }) => moveNode(from, to)(nf)),
),
)
/**
* Moves a node to the left of the parent
*
* @example
* const forest = [
* T.make('a', [
* T.make('b', [
* T.make('d'),
* ]),
* T.make('c'),
* ]),
* ]
* expect(moveUp(n => n.value === 'd')(forest)).toEqual(O.some([
* T.make('a', [
* T.make('d'),
* T.make('b'),
* T.make('c'),
* ]),
* ]))
*/
export const moveUpBefore = (predicate: Predicate>) => (
forest: T.Forest,
): O.Option> =>
name(forest)(nf =>
pipe(
getPath(predicate)(nf),
O.bindTo('from'),
O.bind('to', ({ from }) => getValidatedParent(from)),
O.chain(({ from, to }) => moveNode(from, to)(nf)),
),
)
/**
* Moves a node to the right of the parent
*
* @example
* const forest = [
* T.make('a', [
* T.make('b', [
* T.make('d'),
* ]),
* T.make('c'),
* ]),
* ]
* expect(moveUp(n => n.value === 'd')(forest)).toEqual(O.some([
* T.make('a', [
* T.make('b'),
* T.make('d'),
* T.make('c'),
* ]),
* ]))
*/
export const moveUpAfter = (predicate: Predicate>) => (
forest: T.Forest,
): O.Option> =>
name(forest)(nf =>
pipe(
getPath(predicate)(nf),
O.bindTo('from'),
O.bind('to', ({ from }) =>
pipe(from, getValidatedParent, O.map(flow(the, getRight))),
),
O.chain(({ from, to }) => moveNode(from, coerce(to))(nf)),
),
)
/**
* Moves a node to the right if possible.
*
* @example
* const forest = [
* T.make('a', [
* T.make('b', [
* T.make('d'),
* ]),
* T.make('c'),
* ]),
* ]
* expect(moveDown(n => n.value === 'b')(forest)).toEqual(O.some([
* T.make('a', [
* T.make('c'),
* T.make('b', [
* T.make('d'),
* ]),
* ]),
* ]))
*/
export const moveRight = (predicate: Predicate>) => (
forest: T.Forest,
): O.Option> =>
name(forest)(nf =>
pipe(
getPath(predicate)(nf),
O.bindTo('from'),
O.bind('to', ({ from }) =>
pipe(
getRight(the(from)),
O.some,
O.chainFirst(right => pathToOptional(right).getOption(nf)),
),
),
O.chain(({ from, to }) => moveNode(from, coerce(to))(nf)),
),
)
/**
* Moves a node into the children of the next node, if possible.
*
* @example
* const forest = [
* T.make('a', [
* T.make('b', [
* T.make('d'),
* ]),
* T.make('c'),
* ]),
* ]
* expect(moveInto(n => n.value === 'b')(forest)).toEqual(O.some([
* T.make('a', [
* T.make('c', [
* T.make('b', [
* T.make('d'),
* ]),
* ]),
* ]),
* ]))
*/
export const moveInto = (predicate: Predicate>) => (
forest: T.Forest,
): O.Option> =>
name(forest)(nf =>
pipe(
getPath(predicate)(nf),
O.bindTo('from'),
O.bind('to', ({ from }) =>
pipe(
getRight(the(from)),
O.some,
O.chainFirst(right => pathToOptional(right).getOption(nf)),
O.map(getFirstChild),
),
),
O.chain(({ from, to }) => moveNode(from, coerce(to))(nf)),
),
)
/**
* Returns true if the first node matching the predicate is the leftmost node of
* its forest.
*/
export const isLeftmost = (predicate: Predicate>) => (
forest: T.Forest,
): boolean =>
name(forest)(
flow(
getPath(predicate),
O.fold(
() => false,
p => NEA.last(the(p)) === 0,
),
),
)
/**
* Returns true if the first node matching the predicate has a parent node
*/
export const hasParent = (predicate: Predicate>) => (
forest: T.Forest,
): boolean =>
name(forest)(
flow(
getPath(predicate),
O.fold(
() => false,
p => the(p).length > 1,
),
),
)
/**
* Returns true if the first node matching the predicate is the rightmost node of
* its forest. Returns false otherwise, or if no node is found.
*/
export const isRightmost = (predicate: Predicate>) => (
forest: T.Forest,
): boolean =>
name(forest)(nf =>
pipe(
getPath(predicate)(nf),
O.fold(
() => false,
flow(
the,
getRight,
pathToOptional,
pto => pto.getOption(nf),
O.fold(
() => true,
() => false,
),
),
),
),
)
/**
* Gets all parents of the first matching node
*/
export const getAncestorsOf = (predicate: Predicate>) => (
forest: T.Forest,
): O.Option> =>
name(forest)(nf =>
pipe(
nf,
getPath(predicate),
O.chain(
flow(
the,
// create an array of paths, e.g. [1, 2] => [[1], [1,2]]
Arr.scanLeft([] as Array, Arr.snoc),
// get rid of the last path, since that's the target node
Arr.dropRight(1),
Arr.filterMap(NEA.fromArray),
NEA.fromArray,
),
),
O.map(Arr.map(p => getNode(nf)(coerce(p)))),
),
)
export const findFirst = (predicate: Predicate) => (
forest: T.Forest,
): O.Option =>
pipe(
forest,
Arr.foldMap(O.getFirstMonoid())(
T.foldMap(O.getFirstMonoid())(O.fromPredicate(predicate)),
),
)