Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(collection): optimisations #10552

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
20 changes: 18 additions & 2 deletions packages/collection/__tests__/collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,20 @@ describe('at() tests', () => {
expect(coll.at(0)).toStrictEqual(1);
});

test('positive non-integer index', () => {
expect(coll.at(1.5)).toStrictEqual(2);
});

test('negative index', () => {
expect(coll.at(-1)).toStrictEqual(3);
});

test('negative non-integer index', () => {
expect(coll.at(-2.5)).toStrictEqual(2);
});

test('invalid positive index', () => {
expect(coll.at(4)).toBeUndefined();
expect(coll.at(3)).toBeUndefined();
});

test('invalid negative index', () => {
Expand Down Expand Up @@ -432,12 +440,20 @@ describe('keyAt() tests', () => {
expect(coll.keyAt(0)).toStrictEqual('a');
});

test('positive non-integer index', () => {
expect(coll.keyAt(1.5)).toStrictEqual('b');
});

test('negative index', () => {
expect(coll.keyAt(-1)).toStrictEqual('c');
});

test('negative non-integer index', () => {
expect(coll.keyAt(-2.5)).toStrictEqual('b');
});

test('invalid positive index', () => {
expect(coll.keyAt(4)).toBeUndefined();
expect(coll.keyAt(3)).toBeUndefined();
});

test('invalid negative index', () => {
Expand Down
142 changes: 97 additions & 45 deletions packages/collection/src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,16 @@ export class Collection<Key, Value> extends Map<Key, Value> {
public first(amount?: number): Value | Value[] | undefined {
if (amount === undefined) return this.values().next().value;
if (amount < 0) return this.last(amount * -1);
amount = Math.min(this.size, amount);
if (amount >= this.size) return [...this.values()];

const iter = this.values();
return Array.from({ length: amount }, (): Value => iter.next().value!);
// eslint-disable-next-line unicorn/no-new-array
const results: Value[] = new Array(amount);
for (let index = 0; index < amount; index++) {
results[index] = iter.next().value!;
}

return results;
}

/**
Expand All @@ -102,9 +109,16 @@ export class Collection<Key, Value> extends Map<Key, Value> {
public firstKey(amount?: number): Key | Key[] | undefined {
if (amount === undefined) return this.keys().next().value;
if (amount < 0) return this.lastKey(amount * -1);
amount = Math.min(this.size, amount);
if (amount >= this.size) return [...this.keys()];

const iter = this.keys();
return Array.from({ length: amount }, (): Key => iter.next().value!);
// eslint-disable-next-line unicorn/no-new-array
const results: Key[] = new Array(amount);
for (let index = 0; index < amount; index++) {
results[index] = iter.next().value!;
}

return results;
}

/**
Expand All @@ -117,11 +131,11 @@ export class Collection<Key, Value> extends Map<Key, Value> {
public last(): Value | undefined;
public last(amount: number): Value[];
public last(amount?: number): Value | Value[] | undefined {
const arr = [...this.values()];
if (amount === undefined) return arr[arr.length - 1];
if (amount < 0) return this.first(amount * -1);
if (amount === undefined) return this.at(-1);
if (!amount) return [];
return arr.slice(-amount);
if (amount < 0) return this.first(amount * -1);
const arr = [...this.values()];
return arr.slice(amount * -1);
}

/**
Expand All @@ -134,11 +148,11 @@ export class Collection<Key, Value> extends Map<Key, Value> {
public lastKey(): Key | undefined;
public lastKey(amount: number): Key[];
public lastKey(amount?: number): Key | Key[] | undefined {
const arr = [...this.keys()];
if (amount === undefined) return arr[arr.length - 1];
if (amount < 0) return this.firstKey(amount * -1);
if (amount === undefined) return this.keyAt(-1);
if (!amount) return [];
return arr.slice(-amount);
if (amount < 0) return this.firstKey(amount * -1);
const arr = [...this.keys()];
return arr.slice(amount * -1);
}

/**
Expand All @@ -148,10 +162,21 @@ export class Collection<Key, Value> extends Map<Key, Value> {
*
* @param index - The index of the element to obtain
*/
public at(index: number) {
index = Math.floor(index);
const arr = [...this.values()];
return arr.at(index);
public at(index: number): Value | undefined {
index = Math.trunc(index);
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
Comment on lines +167 to +172
Copy link
Contributor

@Syjalo Syjalo Oct 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better readability

Suggested change
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
if (index < 0) {
index += this.size;
if (index < 0) return undefined;
} else if (index >= this.size) {
return undefined;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think making one addition is faster then multiplication + addition

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's 14 8% faster for negative indexes


const iter = this.values();
for (let skip = 0; skip < index; skip++) {
iter.next();
}

return iter.next().value!;
}

/**
Expand All @@ -161,10 +186,21 @@ export class Collection<Key, Value> extends Map<Key, Value> {
*
* @param index - The index of the key to obtain
*/
public keyAt(index: number) {
index = Math.floor(index);
const arr = [...this.keys()];
return arr.at(index);
public keyAt(index: number): Key | undefined {
index = Math.trunc(index);
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
Comment on lines +191 to +196
Copy link
Contributor

@Syjalo Syjalo Oct 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Suggested change
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
if (index < 0) {
index += this.size;
if (index < 0) return undefined;
} else if (index >= this.size) {
return undefined;
}


const iter = this.keys();
for (let skip = 0; skip < index; skip++) {
iter.next();
}

return iter.next().value!;
}

/**
Expand All @@ -176,13 +212,18 @@ export class Collection<Key, Value> extends Map<Key, Value> {
public random(): Value | undefined;
public random(amount: number): Value[];
public random(amount?: number): Value | Value[] | undefined {
const arr = [...this.values()];
if (amount === undefined) return arr[Math.floor(Math.random() * arr.length)];
if (!arr.length || !amount) return [];
return Array.from(
{ length: Math.min(amount, arr.length) },
(): Value => arr.splice(Math.floor(Math.random() * arr.length), 1)[0]!,
);
if (amount === undefined) return this.at(Math.floor(Math.random() * this.size));
amount = Math.min(this.size, amount);
if (!amount) return [];

const values = [...this.values()];
// eslint-disable-next-line unicorn/no-new-array
const results: Value[] = new Array(amount);
for (let index = 0; index < amount; index++) {
results[index] = values.splice(Math.floor(Math.random() * values.length), 1)[0]!;
}

return results;
}

/**
Expand All @@ -194,13 +235,18 @@ export class Collection<Key, Value> extends Map<Key, Value> {
public randomKey(): Key | undefined;
public randomKey(amount: number): Key[];
public randomKey(amount?: number): Key | Key[] | undefined {
const arr = [...this.keys()];
if (amount === undefined) return arr[Math.floor(Math.random() * arr.length)];
if (!arr.length || !amount) return [];
return Array.from(
{ length: Math.min(amount, arr.length) },
(): Key => arr.splice(Math.floor(Math.random() * arr.length), 1)[0]!,
);
if (amount === undefined) return this.keyAt(Math.floor(Math.random() * this.size));
amount = Math.min(this.size, amount);
if (!amount) return [];

const keys = [...this.keys()];
// eslint-disable-next-line unicorn/no-new-array
const results: Key[] = new Array(amount);
for (let index = 0; index < amount; index++) {
results[index] = keys.splice(Math.floor(Math.random() * keys.length), 1)[0]!;
}

return results;
}

/**
Expand Down Expand Up @@ -511,10 +557,14 @@ export class Collection<Key, Value> extends Map<Key, Value> {
if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`);
if (thisArg !== undefined) fn = fn.bind(thisArg);
const iter = this.entries();
return Array.from({ length: this.size }, (): NewValue => {
// eslint-disable-next-line unicorn/no-new-array
const results: NewValue[] = new Array(this.size);
for (let index = 0; index < this.size; index++) {
const [key, value] = iter.next().value!;
return fn(value, key, this);
});
results[index] = fn(value, key, this);
}

return results;
}

/**
Expand Down Expand Up @@ -959,12 +1009,14 @@ export class Collection<Key, Value> extends Map<Key, Value> {
const hasInSelf = this.has(key);
const hasInOther = other.has(key);

if (hasInSelf && hasInOther) {
const result = whenInBoth(this.get(key)!, other.get(key)!, key);
if (result.keep) coll.set(key, result.value);
} else if (hasInSelf) {
const result = whenInSelf(this.get(key)!, key);
if (result.keep) coll.set(key, result.value);
if (hasInSelf) {
if (hasInOther) {
const result = whenInBoth(this.get(key)!, other.get(key)!, key);
if (result.keep) coll.set(key, result.value);
} else {
const result = whenInSelf(this.get(key)!, key);
if (result.keep) coll.set(key, result.value);
}
} else if (hasInOther) {
const result = whenInOther(other.get(key)!, key);
if (result.keep) coll.set(key, result.value);
Expand Down Expand Up @@ -995,8 +1047,8 @@ export class Collection<Key, Value> extends Map<Key, Value> {
* collection.sorted((userA, userB) => userA.createdTimestamp - userB.createdTimestamp);
* ```
*/
public toSorted(compareFunction: Comparator<Key, Value> = Collection.defaultSort) {
return new this.constructor[Symbol.species](this).sort((av, bv, ak, bk) => compareFunction(av, bv, ak, bk));
public toSorted(compareFunction: Comparator<Key, Value> = Collection.defaultSort): Collection<Key, Value> {
return new this.constructor[Symbol.species](this).sort(compareFunction);
}

public toJSON() {
Expand Down