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

stress: utils like lodash / underscore , but smaller #63

Merged
merged 6 commits into from
Apr 20, 2024
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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

---

Common TypeScript code I used in multiple app.
Simply and generic TypeScript utils.

![Check](https://github.com/jokester/ts-commonutil/workflows/Check/badge.svg)
[![codecov](https://codecov.io/gh/jokester/ts-commonutil/graph/badge.svg?token=95f53H027x)](https://codecov.io/gh/jokester/ts-commonutil)
[![npm version](https://badge.fury.io/js/%40jokester%2Fts-commonutil.svg)](https://badge.fury.io/js/%40jokester%2Fts-commonutil)

## How to Use

```
yarn add @jokester/ts-commonutil
install `@jokester/ts-commonutil` from NPM.

```
## Not included but my go-to libraries

- LRU: [lru-cache](https://www.npmjs.com/package/lru-cache)
- fp: [fp-ts](https://www.npmjs.com/package/fp-ts)
- React hooks
- [foxact](https://foxact.skk.moe/)

## Content

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions src/collection/default-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@
if (!this.has(k)) {
this.set(k, this.createDefault(k));
}
return this.get(k)!;

Check warning on line 13 in src/collection/default-map.ts

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion
}
}

export class WeakDefaultMap<K extends object, V> extends WeakMap<K, V> {
constructor(
private readonly createDefault: (k: K) => V,
entries?: readonly [K, V][],

Check warning on line 20 in src/collection/default-map.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/default-map.ts#L19-L20

Added lines #L19 - L20 were not covered by tests
) {
super(entries);

Check warning on line 22 in src/collection/default-map.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/default-map.ts#L22

Added line #L22 was not covered by tests
}

getOrCreate(k: K): V {

Check warning on line 25 in src/collection/default-map.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/default-map.ts#L25

Added line #L25 was not covered by tests
if (!this.has(k)) {
this.set(k, this.createDefault(k));

Check warning on line 27 in src/collection/default-map.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/default-map.ts#L27

Added line #L27 was not covered by tests
}
return this.get(k)!;

Check warning on line 29 in src/collection/default-map.ts

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion

Check warning on line 29 in src/collection/default-map.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/default-map.ts#L29

Added line #L29 was not covered by tests
}
}
8 changes: 8 additions & 0 deletions src/collection/iterables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,11 @@

type GeneralIterable<T> = Iterable<T> | AsyncIterable<T>;
type MaybePromise<T> = T | PromiseLike<T>;

export function toMap<T, K>(items: Iterable<T>, keyer: (t: T) => K): Map<K, T> {
const ret = new Map<K, T>();
for (const i of items) {
ret.set(keyer(i), i);

Check warning on line 96 in src/collection/iterables.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/iterables.ts#L93-L96

Added lines #L93 - L96 were not covered by tests
}
return ret;

Check warning on line 98 in src/collection/iterables.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/iterables.ts#L98

Added line #L98 was not covered by tests
}
11 changes: 0 additions & 11 deletions src/collection/maps.ts

This file was deleted.

20 changes: 10 additions & 10 deletions src/collection/min-heap.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MinHeap } from './min-heap';
import { NumericOrder } from '../algebra/total-ordered';
import * as fpts from 'fp-ts';

describe('MinHeap', () => {
it('insert elements', () => {
const testee = new MinHeap(NumericOrder);
const testee = new MinHeap(fpts.number.Ord);
expect(testee.slice()).toEqual([]);

expect(testee.insert(5).slice()).toEqual([5]);
Expand All @@ -16,7 +16,7 @@ describe('MinHeap', () => {
});

it('removes element', () => {
const testee = new MinHeap(NumericOrder).insertMany(5, 2, 1, 6, 0);
const testee = new MinHeap(fpts.number.Ord).insertMany(5, 2, 1, 6, 0);

expect(testee.remove()).toEqual(0);
expect(testee.slice()).toEqual([1, 5, 2, 6]);
Expand All @@ -30,15 +30,15 @@ describe('MinHeap', () => {
});

it('removes element - 2', () => {
const testee = new MinHeap(NumericOrder).insert(0).insert(2).insert(3).insert(100).insert(200).insert(4);
const testee = new MinHeap(fpts.number.Ord).insert(0).insert(2).insert(3).insert(100).insert(200).insert(4);

expect(testee.slice()).toEqual([0, 2, 3, 100, 200, 4]);
expect(testee.remove()).toEqual(0);
expect(testee.slice()).toEqual([2, 4, 3, 100, 200]);
});

it('throws when remove from or peek an empty && strict heap', () => {
const testee = new MinHeap(NumericOrder, true);
const testee = new MinHeap(fpts.number.Ord, true);
expect(testee.slice()).toEqual([]);

expect(() => testee.remove()).toThrow(/nothing to remove/);
Expand All @@ -47,25 +47,25 @@ describe('MinHeap', () => {
});

it('can be initialized with initialTree', () => {
const testee = new MinHeap(NumericOrder, false, /* illegal tree */ [1, 0]);
const testee = new MinHeap(fpts.number.Ord, false, /* illegal tree */ [1, 0]);

expect(() => new MinHeap(NumericOrder, true, [1, 0])).toThrow(/assertInvariants/);
expect(() => new MinHeap(fpts.number.Ord, true, [1, 0])).toThrow(/assertInvariants/);
});

it('can be cloned', () => {
const testee = new MinHeap(NumericOrder, false, /* illegal tree */ [1, 0]);
const testee = new MinHeap(fpts.number.Ord, false, /* illegal tree */ [1, 0]);

expect(testee.clone().slice()).toEqual([1, 0]);
});

it('can be shrinked', () => {
const testee = new MinHeap(NumericOrder).insertMany(5, 4, 3, 2, 1, -1);
const testee = new MinHeap(fpts.number.Ord).insertMany(5, 4, 3, 2, 1, -1);

expect(testee.shrink(3).removeMany(3)).toEqual([-1, 1, 2]);
});

it('can shrink to a given upperlimit', () => {
const testee = new MinHeap(NumericOrder).insertMany(5, 4, 3, 2, 1, -1);
const testee = new MinHeap(fpts.number.Ord).insertMany(5, 4, 3, 2, 1, -1);

expect(testee.clone().shrinkUntil(4).removeMany(100)).toEqual([-1, 1, 2, 3]);

Expand Down
21 changes: 13 additions & 8 deletions src/collection/min-heap.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { TotalOrdered } from '../algebra/total-ordered';
import { positions } from './btree';
import { Ord } from 'fp-ts/Ord';

function isBefore<T>(ord: Ord<T>, a: T, b: T): boolean {
return ord.compare(a, b) < 0;
}

export class MinHeap<T> {
private readonly tree: T[] = [];
slice = this.tree.slice.bind(this.tree);

constructor(
private readonly order: TotalOrdered<T>,
private readonly order: Ord<T>,
private readonly strict = false,
initialTree?: T[],
) {
Expand All @@ -27,7 +31,8 @@
j = positions.parent(i);
this.tree[i] = value;

while (i && this.order.before(value /* i.e. this.tree[i] */, this.tree[j])) {
while (i && isBefore(this.order, value /* i.e. this.tree[i] */, this.tree[j])) {
// pop up new value
this.tree[i] = this.tree[j];
this.tree[j] = value;

Expand All @@ -47,7 +52,7 @@
} else {
const ret = this.tree[0];

const v = (this.tree[0] = this.tree.pop()!);

Check warning on line 55 in src/collection/min-heap.ts

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion

for (let i = 0; i < this.tree.length; ) {
const l = positions.leftChild(i),
Expand All @@ -61,20 +66,20 @@
*/
if (r < this.tree.length) {
// when it has 2 children
if (this.order.before(this.tree[l], this.tree[r]) && this.order.before(this.tree[l], v)) {
if (isBefore(this.order, this.tree[l], this.tree[r]) && isBefore(this.order, this.tree[l], v)) {
// swap [i] and [l] and continue
this.tree[i] = this.tree[l];
this.tree[l] = v;
i = l;
} else if (this.order.before(this.tree[r], this.tree[l]) && this.order.before(this.tree[r], v)) {
} else if (isBefore(this.order, this.tree[r], this.tree[l]) && isBefore(this.order, this.tree[r], v)) {
// swap [i] and [l] and continue
this.tree[i] = this.tree[r];
this.tree[r] = v;
i = r;
} else {
break; // v is already before all children
}
} else if (l < this.tree.length && this.order.before(this.tree[l], v)) {
} else if (l < this.tree.length && isBefore(this.order, this.tree[l], v)) {
this.tree[i] = this.tree[l];
this.tree[l] = v;
break; // there cannot be next level
Expand All @@ -94,7 +99,7 @@
removeMany(count: number): T[] {
const ret: T[] = [];
for (let i = 0; i < count && (this.strict /* to let remove() throw */ || this.tree.length); i++) {
ret[i] = this.remove()!;

Check warning on line 102 in src/collection/min-heap.ts

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion
}

return ret;
Expand All @@ -119,9 +124,9 @@
const afterShrink: T[] = [];
while (
this.tree.length &&
(this.order.before(this.tree[0], v) || (inclusive && this.order.equal(this.tree[0], v)))
(isBefore(this.order, this.tree[0], v) || (inclusive && !this.order.compare(this.tree[0], v)))
) {
afterShrink.push(this.remove()!);

Check warning on line 129 in src/collection/min-heap.ts

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion
}
this.tree.splice(0, this.tree.length, ...afterShrink);
return this;
Expand All @@ -130,7 +135,7 @@
private assertInvariants() {
for (let i = 1; i < this.tree.length; i++) {
const p = positions.parent(i);
if (!this.order.before(this.tree[p], this.tree[i])) {
if (!isBefore(this.order, this.tree[p], this.tree[i])) {
throw new Error(`MinHeap#assertInvariants(): expected this.tree[${p} to be ordered before this.tree[${i}]`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/collection/multiset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ describe(Multiset, () => {
testee.setCount('abc', 0);
testee.setCount('abc', 1);
testee.setCount('abc', 1);
testee.setCount('abc', 0, false);
expect(testee.maxCount()).toEqual(1);
testee.setCount('abc', 0);
expect(testee.maxCount()).toBe(1);
expect(testee.getCount('abc')).toEqual(0);
expect(testee.getCount('abd')).toEqual(1);
expect(testee.findByCount(0)).toEqual(['abc']);
Expand Down
4 changes: 2 additions & 2 deletions src/collection/multiset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
private map = new DefaultMap</* count */ number, /* objects */ Set<T>>((k) => new Set());
private countMap = new Map</* object*/ T, /* count */ number>();

setCount(obj: T, count: number, removeOnZeroFreq = true): void {
setCount(obj: T, count: number): void {
const existedCount = this.countMap.get(obj);

if (existedCount === count) {
Expand All @@ -14,9 +14,9 @@
this.countMap.set(obj, count);
this.map.getOrCreate(count).add(obj);

const existedSet = this.map.get(existedCount)!;

Check warning on line 17 in src/collection/multiset.ts

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion
existedSet.delete(obj);
if (!existedSet.size && removeOnZeroFreq) {
if (!existedSet.size) {
this.map.delete(existedCount);
}
} else {
Expand All @@ -39,7 +39,7 @@
const count = this.countMap.get(obj);
if (count !== undefined) {
this.countMap.delete(obj);
const s = this.map.get(count)!;

Check warning on line 42 in src/collection/multiset.ts

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion
s.delete(obj);
if (!s.size) this.map.delete(count);
}
Expand Down
4 changes: 2 additions & 2 deletions src/collection/segment-tree.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Monoid } from '../algebra/monoid';
import { Monoid } from 'fp-ts/lib/Monoid';
import { positions, powOf2 } from './btree';

/**
Expand Down Expand Up @@ -35,7 +35,7 @@
const sums: T[] = (this.sums = []);

for (let i = 0; i < lenSums; i++) {
sums[i] = this.monoid.id;
sums[i] = this.monoid.empty;

Check warning on line 38 in src/collection/segment-tree.ts

View check run for this annotation

Codecov / codecov/patch

src/collection/segment-tree.ts#L38

Added line #L38 was not covered by tests
}

// FIXME: set
Expand Down
2 changes: 1 addition & 1 deletion src/concurrency/resource-pool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe(ResourcePool.name, () => {
testee.use(() => wait(0.5e3));
const becameEmpty = await testee.wait({ freeCount: 2 });
expect(becameEmpty).toBeTruthy();
expect(Date.now() - start).toBeGreaterThan(0.5e3);
expect(Date.now() - start).toBeGreaterThanOrEqual(0.5e3);
});

it('returns false when other tasks running', async () => {
Expand Down
23 changes: 23 additions & 0 deletions src/stress/chunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function* chunk<T>(elements: Iterable<T>, chunkSize: number): Iterable<T[]> {
let currentChunk: T[] = [];
for (const e of elements) {
currentChunk.push(e);

Check warning on line 4 in src/stress/chunk.ts

View check run for this annotation

Codecov / codecov/patch

src/stress/chunk.ts#L1-L4

Added lines #L1 - L4 were not covered by tests
if (currentChunk.length >= chunkSize) {
yield currentChunk;
currentChunk = [];

Check warning on line 7 in src/stress/chunk.ts

View check run for this annotation

Codecov / codecov/patch

src/stress/chunk.ts#L6-L7

Added lines #L6 - L7 were not covered by tests
}
}
if (currentChunk.length) {
yield currentChunk;

Check warning on line 11 in src/stress/chunk.ts

View check run for this annotation

Codecov / codecov/patch

src/stress/chunk.ts#L11

Added line #L11 was not covered by tests
}
}

export function* chunkArray<T>(elements: readonly T[], chunkSize: number): Iterable<T[]> {
for (let s = 0; ; s += chunkSize) {
const chunk = elements.slice(s, s + chunkSize);

Check warning on line 17 in src/stress/chunk.ts

View check run for this annotation

Codecov / codecov/patch

src/stress/chunk.ts#L15-L17

Added lines #L15 - L17 were not covered by tests
if (!chunk.length) {
break;

Check warning on line 19 in src/stress/chunk.ts

View check run for this annotation

Codecov / codecov/patch

src/stress/chunk.ts#L19

Added line #L19 was not covered by tests
}
yield chunk;

Check warning on line 21 in src/stress/chunk.ts

View check run for this annotation

Codecov / codecov/patch

src/stress/chunk.ts#L21

Added line #L21 was not covered by tests
}
}
11 changes: 11 additions & 0 deletions src/stress/groupBy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { groupBy } from './groupBy';

describe(groupBy, () => {
it('groups value with provided keyer()', () => {
expect(groupBy([], () => 0)).toEqual({});
expect(groupBy(new Set([1, 2, 4]), (v) => v % 2)).toEqual({
1: [1],
0: [2, 4],
});
});
});
16 changes: 16 additions & 0 deletions src/stress/groupBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DefaultMap } from '../collection/default-map';

export function groupBy<T, K extends string | number | symbol>(
values: Iterable<T>,
keyer: (value: T) => K,
): Record<K, T[]> {
return Object.fromEntries(groupByAsMap(values, keyer)) as Record<K, T[]>;
}

export function groupByAsMap<T, K>(values: Iterable<T>, keyer: (value: T) => K): ReadonlyMap<K, T[]> {
const map = new DefaultMap<K, T[]>(() => []);
for (const v of values) {
map.getOrCreate(keyer(v)).push(v);
}
return map;
}
12 changes: 12 additions & 0 deletions src/stress/sortBy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { sortBy } from './sortBy';

describe('orderBy', () => {
it('sorts value DESC by "natural" JS ordering', () => {
expect(sortBy([2, 3, 1], (v) => v, false)).toEqual([3, 2, 1]);
expect(sortBy([1, 1, 2], (v) => v, false)).toEqual([2, 1, 1]);
});
it('sorts value ASC by "natural" JS ordering', () => {
expect(sortBy([2, 3, 1], (v) => v)).toEqual([1, 2, 3]);
expect(sortBy([1, 1, 2], (v) => v)).toEqual([1, 1, 2]);
});
});
18 changes: 18 additions & 0 deletions src/stress/sortBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* (for more complicated case, consider fp-ts Ord<T> typeclass)
* @param values
* @param key (if it should be cached, caller should use a cached impl)
* @param asc
*/
export function sortBy<T, O>(values: T[], key: (v: T) => O, asc = true): T[] {
const indexes = values.map((v, i) => ({ v, i }));
return indexes.sort((a, b) => (asc ? compare(a.v, b.v) : -compare(a.v, b.v))).map((_) => _.v);
}

function compare(a: any, b: any): number {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else return 0;
}
Loading
Loading