Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e7db04f
init
chalu Feb 15, 2024
0132918
get started with the implementation
chalu Feb 15, 2024
f6241ef
allow caller filter in elements with a match function
chalu Feb 15, 2024
9ea6c84
setup unit tests
chalu Feb 15, 2024
477c5df
add smoke tests for the list parameter
chalu Feb 15, 2024
a8513ce
add smoke tests for maxItems param
chalu Feb 15, 2024
87d8c36
add smoke tests for the match param
chalu Feb 15, 2024
9b41f0a
refactor param validation into a separate function
chalu Feb 15, 2024
3a9dfbf
add basic CI workflow
chalu Feb 15, 2024
72bf07d
re-add CI workflow file
chalu Feb 15, 2024
3795560
add catch all branch in CI workflow
chalu Feb 15, 2024
e2c42cd
try install pnpm before node in CI workflow
chalu Feb 15, 2024
7c81b61
attempt display test summary on GHA overview page
chalu Feb 15, 2024
18b2461
add TAP reporter for tests in CI
chalu Feb 15, 2024
15eada8
attempt junit reporter for summary in CI
chalu Feb 15, 2024
66f93e3
remove TAP reporter
chalu Feb 15, 2024
e1efbf2
improve setup for tests
chalu Feb 16, 2024
97db447
add matchers from jest-extended
chalu Feb 16, 2024
5aa4f84
add smoke tests
chalu Feb 16, 2024
ac99976
add festures tests
chalu Feb 16, 2024
ae39890
enforce code style with XO
chalu Feb 16, 2024
433c458
add pre-commit hook to run lint, build, and test scripts
chalu Feb 16, 2024
6c9f083
simplify pre-commit script
chalu Feb 16, 2024
c0646b8
add built files in dist folder
chalu Feb 16, 2024
ea42403
Merge pull request #5 from chalu/add-unit-tests
chalu Feb 16, 2024
abb0cd8
add docs and samples (#7)
chalu Feb 17, 2024
5c8e09d
Merge branch 'main' into dev
chalu Feb 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pnpm test
pnpm lint
1 change: 1 addition & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm test
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enable-pre-post-scripts=true
7 changes: 6 additions & 1 deletion .xo-config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"env": ["jest", "node"],
"rules": {
"import/extensions": "off",
"unicorn/prefer-module": "off"
"unicorn/prefer-module": "off",
"unicorn/no-array-for-each": "off",
"unicorn/prefer-top-level-await": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-assignment": "off"
}
}
82 changes: 81 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,82 @@
# n-tuple-array
Get a configurable amount of items from a JS array, instead of only one item

Get a specified amount of items when iterating over a JavaScript array, instead of the 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.

`n-tuple-array` can help you get out the coordinates in pairs (their logical representation), such that you'd go **from**
```json
// flatCoords
["5.7225", "-9.6273", "2.68452", "-30.9501", ...]
```

**to**
```javascript
// generate pairs by default
const coordIterable = tuplesFromArray({ list: flatCoords });

// using for..of, get pairs as ["5.7225", "-9.6273"] ...
for (const pair of coordIterable) {
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
.map(pair => myTransform(pair))
.forEach((pair) => placeOnMap(pair));
```

