Skip to content

Instantly share code, notes, and snippets.

@anthonyjoeseph
Last active February 28, 2021 22:00
Show Gist options
  • Save anthonyjoeseph/a2080d3993d64fad575f8541d88641b1 to your computer and use it in GitHub Desktop.
Save anthonyjoeseph/a2080d3993d64fad575f8541d88641b1 to your computer and use it in GitHub Desktop.
fp-ts-tree-utils
import { sum } from 'fp-ts-std/Array';
import * as A from 'fp-ts/Array';
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as T from 'fp-ts/Tree';
import React from 'react';
// npm package name: fp-ts-tree-contrib ?
export const isLeaf = <A,>(tree: T.Tree<A>): boolean => tree.forest.length === 0
export const numLeaves = T.fold((_, bs: Array<number>) => (bs.length === 0 ? 1 : sum(bs)))
const forestGridByLevelInternal = <A,>(
forest: Array<T.Tree<A> | undefined>
): Array<Array<{ numLeaves: number; value: O.Option<A> }>> => pipe(
forest,
A.chain((col): Array<T.Tree<A> | undefined> => col ? col.forest : [undefined]),
O.fromPredicate(A.some(column => column ? !isLeaf(column) : false)),
O.fold(() => A.empty, forestGridByLevelInternal),
A.cons(pipe(
forest,
A.map(node => ({
value: pipe(node, O.fromPredicate((c): c is T.Tree<A> => (!!c && !isLeaf(c))), O.map(c => c.value)),
numLeaves: node && !isLeaf(node) ? numLeaves(node) : 1
}))
))
)
/**
* Turns a Forest into a grid (2D array), organized by level & excluding leaves.
*
*
* The total 'numLeaves' of each level should all be equivalent.
* Elements w/ empty values are used to achieve this.
*
* @example
*
* const forest: T.Forest<string> = [
* {
* value: 'P1',
* forest: [
* {
* value: 'S1',
* forest: [
* { value: 'Leaf 1', forest: [] },
* { value: 'Leaf 2', forest: [] }
* ]
* }
* ]
* },
* {
* value: 'P2',
* forest: [
* { value: 'Leaf 3', forest: [] },
* { value: 'Leaf 4', forest: [] }
* ]
* }
* ]
*
* const grid: { numLeaves: number; value: O.Option<string> }[][] = [
* [ { numLeaves: 2, value: O.some('P1') }, { numLeaves: 2, value: O.some('P2') } ],
* [ { numLeaves: 2, value: O.some('S1') }, { numLeaves: 2, value: O.none } ],
* ]
*
* assert.deepStrictEqual(forestGridByLevel(forest), grid)
*/
export const forestGridByLevel = <A,>(forest: T.Forest<A>) => forestGridByLevelInternal(forest)
/**
* Turns a Forest into an Array of its leaves.
*
* @example
*
* const forest: T.Forest<string> = [
* {
* value: 'P1',
* forest: [
* {
* value: 'S1',
* forest: [
* { value: 'Leaf 1', forest: [] },
* { value: 'Leaf 2', forest: [] }
* ]
* }
* ]
* },
* {
* value: 'P2',
* forest: [
* { value: 'Leaf 3', forest: [] },
* { value: 'Leaf 4', forest: [] }
* ]
* }
* ]
*
* const leaves: string[] = [
* 'Leaf 1', 'Leaf 2', 'Leaf 3', 'Leaf 4'
* ]
*
* assert.deepStrictEqual(forestLeaves(forest), leaves)
*/
export const forestLeaves = <A,>(forest: T.Forest<A>): Array<A> => pipe(
forest,
A.chain(
col => isLeaf(col)
? [col.value]
: forestLeaves(col.forest)
)
)
// Example usage
type Column<A> = {
header: string
columns: Column<A>[]
} | {
accessor: A
}
const toTree = <A,>(col: Column<A>): T.Tree<A | string> => 'columns' in col
? T.make(col.header, col.columns.map(toTree))
: T.of(col.accessor)
const Table = <A, Key extends keyof A = NonNullable<keyof A>>({
data, columns, headers, cells
}: {
data: A[]
columns: Column<Key>[]
headers: Record<Key, string>
cells: {
[K in Key]: (val: A[K]) => JSX.Element
}
}) => {
const accessors: Key[] = forestLeaves(columns.map(toTree))
.map((accessor: string | Key) => accessor as Key)
return (
<table>
<thead>
{forestGridByLevel(columns.map(toTree)).map(headerGroup => (
<tr>
{headerGroup.map(({ value, numLeaves }) => (
<th colSpan={numLeaves}>{pipe(value, O.toUndefined)}</th>
))}
</tr>
))}
<tr>
{accessors.map(accessor => <th>{headers[accessor]}</th>)}
</tr>
</thead>
<tbody>
{data.map(rowData => (
<tr>
{accessors.map(accessor => <td>{cells[accessor](rowData[accessor])}</td>)}
</tr>
))}
</tbody>
</table>
)
}
interface TestData { firstName: string; lastName: string; strength: number }
const TestDataTable = ({ testDatas }: { testDatas: TestData[] }) => (
<Table
data={testDatas}
columns={[
{
header: 'Names',
columns: [
{ accessor: 'firstName' },
{ accessor: 'lastName' }
]
},
{
header: 'Attributes',
columns: [
{ accessor: 'strength' }
]
}
]}
headers={{
firstName: 'First Name',
lastName: 'Last Name',
strength: 'Strength'
}}
cells={{
firstName: (val) => <div>{val.toUpperCase()}</div>,
lastName: (val) => <div>{val.toLowerCase()}</div>,
strength: (val) => <div>{val + 3}</div>,
}}
/>
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment