diff --git a/README.md b/README.md index 5e59ac7..c05451e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,181 @@ # typescript/functional [![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/functional?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=functional) + +## About + +Provides some common helpers for functional-style programming. + +## Import + +In deno: + +```typescript +import { bool } from "https://js.thunderk.net/functional/mod.ts"; +``` + +In browser: + +```html + +``` + +## Use + +**always** and **never** return true and false respectively + +```typescript +always(); // => true +never(); // => false +``` + +**and**, **or** and **not** allow to combine or negate predicates + +```typescript +const a = and((x: number) => x > 2, (x: number) => x < 5); +a(2); // => false +a(3); // => true +a(4); // => true +a(5); // => false +const o = or((x: number) => x < 2, (x: number) => x > 3); +o(1); // => true +o(2); // => false +o(3); // => false +o(4); // => true +const n = not((x: number) => x == 1); +n(0); // => true +n(1); // => false +n(2); // => true +``` + +**at** gets an array's item by position: + +```typescript +const getthird = at(2); +getthird([2, 4, 8, 16]); // => 8 +getthird([2, 4]); // => undefined +const getsecondlast = at(-2); +getsecondlast([1, 2, 3, 4]); // => 3 +getsecondlast([1]); // => undefined +``` + +**attr** gets an object's attribute: + +```typescript +const getx = attr("x"); +getx({ x: 3 }); // => 3 +``` + +**bool** checks for boolean equivalence (in a broader sense than !(!(val))): + +```typescript +bool(undefined); // => false +bool(null); // => false + +bool(-1); // => true +bool(0); // => false +bool(1); // => true + +bool(""); // => false +bool(" "); // => true +bool("abc"); // => true + +bool([]); // => false +bool([1, 2, 3]); // => true + +bool({}); // => false +bool({ x: 1 }); // => true +``` + +**cmp** simplifies the use of array sorting: + +```typescript +[8, 3, 5].sort(cmp()) // => [3, 5, 8] +[8, 3, 5].sort(cmp({ reverse: true })) // => [8, 5, 3] +[-2, 8, -7].sort(cmp({ key: Math.abs })); // => [-2, -7, 8] +``` + +**defined** removes undefined values from an object: + +```typescript +defined({ a: 1, b: undefined }); // => {a: 1} +``` + +**first** and **last** return the first or last item of an array: + +```typescript +first([1, 2, 3]); // => 1 +first([]); // => undefined +last([1, 2, 3]); // => 3 +last([]); // => undefined +``` + +**identity** returns its argument untouched: + +```typescript +a === identity(a); // => true +``` + +**is** and **isinstance** checks for strict equality and inheritance: + +```typescript +const f = is(8); +f(8); // => true +f(5 + 3); // => true +f("8"); // => false +f(null); // => false + +class A {} +class A1 extends A {} +class A2 extends A {} +class B {} +const f: any[] = [5, null, undefined, new A(), new A1(), new B(), new A2()]; +const result: A[] = f.filter(isinstance(A)); // => [f[3], f[4], f[6]] +``` + +**nn**, **nu** and **nnu** checks at run-time for null or undefined: + +```typescript +nn(undefined); // => undefined +nn(null); // => Error +nn(1); // => 1 + +nu(undefined); // => Error +nu(null); // => null +nu(1); // => 1 + +nnu(undefined); // => Error +nnu(null); // => Error +nnu(1); // => 1 +``` + +**nop** does nothing (useful for some callbacks): + +```typescript +new ConstructorWithMandatoryCallback(nop); +``` + +**options** merges options over default values: + +```typescript +options({ a: 1, b: 2, c: 3, d: 4 }, { a: 11, c: 33 }, { b: 22 }); +// => {a: 11, b: 22, c: 33, d: 4} +``` + +**partial** applies a partial configuration object as first argument of +compatible functions: + +```typescript +const sum = (args: { a: number; b: number }) => args.a + args.b; +const plus1 = partial({ a: 1 }, sum); +plus1({ b: 8 }); // => 9 +``` + +**pipe** chains two functions as one: + +```typescript +const f = pipe((x: number) => x * 2, (x: number) => x + 1); +f(3); // => 7 ((3 * 2) + 1) +``` diff --git a/comparison.test.ts b/comparison.test.ts index 29505b0..b369cc7 100644 --- a/comparison.test.ts +++ b/comparison.test.ts @@ -1,5 +1,5 @@ import { bool, cmp, is } from "./comparison.ts"; -import { expect, it } from "./deps.test.ts"; +import { expect, it } from "./deps.testing.ts"; it("cmp", () => { expect([8, 3, 5].sort(cmp())).toEqual([3, 5, 8]); diff --git a/composition.test.ts b/composition.test.ts index c3343bb..64cb57d 100644 --- a/composition.test.ts +++ b/composition.test.ts @@ -1,5 +1,5 @@ import { identity, nop, partial, pipe } from "./composition.ts"; -import { expect, it } from "./deps.test.ts"; +import { expect, it } from "./deps.testing.ts"; it("nop", () => { expect(nop()).toBeUndefined(); diff --git a/deps.test.ts b/deps.test.ts deleted file mode 100644 index c973a09..0000000 --- a/deps.test.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts"; diff --git a/deps.testing.ts b/deps.testing.ts new file mode 100644 index 0000000..c0653a1 --- /dev/null +++ b/deps.testing.ts @@ -0,0 +1,5 @@ +export { + describe, + expect, + it, +} from "https://js.thunderk.net/devtools@1.3.1/testing.ts"; diff --git a/doc/about.md b/doc/about.md new file mode 100644 index 0000000..7e45c3b --- /dev/null +++ b/doc/about.md @@ -0,0 +1,3 @@ +## About + +Provides some common helpers for functional-style programming. diff --git a/doc/import.md b/doc/import.md new file mode 100644 index 0000000..6374c90 --- /dev/null +++ b/doc/import.md @@ -0,0 +1,15 @@ +## Import + +In deno: + +```typescript +import { bool } from "https://js.thunderk.net/functional/mod.ts"; +``` + +In browser: + +```html + +``` diff --git a/doc/index b/doc/index new file mode 100644 index 0000000..9a75f00 --- /dev/null +++ b/doc/index @@ -0,0 +1,3 @@ +about +import +use \ No newline at end of file diff --git a/doc/use.md b/doc/use.md new file mode 100644 index 0000000..16f3e46 --- /dev/null +++ b/doc/use.md @@ -0,0 +1,157 @@ +## Use + +**always** and **never** return true and false respectively + +```typescript +always(); // => true +never(); // => false +``` + +**and**, **or** and **not** allow to combine or negate predicates + +```typescript +const a = and((x: number) => x > 2, (x: number) => x < 5); +a(2); // => false +a(3); // => true +a(4); // => true +a(5); // => false +const o = or((x: number) => x < 2, (x: number) => x > 3); +o(1); // => true +o(2); // => false +o(3); // => false +o(4); // => true +const n = not((x: number) => x == 1); +n(0); // => true +n(1); // => false +n(2); // => true +``` + +**at** gets an array's item by position: + +```typescript +const getthird = at(2); +getthird([2, 4, 8, 16]); // => 8 +getthird([2, 4]); // => undefined +const getsecondlast = at(-2); +getsecondlast([1, 2, 3, 4]); // => 3 +getsecondlast([1]); // => undefined +``` + +**attr** gets an object's attribute: + +```typescript +const getx = attr("x"); +getx({ x: 3 }); // => 3 +``` + +**bool** checks for boolean equivalence (in a broader sense than !(!(val))): + +```typescript +bool(undefined); // => false +bool(null); // => false + +bool(-1); // => true +bool(0); // => false +bool(1); // => true + +bool(""); // => false +bool(" "); // => true +bool("abc"); // => true + +bool([]); // => false +bool([1, 2, 3]); // => true + +bool({}); // => false +bool({ x: 1 }); // => true +``` + +**cmp** simplifies the use of array sorting: + +```typescript +[8, 3, 5].sort(cmp()) // => [3, 5, 8] +[8, 3, 5].sort(cmp({ reverse: true })) // => [8, 5, 3] +[-2, 8, -7].sort(cmp({ key: Math.abs })); // => [-2, -7, 8] +``` + +**defined** removes undefined values from an object: + +```typescript +defined({ a: 1, b: undefined }); // => {a: 1} +``` + +**first** and **last** return the first or last item of an array: + +```typescript +first([1, 2, 3]); // => 1 +first([]); // => undefined +last([1, 2, 3]); // => 3 +last([]); // => undefined +``` + +**identity** returns its argument untouched: + +```typescript +a === identity(a); // => true +``` + +**is** and **isinstance** checks for strict equality and inheritance: + +```typescript +const f = is(8); +f(8); // => true +f(5 + 3); // => true +f("8"); // => false +f(null); // => false + +class A {} +class A1 extends A {} +class A2 extends A {} +class B {} +const f: any[] = [5, null, undefined, new A(), new A1(), new B(), new A2()]; +const result: A[] = f.filter(isinstance(A)); // => [f[3], f[4], f[6]] +``` + +**nn**, **nu** and **nnu** checks at run-time for null or undefined: + +```typescript +nn(undefined); // => undefined +nn(null); // => Error +nn(1); // => 1 + +nu(undefined); // => Error +nu(null); // => null +nu(1); // => 1 + +nnu(undefined); // => Error +nnu(null); // => Error +nnu(1); // => 1 +``` + +**nop** does nothing (useful for some callbacks): + +```typescript +new ConstructorWithMandatoryCallback(nop); +``` + +**options** merges options over default values: + +```typescript +options({ a: 1, b: 2, c: 3, d: 4 }, { a: 11, c: 33 }, { b: 22 }); +// => {a: 11, b: 22, c: 33, d: 4} +``` + +**partial** applies a partial configuration object as first argument of +compatible functions: + +```typescript +const sum = (args: { a: number; b: number }) => args.a + args.b; +const plus1 = partial({ a: 1 }, sum); +plus1({ b: 8 }); // => 9 +``` + +**pipe** chains two functions as one: + +```typescript +const f = pipe((x: number) => x * 2, (x: number) => x + 1); +f(3); // => 7 ((3 * 2) + 1) +``` diff --git a/iterables.test.ts b/iterables.test.ts index ad8b3bd..015e935 100644 --- a/iterables.test.ts +++ b/iterables.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "./deps.test.ts"; +import { expect, it } from "./deps.testing.ts"; import { at, first, fmap, last, second, third } from "./iterables.ts"; it("at", () => { diff --git a/all.ts b/mod.ts similarity index 100% rename from all.ts rename to mod.ts diff --git a/objects.test.ts b/objects.test.ts index ad411c8..e8be8ca 100644 --- a/objects.test.ts +++ b/objects.test.ts @@ -1,8 +1,31 @@ -import { expect, it } from "./deps.test.ts"; -import { attr } from "./objects.ts"; +import { expect, it } from "./deps.testing.ts"; +import { attr, defined, options } from "./objects.ts"; it("attr", () => { const getx = attr("x"); expect(getx({ x: 4, y: 5 })).toBe(4); expect(getx({ x: undefined, y: 5 })).toBeUndefined(); }); + +it("defined", () => { + expect(defined({})).toEqual({}); + expect(defined({ x: 1 })).toEqual({ x: 1 }); + expect(defined({ x: 1, y: undefined })).toEqual({ x: 1 }); + + const obj = defined({ x: 1, y: undefined }); + // @ts-expect-error + expect(obj.y).toBeUndefined(); +}); + +it("options", () => { + expect(options({})).toEqual({}); + expect(options({ x: 1, y: 2 }, { x: 3 })).toEqual({ x: 3, y: 2 }); + expect(options({ x: 1, y: 2 }, { x: 3 }, { x: 4 })).toEqual({ x: 4, y: 2 }); + expect(options({ x: 1, y: 2 }, { x: 3 }, { y: 0 })).toEqual({ x: 3, y: 0 }); + expect(options({ x: 1, y: 2 }, { x: undefined, y: 3 })).toEqual({ + x: 1, + y: 3, + }); + // @ts-expect-error + expect(options({ x: 1 }, { y: 2 })).toEqual({ x: 1, y: 2 }); +}); diff --git a/objects.ts b/objects.ts index ea06fa2..23cd265 100644 --- a/objects.ts +++ b/objects.ts @@ -1,8 +1,28 @@ -/** - * Attribute getter - */ +// Attribute getter export function attr( name: K, ): >(obj: O) => O[K] { return (obj) => obj[name]; } + +type DefinedKeys = + ({ [P in keyof T]: T[P] extends undefined ? never : P })[keyof T]; +type Defined = Pick>; + +// Removes undefined values from objects +export function defined(obj: T): Defined { + return Object.fromEntries( + Object.entries(obj).filter(([_, value]) => typeof value !== "undefined"), + ) as any; +} + +// Apply options to a default object +export function options( + defaults: T, + ...options: Partial[] +) { + for (let opts of options) { + defaults = { ...defaults, ...defined(opts) }; + } + return defaults; +} diff --git a/predicates.test.ts b/predicates.test.ts index 2482499..fd1268a 100644 --- a/predicates.test.ts +++ b/predicates.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "./deps.test.ts"; +import { expect, it } from "./deps.testing.ts"; import { always, and, never, not, or } from "./predicates.ts"; it("always", () => { diff --git a/run b/run new file mode 100755 index 0000000..74d1c6d --- /dev/null +++ b/run @@ -0,0 +1,19 @@ +#!/bin/sh +# Simplified run tool for deno commands + +if test $# -eq 0 +then + echo "Usage: $0 [file or command]" + exit 1 +elif echo $1 | grep -q '.*.ts' +then + denocmd=run + denoargs=$1 + shift +else + denocmd=$1 + shift +fi + +denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@" +exec deno $denocmd $denoargs diff --git a/typing.test.ts b/typing.test.ts index 94e1738..7568414 100644 --- a/typing.test.ts +++ b/typing.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "./deps.test.ts"; +import { expect, it } from "./deps.testing.ts"; import { isinstance, nn, nnu, nu } from "./typing.ts"; it("isinstance", () => {