diff --git a/.husky/pre-commit b/.husky/pre-commit index 009b3f8..859bd52 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -pnpm lint +pnpm lint:MA diff --git a/.xo-config.json b/.xo-config.json index 1a87094..bfe3926 100644 --- a/.xo-config.json +++ b/.xo-config.json @@ -1,6 +1,7 @@ { "env": ["jest", "node"], "rules": { + "no-await-in-loop": "off", "import/extensions": "off", "unicorn/prefer-module": "off", "unicorn/no-array-for-each": "off", diff --git a/README.md b/README.md index f385ae2..926aeb9 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,60 @@ # n-tuple-array -Get a specified amount of items when iterating over a JavaScript array, instead of the single item that arrays provide per iteration, by default. +Get a **`configurable`** amount of items when iterating over a JavaScript array, instead of a single item that arrays provide per iteration, by default. ## Motivation -Imagine that you received a collection of coordinates (latitude and longitude), but they were sent -as a flat array of values to speed up the data transfers. +Imagine that you received a large collection of coordinates (latitude and longitude), but they were sent +as a flat array of values to speed up the data transfer. -`n-tuple-array` can help you get out the coordinates in pairs (their logical representation), such that you'd go **from** +`n-tuple-array` can help you get out the coordinates in pairs (i.e their logical representation), such that you'd go + +**from** ```json -// flatCoords +// flat coordinates ["5.7225", "-9.6273", "2.68452", "-30.9501", ...] ``` **to** ```javascript -// generate pairs by default -const coordIterable = tuplesFromArray({ list: flatCoords }); +// the iterable will generate pairs by default +const coordsIterable = tuplesFromArray({ list: flatCoords }); // using for..of, get pairs as ["5.7225", "-9.6273"] ... -for (const pair of coordIterable) { +for (const pair of coordsIterable) { console.log(pair); } -// OR manipulate pairs with regular array functions -const coordPairs = Array.from(coordIterable); -console.log(Array.isArray(coordPairs)); // true -// prints ["5.7225", "-9.6273"] ... -coordPairs +// OR manipulate pairs with regular array +// functions like map, filter, forEach ... +const coordsInPairs = Array.from(coordsIterable); +console.log(Array.isArray(coordsInPairs)); // true +coordsInPairs .map(pair => { + // pair is ["5.7225", "-9.6273"] ... return myTransform(pair); }) .forEach((pair) => { + // pair is ["5.7225", "-9.6273"] ... placeOnMap(pair); }); ``` ### Some Real World Examples -#### Wole Joko +#### 1. Wole Joko -I first tried my hands on this concept when [building wole-joko](https://github.com/chalu/wole-joko/blob/dev/src/js/utils.js#L57-L92), a _live coding task_ I was asked to do in an engineering manager interview :man_shrugging. It was a simulation of people entering an event hall to get seated, but only two could get in at a time. I later took some time to [give the project more life](https://wole-joko.netlify.app/) +I first tried my hands on this concept when [fleshing out wole-joko](https://github.com/chalu/wole-joko/blob/dev/src/js/utils.js#L57-L92), which strated as a _live coding task_ I was asked to do in an engineering manager interview :man_shrugging +It was a simulation of people entering an event hall to get seated, but **only two** could get in at a time - https://wole-joko.netlify.app/ -#### Execute max of X tasks in parallel +#### 2. Execute max of `N` async tasks -JS challenge by [@thdxr on X.com](https://twitter.com/thdxr)
![](./assets/the-dax-js-challenge.png "JS challenge by @thdxr") -

> The below was adapted for more concise terminal output -`n-tuple-array` solution. View [code here](https://github.com/chalu/n-tuple-array/blob/main/src/demo/demo-classic.ts#L6-L40)
+`n-tuple-array` solution. View [code here](https://github.com/chalu/n-tuple-array/blob/main/src/examples/classic.ts#L6-L40)
![](./assets/demo-classic.png "n-tuple-array solution")

@@ -92,5 +95,5 @@ for (const quintet of quintetIterator) { } ``` -See more examples in [src/demo](./src/demo/) +See more examples in [src/examples](./src/examples/) diff --git a/dist/cjs/index.js b/dist/cjs/index.js index 927cbf0..8d4c23e 100644 --- a/dist/cjs/index.js +++ b/dist/cjs/index.js @@ -1,6 +1,10 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.tuplesFromArray = exports.InvalidInvocationParameterError = void 0; +/** + * An exception than can be thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + */ class InvalidInvocationParameterError extends Error { } exports.InvalidInvocationParameterError = InvalidInvocationParameterError; @@ -18,6 +22,33 @@ const validateParametersOrThrow = (list, maxItems, match) => { throw new InvalidInvocationParameterError(message); } }; +/** + * Returns an iterable iterator that ouputs a configured + * list of items when iterating over a given array + * + * @typeParam T - Type of items the input list contains + * + * @param config - An object to indicate the input array `config.list`, and set the + * max size of items per interation `config.maxItems`. You can also optionally specify `config.match` + * as a function that should return true to filter in items from the input array + * (or false to filter them out) when deciding what items is to be included per iteration + * + * @function + * @throws InvalidInvocationParameterError + * This exception is thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + * + * @returns an IterableIterator + * + * @example + * Here is an example that will get max of 3 items from + * each iteration on the returned iterable + * ```javascript + * const iterable = tuplesFromArray({ + * list:[], maxSize: 3, match: (itm) => !!itm + * }); + * ``` + */ const tuplesFromArray = (config) => { const { list, match, maxItems = 2 } = config; validateParametersOrThrow(list, maxItems, match); @@ -50,13 +81,13 @@ const tuplesFromArray = (config) => { return { value: items, done: items.length === 0 }; }; const iterable = { + next: proceedNext, [Symbol.iterator]() { - return { - next: proceedNext, - }; + return this; }, }; return iterable; }; exports.tuplesFromArray = tuplesFromArray; exports.default = exports.tuplesFromArray; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/cjs/index.js.map b/dist/cjs/index.js.map new file mode 100644 index 0000000..402b09c --- /dev/null +++ b/dist/cjs/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAuBA;;;GAGG;AACH,MAAa,+BAAgC,SAAQ,KAAK;CAAG;AAA7D,0EAA6D;AAE7D,MAAM,yBAAyB,GAAG,CAAI,IAAS,EAAE,QAAgB,EAAE,KAA6B,EAAE,EAAE;IACnG,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,+BAA+B,CAAC,8BAA8B,CAAC,CAAC;IAC3E,CAAC;IAED,IACC,OAAO,QAAQ,KAAK,QAAQ;WACnB,CAAC,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,IAAI,CAAC,CAAC,EACvD,CAAC;QACF,MAAM,OAAO,GAAG,0EAA0E,CAAC;QAC3F,MAAM,IAAI,+BAA+B,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;QACxD,MAAM,OAAO,GAAG,iDAAiD,CAAC;QAClE,MAAM,IAAI,+BAA+B,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;AACF,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACI,MAAM,eAAe,GAAG,CAAI,MAAsB,EAAE,EAAE;IAC5D,MAAM,EAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAG,CAAC,EAAC,GAAG,MAAM,CAAC;IAC3C,yBAAyB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAEjD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAEvD,MAAM,WAAW,GAAG,GAAc,EAAE;QACnC,MAAM,KAAK,GAAyB,EAAE,CAAC;QAEvC,IAAI,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,EAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAC,CAAC;QAChC,CAAC;QAED,MAAM,QAAQ,GAAG,KAAK,KAAK,SAAS;YACnC,yDAAyD;YACzD,8CAA8C;YAC9C,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;YAE7C,4DAA4D;YAC5D,4DAA4D;YAC5D,uBAAuB;YACvB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;QAEf,OAAO,MAAM,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1B,MAAM,IAAI,CAAC,CAAC;YAEZ,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3B,SAAS;YACV,CAAC;YAED,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEjB,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;gBAC3C,MAAM;YACP,CAAC;QACF,CAAC;QAED,OAAO,EAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,KAAK,CAAC,EAAC,CAAC;IACjD,CAAC,CAAC;IAEF,MAAM,QAAQ,GAA2C;QACxD,IAAI,EAAE,WAAW;QAEjB,CAAC,MAAM,CAAC,QAAQ,CAAC;YAChB,OAAO,IAAI,CAAC;QACb,CAAC;KACD,CAAC;IAEF,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AAnDW,QAAA,eAAe,mBAmD1B;AAEF,kBAAe,uBAAe,CAAC"} \ No newline at end of file diff --git a/dist/cjs/package.json b/dist/cjs/package.json new file mode 100644 index 0000000..1cd945a --- /dev/null +++ b/dist/cjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/dist/cjs/types/index.d.ts b/dist/cjs/types/index.d.ts index cb5aa72..cbe5290 100644 --- a/dist/cjs/types/index.d.ts +++ b/dist/cjs/types/index.d.ts @@ -1,13 +1,53 @@ -type Item = T | undefined; -type Value = Array>; export type Matcher = (item: T | unknown) => boolean; export type TupleConfig = { + /** + * The input array to use + */ list: T[]; + /** + * The max number of items to return from the input array, per iteration + * @defaultValue 2 + */ maxItems?: number; + /** + * When provided, a function used to determine which items in the input array + * are eligible to be included, per iteration + */ match?: Matcher; }; +/** + * An exception than can be thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + */ export declare class InvalidInvocationParameterError extends Error { } -export declare const tuplesFromArray: (config: TupleConfig) => Iterable>; +/** + * Returns an iterable iterator that ouputs a configured + * list of items when iterating over a given array + * + * @typeParam T - Type of items the input list contains + * + * @param config - An object to indicate the input array `config.list`, and set the + * max size of items per interation `config.maxItems`. You can also optionally specify `config.match` + * as a function that should return true to filter in items from the input array + * (or false to filter them out) when deciding what items is to be included per iteration + * + * @function + * @throws InvalidInvocationParameterError + * This exception is thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + * + * @returns an IterableIterator + * + * @example + * Here is an example that will get max of 3 items from + * each iteration on the returned iterable + * ```javascript + * const iterable = tuplesFromArray({ + * list:[], maxSize: 3, match: (itm) => !!itm + * }); + * ``` + */ +export declare const tuplesFromArray: (config: TupleConfig) => IterableIterator<(T | undefined)[]>; export default tuplesFromArray; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/cjs/types/index.d.ts.map b/dist/cjs/types/index.d.ts.map index 8d87987..cb8e02a 100644 --- a/dist/cjs/types/index.d.ts.map +++ b/dist/cjs/types/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;AAC7B,KAAK,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAG/B,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC;AACxD,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC5B,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACnB,CAAC;AAEF,qBAAa,+BAAgC,SAAQ,KAAK;CAAG;AAqB7D,eAAO,MAAM,eAAe,mDAmD3B,CAAC;AAEF,eAAe,eAAe,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC;AAExD,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC5B;;OAEG;IACH,IAAI,EAAE,CAAC,EAAE,CAAC;IAEV;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,qBAAa,+BAAgC,SAAQ,KAAK;CAAG;AAqB7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,eAAe,oEAmD3B,CAAC;AAEF,eAAe,eAAe,CAAC"} \ No newline at end of file diff --git a/dist/esm/index.js.map b/dist/esm/index.js.map new file mode 100644 index 0000000..c284d3b --- /dev/null +++ b/dist/esm/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAuBA;;;GAGG;AACH,MAAM,OAAO,+BAAgC,SAAQ,KAAK;CAAG;AAE7D,MAAM,yBAAyB,GAAG,CAAI,IAAS,EAAE,QAAgB,EAAE,KAA6B,EAAE,EAAE;IACnG,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,+BAA+B,CAAC,8BAA8B,CAAC,CAAC;IAC3E,CAAC;IAED,IACC,OAAO,QAAQ,KAAK,QAAQ;WACnB,CAAC,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,IAAI,CAAC,CAAC,EACvD,CAAC;QACF,MAAM,OAAO,GAAG,0EAA0E,CAAC;QAC3F,MAAM,IAAI,+BAA+B,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;QACxD,MAAM,OAAO,GAAG,iDAAiD,CAAC;QAClE,MAAM,IAAI,+BAA+B,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;AACF,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAI,MAAsB,EAAE,EAAE;IAC5D,MAAM,EAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAG,CAAC,EAAC,GAAG,MAAM,CAAC;IAC3C,yBAAyB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAEjD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAEvD,MAAM,WAAW,GAAG,GAAc,EAAE;QACnC,MAAM,KAAK,GAAyB,EAAE,CAAC;QAEvC,IAAI,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,EAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAC,CAAC;QAChC,CAAC;QAED,MAAM,QAAQ,GAAG,KAAK,KAAK,SAAS;YACnC,yDAAyD;YACzD,8CAA8C;YAC9C,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;YAE7C,4DAA4D;YAC5D,4DAA4D;YAC5D,uBAAuB;YACvB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;QAEf,OAAO,MAAM,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1B,MAAM,IAAI,CAAC,CAAC;YAEZ,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3B,SAAS;YACV,CAAC;YAED,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEjB,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;gBAC3C,MAAM;YACP,CAAC;QACF,CAAC;QAED,OAAO,EAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,KAAK,CAAC,EAAC,CAAC;IACjD,CAAC,CAAC;IAEF,MAAM,QAAQ,GAA2C;QACxD,IAAI,EAAE,WAAW;QAEjB,CAAC,MAAM,CAAC,QAAQ,CAAC;YAChB,OAAO,IAAI,CAAC;QACb,CAAC;KACD,CAAC;IAEF,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AAEF,eAAe,eAAe,CAAC"} \ No newline at end of file diff --git a/dist/esm/index.mjs b/dist/esm/index.mjs index 927cbf0..5bd2ab4 100644 --- a/dist/esm/index.mjs +++ b/dist/esm/index.mjs @@ -1,9 +1,9 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.tuplesFromArray = exports.InvalidInvocationParameterError = void 0; -class InvalidInvocationParameterError extends Error { +/** + * An exception than can be thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + */ +export class InvalidInvocationParameterError extends Error { } -exports.InvalidInvocationParameterError = InvalidInvocationParameterError; const validateParametersOrThrow = (list, maxItems, match) => { if (!list || !Array.isArray(list)) { throw new InvalidInvocationParameterError('expected list to be an array'); @@ -18,7 +18,34 @@ const validateParametersOrThrow = (list, maxItems, match) => { throw new InvalidInvocationParameterError(message); } }; -const tuplesFromArray = (config) => { +/** + * Returns an iterable iterator that ouputs a configured + * list of items when iterating over a given array + * + * @typeParam T - Type of items the input list contains + * + * @param config - An object to indicate the input array `config.list`, and set the + * max size of items per interation `config.maxItems`. You can also optionally specify `config.match` + * as a function that should return true to filter in items from the input array + * (or false to filter them out) when deciding what items is to be included per iteration + * + * @function + * @throws InvalidInvocationParameterError + * This exception is thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + * + * @returns an IterableIterator + * + * @example + * Here is an example that will get max of 3 items from + * each iteration on the returned iterable + * ```javascript + * const iterable = tuplesFromArray({ + * list:[], maxSize: 3, match: (itm) => !!itm + * }); + * ``` + */ +export const tuplesFromArray = (config) => { const { list, match, maxItems = 2 } = config; validateParametersOrThrow(list, maxItems, match); let cursor = 0; @@ -50,13 +77,12 @@ const tuplesFromArray = (config) => { return { value: items, done: items.length === 0 }; }; const iterable = { + next: proceedNext, [Symbol.iterator]() { - return { - next: proceedNext, - }; + return this; }, }; return iterable; }; -exports.tuplesFromArray = tuplesFromArray; -exports.default = exports.tuplesFromArray; +export default tuplesFromArray; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/esm/package.json b/dist/esm/package.json new file mode 100644 index 0000000..4720025 --- /dev/null +++ b/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/dist/esm/types/index.d.ts b/dist/esm/types/index.d.ts index cb5aa72..cbe5290 100644 --- a/dist/esm/types/index.d.ts +++ b/dist/esm/types/index.d.ts @@ -1,13 +1,53 @@ -type Item = T | undefined; -type Value = Array>; export type Matcher = (item: T | unknown) => boolean; export type TupleConfig = { + /** + * The input array to use + */ list: T[]; + /** + * The max number of items to return from the input array, per iteration + * @defaultValue 2 + */ maxItems?: number; + /** + * When provided, a function used to determine which items in the input array + * are eligible to be included, per iteration + */ match?: Matcher; }; +/** + * An exception than can be thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + */ export declare class InvalidInvocationParameterError extends Error { } -export declare const tuplesFromArray: (config: TupleConfig) => Iterable>; +/** + * Returns an iterable iterator that ouputs a configured + * list of items when iterating over a given array + * + * @typeParam T - Type of items the input list contains + * + * @param config - An object to indicate the input array `config.list`, and set the + * max size of items per interation `config.maxItems`. You can also optionally specify `config.match` + * as a function that should return true to filter in items from the input array + * (or false to filter them out) when deciding what items is to be included per iteration + * + * @function + * @throws InvalidInvocationParameterError + * This exception is thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + * + * @returns an IterableIterator + * + * @example + * Here is an example that will get max of 3 items from + * each iteration on the returned iterable + * ```javascript + * const iterable = tuplesFromArray({ + * list:[], maxSize: 3, match: (itm) => !!itm + * }); + * ``` + */ +export declare const tuplesFromArray: (config: TupleConfig) => IterableIterator<(T | undefined)[]>; export default tuplesFromArray; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/esm/types/index.d.ts.map b/dist/esm/types/index.d.ts.map index 8d87987..cb8e02a 100644 --- a/dist/esm/types/index.d.ts.map +++ b/dist/esm/types/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;AAC7B,KAAK,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAG/B,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC;AACxD,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC5B,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACnB,CAAC;AAEF,qBAAa,+BAAgC,SAAQ,KAAK;CAAG;AAqB7D,eAAO,MAAM,eAAe,mDAmD3B,CAAC;AAEF,eAAe,eAAe,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC;AAExD,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC5B;;OAEG;IACH,IAAI,EAAE,CAAC,EAAE,CAAC;IAEV;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,qBAAa,+BAAgC,SAAQ,KAAK;CAAG;AAqB7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,eAAe,oEAmD3B,CAAC;AAEF,eAAe,eAAe,CAAC"} \ No newline at end of file diff --git a/fix-pkgs b/fix-pkgs new file mode 100755 index 0000000..e1f4413 --- /dev/null +++ b/fix-pkgs @@ -0,0 +1,11 @@ +cat >dist/cjs/package.json <dist/esm/package.json < i + 1); +const isEven = item => { + if ( + !item + || typeof item !== 'number' + || item % 2 !== 0 + ) { + return false; + } + + return true; +}; + +// Use the lib +const quintetIterator = tuplesFromArray({ + list: numbers, maxItems: 5, match: isEven, +}); + +for (const quintet of quintetIterator) { + // Prints [ 2, 4, 6, 8, 10 ] ... [ 92, 94, 96, 98, 100 ] + console.log(quintet); +} diff --git a/src/examples/js/basic.mjs b/src/examples/js/basic.mjs new file mode 100644 index 0000000..72d27df --- /dev/null +++ b/src/examples/js/basic.mjs @@ -0,0 +1,25 @@ +import tuplesFromArray from '../../../dist/esm/index.mjs'; + +// Some setup +const numbers = Array.from({length: 100}, (_, i) => i + 1); +const isEven = item => { + if ( + !item + || typeof item !== 'number' + || item % 2 !== 0 + ) { + return false; + } + + return true; +}; + +// Use the lib +const quintetIterator = tuplesFromArray({ + list: numbers, maxItems: 5, match: isEven, +}); + +for (const quintet of quintetIterator) { + // Prints [ 2, 4, 6, 8, 10 ] ... [ 92, 94, 96, 98, 100 ] + console.log(quintet); +} diff --git a/src/examples/js/concurrent.js b/src/examples/js/concurrent.js new file mode 100644 index 0000000..0ad348e --- /dev/null +++ b/src/examples/js/concurrent.js @@ -0,0 +1,39 @@ +const indexToDate = (index = 0) => batchId => new Promise(resolve => { + console.log(`[Batch ${batchId}] task ${index + 1} starting`); + const delay = Math.random() * 3000; + const dt = new Date(`2024-07-${(index + 1) % 30}`); + setTimeout(() => { + console.log(`[Batch ${batchId}] task ${index + 1} completed in ${delay.toFixed(2)} ms`); + resolve(dt); + }, delay); +}); + +const processTask = async (todo, batchId) => { + const out = await todo(batchId); + return out; +}; + +const executeConcurrent = async (todos, maxBatchSize) => { + const done = []; + const todosCopy = todos.slice(); + const worker = async (_, batchIndex) => { + let todo = todosCopy.shift(); + while (todo) { + const [result] = await Promise.allSettled([processTask(todo, batchIndex + 1)]); + done.push(result); + todo = todosCopy.shift(); + } + }; + + const batchStarters = Array.from({length: maxBatchSize}, worker); + await Promise.all(batchStarters); + return done; +}; + +(async () => { + const tasks = Array.from({length: 5}, (_, i) => indexToDate(i)); + const maxTasksPerTime = 2; + + const allDone = await executeConcurrent(tasks, maxTasksPerTime); + console.log(allDone); +})(); diff --git a/src/examples/js/with-lib.mjs b/src/examples/js/with-lib.mjs new file mode 100644 index 0000000..02a5c6f --- /dev/null +++ b/src/examples/js/with-lib.mjs @@ -0,0 +1,43 @@ +import tuplesFromArray from '../../../dist/esm/index.mjs'; + +const indexToDate = (index = 0) => batchId => new Promise(resolve => { + console.log(`[Batch ${batchId}] task ${index + 1} starting`); + const delay = Math.random() * 3000; + const dt = new Date(`2024-07-${(index + 1) % 30}`); + setTimeout(() => { + console.log(`[Batch ${batchId}] task ${index + 1} completed in ${delay.toFixed(2)} ms`); + resolve(dt); + }, delay); +}); + +const processTask = async (todo, batchId) => { + const out = await todo(batchId); + return out; +}; + +const executeWitLibrary = async (todos, maxBatchSize) => { + console.log(`do <= ${maxBatchSize} of ${todos.length} tasks, at any given time`); + const done = []; + + let batchIndex = 1; + const todosBatchIterable = tuplesFromArray({list: todos, maxItems: maxBatchSize}); + for (const batch of todosBatchIterable) { + console.log(`----- starting batch [${batchIndex}], ${batch.length} todos`); + const results = await Promise.allSettled( + batch.map(todo => processTask(todo, batchIndex)), + ); + done.push(...results); + + batchIndex += 1; + } + + return done; +}; + +(async () => { + const tasks = Array.from({length: 5}, (_, i) => indexToDate(i)); + const maxTasksPerTime = 2; + + const allDone = await executeWitLibrary(tasks, maxTasksPerTime); + console.log(allDone); +})(); diff --git a/src/examples/js/without-lib.js b/src/examples/js/without-lib.js new file mode 100644 index 0000000..e9803b7 --- /dev/null +++ b/src/examples/js/without-lib.js @@ -0,0 +1,52 @@ +const indexToDate = (index = 0) => batchId => new Promise(resolve => { + console.log(`[Batch ${batchId}] task ${index + 1} starting`); + const delay = Math.random() * 3000; + const dt = new Date(`2024-07-${(index + 1) % 30}`); + setTimeout(() => { + console.log(`[Batch ${batchId}] task ${index + 1} completed in ${delay.toFixed(2)} ms`); + resolve(dt); + }, delay); +}); + +const processTask = async (todo, batchId) => { + const out = await todo(batchId); + return out; +}; + +// Everywhere start/end is used is repititive and error prone +const executeWithoutLibrary = async (todos, maxBatchSize) => { + console.log(`do <= ${maxBatchSize} of ${todos.length} tasks, at any given time`); + const done = []; + + let start = 0; + let end = start + maxBatchSize; + let batchIndex = 1; + let batch = todos.slice(start, end); + while (batch.length > 0) { + console.log(`----- starting batch [${batchIndex}], ${batch.length} todos`); + const results = await Promise.allSettled( + batch.map(todo => processTask(todo, batchIndex)), + ); + done.push(...results); + + start = end; + if (start >= todos.length) { + break; + } + + end = Math.min(start + maxBatchSize, todos.length); + batch = todos.slice(start, end); + + batchIndex += 1; + } + + return done; +}; + +(async () => { + const tasks = Array.from({length: 5}, (_, i) => indexToDate(i)); + const maxTasksPerTime = 2; + + const allDone = await executeWithoutLibrary(tasks, maxTasksPerTime); + console.log(allDone); +})(); diff --git a/src/demo/demo-basics.ts b/src/examples/ts/basic.ts similarity index 95% rename from src/demo/demo-basics.ts rename to src/examples/ts/basic.ts index e0c13df..68fb5e9 100644 --- a/src/demo/demo-basics.ts +++ b/src/examples/ts/basic.ts @@ -1,7 +1,7 @@ -import {tuplesFromArray} from '../index'; +import {tuplesFromArray} from '../../index'; import { hexadecimals, isDate, dates, uuids, -} from './demo-utils'; +} from './utils'; console.log('----- Example 1 [10 Hex in twos. Used default param values] ------'); for (const hexPair of tuplesFromArray({list: hexadecimals(10)})) { diff --git a/src/demo/demo-classic.ts b/src/examples/ts/classic.ts similarity index 94% rename from src/demo/demo-classic.ts rename to src/examples/ts/classic.ts index e191584..922f8f2 100644 --- a/src/demo/demo-classic.ts +++ b/src/examples/ts/classic.ts @@ -1,7 +1,7 @@ -import {tuplesFromArray} from '../index'; +import {tuplesFromArray} from '../../index'; import { isDate, dates, canadaDateFormat, delay, formatCount, -} from './demo-utils'; +} from './utils'; const processItem = async (item: T | undefined) => { await delay(Math.random() * 500); // Simulate some async workload diff --git a/src/demo/demo-utils.ts b/src/examples/ts/utils.ts similarity index 100% rename from src/demo/demo-utils.ts rename to src/examples/ts/utils.ts diff --git a/src/index.ts b/src/index.ts index 6adea2f..ea90e85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,30 @@ -type Item = T | undefined; -type Value = Array>; -type Result = IteratorResult, Value>; +type Result = IteratorResult, Array>; export type Matcher = (item: T | unknown) => boolean; + export type TupleConfig = { + /** + * The input array to use + */ list: T[]; + + /** + * The max number of items to return from the input array, per iteration + * @defaultValue 2 + */ maxItems?: number; + + /** + * When provided, a function used to determine which items in the input array + * are eligible to be included, per iteration + */ match?: Matcher; }; +/** + * An exception than can be thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + */ export class InvalidInvocationParameterError extends Error {} const validateParametersOrThrow = (list: T[], maxItems: number, match: Matcher | undefined) => { @@ -30,6 +46,33 @@ const validateParametersOrThrow = (list: T[], maxItems: number, match: Matche } }; +/** + * Returns an iterable iterator that ouputs a configured + * list of items when iterating over a given array + * + * @typeParam T - Type of items the input list contains + * + * @param config - An object to indicate the input array `config.list`, and set the + * max size of items per interation `config.maxItems`. You can also optionally specify `config.match` + * as a function that should return true to filter in items from the input array + * (or false to filter them out) when deciding what items is to be included per iteration + * + * @function + * @throws InvalidInvocationParameterError + * This exception is thrown if there is no input array, `maxItems` is <= 0 or is not + * a number, or `match` is not a function + * + * @returns an IterableIterator + * + * @example + * Here is an example that will get max of 3 items from + * each iteration on the returned iterable + * ```javascript + * const iterable = tuplesFromArray({ + * list:[], maxSize: 3, match: (itm) => !!itm + * }); + * ``` + */ export const tuplesFromArray = (config: TupleConfig) => { const {list, match, maxItems = 2} = config; validateParametersOrThrow(list, maxItems, match); @@ -38,7 +81,7 @@ export const tuplesFromArray = (config: TupleConfig) => { const maxItemSize = Number.parseInt(`${maxItems}`, 10); const proceedNext = (): Result => { - const items: Value = []; + const items: Array = []; if (cursor >= list.length) { return {done: true, value: []}; @@ -72,11 +115,11 @@ export const tuplesFromArray = (config: TupleConfig) => { return {value: items, done: items.length === 0}; }; - const iterable: Iterable> = { - [Symbol.iterator](): Iterator> { - return { - next: proceedNext, - }; + const iterable: IterableIterator> = { + next: proceedNext, + + [Symbol.iterator](): IterableIterator> { + return this; }, }; diff --git a/tsconfig.base.json b/tsconfig.base.json index 211db10..18ddf57 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,21 +1,35 @@ { "compilerOptions": { "strict": true, - "rootDir": "./src", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, + "pretty": true, "checkJs": true, "allowJs": true, + "rootDir": "./src", + "skipLibCheck": true, "declaration": true, + "esModuleInterop": true, "declarationMap": true, + "sourceMap": true, + "inlineSourceMap": false, + "resolveJsonModule": true, + "moduleResolution": "Node", + "moduleDetection": "force", "noUncheckedIndexedAccess": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ] }, "include": [ "src/**/*" ], "exclude": [ - "src/demo/**" + "src/examples/**", + "node_modules", + "dist" ] } \ No newline at end of file diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 5d35037..5801811 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,13 +1,8 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "lib": [ - "ES2022", - "DOM" - ], "target": "ES2022", "module": "CommonJS", - "moduleResolution": "Node", "outDir": "dist/cjs", "declarationDir": "dist/cjs/types" } diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 8beeb6a..bed57b6 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -1,13 +1,8 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "lib": [ - "ES2022", - "DOM" - ], - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "target": "ESNext", + "module": "ESNext", "outDir": "dist/esm", "declarationDir": "dist/esm/types" }