Skip to content
31 changes: 31 additions & 0 deletions helpers/array/chunk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { describe, expect, it } from "bun:test";
import { chunk } from "./chunk";

describe("chunk", () => {
it("should chunk array into specified size", () => {
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});

it("should work with exact divisions", () => {
expect(chunk([1, 2, 3, 4], 2)).toEqual([[1, 2], [3, 4]]);
});

it("should work with size 1", () => {
expect(chunk([1, 2, 3], 1)).toEqual([[1], [2], [3]]);
});

it("should return empty array for size 0 or negative", () => {
expect(chunk([1, 2, 3], 0)).toEqual([]);
expect(chunk([1, 2, 3], -1)).toEqual([]);
});

it("should work with empty array", () => {
expect(chunk([], 2)).toEqual([]);
});
});
20 changes: 20 additions & 0 deletions helpers/array/chunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Chunks an array into smaller arrays of specified size
* @param array - The array to chunk
* @param size - The size of each chunk
* @returns Array of chunks
*/
export function chunk<T>(array: T[], size: number): T[][] {
if (size <= 0) return [];
const result: T[][] = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
}
31 changes: 31 additions & 0 deletions helpers/array/difference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { describe, expect, it } from "bun:test";
import { difference } from "./difference";

describe("difference", () => {
it("should return items in first array but not in second", () => {
expect(difference([1, 2, 3, 4], [2, 4])).toEqual([1, 3]);
});

it("should work with strings", () => {
expect(difference(['a', 'b', 'c'], ['b'])).toEqual(['a', 'c']);
});

it("should return empty array when all items are common", () => {
expect(difference([1, 2, 3], [1, 2, 3, 4])).toEqual([]);
});

it("should return first array when no items are common", () => {
expect(difference([1, 2, 3], [4, 5, 6])).toEqual([1, 2, 3]);
});

it("should work with empty arrays", () => {
expect(difference([], [1, 2])).toEqual([]);
expect(difference([1, 2], [])).toEqual([1, 2]);
});
});
16 changes: 16 additions & 0 deletions helpers/array/difference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Returns the difference between two arrays (items in first array but not in second)
* @param array1 - First array
* @param array2 - Second array
* @returns Array with items from first array not present in second array
*/
export function difference<T>(array1: T[], array2: T[]): T[] {
const set2 = new Set(array2);
return array1.filter(item => !set2.has(item));
}
125 changes: 125 additions & 0 deletions helpers/array/sort.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { describe, expect, it } from "bun:test";
import {
sortNumberAscFn,
sortNumberDescFn,
sortStringAscFn,
sortStringDescFn,
sortStringAscInsensitiveFn,
createSortByStringFn,
createSortByNumberFn,
createSortByDateFn
} from "./sort";

describe("sort functions", () => {
describe("number sorting", () => {
it("should sort numbers ascending", () => {
const arr = [3, 1, 4, 1, 5];
arr.sort(sortNumberAscFn);
expect(arr).toEqual([1, 1, 3, 4, 5]);
});

it("should sort numbers descending", () => {
const arr = [3, 1, 4, 1, 5];
arr.sort(sortNumberDescFn);
expect(arr).toEqual([5, 4, 3, 1, 1]);
});
});

describe("string sorting", () => {
it("should sort strings ascending", () => {
const arr = ['cherry', 'apple', 'banana'];
arr.sort(sortStringAscFn);
expect(arr).toEqual(['apple', 'banana', 'cherry']);
});

it("should sort strings descending", () => {
const arr = ['cherry', 'apple', 'banana'];
arr.sort(sortStringDescFn);
expect(arr).toEqual(['cherry', 'banana', 'apple']);
});

it("should sort strings case insensitive", () => {
const arr = ['Cherry', 'apple', 'Banana'];
arr.sort(sortStringAscInsensitiveFn);
expect(arr).toEqual(['apple', 'Banana', 'Cherry']);
});
});

describe("property sorting", () => {
const users = [
{ name: 'John', age: 30, joined: new Date('2020-01-01') },
{ name: 'alice', age: 25, joined: new Date('2021-01-01') },
{ name: 'Bob', age: 35, joined: new Date('2019-01-01') }
];

it("should sort by string property", () => {
const sorted = [...users].sort(createSortByStringFn('name'));
expect(sorted.map(u => u.name)).toEqual(['alice', 'Bob', 'John']);
});

it("should sort by string property case insensitive", () => {
const sorted = [...users].sort(createSortByStringFn('name', true));
expect(sorted.map(u => u.name)).toEqual(['alice', 'Bob', 'John']);
});

it("should sort by number property", () => {
const sorted = [...users].sort(createSortByNumberFn('age'));
expect(sorted.map(u => u.age)).toEqual([25, 30, 35]);
});

it("should sort by date property", () => {
const sorted = [...users].sort(createSortByDateFn('joined'));
expect(sorted.map(u => u.joined.getFullYear())).toEqual([2019, 2020, 2021]);
});

it("should use default properties", () => {
const items = [
{ value: 'z' },
{ value: 'a' },
{ value: 'c' }
];

const sorted = [...items].sort(createSortByStringFn());
expect(sorted.map(i => i.value)).toEqual(['a', 'c', 'z']);
});

it("should fallback to label when all objects have label", () => {
const items = [
{ label: 'z' },
{ label: 'a' },
{ label: 'c' }
];

const sorted = [...items].sort(createSortByStringFn());
expect(sorted.map(i => i.label)).toEqual(['a', 'c', 'z']);
});

it("should use default number property", () => {
const items = [
{ value: 30 },
{ value: 10 },
{ value: 20 }
];

const sorted = [...items].sort(createSortByNumberFn());
expect(sorted.map(i => i.value)).toEqual([10, 20, 30]);
});

it("should use default date property", () => {
const items = [
{ date: new Date('2022-01-01') },
{ date: new Date('2021-01-01') },
{ date: new Date('2023-01-01') }
];

const sorted = [...items].sort(createSortByDateFn());
expect(sorted.map(i => i.date.getFullYear())).toEqual([2021, 2022, 2023]);
});
});
});
117 changes: 117 additions & 0 deletions helpers/array/sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Sort function type for arrays
*/
export type SortFn<T> = (a: T, b: T) => number;

