Skip to content

Commit

Permalink
Merge pull request #88 from poteat/poteat/object-countby
Browse files Browse the repository at this point in the history
Add countBy and other List utilities, + some runtime functions and tests.
  • Loading branch information
poteat authored Sep 29, 2024
2 parents 6bbfa83 + ad3b859 commit 51026b7
Show file tree
Hide file tree
Showing 28 changed files with 535 additions and 40 deletions.
30 changes: 0 additions & 30 deletions .github/workflows/publish-docs.yml

This file was deleted.

3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ module.exports = {
transform: {
'^.+.tsx?$': ['ts-jest', {}]
},
testMatch: [
'**/*.test.ts', // Include only 'test' files, excluding type-only spec files
],
passWithNoTests: true
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "hkt-toolbelt",
"private": true,
"scripts": {
"test": "NODE_OPTIONS=--max-old-space-size=8192 tsc --project tsconfig.spec.json",
"jest": "jest",
"test": "NODE_OPTIONS=--max-old-space-size=8192 tsc --project tsconfig.spec.json && jest",
"stress": "NODE_OPTIONS=--max-old-space-size=24576 tsc --project tsconfig.stress.json",
"build": "./build.sh",
"build:docs": "NODE_OPTIONS=--max-old-space-size=8192 typedoc --skipErrorChecking",
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ type Identity_Spec = [
*/
Test.Expect<$<Function.Identity, Function.Identity>, Function.Identity>
]

it('should return the input', () => {
expect(Function.identity('foo')).toBe('foo')
})
15 changes: 15 additions & 0 deletions src/function/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,18 @@ import { Kind } from '..'
export interface Identity extends Kind.Kind {
f(x: this[Kind._]): typeof x
}

/**

Check warning on line 27 in src/function/identity.ts

View workflow job for this annotation

GitHub Actions / build-stress

Missing JSDoc @returns declaration
* Given a value, return the value.
*
* @param {unknown} x - The value to return.

Check warning on line 30 in src/function/identity.ts

View workflow job for this annotation

GitHub Actions / build-stress

Types are not permitted on @param
*
* @example
* ```ts
* import { Function } from "hkt-toolbelt";
*
* const result = Function.identity('foo')
* // ^? foo
* ```
*/
export const identity = ((x: unknown) => x) as Kind._$reify<Identity>
File renamed without changes.
19 changes: 13 additions & 6 deletions src/kind/pipe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Function, Kind, List, Type } from '..'
import { $, Function, Kind, List, Type } from '..'

/**
* `_$pipe` is a type-level function that allows users to compose
Expand Down Expand Up @@ -29,12 +29,16 @@ import { Function, Kind, List, Type } from '..'
* and `Kind.OutputOf` type-level functions to inspect the input and output
* types of the type-level functions that you are piping together.
*/
export type _$pipe<FX extends Kind.Kind[], X> = Kind._$compose<
List._$reverse<FX>,
X
>
export type _$pipe<T extends Kind.Kind[], X> = T extends [
infer Head extends Kind.Kind,
...infer Tail extends Kind.Kind[]
]
? [X] extends [never]
? never
: _$pipe<Tail, $<Head, Type._$cast<X, Kind._$inputOf<Head>>>>
: X

interface Pipe_T<FX extends Kind.Kind[]> extends Kind.Kind {
export interface Pipe_T<FX extends Kind.Kind[]> extends Kind.Kind {
f(
x: Type._$cast<
this[Kind._],
Expand Down Expand Up @@ -84,6 +88,9 @@ export interface Pipe extends Kind.Kind {
* Given a list of functions, pipe the functions together, applying them in
* order from left to right.
*
* @param {Kind.Kind[]} fx - A list of functions to pipe together.

Check warning on line 91 in src/kind/pipe.ts

View workflow job for this annotation

GitHub Actions / build-stress

Types are not permitted on @param
* @param {InputOf<FX[0]>} x - The input to pipe through the functions.

Check warning on line 92 in src/kind/pipe.ts

View workflow job for this annotation

GitHub Actions / build-stress

@param "x" does not match an existing function parameter

Check warning on line 92 in src/kind/pipe.ts

View workflow job for this annotation

GitHub Actions / build-stress

Types are not permitted on @param
*
* @example
* ```ts
* import { Kind, NaturalNumber } from "hkt-toolbelt";
Expand Down
43 changes: 43 additions & 0 deletions src/list/countBy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { $, Function, List, Test } from '..'

type CountBy_Spec = [
/**
* Can count by a predicate.
*/
Test.Expect<
$<$<List.CountBy, Function.Identity>, [1, 1, 1, 2, 3]>,
{
1: 3
2: 1
3: 1
}
>,

/**
* Can count by a predicate with a constant.
*/
Test.Expect<
$<$<List.CountBy, $<Function.Constant, 'foo'>>, ['foo', 'foo', 'bar']>,
{
foo: 3
}
>,

/**
* Can count by a 'length' predicate over lists.
*/
Test.Expect<
$<$<List.CountBy, List.Length>, [[1, 2, 3], [1, 2, 3, 4], [1, 2, 3]]>,
{
3: 2
4: 1
}
>
]

it('should return the count of each unique element', () => {
expect(List.countBy(Function.identity)(['foo', 'foo', 'bar'])).toEqual({
foo: 2,
bar: 1
})
})
91 changes: 91 additions & 0 deletions src/list/countBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { $, Kind, NaturalNumber, Object, Type, Function } from '..'

type _$countBy2<
F extends Kind.Kind,
T extends unknown[],
O extends Record<string | number | symbol, number> = {}
> = T extends [infer Head extends Kind._$inputOf<F>, ...infer Tail]
? _$countBy2<
F,
Tail,
$<F, Head> extends infer NewKey extends PropertyKey
? Object._$assign<
NewKey,
NewKey extends keyof O ? NaturalNumber._$increment<O[NewKey]> : 1,
O
>
: never
>
: O

/**
* `_$countBy` is a type-level function that takes in a kind `K` that returns a
* string, number, or symbol, and a list `T`, and returns a map of the counts
* of the elements in `T` mapped by the result of applying `K` to each element.
*
* @template {Kind} F - The kind that returns a string, number, or symbol.
* @template {unknown[]} T - The list to count the elements of.
*
* @example
* ```ts
* type T0 = _$countBy<Function.Identity, ['foo', 'foo', 'bar']> // { foo: 2, bar: 1 }
* type T1 = _$countBy<String.Length, ['foo', 'foo', 'quxes']> // { 3: 2, 5: 1 }
* ```
*/
export type _$countBy<
F extends Kind.Kind,
T extends unknown[],
O extends Record<string | number | symbol, number> = {}
> = Type._$display<_$countBy2<F, T, O>>

export interface CountBy_T<F extends Kind.Kind> extends Kind.Kind {
f(x: Type._$cast<this[Kind._], unknown[]>): _$countBy<F, typeof x>
}

/**
* `CountBy` is a type-level function that takes in a kind `K` that returns a
* string, number, or symbol, and a list `T`, and returns a map of the counts
* of the elements in `T` mapped by the result of applying `K` to each element.
*
* @template {Kind} F - The kind that returns a string, number, or symbol.
* @template {unknown[]} T - The list to count the elements of.
*
* @example
* ```ts
* type T0 = $<$<$<List.CountBy, Function.Identity>, ['foo', 'foo', 'bar']> // { foo: 2, bar: 1 }
* type T1 = $<$<$<List.CountBy, String.Length>, ['foo', 'foo', 'quxes']> // { 3: 2, 5: 1 }
* ```
*/
export interface CountBy extends Kind.Kind {
f(x: Type._$cast<this[Kind._], Kind.Kind>): CountBy_T<typeof x>
}

/**
* Given a list, return a new list containing the count of each unique element.
*
* @param {Kind.Kind} f - The function to apply to each element to get the key.
* @param {unknown[]} values - The list to count the elements of.
*
* @example
* ```ts
* import { List, Function } from "hkt-toolbelt";
*
* const result = List.countBy(Function.identity)(['foo', 'foo', 'bar'])
* // ^? { foo: 2, bar: 1 }
* ```
*/
export const countBy = ((f: Function.Function) => (values: unknown[]) => {
const result = {} as Record<string | number | symbol, number>

for (const element of values) {
const key = f(element as never) as string | number | symbol

if (key in result) {
result[key]++
} else {
result[key] = 1
}
}

return result
}) as Kind._$reify<CountBy>
File renamed without changes.
4 changes: 4 additions & 0 deletions src/list/first.spec.ts → src/list/first.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ type First_Spec = [
// @ts-expect-error
$<List.First, number>
]

it('should return the first element of a list', () => {
expect(List.first([1, 2, 3])).toBe(1)
})
15 changes: 15 additions & 0 deletions src/list/first.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,18 @@ export type _$first<T extends unknown[]> = T extends [] ? never : T[0]
export interface First extends Kind.Kind {
f(x: Type._$cast<this[Kind._], unknown[]>): _$first<typeof x>
}

/**
* Returns the first element of a list.
*
* @param {unknown[]} x - The list to get the first element of.
*
* @example
* ```ts
* import { List } from "hkt-toolbelt";
*
* const result = List.first([1, 2, 3])
* // ^? 1
* ```
*/
export const first = ((x: unknown[]) => x[0]) as Kind._$reify<First>
3 changes: 3 additions & 0 deletions src/list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './accumulate'
export * from './at'
export * from './chunk'
export * from './concat'
export * from './countBy'
export * from './duplicates'
export * from './every'
export * from './filter'
Expand All @@ -22,6 +23,8 @@ export * from './length'
export * from './list'
export * from './map'
export * from './map-n'
export * from './max-by'
export * from './min-by'
export * from './pair'
export * from './pop'
export * from './pop-n'
Expand Down
4 changes: 4 additions & 0 deletions src/list/last.spec.ts → src/list/last.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ type Last_Spec = [
*/
Test.Expect<$<List.Last, [string]>, string>
]

it('should return the last element of a list', () => {
expect(List.last([1, 2, 3])).toBe(3)
})
15 changes: 15 additions & 0 deletions src/list/last.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,18 @@ export type _$last<T extends readonly unknown[]> = T extends [infer X]
export interface Last extends Kind.Kind {
f(x: Type._$cast<this[Kind._], readonly unknown[]>): _$last<typeof x>
}

/**
* Returns the last element of a list.
*
* @param {unknown[]} x - The list to get the last element of.
*
* @example
* ```ts
* import { List } from "hkt-toolbelt";
*
* const result = List.last([1, 2, 3])
* // ^? 3
* ```
*/
export const last = ((x: unknown[]) => x[x.length - 1]) as Kind._$reify<Last>
File renamed without changes.
17 changes: 17 additions & 0 deletions src/list/max-by.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { $, Test, List, Function, String } from '..'

type MaxBy_Spec = [
/**
* Can find the maximum element of a list of numbers.
*/
Test.Expect<$<$<List.MaxBy, Function.Identity>, [1, 2, 3]>, 3>,

/**
* Can find the maximum element of a list of strings, based on length.
*/
Test.Expect<$<$<List.MaxBy, String.Length>, ['foo', 'bars', 'qux']>, 'bars'>
]

it('should return the element in the list that has the highest score', () => {
expect(List.maxBy(Function.identity)([1, 2, 3])).toBe(3)
})
Loading

0 comments on commit 51026b7

Please sign in to comment.