Skip to content

Commit 8b2f0b3

Browse files
committed
Add removeMatch() and updateMatch()
1 parent 89db2cd commit 8b2f0b3

9 files changed

+243
-3
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 1.1.0 (2023-12-13)
4+
5+
- Add: `removeMatch()` removes some items matching the given predicate. It's a
6+
combination of `[].findIndex()` and `remove()` or `[].filter()` with inversion
7+
and limit.
8+
- Add: `updateMatch()` updates some items matching the given predicate. It's a
9+
combination of `[].map()` with limit.
10+
311
## 1.0.0 (2022-06-25)
412

513
- Initial release.

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,38 @@ remove(input, 1, 2); // => [10, 40]
163163
remove(input, 1, 10); // => [10]
164164
```
165165

166+
### `removeMatch()`
167+
168+
```ts
169+
removeMatch<T>(
170+
array: readonly T[],
171+
removePredicate: (value: T, index: number) => unknown,
172+
limit: number = 1,
173+
): readonly T[]
174+
```
175+
176+
Remove items matching the given predicate
177+
178+
Creates new array from the input `array` by *omitting* items *matching*
179+
predicate `removePredicate`. This is opposite to `Array.prototype.filter()`.
180+
181+
Positive `limit` (defaults to `1`) will omit at most that number of items.
182+
Negative `limit` means "no limit".
183+
184+
- Will return input `array` when it's nothing to change.
185+
186+
```js
187+
const input = [10, 20, 30, 40];
188+
removeMatch(input, (v) => v % 20 === 0);
189+
// => [10, 30, 40]
190+
removeMatch(input, (v) => v % 10 === 0, 2);
191+
// => [30, 40]
192+
removeMatch(input, (v) => v % 2);
193+
// => [10, 20, 30, 40]
194+
removeMatch(input, (v) => v % 2 === 0, -1);
195+
// => []
196+
```
197+
166198
### `set()`
167199

168200
```ts
@@ -235,3 +267,35 @@ const input = [10, 20, 30, 40];
235267
update(input, 2, (v, i) => 1000 * i + v);
236268
// => [10, 20, 2030, 40]
237269
```
270+
271+
### `updateMatch()`
272+
273+
```ts
274+
update(
275+
array: readonly T[],
276+
predicate: (value: T, index: number) => T,
277+
updater: (prev: T, index: number) => T,
278+
limit: number = 1,
279+
): readonly T[]
280+
```
281+
282+
Creates new array from input `array` with some item matching `predicate` (at
283+
most `limit`) updated by `updater`.
284+
285+
Callbacks `predicate(value, index)` and `updater(prev, index)` will receive item
286+
value and index. The `predicate` returns whether the given item need be updated.
287+
The `updater` will be called for those items matched by `predicated` until
288+
`limit` exceeds.
289+
290+
Positive `limit` (defaults to `1`) will update at most that number of items.
291+
Negative `limit` means "no limit".
292+
293+
- `limit` applies to `predicate` only without care if some previous item was
294+
actually changed by `updater`.
295+
- Will return input `array` when nothing to change (`===` check).
296+
297+
```js
298+
const input = [10, 20, 30, 40];
299+
updateMatch(input, v => v % 20 === 0, (v, i) => 1000 * i + v);
300+
// => [10, 1020, 30, 40]
301+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cubux/readonly-array",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Functions to work with read-only arrays",
55
"keywords": [
66
"array",

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export { default as append } from './append';
22
export { default as insert } from './insert';
33
export { default as remove } from './remove';
4+
export { default as removeMatch } from './removeMatch';
45
export { default as set } from './set';
56
export { default as swap } from './swap';
67
export { default as update } from './update';
8+
export { default as updateMatch } from './updateMatch';

src/removeMatch.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Remove items matching the given predicate
3+
*
4+
* Creates new array from the input `array` by *omitting* items *matching*
5+
* predicate `removePredicate`. This is opposite to `Array.prototype.filter()`.
6+
*
7+
* Positive `limit` (defaults to `1`) will omit at most that number of items.
8+
* Negative `limit` means "no limit".
9+
*
10+
* - Will return input `array` when it's nothing to change.
11+
*
12+
* ```js
13+
* const input = [10, 20, 30, 40];
14+
* removeMatch(input, (v) => v % 20 === 0);
15+
* // => [10, 30, 40]
16+
* removeMatch(input, (v) => v % 10 === 0, 2);
17+
* // => [30, 40]
18+
* removeMatch(input, (v) => v % 2);
19+
* // => [10, 20, 30, 40]
20+
* removeMatch(input, (v) => v % 2 === 0, -1);
21+
* // => []
22+
* ```
23+
*
24+
* @param array Source array
25+
* @param removePredicate A callback to check whether the item need be removed
26+
* @param limit Limit number of items to remove
27+
*/
28+
function removeMatch<T>(
29+
array: readonly T[],
30+
removePredicate: (value: T, index: number) => unknown,
31+
limit = 1,
32+
): readonly T[] {
33+
if (limit === 0) {
34+
return array;
35+
}
36+
const next: T[] = [];
37+
for (let i = 0, L = array.length; i < L; i++) {
38+
// here only previously positive became can become 0
39+
if (limit === 0) {
40+
// add all the rest and done
41+
next.push(...array.slice(i));
42+
break;
43+
}
44+
45+
const value = array[i];
46+
// if (limit < 0) t.i. no limit
47+
// or (limit > 0) still
48+
// positive limit will not reach 0 here
49+
if (removePredicate(value, i)) {
50+
// skip current item
51+
// and reduce positive limit until 0
52+
if (limit > 0) {
53+
limit--;
54+
}
55+
} else {
56+
next.push(value);
57+
}
58+
}
59+
return next.length === array.length ? array : next;
60+
}
61+
62+
export default removeMatch;

src/updateMatch.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Update some matching items with a callback
3+
*
4+
* Creates new array from input `array` with some item matching `predicate` (at
5+
* most `limit`) updated by `updater`.
6+
*
7+
* Callbacks `predicate(value, index)` and `updater(prev, index)` will receive
8+
* item value and index. The `predicate` returns whether the given item need be
9+
* updated. The `updater` will be called for those items matched by `predicated`
10+
* until `limit` exceeds.
11+
*
12+
* Positive `limit` (defaults to `1`) will update at most that number of items.
13+
* Negative `limit` means "no limit".
14+
*
15+
* - `limit` applies to `predicate` only without care if some previous item was
16+
* actually changed by `updater`.
17+
* - Will return input `array` when nothing to change (`===` check).
18+
*
19+
* @param array Source array
20+
* @param predicate Callback to check whether the item need be updated
21+
* @param updater Callback to calculate new value
22+
* @param limit Limit number of items to remove
23+
*/
24+
function updateMatch<T>(
25+
array: readonly T[],
26+
predicate: (value: T, index: number) => unknown,
27+
updater: (prev: T, index: number) => T,
28+
limit = 1,
29+
): readonly T[] {
30+
if (limit === 0) {
31+
return array;
32+
}
33+
const next = [...array];
34+
let changed = false;
35+
for (let i = 0, L = array.length; i < L; i++) {
36+
// here only previously positive became can become 0
37+
if (limit === 0) {
38+
// done
39+
break;
40+
}
41+
42+
const value = array[i];
43+
if (predicate(value, i)) {
44+
const nextItem = updater(value, i);
45+
if (nextItem !== value) {
46+
next[i] = nextItem;
47+
changed = true;
48+
}
49+
if (limit > 0) {
50+
limit--;
51+
}
52+
}
53+
}
54+
return changed ? next : array;
55+
}
56+
57+
export default updateMatch;

test/index.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1-
import { append, insert, remove, set, swap, update } from '../src';
1+
import {
2+
append,
3+
insert,
4+
remove,
5+
removeMatch,
6+
set,
7+
swap,
8+
update,
9+
updateMatch,
10+
} from '../src';
211

312
it('direct', () => {
413
expect(append([10, 20], 30, 40, 50)).toEqual([10, 20, 30, 40, 50]);
514
expect(insert([10, 20, 30], 2, 40, 50)).toEqual([10, 20, 40, 50, 30]);
615
expect(remove([10, 20, 30], 1)).toEqual([10, 30]);
7-
expect(remove([10, 20, 30], 1)).toEqual([10, 30]);
16+
expect(removeMatch([10, 20, 40], n => n % 20 === 0)).toEqual([10, 40]);
817
expect(set([10, 20, 30, 40], 2, 42)).toEqual([10, 20, 42, 40]);
918
expect(swap([10, 20, 30, 40], 0, 2)).toEqual([30, 20, 10, 40]);
1019
expect(update([10, 20, 30], 1, n => -n)).toEqual([10, -20, 30]);
20+
expect(
21+
updateMatch(
22+
[2, 3, 5],
23+
n => n % 2,
24+
n => -n,
25+
),
26+
).toEqual([2, -3, 5]);
1127
});

test/removeMatch.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import removeMatch from '../src/removeMatch';
2+
3+
it('removeMatch', () => {
4+
const input = [10, 20, 30, 40] as const;
5+
6+
expect(removeMatch(input, v => v % 20 === 0)).toEqual([10, 30, 40]);
7+
expect(removeMatch(input, v => v % 20 === 0, -1)).toEqual([10, 30]);
8+
expect(removeMatch(input, v => v % 10 === 0, 2)).toEqual([30, 40]);
9+
expect(removeMatch(input, v => v % 2)).toBe(input);
10+
expect(removeMatch(input, v => v % 2 === 0, -1)).toEqual([]);
11+
12+
expect(removeMatch(input, v => v % 2, 0)).toBe(input);
13+
14+
expect(input).toEqual([10, 20, 30, 40]);
15+
});

test/updateMatch.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import updateMatch from '../src/updateMatch';
2+
3+
it('updateMatch', () => {
4+
const a = [10, 20, 30, 40] as const;
5+
const inc = (n: number) => n + 1;
6+
7+
expect(updateMatch(a, v => v % 20 === 0, inc)).toEqual([10, 21, 30, 40]);
8+
expect(updateMatch(a, v => v % 20 === 0, inc, -1)).toEqual([10, 21, 30, 41]);
9+
expect(updateMatch(a, v => v % 10 === 0, inc, 2)).toEqual([11, 21, 30, 40]);
10+
expect(updateMatch(a, v => v % 2, inc)).toBe(a);
11+
expect(updateMatch(a, v => v % 2 === 0, inc, -1)).toEqual([11, 21, 31, 41]);
12+
13+
expect(updateMatch(a, v => v % 2, inc, 0)).toBe(a);
14+
15+
expect(a).toEqual([10, 20, 30, 40]);
16+
});

0 commit comments

Comments
 (0)