/**
* Sort numbers in ascending order
* @param a - First number
* @param b - Second number
* @returns Sort order
*/
export const sortNumberAscFn: SortFn<number> = (a: number, b: number) => a - b;

/**
* Sort numbers in descending order
* @param a - First number
* @param b - Second number
* @returns Sort order
*/
export const sortNumberDescFn: SortFn<number> = (a: number, b: number) => b - a;

/**
* Sort strings in ascending order
* @param a - First string
* @param b - Second string
* @returns Sort order
*/
export const sortStringAscFn: SortFn<string> = (a: string, b: string) => a.localeCompare(b);

/**
* Sort strings in descending order
* @param a - First string
* @param b - Second string
* @returns Sort order
*/
export const sortStringDescFn: SortFn<string> = (a: string, b: string) => b.localeCompare(a);

/**
* Sort strings in ascending order (case insensitive)
* @param a - First string
* @param b - Second string
* @returns Sort order
*/
export const sortStringAscInsensitiveFn: SortFn<string> = (a: string, b: string) =>
a.toLowerCase().localeCompare(b.toLowerCase());

/**
* Creates a sort function for objects by string property
* @param property - The property to sort by (defaults to trying 'value', 'label', 'title', 'description')
* @param caseInsensitive - Whether to ignore case
* @returns Sort function
*/
export function createSortByStringFn<T extends Record<string, any>>(
property?: keyof T,
caseInsensitive: boolean = false
): SortFn<T> {
return (a: T, b: T) => {
let aVal = '';
let bVal = '';

if (property) {
aVal = String(a[property] ?? '');
bVal = String(b[property] ?? '');
} else {
// Try default properties in order
for (const prop of ['value', 'label', 'title', 'description']) {
if (prop in a && prop in b) {
aVal = String(a[prop] ?? '');
bVal = String(b[prop] ?? '');
break;
}
}
}

return caseInsensitive
? aVal.toLowerCase().localeCompare(bVal.toLowerCase())
: aVal.localeCompare(bVal);
};
}

/**
* Creates a sort function for objects by number property
* @param property - The property to sort by (defaults to 'value')
* @returns Sort function
*/
export function createSortByNumberFn<T extends Record<string, any>>(
property?: keyof T
): SortFn<T> {
const prop = property || 'value';
return (a: T, b: T) => {
const aVal = Number(a[prop] ?? 0);
const bVal = Number(b[prop] ?? 0);
return aVal - bVal;
};
}

/**
* Creates a sort function for objects by date property
* @param property - The property to sort by (defaults to 'date')
* @returns Sort function
*/
export function createSortByDateFn<T extends Record<string, any>>(
property?: keyof T
): SortFn<T> {
const prop = property || 'date';
return (a: T, b: T) => {
const aVal = new Date(a[prop] as any).getTime();
const bVal = new Date(b[prop] as any).getTime();
return aVal - bVal;
};
}
26 changes: 26 additions & 0 deletions helpers/array/unique.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { describe, expect, it } from "bun:test";
import { unique } from "./unique";

describe("unique", () => {
it("should remove duplicates from array", () => {
expect(unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
});

it("should work with strings", () => {
expect(unique(['a', 'b', 'a', 'c', 'b'])).toEqual(['a', 'b', 'c']);
});

it("should work with empty array", () => {
expect(unique([])).toEqual([]);
});

it("should preserve order for first occurrence", () => {
expect(unique([3, 1, 2, 1, 3])).toEqual([3, 1, 2]);
});
});
14 changes: 14 additions & 0 deletions helpers/array/unique.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* This file is part of helpers4.
* Copyright (C) 2025 baxyz
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Removes duplicate values from an array
* @param array - The array to remove duplicates from
* @returns New array with unique values only
*/
export function unique<T>(array: T[]): T[] {
return Array.from(new Set(array));
}
4 changes: 4 additions & 0 deletions helpers/date/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "date",
"description": "Date and time utility functions"
}
Loading