### Some Real World Examples
> I first had this idea and tried my hands on it 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. It was a simulation of people entering an event hall to get seated, but only two could get it/be attended to at a time. I later took some time to [bring the project to live](https://wole-joko.netlify.app/)

> The below example was adapted for more concise terminal output

<br>

JS challenge by @thdxr on X.com <br>
![](./assets/the-dax-js-challenge.png "JS challenge by @thdxr")
<br> <br>

`n-tuple-array` solution code <br>
[<img src="./assets/demo-classic.png">](./assets/demo-classic.svg)
<br> <br>

`n-tuple-array` n-tuple-array solution demo <br>
![](./assets/ntuple-array-demo-optimized.gif "n-tuple-array solution demo")



## Setup & Usage

```bash
npm install n-tuple-array
```

```javascript
import { tuplesFromArray } from 'n-tuple-array';

const numbers = Array.from({length: 100}, (_, i) => i + 1);
const isEven = (item) => {
if (
!item
|| typeof item !== 'number'
|| item % 2 !== 0
) return false;

return true;
};

for (const triplets of tuplesFromArray({list: numbers, maxItems: 3, match: isEven})) {
console.log(triplets);
}
```

See more examples in [src/demo](./src/demo/)

Binary file added assets/demo-classic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 224 additions & 0 deletions assets/demo-classic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/ntuple-array-demo-optimized.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/the-dax-js-challenge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions dist/demo/demo-basics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const index_1 = require("../index");
const demo_utils_1 = require("./demo-utils");
console.log('----- Example 1 [10 Hex in twos. Used default param values] ------');
for (const hexPair of (0, index_1.tuplesFromArray)({ list: (0, demo_utils_1.hexadecimals)(10) })) {
console.log(hexPair);
}
console.log('\n----- Example 2 [30 Hex in fives. Specified maxItems] ------');
for (const hexQuintet of (0, index_1.tuplesFromArray)({ list: (0, demo_utils_1.hexadecimals)(30), maxItems: 5 })) {
console.log(hexQuintet);
}
console.log('\n----- Example 3 [Dates in twos. Filtered in by a match function] ------');
// Create an array of 50 elements which include some dates
const data = [...(0, demo_utils_1.hexadecimals)(20), ...(0, demo_utils_1.dates)(10), ...(0, demo_utils_1.uuids)(20)];
// Use a basic/weak shuffle algo to shuffle the array items
data.sort(() => Math.random() - 0.5);
for (const dateTriplet of (0, index_1.tuplesFromArray)({ list: data, match: demo_utils_1.isDate })) {
console.log(dateTriplet);
}
console.log('\n----- Example 4 [Tuples can be "remainders", see the last array/tuple] ------');
for (const idTriplets of (0, index_1.tuplesFromArray)({ list: (0, demo_utils_1.uuids)(8), maxItems: 3 })) {
console.log(idTriplets);
}
const dozenHexValues = (0, demo_utils_1.hexadecimals)(12);
const hexIterable = (0, index_1.tuplesFromArray)({ list: dozenHexValues, maxItems: 2 });
const hexPairs = Array.from(hexIterable);
console.log('Array.from( tuplesFromArray(...) ) is an Array:', Array.isArray(hexPairs));
// Use native Array methods
hexPairs
.map(pair => pair.toString().toUpperCase().split(','))
.forEach(pair => {
console.log(pair);
});
32 changes: 32 additions & 0 deletions dist/demo/demo-classic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const index_1 = require("../index");
const demo_utils_1 = require("./demo-utils");
const processItem = async (item) => {
await (0, demo_utils_1.delay)(Math.random() * 500); // Simulate some async workload
const processed = (item instanceof Date && (0, demo_utils_1.isDate)(item)) ? demo_utils_1.canadaDateFormat.format(item) : item;
return processed;
};
const parallelPool = async (tasks, prarallelSize) => {
const processed = [];
const parallelSizeAwareIterable = (0, index_1.tuplesFromArray)({
list: tasks,
maxItems: prarallelSize,
});
let count = 1;
for await (const cohort of parallelSizeAwareIterable) {
const results = await Promise.allSettled(cohort.map(async (itm) => processItem(itm)));
processed.push(...results);
console.log(`processed ${(0, demo_utils_1.formatCount)(count)} batch of <= ${prarallelSize} items`);
if ((count * prarallelSize) >= tasks.length) {
console.log('DONE!');
}
count += 1;
}
return processed;
};
(async () => {
const canadianDates = await parallelPool((0, demo_utils_1.dates)(12), 3);
console.log('-------------------');
console.log(canadianDates);
})();
36 changes: 36 additions & 0 deletions dist/demo/demo-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.delay = exports.formatCount = exports.canadaDateFormat = exports.isDate = exports.hexadecimals = exports.dates = exports.uuids = void 0;
const faker_1 = require("@faker-js/faker");
/**
* Some Helper Functions
*/
const factory = (generator, howMany = 20) => faker_1.simpleFaker.helpers.multiple(generator, { count: howMany });
const uuids = (howMany) => factory(faker_1.simpleFaker.string.uuid, howMany);
exports.uuids = uuids;
const dates = (howMany) => factory(faker_1.simpleFaker.date.birthdate, howMany);
exports.dates = dates;
const hexadecimals = (howMany) => factory(faker_1.simpleFaker.string.hexadecimal, howMany);
exports.hexadecimals = hexadecimals;
const isDate = (date) => (date instanceof Date) && !Number.isNaN(date.getTime());
exports.isDate = isDate;
exports.canadaDateFormat = new Intl.DateTimeFormat('en-CA', {
dateStyle: 'medium',
});
const plural = new Intl.PluralRules('en-US', { type: 'ordinal' });
const suffixes = new Map([
['one', 'st'],
['two', 'nd'],
['few', 'rd'],
['other', 'th'],
]);
const formatCount = (n) => {
const rule = plural.select(n);
const suffix = suffixes.get(rule);
return `${n}${suffix}`;
};
exports.formatCount = formatCount;
const delay = async (ms) => new Promise(resolve => {
setTimeout(resolve, ms);
});
exports.delay = delay;
15 changes: 5 additions & 10 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
type IterationResult<T> = {
done: boolean;
value: Array<T | undefined>;
};
type Matcher<T> = (item: T | undefined) => boolean;
type Item<T> = T | undefined;
type Value<T> = Array<Item<T>>;
export type Matcher<T> = (item: T | unknown) => boolean;
export type TupleConfig<T> = {
list: T[];
maxItems?: number;
match?: Matcher<T>;
};
export declare class InvalidInvocationParameterError extends Error {
}
export declare const tuplesFromArray: <T>(config: TupleConfig<T>) => {
[Symbol.iterator](): any;
next(): IterationResult<T>;
};
export {};
export declare const tuplesFromArray: <T>(config: TupleConfig<T>) => Iterable<Value<T>>;
export default tuplesFromArray;
57 changes: 30 additions & 27 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,40 @@ const tuplesFromArray = (config) => {
validateParametersOrThrow(list, maxItems, match);
let cursor = 0;
const maxItemSize = Number.parseInt(`${maxItems}`, 10);
const iterable = {
[Symbol.iterator]() {
return this;
},
next() {
if (cursor >= list.length) {
return { done: true, value: [] };
const proceedNext = () => {
const items = [];
if (cursor >= list.length) {
return { done: true, value: [] };
}
const endIndex = match === undefined
// A match funtion was provided. Okay to run to array end
// or until we've matched maxItemSize elements
? Math.min(cursor + maxItemSize, list.length)
// No match function was provided. We should run till we are
// out of items (list.length) or till we gotten the next set
// of maxItemSize items
: list.length;
while (cursor < endIndex) {
const item = list[cursor];
cursor += 1;
if (match && !match(item)) {
continue;
}
const items = [];
const endIndex = match === undefined
// A match funtion was provided. Okay to run to array end
// or until we've matched maxItemSize elements
? Math.min(cursor + maxItemSize, list.length)
// No match function was provided. We should run till we are
// out of items (list.length) or till we gotten the next set
// of maxItemSize items
: list.length;
while (cursor < endIndex) {
const item = list[cursor];
cursor += 1;
if (match && !match(item)) {
continue;
}
items.push(item);
if (match && items.length === maxItemSize) {
break;
}
items.push(item);
if (match && items.length === maxItemSize) {
break;
}
return { done: false, value: items };
}
return { value: items, done: items.length === 0 };
};
const iterable = {
[Symbol.iterator]() {
return {
next: proceedNext,
};
},
};
return iterable;
};
exports.tuplesFromArray = tuplesFromArray;
exports.default = exports.tuplesFromArray;
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
{
"name": "n-tuple-array",
"version": "0.1",
"description": "Get a specified amount of items when iterating over an array, instead of only one item!",
"description": "Get a specified amount of items when iterating over a JavaScript array, instead of just a single item that native arrays provide!",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"type": "module",
"scripts": {
"build": "tsc",
"lint": "xo --env=jest",
"lint:fix": "xo --env=jest --fix",
"test": "pnpm lint && pnpm build && jest --runInBand",
"prebuild": "rm -rf dist/*",
"postbuild": "rm -rf dist/demo/*.d.*",
"lint": "xo",
"lint:fix": "xo --fix",
"test": "jest --runInBand",
"pretest": "pnpm lint && pnpm build",
"test:ci": "jest --ci --config='./jest.config.ci.ts'",
"prepare": "husky"
},
"keywords": [],
"keywords": ["array", "tuple", "arrays", "tuples", "iterables", "iterators", "symbol.iterator", "javascript", "typescript"],
"author": "Charles Odili <chaluwa@gmail.com>",
"license": "MIT",
"devDependencies": {
Expand Down
40 changes: 40 additions & 0 deletions src/demo/demo-basics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {tuplesFromArray} from '../index';
import {
hexadecimals, isDate, dates, uuids,
} from './demo-utils';

console.log('----- Example 1 [10 Hex in twos. Used default param values] ------');
for (const hexPair of tuplesFromArray({list: hexadecimals(10)})) {
console.log(hexPair);
}

console.log('\n----- Example 2 [30 Hex in fives. Specified maxItems] ------');
for (const hexQuintet of tuplesFromArray({list: hexadecimals(30), maxItems: 5})) {
console.log(hexQuintet);
}

console.log('\n----- Example 3 [Dates in twos. Filtered in by a match function] ------');
// Create an array of 50 elements which include some dates
const data = [...hexadecimals(20), ...dates(10), ...uuids(20)] as Array<string | Date>;
// Use a basic/weak shuffle algo to shuffle the array items
data.sort(() => Math.random() - 0.5);

for (const dateTriplet of tuplesFromArray({list: data, match: isDate})) {
console.log(dateTriplet);
}

console.log('\n----- Example 4 [Tuples can be "remainders", see the last array/tuple] ------');
for (const idTriplets of tuplesFromArray({list: uuids(8), maxItems: 3})) {
console.log(idTriplets);
}

const dozenHexValues = hexadecimals(12);
const hexIterable = tuplesFromArray({list: dozenHexValues, maxItems: 2});
const hexPairs = Array.from(hexIterable);
console.log('Array.from( tuplesFromArray(...) ) is an Array:', Array.isArray(hexPairs));
// Use native Array methods
hexPairs
.map(pair => pair.toString().toUpperCase().split(','))
.forEach(pair => {
console.log(pair);
});
Loading