From fc58e4b308e6e380133b2743f6a6abb788d757d2 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 5 Jan 2025 17:28:03 +0100 Subject: [PATCH] chore: wip --- src/collect.ts | 131 ++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 48 ++++++++++++++++ test/index.test.ts | 133 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+) diff --git a/src/collect.ts b/src/collect.ts index 2145cc0..f5e69ac 100644 --- a/src/collect.ts +++ b/src/collect.ts @@ -706,6 +706,137 @@ function createCollectionOperations(collection: Collection): CollectionOpe }, 0) }, + round(this: CollectionOperations, keyOrPrecision?: keyof T | number, precisionOrUndefined?: number) { + function roundNumberCollection(this: CollectionOperations, precision: number = 0): CollectionOperations { + const multiplier = 10 ** precision + return collect( + collection.items.map(item => + Math.round(Number(item) * multiplier) / multiplier + ) + ) + } + + function roundObjectCollection(key: K, precision: number = 0): CollectionOperations { + const multiplier = 10 ** precision + return collect( + collection.items.map(item => ({ + ...item, + [key]: Math.round(Number(item[key]) * multiplier) / multiplier + })) + ) + } + + // If first argument is number or undefined, treat as number collection + if (typeof keyOrPrecision === 'number' || keyOrPrecision === undefined) { + return roundNumberCollection.call(this, keyOrPrecision ?? 0) + } + + // Otherwise treat as object collection + return roundObjectCollection(keyOrPrecision, precisionOrUndefined ?? 0) + }, + + ceil(this: CollectionOperations, keyOrPrecision?: keyof T | number, precisionOrUndefined?: number) { + function ceilNumberCollection(this: CollectionOperations, precision: number = 0): CollectionOperations { + const multiplier = 10 ** precision + return collect( + collection.items.map(item => + Math.ceil(Number(item) * multiplier) / multiplier + ) + ) + } + + function ceilObjectCollection(key: K, precision: number = 0): CollectionOperations { + const multiplier = 10 ** precision + return collect( + collection.items.map(item => ({ + ...item, + [key]: Math.ceil(Number(item[key]) * multiplier) / multiplier + })) + ) + } + + if (typeof keyOrPrecision === 'number' || keyOrPrecision === undefined) { + return ceilNumberCollection.call(this, keyOrPrecision ?? 0) + } + return ceilObjectCollection(keyOrPrecision, precisionOrUndefined ?? 0) + }, + + floor(this: CollectionOperations, keyOrPrecision?: keyof T | number, precisionOrUndefined?: number) { + function floorNumberCollection(this: CollectionOperations, precision: number = 0): CollectionOperations { + const multiplier = 10 ** precision + return collect( + collection.items.map(item => + Math.floor(Number(item) * multiplier) / multiplier + ) + ) + } + + function floorObjectCollection(key: K, precision: number = 0): CollectionOperations { + const multiplier = 10 ** precision + return collect( + collection.items.map(item => ({ + ...item, + [key]: Math.floor(Number(item[key]) * multiplier) / multiplier + })) + ) + } + + if (typeof keyOrPrecision === 'number' || keyOrPrecision === undefined) { + return floorNumberCollection.call(this, keyOrPrecision ?? 0) + } + return floorObjectCollection(keyOrPrecision, precisionOrUndefined ?? 0) + }, + + abs(this: CollectionOperations, key?: keyof T) { + function absNumberCollection(this: CollectionOperations): CollectionOperations { + return collect( + collection.items.map(item => Math.abs(Number(item))) + ) + } + + function absObjectCollection(key: K): CollectionOperations { + return collect( + collection.items.map(item => ({ + ...item, + [key]: Math.abs(Number(item[key])) + })) + ) + } + + if (key === undefined) { + return absNumberCollection.call(this) + } + return absObjectCollection(key) + }, + + clamp(this: CollectionOperations, keyOrMin: keyof T | number, minOrMax: number, maxOrUndefined?: number) { + function clampNumberCollection(this: CollectionOperations, min: number, max: number): CollectionOperations { + return collect( + collection.items.map(item => + Math.min(Math.max(Number(item), min), max) + ) + ) + } + + function clampObjectCollection(key: K, min: number, max: number): CollectionOperations { + return collect( + collection.items.map(item => ({ + ...item, + [key]: Math.min(Math.max(Number(item[key]), min), max) + })) + ) + } + + if (typeof keyOrMin === 'number') { + return clampNumberCollection.call(this, keyOrMin, minOrMax) + } + return clampObjectCollection(keyOrMin, minOrMax, maxOrUndefined!) + }, + + mean(this: CollectionOperations, key?: keyof T) { + return this.avg(key) + }, + avg(key?: keyof T): number { return collection.length ? this.sum(key) / collection.length : 0 }, diff --git a/src/types.ts b/src/types.ts index 9cf1069..5c0b136 100644 --- a/src/types.ts +++ b/src/types.ts @@ -323,6 +323,54 @@ export interface CollectionOperations extends Collection { median: (key?: keyof T) => number | undefined mode: (key?: keyof T) => T | undefined + /** + * Round numeric values to the specified precision + */ + round: { + (this: CollectionOperations, precision?: number): CollectionOperations + (key: K, precision?: number): CollectionOperations + } + + /** + * Ceil numeric values + */ + ceil: { + (this: CollectionOperations, precision?: number): CollectionOperations + (key: K, precision?: number): CollectionOperations + } + + /** + * Floor numeric values + */ + floor: { + (this: CollectionOperations, precision?: number): CollectionOperations + (key: K, precision?: number): CollectionOperations + } + + /** + * Get absolute values + */ + abs: { + (this: CollectionOperations): CollectionOperations + (key: K): CollectionOperations + } + + /** + * Clamp values between min and max + */ + clamp: { + (this: CollectionOperations, min: number, max: number): CollectionOperations + (key: K, min: number, max: number): CollectionOperations + } + + /** + * Calculate mean (alias for avg) + */ + mean: { + (this: CollectionOperations): number + (key: K): number + } + // Advanced Mathematical Operations product: (key?: keyof T) => number standardDeviation: (key?: keyof T) => StandardDeviationResult diff --git a/test/index.test.ts b/test/index.test.ts index dfa1c60..1c3301f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1956,6 +1956,139 @@ describe('Collection Aggregation Methods', () => { }) }) + describe('round()', () => { + it('should round numbers', () => { + const numbers = collect([1.234, 5.678, 9.123]) + expect(numbers.round().toArray()).toEqual([1, 6, 9]) + expect(numbers.round(2).toArray()).toEqual([1.23, 5.68, 9.12]) + }) + + it('should round by key', () => { + const items = collect([ + { value: 1.234 }, + { value: 5.678 }, + { value: 9.123 }, + ]) + expect(items.round('value', 2).toArray()).toEqual([ + { value: 1.23 }, + { value: 5.68 }, + { value: 9.12 }, + ]) + }) + }) + + describe('ceil()', () => { + it('should ceil numbers', () => { + const numbers = collect([1.234, 5.678, 9.123]) + expect(numbers.ceil().toArray()).toEqual([2, 6, 10]) + expect(numbers.ceil(2).toArray()).toEqual([1.24, 5.68, 9.13]) + }) + + it('should ceil by key', () => { + const items = collect([ + { value: 1.234 }, + { value: 5.678 }, + { value: 9.123 }, + ]) + expect(items.ceil('value', 2).toArray()).toEqual([ + { value: 1.24 }, + { value: 5.68 }, + { value: 9.13 }, + ]) + }) + }) + + describe('floor()', () => { + it('should floor numbers', () => { + const numbers = collect([1.234, 5.678, 9.123]) + expect(numbers.floor().toArray()).toEqual([1, 5, 9]) + expect(numbers.floor(2).toArray()).toEqual([1.23, 5.67, 9.12]) + }) + + it('should floor by key', () => { + const items = collect([ + { value: 1.234 }, + { value: 5.678 }, + { value: 9.123 }, + ]) + expect(items.floor('value', 2).toArray()).toEqual([ + { value: 1.23 }, + { value: 5.67 }, + { value: 9.12 }, + ]) + }) + }) + + describe('abs()', () => { + it('should get absolute values', () => { + const numbers = collect([-1.5, 2.5, -3.5, 4.5]) + expect(numbers.abs().toArray()).toEqual([1.5, 2.5, 3.5, 4.5]) + }) + + it('should get absolute values by key', () => { + const items = collect([ + { value: -1.5 }, + { value: 2.5 }, + { value: -3.5 }, + ]) + expect(items.abs('value').toArray()).toEqual([ + { value: 1.5 }, + { value: 2.5 }, + { value: 3.5 }, + ]) + }) + }) + + describe('clamp()', () => { + it('should clamp numbers', () => { + const numbers = collect([1, 5, 10, 15, 20]) + expect(numbers.clamp(5, 15).toArray()).toEqual([5, 5, 10, 15, 15]) + }) + + it('should clamp by key', () => { + const items = collect([ + { value: 1 }, + { value: 5 }, + { value: 10 }, + { value: 15 }, + { value: 20 }, + ]) + expect(items.clamp('value', 5, 15).toArray()).toEqual([ + { value: 5 }, + { value: 5 }, + { value: 10 }, + { value: 15 }, + { value: 15 }, + ]) + }) + }) + + describe('mean()', () => { + it('should calculate mean of numbers', () => { + const numbers = collect([1, 2, 3, 4, 5]) + expect(numbers.mean()).toBe(3) + }) + + it('should calculate mean by key', () => { + const items = collect([ + { value: 10 }, + { value: 20 }, + { value: 30 }, + ]) + expect(items.mean('value')).toBe(20) + }) + + it('should handle empty collections', () => { + const empty = collect([]) + expect(empty.mean()).toBe(0) + }) + + it('should handle NaN values', () => { + const withNaN = collect([1, Number.NaN, 3, 4, Number.NaN]) + expect(withNaN.mean()).toBe(1.6) // (1 + 0 + 3 + 4 + 0) / 5 + }) + }) + describe('median()', () => { it('should find median of odd length collection', () => { const numbers = collect([1, 2, 3, 4, 5])