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
5 changes: 5 additions & 0 deletions .changeset/fresh-pens-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@js-utils-kit/types': minor
---

Add a `Falsy` type representing all JavaScript falsy values.
5 changes: 5 additions & 0 deletions .changeset/honest-turtles-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@js-utils-kit/array': minor
---

Add `unique` utility to remove duplicate values from arrays with optional sorting support.
5 changes: 5 additions & 0 deletions .changeset/little-queens-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@js-utils-kit/array': minor
---

Add `lastElement` utility to return the last element of an array.
Comment thread
teneplaysofficial marked this conversation as resolved.
5 changes: 5 additions & 0 deletions .changeset/ninety-wasps-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@js-utils-kit/array': minor
---

Add `compact` utility to remove falsy values from arrays.
5 changes: 5 additions & 0 deletions .changeset/strict-ties-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@js-utils-kit/array': minor
---

Introduce `chunk`, a generic array utility for splitting collections into fixed-size chunks with safe immutable input handling.
44 changes: 44 additions & 0 deletions packages/array/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@js-utils-kit/array",
"version": "0.0.0",
"description": "Array utilities",
"private": false,
"license": "MIT",
"author": {
"name": "Sriman",
"email": "136729116+TenEplaysOfficial@users.noreply.github.com",
"url": "https://tene.vercel.app"
},
"homepage": "https://js-utils.js.org",
"repository": {
"type": "git",
"url": "https://github.com/teneplaysofficial/js-utils-kit",
"directory": "packages/array"
},
"bugs": {
"url": "https://github.com/teneplaysofficial/js-utils-kit/issues"
},
"files": [
"dist"
],
"engines": {
"node": ">=22"
},
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.cts",
"exports": {
".": {
"require": "./dist/index.cjs",
"import": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsdown",
"test": "vitest run"
},
"dependencies": {
"@js-utils-kit/types": "workspace:*"
}
}
30 changes: 30 additions & 0 deletions packages/array/src/chunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Splits an array into chunks of a specified size.
*
* @returns list of grouped chunks
*
* @example
* ```ts
* chunk([1, 2, 3, 4], 2);
* // [[1, 2], [3, 4]]
*
* chunk(['a', 'b', 'c'], 1);
* // [['a'], ['b'], ['c']]
* ```
*/
export function chunk<T>(
/**
* List of elements.
*/
array: readonly T[],
/**
* Length of each chunk to group.
*/
size: number = 1,
): T[][] {
if (size <= 0) throw new RangeError('chunk size must be greater than 0');

return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>
array.slice(i * size, i * size + size),
);
}
28 changes: 28 additions & 0 deletions packages/array/src/compact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Falsy } from '@js-utils-kit/types';

/**
* Removes all falsy values from an array.
*
* Falsy values include: `false`, `null`, `0`, `""`, `undefined`, and `NaN`.
Comment thread
teneplaysofficial marked this conversation as resolved.
*
* @returns A new array with all falsy values removed
*
* @example
* ```ts
* compact([0, 1, false, 2, '', 3, null]);
* // [1, 2, 3]
*
* compact(['a', '', 'b', undefined]);
* // ['a', 'b']
* ```
*/
export function compact<T>(
/**
* A list of elements to compact
*/
array: readonly T[],
): Exclude<T, Falsy>[] {
return array.filter(
(value): value is Exclude<T, Falsy> => Boolean(value) && !Number.isNaN(value),
);
}
Comment thread
teneplaysofficial marked this conversation as resolved.
5 changes: 5 additions & 0 deletions packages/array/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from '@js-utils-kit/types';
export * from './chunk';
export * from './compact';
export * from './lastElement';
export * from './unique';
20 changes: 20 additions & 0 deletions packages/array/src/lastElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Returns the last element of an array.
*
* @returns The last element, or `undefined` if the array is empty
*
* @example
* lastElement([1, 2, 3]);
* // 3
*
* lastElement([]);
* // undefined
*/
Comment thread
teneplaysofficial marked this conversation as resolved.
export function lastElement<T>(
/**
* A list of elements.
*/
array: readonly T[],
): T | undefined {
return array.at(-1);
}
59 changes: 59 additions & 0 deletions packages/array/src/unique.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Returns a new array with duplicate values removed.
*
* Uniqueness is determined using `Set`, meaning object uniqueness is based on reference equality.
*
* @example
* ```ts
* unique([3, 1, 2, 1]);
* // [3, 1, 2]
*
* unique([3, 1, 2, 1], { sort: true });
* // [1, 2, 3]
*
* const user1 = { id: 1, name: 'Alice' };
* const user2 = { id: 2, name: 'Bob' };
* const user3 = { id: 3, name: 'Charlie' };
* const users = [user3, user1, user2, user1];
*
* unique(users, {
* sort: true,
* compareFn: (a, b) => a.id - b.id,
* });
*
* // [
* // { id: 1, name: 'Alice' },
* // { id: 2, name: 'Bob' },
* // { id: 3, name: 'Charlie' }
* // ]
* ```
*/
export function unique<T>(
/**
* The source array.
*/
array: readonly T[],
{
sort = false,
compareFn,
}: {
/**
* Whether to sort the result.
*
* @default false
*/
sort?: boolean;
/**
* Custom compare function for sorting.
*/
compareFn?: (a: T, b: T) => number;
} = {},
): T[] {
const result = [...new Set(array)];

if (!sort) {
return result;
}

return compareFn ? result.sort(compareFn) : result.sort();
}
60 changes: 60 additions & 0 deletions packages/array/test/chunk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { it, expect } from 'vitest';
import { chunk } from '../src';

