Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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