diff --git a/.github/contributing.md b/.github/contributing.md index 5eac2389..7ab8b2b9 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -16,8 +16,8 @@ Scan through the [existing issues](https://github.com/aleclarson/radashi/issues) ## You want to write code? -- To get started, run `yarn` in the project's root directory to install the dependencies. -- You can run the unit tests with `yarn test`. They require Node v16+. You can run `nvm use` in the root directory to change to the correct Node version. The tests should pass (duh) and maintain 100% code coverage. +- To get started, run `pnpm i` in the project's root directory to install the dependencies. +- You can run the unit tests with `pnpm test`. They require Node v16+. You can run `nvm use` in the root directory to change to the correct Node version. The tests should pass (duh) and maintain 100% code coverage. - To get familiar with the existing code I would recommend looking through the docs and the codebase in parallel. For each function in the docs, find the implementation in the source and skim over the code. - When coding, try not to create internal APIs (any function or module that is not exported to be used by the client). - Also, try not to use functions from other Radashi modules. This is a softer rule but we want to make it easy for readers to understand a function without having to navigate the codebase. As a utility library users should ideally be able to copy/paste a function from Radashi into their project. Most Radashi functions should be easy to write in isolation. diff --git a/docs/array/select.mdx b/docs/array/select.mdx index d2b6015c..c0d3f356 100644 --- a/docs/array/select.mdx +++ b/docs/array/select.mdx @@ -7,6 +7,7 @@ description: Filter and map an array ## Basic usage Applies a filter and a map operation at once and in one pass. +If the filter is omitted, returns all non-nullish mapped values. ```ts import * as _ from 'radashi' diff --git a/docs/array/selectFirst.mdx b/docs/array/selectFirst.mdx new file mode 100644 index 00000000..5fb83d25 --- /dev/null +++ b/docs/array/selectFirst.mdx @@ -0,0 +1,38 @@ +--- +title: selectFirst +group: 'Array' +description: Array find + map +--- + +## Basic usage + +Returns the mapped value for the first element that satisfies the specified condition--else undefined. +If the filter is omitted, returns the first non-nullish mapped value. + +```ts +import * as _ from 'radashi' + +const fish = [ + { + name: 'Marlin', + weight: 105, + source: 'ocean', + }, + { + name: 'Bass', + weight: 8, + source: 'lake', + }, + { + name: 'Trout', + weight: 13, + source: 'lake', + }, +] + +_.selectFirst( + fish, + f => f.weight, + f => f.source === 'lake', +) // => 8 +``` diff --git a/src/array/select.ts b/src/array/select.ts index 1ba9ece8..040d96cc 100644 --- a/src/array/select.ts +++ b/src/array/select.ts @@ -1,6 +1,7 @@ /** * Select performs a filter and a mapper inside of a reduce, only - * iterating the list one time. + * iterating the list one time. If condition is omitted, will + * select all mapped values that are non-nullish. * * ```ts * select( diff --git a/src/array/selectFirst.ts b/src/array/selectFirst.ts new file mode 100644 index 00000000..12ddbcf8 --- /dev/null +++ b/src/array/selectFirst.ts @@ -0,0 +1,29 @@ +/** + * Select performs a find + map operation, short-circuiting on the first + * element that satisfies the prescribed condition. If condition is omitted, + * will select the first mapped value which is non-nullish. + * + * ```ts + * selectFirst( + * [1, 2, 3, 4], + * x => x * x, + * x => x > 2 + * ) + * // => 9 + * ``` + */ +export function selectFirst( + array: readonly T[], + mapper: (item: T, index: number) => U, + condition?: (item: T, index: number) => boolean, +): U | undefined { + if (!array) { + return undefined + } + let foundIndex = -1 + const found = array.find((item, index) => { + foundIndex = index + return condition ? condition(item, index) : mapper(item, index) != null + }) + return found === undefined ? undefined : mapper(found, foundIndex) +} diff --git a/src/array/tests/select.test.ts b/src/array/tests/select.test.ts index a8d8e4f3..3090fca8 100644 --- a/src/array/tests/select.test.ts +++ b/src/array/tests/select.test.ts @@ -52,7 +52,7 @@ describe('select function', () => { ) expect(result).toEqual(['c2', 'd3']) }) - test('works without a condition callback', () => { + test('works without a condition callback, filtering nullish mapped values', () => { const list = [{ a: 1 }, { b: 2 }, { a: 3 }, { a: null }, { a: undefined }] const result = _.select(list, obj => obj.a) expect(result).toEqual([1, 3]) diff --git a/src/array/tests/selectFirst.test.ts b/src/array/tests/selectFirst.test.ts new file mode 100644 index 00000000..54d3ca5f --- /dev/null +++ b/src/array/tests/selectFirst.test.ts @@ -0,0 +1,60 @@ +import * as _ from 'radashi' + +const cast = (value: any): T => value + +describe('selectFirst function', () => { + test('does not fail on bad input', () => { + expect( + _.selectFirst( + cast(null), + x => x, + x => x, + ), + ).toBeUndefined() + expect( + _.selectFirst( + cast(undefined), + x => x, + x => x, + ), + ).toBeUndefined() + }) + test('returns mapped result of first value that meets the condition', () => { + const list = [ + { group: 'a', word: 'hello' }, + { group: 'b', word: 'bye' }, + { group: 'a', word: 'oh' }, + { group: 'b', word: 'hey' }, + { group: 'c', word: 'ok' }, + ] + const result = _.selectFirst( + list, + x => x.word, + x => x.group === 'b', + ) + expect(result).toEqual('bye') + }) + test('does not fail on empty input list', () => { + const list: any[] = [] + const result = _.selectFirst( + list, + (x: any) => x.word, + x => x.group === 'a', + ) + expect(result).toBeUndefined() + }) + test('works with index', () => { + const letters = ['a', 'b', 'c', 'd'] + const result = _.selectFirst( + letters, + (l, idx) => `${l}${idx}`, + (_, idx) => idx > 1, + ) + expect(result).toEqual('c2') + }) + test('works without a condition callback, filtering nullish mapped values', () => { + const list = [{ a: null }, { a: undefined }, { b: 2 }, { a: 1 }, { a: 3 }] + const result = _.selectFirst(list, el => el.a) + expect(result).toEqual(1) + }) +}) diff --git a/src/mod.ts b/src/mod.ts index 9b828142..8da283a5 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -19,6 +19,7 @@ export * from './array/range' export * from './array/replace' export * from './array/replaceOrAppend' export * from './array/select' +export * from './array/selectFirst' export * from './array/shift' export * from './array/sift' export * from './array/sort'