it('splits an array into equal-sized chunks', () => {
expect(chunk([1, 2, 3, 4], 2)).toEqual([
[1, 2],
[3, 4],
]);
});

it('creates a smaller last chunk if array length is not divisible by size', () => {
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});

it('returns each element as its own chunk when size is 1', () => {
expect(chunk(['a', 'b', 'c'], 1)).toEqual([['a'], ['b'], ['c']]);
});

it('returns the entire array as a single chunk when size is greater than array length', () => {
expect(chunk([1, 2, 3], 10)).toEqual([[1, 2, 3]]);
});

it('uses a default chunk size of 1', () => {
expect(chunk([true, false])).toEqual([[true], [false]]);
});

it('returns an empty array when input array is empty', () => {
expect(chunk([], 3)).toEqual([]);
});

it('does not mutate the original array', () => {
const input = [1, 2, 3, 4];
const copy = [...input];

chunk(input, 2);

expect(input).toEqual(copy);
});

it('throws a RangeError when size is 0', () => {
expect(() => chunk([1, 2, 3], 0)).toThrow(RangeError);
});

it('throws a RangeError when size is negative', () => {
expect(() => chunk([1, 2, 3], -1)).toThrow('chunk size must be greater than 0');
});

it('works with readonly arrays', () => {
const input = [1, 2, 3, 4] as const;
expect(chunk(input, 2)).toEqual([
[1, 2],
[3, 4],
]);
});

it('handles arrays of objects correctly', () => {
const data = [{ id: 1 }, { id: 2 }, { id: 3 }];

expect(chunk(data, 2)).toEqual([[{ id: 1 }, { id: 2 }], [{ id: 3 }]]);
});
50 changes: 50 additions & 0 deletions packages/array/test/compact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { it, expect } from 'vitest';
import { compact } from '../src';

it('removes all falsy values from an array', () => {
expect(compact([0, 1, false, 2, '', 3, null, undefined, NaN])).toEqual([1, 2, 3]);
});

it('works with strings', () => {
expect(compact(['a', '', 'b', undefined])).toEqual(['a', 'b']);
});

it('returns an empty array when all values are falsy', () => {
expect(compact([0, false, '', null, undefined, NaN])).toEqual([]);
});

it('returns an empty array when input is empty', () => {
expect(compact([])).toEqual([]);
});

it('does not remove truthy objects or arrays', () => {
const obj = {};
const arr: number[] = [];

expect(compact([obj, arr, 1])).toEqual([obj, arr, 1]);
});

it('does not mutate the original array', () => {
const input = [0, 1, false, 2];
const copy = [...input];

compact(input);

expect(input).toEqual(copy);
});

it('works with readonly arrays', () => {
const input = [0, 1, 2, false, 3] as const;

expect(compact(input)).toEqual([1, 2, 3]);
});

it('preserves element order', () => {
expect(compact([1, 0, 2, false, 3])).toEqual([1, 2, 3]);
});

it('narrows values at runtime (truthy filtering)', () => {
const result = compact([0, 'a', '', 'b']);

expect(result).toEqual(['a', 'b']);
});
49 changes: 49 additions & 0 deletions packages/array/test/lastElement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { it, expect } from 'vitest';
import { lastElement } from '../src';

it('returns the last element of an array', () => {
expect(lastElement([1, 2, 3])).toBe(3);
});

it('returns undefined for an empty array', () => {
expect(lastElement([])).toBeUndefined();
});

it('returns the only element for a single-element array', () => {
expect(lastElement(['only'])).toBe('only');
});

it('works with different types', () => {
expect(lastElement([true, false])).toBe(false);
expect(lastElement(['a', 'b', 'c'])).toBe('c');
});

it('returns undefined when the array contains only undefined values', () => {
expect(lastElement([undefined, undefined])).toBeUndefined();
});

it('does not mutate the original array', () => {
const input = [1, 2, 3];
const copy = [...input];

lastElement(input);

expect(input).toEqual(copy);
});

it('works with readonly arrays', () => {
const input = [1, 2, 3] as const;

expect(lastElement(input)).toBe(3);
});

it('handles arrays of objects correctly', () => {
const a = { id: 1 };
const b = { id: 2 };

expect(lastElement([a, b])).toBe(b);
});

it('returns null if the last element is null', () => {
expect(lastElement([1, null])).toBeNull();
});
Loading
Loading