rambda 7.1.4 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -3
- package/README.md +246 -279
- package/dist/rambda.js +110 -56
- package/dist/rambda.mjs +107 -57
- package/dist/rambda.umd.js +1 -1
- package/immutable.d.ts +34 -15
- package/index.d.ts +34 -15
- package/package.json +17 -13
- package/src/map.js +3 -0
- package/src/modifyPath.js +33 -0
- package/src/propEq.js +3 -1
- package/src/reduce.js +10 -0
- package/src/uniqBy.js +13 -0
- package/dist/rambda.esm.js +0 -2295
package/README.md
CHANGED
|
@@ -98,8 +98,6 @@ R.pick('a,b', {a: 1 , b: 2, c: 3} })
|
|
|
98
98
|
|
|
99
99
|
**Rambda** is generally more performant than `Ramda` as the [benchmarks](#-benchmarks) can prove that.
|
|
100
100
|
|
|
101
|
-
### Deno
|
|
102
|
-
|
|
103
101
|
### Support
|
|
104
102
|
|
|
105
103
|
As the library is smaller than Ramda, issues are much faster resolved.
|
|
@@ -112,7 +110,7 @@ Closing the issue is usually accompanied by publishing a new patch version of `R
|
|
|
112
110
|
|
|
113
111
|
<details>
|
|
114
112
|
<summary>
|
|
115
|
-
Click to see the full list of
|
|
113
|
+
Click to see the full list of 78 Ramda methods not implemented in Rambda
|
|
116
114
|
</summary>
|
|
117
115
|
|
|
118
116
|
- __
|
|
@@ -157,7 +155,6 @@ Closing the issue is usually accompanied by publishing a new patch version of `R
|
|
|
157
155
|
- mergeDeepWithKey
|
|
158
156
|
- mergeWithKey
|
|
159
157
|
- modify
|
|
160
|
-
- modifyPath
|
|
161
158
|
- nAry
|
|
162
159
|
- nthArg
|
|
163
160
|
- o
|
|
@@ -187,7 +184,6 @@ Closing the issue is usually accompanied by publishing a new patch version of `R
|
|
|
187
184
|
- uncurryN
|
|
188
185
|
- unfold
|
|
189
186
|
- unionWith
|
|
190
|
-
- uniqBy
|
|
191
187
|
- unnest
|
|
192
188
|
- until
|
|
193
189
|
- useWith
|
|
@@ -261,8 +257,8 @@ method | Rambda | Ramda | Lodash
|
|
|
261
257
|
--- |--- | --- | ---
|
|
262
258
|
*add* | 🚀 Fastest | 21.52% slower | 82.15% slower
|
|
263
259
|
*adjust* | 8.48% slower | 🚀 Fastest | 🔳
|
|
264
|
-
*all* | 🚀 Fastest |
|
|
265
|
-
*allPass* | 🚀 Fastest |
|
|
260
|
+
*all* | 🚀 Fastest | 1.81% slower | 🔳
|
|
261
|
+
*allPass* | 🚀 Fastest | 91.09% slower | 🔳
|
|
266
262
|
*allPass* | 🚀 Fastest | 98.56% slower | 🔳
|
|
267
263
|
*and* | 🚀 Fastest | 89.09% slower | 🔳
|
|
268
264
|
*any* | 🚀 Fastest | 92.87% slower | 45.82% slower
|
|
@@ -271,7 +267,7 @@ method | Rambda | Ramda | Lodash
|
|
|
271
267
|
*applySpec* | 🚀 Fastest | 80.43% slower | 🔳
|
|
272
268
|
*assoc* | 72.32% slower | 60.08% slower | 🚀 Fastest
|
|
273
269
|
*clone* | 🚀 Fastest | 91.86% slower | 86.48% slower
|
|
274
|
-
*compose* |
|
|
270
|
+
*compose* | 🚀 Fastest | 32.45% slower | 13.68% slower
|
|
275
271
|
*converge* | 78.63% slower | 🚀 Fastest | 🔳
|
|
276
272
|
*curry* | 🚀 Fastest | 28.86% slower | 🔳
|
|
277
273
|
*curryN* | 🚀 Fastest | 41.05% slower | 🔳
|
|
@@ -284,8 +280,8 @@ method | Rambda | Ramda | Lodash
|
|
|
284
280
|
*findIndex* | 🚀 Fastest | 86.48% slower | 72.27% slower
|
|
285
281
|
*flatten* | 🚀 Fastest | 95.26% slower | 10.27% slower
|
|
286
282
|
*ifElse* | 🚀 Fastest | 58.56% slower | 🔳
|
|
287
|
-
*includes* | 🚀 Fastest | 84.
|
|
288
|
-
*indexOf* | 🚀 Fastest |
|
|
283
|
+
*includes* | 🚀 Fastest | 84.63% slower | 🔳
|
|
284
|
+
*indexOf* | 🚀 Fastest | 76.63% slower | 🔳
|
|
289
285
|
*indexOf* | 🚀 Fastest | 82.2% slower | 🔳
|
|
290
286
|
*init* | 🚀 Fastest | 92.24% slower | 13.3% slower
|
|
291
287
|
*is* | 🚀 Fastest | 57.69% slower | 🔳
|
|
@@ -301,7 +297,7 @@ method | Rambda | Ramda | Lodash
|
|
|
301
297
|
*over* | 🚀 Fastest | 56.23% slower | 🔳
|
|
302
298
|
*path* | 37.81% slower | 77.81% slower | 🚀 Fastest
|
|
303
299
|
*pick* | 🚀 Fastest | 19.07% slower | 80.2% slower
|
|
304
|
-
*pipe* | 0.
|
|
300
|
+
*pipe* | 0.87% slower | 🚀 Fastest | 🔳
|
|
305
301
|
*prop* | 🚀 Fastest | 87.95% slower | 🔳
|
|
306
302
|
*propEq* | 🚀 Fastest | 91.92% slower | 🔳
|
|
307
303
|
*range* | 🚀 Fastest | 61.8% slower | 57.44% slower
|
|
@@ -317,8 +313,8 @@ method | Rambda | Ramda | Lodash
|
|
|
317
313
|
*takeLast* | 🚀 Fastest | 93.39% slower | 19.22% slower
|
|
318
314
|
*test* | 🚀 Fastest | 82.34% slower | 🔳
|
|
319
315
|
*type* | 🚀 Fastest | 48.6% slower | 🔳
|
|
320
|
-
*uniq* | 🚀 Fastest |
|
|
321
|
-
*uniqWith* |
|
|
316
|
+
*uniq* | 🚀 Fastest | 90.24% slower | 🔳
|
|
317
|
+
*uniqWith* | 18.09% slower | 🚀 Fastest | 🔳
|
|
322
318
|
*uniqWith* | 14.23% slower | 🚀 Fastest | 🔳
|
|
323
319
|
*update* | 🚀 Fastest | 52.35% slower | 🔳
|
|
324
320
|
*view* | 🚀 Fastest | 76.15% slower | 🔳
|
|
@@ -727,7 +723,7 @@ describe('all', () => {
|
|
|
727
723
|
|
|
728
724
|
<details>
|
|
729
725
|
|
|
730
|
-
<summary>Rambda is faster than Ramda with
|
|
726
|
+
<summary>Rambda is faster than Ramda with 1.81%</summary>
|
|
731
727
|
|
|
732
728
|
```text
|
|
733
729
|
const R = require('../../dist/rambda.js')
|
|
@@ -864,7 +860,7 @@ test('works with multiple inputs', () => {
|
|
|
864
860
|
<summary><strong>Typescript</strong> test</summary>
|
|
865
861
|
|
|
866
862
|
```typescript
|
|
867
|
-
import {allPass} from 'rambda'
|
|
863
|
+
import {allPass, filter} from 'rambda'
|
|
868
864
|
|
|
869
865
|
describe('allPass', () => {
|
|
870
866
|
it('happy', () => {
|
|
@@ -880,6 +876,16 @@ describe('allPass', () => {
|
|
|
880
876
|
|
|
881
877
|
x // $ExpectType boolean
|
|
882
878
|
})
|
|
879
|
+
it('issue #642', () => {
|
|
880
|
+
const isGreater = (num: number) => num > 5;
|
|
881
|
+
const pred = allPass([isGreater]);
|
|
882
|
+
const xs = [0, 1, 2, 3];
|
|
883
|
+
|
|
884
|
+
const filtered1 = filter(pred)(xs);
|
|
885
|
+
filtered1 // $ExpectType number[]
|
|
886
|
+
const filtered2 = xs.filter(pred);
|
|
887
|
+
filtered2 // $ExpectType number[]
|
|
888
|
+
})
|
|
883
889
|
})
|
|
884
890
|
```
|
|
885
891
|
|
|
@@ -887,7 +893,7 @@ describe('allPass', () => {
|
|
|
887
893
|
|
|
888
894
|
<details>
|
|
889
895
|
|
|
890
|
-
<summary>Rambda is faster than Ramda with
|
|
896
|
+
<summary>Rambda is faster than Ramda with 91.09%</summary>
|
|
891
897
|
|
|
892
898
|
```text
|
|
893
899
|
const R = require('../../dist/rambda.js')
|
|
@@ -1156,7 +1162,7 @@ const any = [
|
|
|
1156
1162
|
|
|
1157
1163
|
```typescript
|
|
1158
1164
|
|
|
1159
|
-
anyPass<T>(predicates:
|
|
1165
|
+
anyPass<T>(predicates: ((x: T) => boolean)[]): (input: T) => boolean
|
|
1160
1166
|
```
|
|
1161
1167
|
|
|
1162
1168
|
It accepts list of `predicates` and returns a function. This function with its `input` will return `true`, if any of `predicates` returns `true` for this `input`.
|
|
@@ -1166,7 +1172,7 @@ It accepts list of `predicates` and returns a function. This function with its `
|
|
|
1166
1172
|
<summary>All Typescript definitions</summary>
|
|
1167
1173
|
|
|
1168
1174
|
```typescript
|
|
1169
|
-
anyPass<T>(predicates:
|
|
1175
|
+
anyPass<T>(predicates: ((x: T) => boolean)[]): (input: T) => boolean;
|
|
1170
1176
|
```
|
|
1171
1177
|
|
|
1172
1178
|
</details>
|
|
@@ -1211,7 +1217,6 @@ test('happy', () => {
|
|
|
1211
1217
|
const rules = [x => typeof x === 'string', x => x > 10]
|
|
1212
1218
|
|
|
1213
1219
|
expect(anyPass(rules)(11)).toBeTrue()
|
|
1214
|
-
|
|
1215
1220
|
expect(anyPass(rules)(undefined)).toBeFalse()
|
|
1216
1221
|
})
|
|
1217
1222
|
|
|
@@ -1253,7 +1258,7 @@ test('works with multiple inputs', () => {
|
|
|
1253
1258
|
<summary><strong>Typescript</strong> test</summary>
|
|
1254
1259
|
|
|
1255
1260
|
```typescript
|
|
1256
|
-
import {anyPass} from 'rambda'
|
|
1261
|
+
import {anyPass, filter} from 'rambda'
|
|
1257
1262
|
|
|
1258
1263
|
describe('anyPass', () => {
|
|
1259
1264
|
it('happy', () => {
|
|
@@ -1269,6 +1274,16 @@ describe('anyPass', () => {
|
|
|
1269
1274
|
|
|
1270
1275
|
x // $ExpectType boolean
|
|
1271
1276
|
})
|
|
1277
|
+
it('issue #642', () => {
|
|
1278
|
+
const isGreater = (num: number) => num > 5;
|
|
1279
|
+
const pred = anyPass([isGreater]);
|
|
1280
|
+
const xs = [0, 1, 2, 3];
|
|
1281
|
+
|
|
1282
|
+
const filtered1 = filter(pred)(xs);
|
|
1283
|
+
filtered1 // $ExpectType number[]
|
|
1284
|
+
const filtered2 = xs.filter(pred);
|
|
1285
|
+
filtered2 // $ExpectType number[]
|
|
1286
|
+
})
|
|
1272
1287
|
})
|
|
1273
1288
|
```
|
|
1274
1289
|
|
|
@@ -1522,7 +1537,7 @@ describe('R.apply', () => {
|
|
|
1522
1537
|
|
|
1523
1538
|
```typescript
|
|
1524
1539
|
|
|
1525
|
-
applySpec<Spec extends Record<string,
|
|
1540
|
+
applySpec<Spec extends Record<string, AnyFunction>>(
|
|
1526
1541
|
spec: Spec
|
|
1527
1542
|
): (
|
|
1528
1543
|
...args: Parameters<ValueOfRecord<Spec>>
|
|
@@ -1534,12 +1549,12 @@ applySpec<Spec extends Record<string, (...args: any[]) => any>>(
|
|
|
1534
1549
|
<summary>All Typescript definitions</summary>
|
|
1535
1550
|
|
|
1536
1551
|
```typescript
|
|
1537
|
-
applySpec<Spec extends Record<string,
|
|
1552
|
+
applySpec<Spec extends Record<string, AnyFunction>>(
|
|
1538
1553
|
spec: Spec
|
|
1539
1554
|
): (
|
|
1540
1555
|
...args: Parameters<ValueOfRecord<Spec>>
|
|
1541
1556
|
) => { [Key in keyof Spec]: ReturnType<Spec[Key]> };
|
|
1542
|
-
applySpec<T>(spec: any): (...args:
|
|
1557
|
+
applySpec<T>(spec: any): (...args: unknown[]) => T;
|
|
1543
1558
|
```
|
|
1544
1559
|
|
|
1545
1560
|
</details>
|
|
@@ -2494,7 +2509,7 @@ describe('R.assocPath - curried', () => {
|
|
|
2494
2509
|
|
|
2495
2510
|
```typescript
|
|
2496
2511
|
|
|
2497
|
-
bind<F extends
|
|
2512
|
+
bind<F extends AnyFunction, T>(fn: F, thisObj: T): (...args: Parameters<F>) => ReturnType<F>
|
|
2498
2513
|
```
|
|
2499
2514
|
|
|
2500
2515
|
Creates a function that is bound to a context.
|
|
@@ -2504,8 +2519,8 @@ Creates a function that is bound to a context.
|
|
|
2504
2519
|
<summary>All Typescript definitions</summary>
|
|
2505
2520
|
|
|
2506
2521
|
```typescript
|
|
2507
|
-
bind<F extends
|
|
2508
|
-
bind<F extends
|
|
2522
|
+
bind<F extends AnyFunction, T>(fn: F, thisObj: T): (...args: Parameters<F>) => ReturnType<F>;
|
|
2523
|
+
bind<F extends AnyFunction, T>(fn: F): (thisObj: T) => (...args: Parameters<F>) => ReturnType<F>;
|
|
2509
2524
|
```
|
|
2510
2525
|
|
|
2511
2526
|
</details>
|
|
@@ -6461,7 +6476,11 @@ describe('R.identity', () => {
|
|
|
6461
6476
|
|
|
6462
6477
|
```typescript
|
|
6463
6478
|
|
|
6464
|
-
ifElse<
|
|
6479
|
+
ifElse<T, TFiltered extends T, TOnTrueResult, TOnFalseResult>(
|
|
6480
|
+
pred: (a: T) => a is TFiltered,
|
|
6481
|
+
onTrue: (a: TFiltered) => TOnTrueResult,
|
|
6482
|
+
onFalse: (a: Exclude<T, TFiltered>) => TOnFalseResult,
|
|
6483
|
+
): (a: T) => TOnTrueResult | TOnFalseResult
|
|
6465
6484
|
```
|
|
6466
6485
|
|
|
6467
6486
|
It expects `condition`, `onTrue` and `onFalse` functions as inputs and it returns a new function with example name of `fn`.
|
|
@@ -6473,6 +6492,11 @@ When `fn`` is called with `input` argument, it will return either `onTrue(input)
|
|
|
6473
6492
|
<summary>All Typescript definitions</summary>
|
|
6474
6493
|
|
|
6475
6494
|
```typescript
|
|
6495
|
+
ifElse<T, TFiltered extends T, TOnTrueResult, TOnFalseResult>(
|
|
6496
|
+
pred: (a: T) => a is TFiltered,
|
|
6497
|
+
onTrue: (a: TFiltered) => TOnTrueResult,
|
|
6498
|
+
onFalse: (a: Exclude<T, TFiltered>) => TOnFalseResult,
|
|
6499
|
+
): (a: T) => TOnTrueResult | TOnFalseResult;
|
|
6476
6500
|
ifElse<TArgs extends any[], TOnTrueResult, TOnFalseResult>(fn: (...args: TArgs) => boolean, onTrue: (...args: TArgs) => TOnTrueResult, onFalse: (...args: TArgs) => TOnFalseResult): (...args: TArgs) => TOnTrueResult | TOnFalseResult;
|
|
6477
6501
|
```
|
|
6478
6502
|
|
|
@@ -6624,6 +6648,25 @@ describe('R.ifElse', () => {
|
|
|
6624
6648
|
const result = fn(3, 'hello')
|
|
6625
6649
|
result // $ExpectType string
|
|
6626
6650
|
})
|
|
6651
|
+
test('DefinitelyTyped#59291', () => {
|
|
6652
|
+
const getLengthIfStringElseDouble = ifElse(
|
|
6653
|
+
(a: string | number): a is string => true,
|
|
6654
|
+
a => a.length,
|
|
6655
|
+
a => a * 2
|
|
6656
|
+
)
|
|
6657
|
+
|
|
6658
|
+
getLengthIfStringElseDouble('foo') // $ExpectType number
|
|
6659
|
+
getLengthIfStringElseDouble(3) // $ExpectType number
|
|
6660
|
+
const result = ifElse(
|
|
6661
|
+
(a: {
|
|
6662
|
+
foo?: string,
|
|
6663
|
+
bar: number | string,
|
|
6664
|
+
}): a is {foo: string, bar: string} => true,
|
|
6665
|
+
(a): [string, string] => [a.foo, a.bar],
|
|
6666
|
+
(a): [string | undefined, string | number] => [a.foo, a.bar]
|
|
6667
|
+
)
|
|
6668
|
+
result // $ExpectType (a: { foo?: string | undefined; bar: string | number; }) => [string, string] | [string | undefined, string | number]
|
|
6669
|
+
})
|
|
6627
6670
|
})
|
|
6628
6671
|
```
|
|
6629
6672
|
|
|
@@ -6826,7 +6869,7 @@ describe('R.includes', () => {
|
|
|
6826
6869
|
|
|
6827
6870
|
<details>
|
|
6828
6871
|
|
|
6829
|
-
<summary>Rambda is faster than Ramda with 84.
|
|
6872
|
+
<summary>Rambda is faster than Ramda with 84.63%</summary>
|
|
6830
6873
|
|
|
6831
6874
|
```text
|
|
6832
6875
|
const R = require('../../dist/rambda.js')
|
|
@@ -8458,6 +8501,9 @@ export function mapArray(
|
|
|
8458
8501
|
}
|
|
8459
8502
|
|
|
8460
8503
|
export function mapObject(fn, obj){
|
|
8504
|
+
if (arguments.length === 1){
|
|
8505
|
+
return _obj => mapObject(fn, _obj)
|
|
8506
|
+
}
|
|
8461
8507
|
let index = 0
|
|
8462
8508
|
const keys = _keys(obj)
|
|
8463
8509
|
const len = keys.length
|
|
@@ -9241,6 +9287,11 @@ test('ramda compatible test 3', () => {
|
|
|
9241
9287
|
}
|
|
9242
9288
|
expect(result).toEqual(expected)
|
|
9243
9289
|
})
|
|
9290
|
+
|
|
9291
|
+
test('functions are discarded', () => {
|
|
9292
|
+
const obj = { foo : () => {} }
|
|
9293
|
+
expect(mergeDeepRight(obj, {})).toEqual({})
|
|
9294
|
+
})
|
|
9244
9295
|
```
|
|
9245
9296
|
|
|
9246
9297
|
</details>
|
|
@@ -9490,7 +9541,7 @@ const B = {
|
|
|
9490
9541
|
describe('R.mergeWith', () => {
|
|
9491
9542
|
test('no curry | without explicit types', () => {
|
|
9492
9543
|
const result = mergeWith(concat, A, B)
|
|
9493
|
-
result // $ExpectType
|
|
9544
|
+
result // $ExpectType Record<string, unknown>
|
|
9494
9545
|
})
|
|
9495
9546
|
test('no curry | with explicit types', () => {
|
|
9496
9547
|
const result = mergeWith<Output>(concat, A, B)
|
|
@@ -9498,7 +9549,7 @@ describe('R.mergeWith', () => {
|
|
|
9498
9549
|
})
|
|
9499
9550
|
test('curry 1 | without explicit types', () => {
|
|
9500
9551
|
const result = mergeWith(concat, A)(B)
|
|
9501
|
-
result // $ExpectType
|
|
9552
|
+
result // $ExpectType Record<string, unknown>
|
|
9502
9553
|
})
|
|
9503
9554
|
test('curry 1 | with explicit types', () => {
|
|
9504
9555
|
const result = mergeWith<Output>(concat, A)(B)
|
|
@@ -9506,7 +9557,7 @@ describe('R.mergeWith', () => {
|
|
|
9506
9557
|
})
|
|
9507
9558
|
test('curry 2 | without explicit types', () => {
|
|
9508
9559
|
const result = mergeWith(concat)(A, B)
|
|
9509
|
-
result // $ExpectType
|
|
9560
|
+
result // $ExpectType Record<string, unknown>
|
|
9510
9561
|
})
|
|
9511
9562
|
test('curry 2 | with explicit types', () => {
|
|
9512
9563
|
const result = mergeWith<Output>(concat)(A, B)
|
|
@@ -9531,6 +9582,120 @@ It returns the lesser value between `x` and `y` according to `compareFn` functio
|
|
|
9531
9582
|
|
|
9532
9583
|
[](#minBy)
|
|
9533
9584
|
|
|
9585
|
+
### modifyPath
|
|
9586
|
+
|
|
9587
|
+
```typescript
|
|
9588
|
+
|
|
9589
|
+
modifyPath<T extends Record<string, unknown>>(path: Path, fn: (x: any) => unknown, object: Record<string, unknown>): T
|
|
9590
|
+
```
|
|
9591
|
+
|
|
9592
|
+
It changes a property of object on the base of provided path and transformer function.
|
|
9593
|
+
|
|
9594
|
+
<details>
|
|
9595
|
+
|
|
9596
|
+
<summary>All Typescript definitions</summary>
|
|
9597
|
+
|
|
9598
|
+
```typescript
|
|
9599
|
+
modifyPath<T extends Record<string, unknown>>(path: Path, fn: (x: any) => unknown, object: Record<string, unknown>): T;
|
|
9600
|
+
modifyPath<T extends Record<string, unknown>>(path: Path, fn: (x: any) => unknown): (object: Record<string, unknown>) => T;
|
|
9601
|
+
modifyPath<T extends Record<string, unknown>>(path: Path): (fn: (x: any) => unknown) => (object: Record<string, unknown>) => T;
|
|
9602
|
+
```
|
|
9603
|
+
|
|
9604
|
+
</details>
|
|
9605
|
+
|
|
9606
|
+
<details>
|
|
9607
|
+
|
|
9608
|
+
<summary><strong>R.modifyPath</strong> source</summary>
|
|
9609
|
+
|
|
9610
|
+
```javascript
|
|
9611
|
+
import { _isArray } from './_internals/_isArray.js'
|
|
9612
|
+
import { createPath } from './_internals/createPath.js'
|
|
9613
|
+
import { assoc } from './assoc.js'
|
|
9614
|
+
import { curry } from './curry.js'
|
|
9615
|
+
import { path as pathModule } from './path.js'
|
|
9616
|
+
|
|
9617
|
+
export function modifyPathFn(
|
|
9618
|
+
pathInput, fn, object
|
|
9619
|
+
){
|
|
9620
|
+
const path = createPath(pathInput)
|
|
9621
|
+
if (path.length === 1){
|
|
9622
|
+
return {
|
|
9623
|
+
...object,
|
|
9624
|
+
[ path[0] ] : fn(object[ path[0] ]),
|
|
9625
|
+
}
|
|
9626
|
+
}
|
|
9627
|
+
if (pathModule(path, object) === undefined) return object
|
|
9628
|
+
|
|
9629
|
+
const val = modifyPath(
|
|
9630
|
+
Array.prototype.slice.call(path, 1),
|
|
9631
|
+
fn,
|
|
9632
|
+
object[ path[ 0 ] ]
|
|
9633
|
+
)
|
|
9634
|
+
if (val === object[ path[ 0 ] ]){
|
|
9635
|
+
return object
|
|
9636
|
+
}
|
|
9637
|
+
|
|
9638
|
+
return assoc(
|
|
9639
|
+
path[ 0 ], val, object
|
|
9640
|
+
)
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
export const modifyPath = curry(modifyPathFn)
|
|
9644
|
+
```
|
|
9645
|
+
|
|
9646
|
+
</details>
|
|
9647
|
+
|
|
9648
|
+
<details>
|
|
9649
|
+
|
|
9650
|
+
<summary><strong>Tests</strong></summary>
|
|
9651
|
+
|
|
9652
|
+
```javascript
|
|
9653
|
+
import { modifyPath } from './modifyPath.js'
|
|
9654
|
+
|
|
9655
|
+
test('happy', () => {
|
|
9656
|
+
const result = modifyPath(
|
|
9657
|
+
'a.b.c', x => x + 1, { a : { b : { c : 1 } } }
|
|
9658
|
+
)
|
|
9659
|
+
expect(result).toEqual({ a : { b : { c : 2 } } })
|
|
9660
|
+
})
|
|
9661
|
+
|
|
9662
|
+
test('with array', () => {
|
|
9663
|
+
const input = {foo: [{ bar: '123' }]}
|
|
9664
|
+
const result = modifyPath('foo.0.bar', x => x + 'foo', input)
|
|
9665
|
+
expect(result).toEqual({ foo: { '0': { bar: '123foo' } } })
|
|
9666
|
+
})
|
|
9667
|
+
```
|
|
9668
|
+
|
|
9669
|
+
</details>
|
|
9670
|
+
|
|
9671
|
+
<details>
|
|
9672
|
+
|
|
9673
|
+
<summary><strong>Typescript</strong> test</summary>
|
|
9674
|
+
|
|
9675
|
+
```typescript
|
|
9676
|
+
import {modifyPath} from 'rambda'
|
|
9677
|
+
|
|
9678
|
+
const obj = {a:{b: {c:1}}}
|
|
9679
|
+
|
|
9680
|
+
describe('R.modifyPath', () => {
|
|
9681
|
+
it('happy', () => {
|
|
9682
|
+
const result = modifyPath('a.b.c', (x: number) => x + 1, obj)
|
|
9683
|
+
result // $ExpectType Record<string, unknown>
|
|
9684
|
+
})
|
|
9685
|
+
it('explicit return type', () => {
|
|
9686
|
+
interface Foo extends Record<string, unknown>{
|
|
9687
|
+
a: 1
|
|
9688
|
+
}
|
|
9689
|
+
const result = modifyPath<Foo>('a.b.c', (x: number) => x + 1, obj)
|
|
9690
|
+
result // $ExpectType Foo
|
|
9691
|
+
})
|
|
9692
|
+
})
|
|
9693
|
+
```
|
|
9694
|
+
|
|
9695
|
+
</details>
|
|
9696
|
+
|
|
9697
|
+
[](#modifyPath)
|
|
9698
|
+
|
|
9534
9699
|
### modulo
|
|
9535
9700
|
|
|
9536
9701
|
Curried version of `x%y`.
|
|
@@ -10170,7 +10335,7 @@ This method is also known as P combinator.
|
|
|
10170
10335
|
|
|
10171
10336
|
```typescript
|
|
10172
10337
|
|
|
10173
|
-
once<T extends
|
|
10338
|
+
once<T extends AnyFunction>(func: T): T
|
|
10174
10339
|
```
|
|
10175
10340
|
|
|
10176
10341
|
It returns a function, which invokes only once `fn` function.
|
|
@@ -10180,7 +10345,7 @@ It returns a function, which invokes only once `fn` function.
|
|
|
10180
10345
|
<summary>All Typescript definitions</summary>
|
|
10181
10346
|
|
|
10182
10347
|
```typescript
|
|
10183
|
-
once<T extends
|
|
10348
|
+
once<T extends AnyFunction>(func: T): T;
|
|
10184
10349
|
```
|
|
10185
10350
|
|
|
10186
10351
|
</details>
|
|
@@ -12434,13 +12599,15 @@ propEq<K extends string | number>(propToFind: K): {
|
|
|
12434
12599
|
|
|
12435
12600
|
```javascript
|
|
12436
12601
|
import { curry } from './curry.js'
|
|
12602
|
+
import { equals } from './equals.js'
|
|
12603
|
+
import { prop } from './prop.js'
|
|
12437
12604
|
|
|
12438
12605
|
function propEqFn(
|
|
12439
12606
|
propToFind, valueToMatch, obj
|
|
12440
12607
|
){
|
|
12441
12608
|
if (!obj) return false
|
|
12442
12609
|
|
|
12443
|
-
return
|
|
12610
|
+
return equals(valueToMatch, prop(propToFind, obj))
|
|
12444
12611
|
}
|
|
12445
12612
|
|
|
12446
12613
|
export const propEq = curry(propEqFn)
|
|
@@ -12463,6 +12630,15 @@ test('happy', () => {
|
|
|
12463
12630
|
'foo', 'bar', null
|
|
12464
12631
|
)).toBeFalse()
|
|
12465
12632
|
})
|
|
12633
|
+
|
|
12634
|
+
test('returns false if called with a null or undefined object', () => {
|
|
12635
|
+
expect(propEq(
|
|
12636
|
+
'name', 'Abby', null
|
|
12637
|
+
)).toBeFalse()
|
|
12638
|
+
expect(propEq(
|
|
12639
|
+
'name', 'Abby', undefined
|
|
12640
|
+
)).toBeFalse()
|
|
12641
|
+
})
|
|
12466
12642
|
```
|
|
12467
12643
|
|
|
12468
12644
|
</details>
|
|
@@ -12574,7 +12750,7 @@ const propEq = [
|
|
|
12574
12750
|
|
|
12575
12751
|
```typescript
|
|
12576
12752
|
|
|
12577
|
-
propIs<C extends
|
|
12753
|
+
propIs<C extends AnyFunction, K extends keyof any>(type: C, name: K, obj: any): obj is Record<K, ReturnType<C>>
|
|
12578
12754
|
```
|
|
12579
12755
|
|
|
12580
12756
|
It returns `true` if `property` of `obj` is from `target` type.
|
|
@@ -12584,15 +12760,15 @@ It returns `true` if `property` of `obj` is from `target` type.
|
|
|
12584
12760
|
<summary>All Typescript definitions</summary>
|
|
12585
12761
|
|
|
12586
12762
|
```typescript
|
|
12587
|
-
propIs<C extends
|
|
12588
|
-
propIs<C extends
|
|
12589
|
-
propIs<C extends
|
|
12590
|
-
propIs<C extends
|
|
12591
|
-
propIs<C extends
|
|
12763
|
+
propIs<C extends AnyFunction, K extends keyof any>(type: C, name: K, obj: any): obj is Record<K, ReturnType<C>>;
|
|
12764
|
+
propIs<C extends AnyConstructor, K extends keyof any>(type: C, name: K, obj: any): obj is Record<K, InstanceType<C>>;
|
|
12765
|
+
propIs<C extends AnyFunction, K extends keyof any>(type: C, name: K): (obj: any) => obj is Record<K, ReturnType<C>>;
|
|
12766
|
+
propIs<C extends AnyConstructor, K extends keyof any>(type: C, name: K): (obj: any) => obj is Record<K, InstanceType<C>>;
|
|
12767
|
+
propIs<C extends AnyFunction>(type: C): {
|
|
12592
12768
|
<K extends keyof any>(name: K, obj: any): obj is Record<K, ReturnType<C>>;
|
|
12593
12769
|
<K extends keyof any>(name: K): (obj: any) => obj is Record<K, ReturnType<C>>;
|
|
12594
12770
|
};
|
|
12595
|
-
propIs<C extends
|
|
12771
|
+
propIs<C extends AnyFunction>(type: C): {
|
|
12596
12772
|
<K extends keyof any>(name: K, obj: any): obj is Record<K, InstanceType<C>>;
|
|
12597
12773
|
<K extends keyof any>(name: K): (obj: any) => obj is Record<K, InstanceType<C>>;
|
|
12598
12774
|
};
|
|
@@ -13112,237 +13288,6 @@ const range = [
|
|
|
13112
13288
|
|
|
13113
13289
|
### reduce
|
|
13114
13290
|
|
|
13115
|
-
```typescript
|
|
13116
|
-
|
|
13117
|
-
reduce<T, TResult>(reducer: (prev: TResult, current: T, i: number) => TResult, initialValue: TResult, list: T[]): TResult
|
|
13118
|
-
```
|
|
13119
|
-
|
|
13120
|
-
<details>
|
|
13121
|
-
|
|
13122
|
-
<summary>All Typescript definitions</summary>
|
|
13123
|
-
|
|
13124
|
-
```typescript
|
|
13125
|
-
reduce<T, TResult>(reducer: (prev: TResult, current: T, i: number) => TResult, initialValue: TResult, list: T[]): TResult;
|
|
13126
|
-
reduce<T, TResult>(reducer: (prev: TResult, current: T) => TResult, initialValue: TResult, list: T[]): TResult;
|
|
13127
|
-
reduce<T, TResult>(reducer: (prev: TResult, current: T, i?: number) => TResult): (initialValue: TResult, list: T[]) => TResult;
|
|
13128
|
-
reduce<T, TResult>(reducer: (prev: TResult, current: T, i?: number) => TResult, initialValue: TResult): (list: T[]) => TResult;
|
|
13129
|
-
```
|
|
13130
|
-
|
|
13131
|
-
</details>
|
|
13132
|
-
|
|
13133
|
-
<details>
|
|
13134
|
-
|
|
13135
|
-
<summary><strong>R.reduce</strong> source</summary>
|
|
13136
|
-
|
|
13137
|
-
```javascript
|
|
13138
|
-
import { _isArray } from './_internals/_isArray.js'
|
|
13139
|
-
import { _keys } from './_internals/_keys.js'
|
|
13140
|
-
import { curry } from './curry.js'
|
|
13141
|
-
|
|
13142
|
-
export function reduceFn(
|
|
13143
|
-
reducer, acc, list
|
|
13144
|
-
){
|
|
13145
|
-
if (!_isArray(list)){
|
|
13146
|
-
throw new TypeError('reduce: list must be array or iterable')
|
|
13147
|
-
}
|
|
13148
|
-
let index = 0
|
|
13149
|
-
const len = list.length
|
|
13150
|
-
|
|
13151
|
-
while (index < len){
|
|
13152
|
-
acc = reducer(
|
|
13153
|
-
acc, list[ index ], index, list
|
|
13154
|
-
)
|
|
13155
|
-
index++
|
|
13156
|
-
}
|
|
13157
|
-
|
|
13158
|
-
return acc
|
|
13159
|
-
}
|
|
13160
|
-
|
|
13161
|
-
export const reduce = curry(reduceFn)
|
|
13162
|
-
```
|
|
13163
|
-
|
|
13164
|
-
</details>
|
|
13165
|
-
|
|
13166
|
-
<details>
|
|
13167
|
-
|
|
13168
|
-
<summary><strong>Tests</strong></summary>
|
|
13169
|
-
|
|
13170
|
-
```javascript
|
|
13171
|
-
import { reduce } from './reduce.js'
|
|
13172
|
-
|
|
13173
|
-
const reducer = (
|
|
13174
|
-
prev, current, i
|
|
13175
|
-
) => {
|
|
13176
|
-
expect(i).toBeNumber()
|
|
13177
|
-
|
|
13178
|
-
return prev + current
|
|
13179
|
-
}
|
|
13180
|
-
const initialValue = 1
|
|
13181
|
-
const list = [ 1, 2, 3 ]
|
|
13182
|
-
const ERROR = 'reduce: list must be array or iterable'
|
|
13183
|
-
|
|
13184
|
-
test('happy', () => {
|
|
13185
|
-
expect(reduce(
|
|
13186
|
-
reducer, initialValue, list
|
|
13187
|
-
)).toEqual(7)
|
|
13188
|
-
})
|
|
13189
|
-
|
|
13190
|
-
test('with object as iterable', () => {
|
|
13191
|
-
expect(() =>
|
|
13192
|
-
reduce(
|
|
13193
|
-
reducer, initialValue, {
|
|
13194
|
-
a : 1,
|
|
13195
|
-
b : 2,
|
|
13196
|
-
}
|
|
13197
|
-
)).toThrowWithMessage(TypeError, ERROR)
|
|
13198
|
-
})
|
|
13199
|
-
|
|
13200
|
-
test('with undefined as iterable', () => {
|
|
13201
|
-
expect(() => reduce(
|
|
13202
|
-
reducer, 0, null
|
|
13203
|
-
)).toThrowWithMessage(TypeError, ERROR)
|
|
13204
|
-
})
|
|
13205
|
-
```
|
|
13206
|
-
|
|
13207
|
-
</details>
|
|
13208
|
-
|
|
13209
|
-
<details>
|
|
13210
|
-
|
|
13211
|
-
<summary><strong>Typescript</strong> test</summary>
|
|
13212
|
-
|
|
13213
|
-
```typescript
|
|
13214
|
-
import {reduce} from 'rambda'
|
|
13215
|
-
|
|
13216
|
-
describe('R.reduce', () => {
|
|
13217
|
-
it('happy', () => {
|
|
13218
|
-
const result = reduce<number, number>(
|
|
13219
|
-
(acc, elem) => {
|
|
13220
|
-
acc // $ExpectType number
|
|
13221
|
-
elem // $ExpectType number
|
|
13222
|
-
return acc + elem
|
|
13223
|
-
},
|
|
13224
|
-
1,
|
|
13225
|
-
[1, 2, 3]
|
|
13226
|
-
)
|
|
13227
|
-
|
|
13228
|
-
result // $ExpectType number
|
|
13229
|
-
})
|
|
13230
|
-
|
|
13231
|
-
it('with two types', () => {
|
|
13232
|
-
const result = reduce<number, string>(
|
|
13233
|
-
(acc, elem) => {
|
|
13234
|
-
acc // $ExpectType string
|
|
13235
|
-
elem // $ExpectType number
|
|
13236
|
-
|
|
13237
|
-
return `${acc}${elem}`
|
|
13238
|
-
},
|
|
13239
|
-
'foo',
|
|
13240
|
-
[1, 2, 3]
|
|
13241
|
-
)
|
|
13242
|
-
|
|
13243
|
-
result // $ExpectType string
|
|
13244
|
-
})
|
|
13245
|
-
|
|
13246
|
-
it('with index', () => {
|
|
13247
|
-
const result = reduce<number, number>(
|
|
13248
|
-
(acc, elem, i) => {
|
|
13249
|
-
acc // $ExpectType number
|
|
13250
|
-
elem // $ExpectType number
|
|
13251
|
-
i // $ExpectType number
|
|
13252
|
-
return acc + elem
|
|
13253
|
-
},
|
|
13254
|
-
1,
|
|
13255
|
-
[1, 2, 3]
|
|
13256
|
-
)
|
|
13257
|
-
|
|
13258
|
-
result // $ExpectType number
|
|
13259
|
-
})
|
|
13260
|
-
|
|
13261
|
-
it('fallback', () => {
|
|
13262
|
-
const result = reduce(
|
|
13263
|
-
(acc, val) => {
|
|
13264
|
-
acc // $ExpectType number
|
|
13265
|
-
return acc + val
|
|
13266
|
-
},
|
|
13267
|
-
1,
|
|
13268
|
-
[1, 2, 3]
|
|
13269
|
-
)
|
|
13270
|
-
|
|
13271
|
-
result // $ExpectType number
|
|
13272
|
-
})
|
|
13273
|
-
|
|
13274
|
-
it('fallback with index', () => {
|
|
13275
|
-
const result = reduce(
|
|
13276
|
-
(acc, val, i) => {
|
|
13277
|
-
acc // $ExpectType number
|
|
13278
|
-
i // $ExpectType number
|
|
13279
|
-
return acc + val
|
|
13280
|
-
},
|
|
13281
|
-
1,
|
|
13282
|
-
[1, 2, 3]
|
|
13283
|
-
)
|
|
13284
|
-
|
|
13285
|
-
result // $ExpectType number
|
|
13286
|
-
})
|
|
13287
|
-
|
|
13288
|
-
it('fallback with two types', () => {
|
|
13289
|
-
const result = reduce(
|
|
13290
|
-
(acc, val) => {
|
|
13291
|
-
acc // $ExpectType string
|
|
13292
|
-
return acc + val
|
|
13293
|
-
},
|
|
13294
|
-
'foo',
|
|
13295
|
-
[1, 2, 3]
|
|
13296
|
-
)
|
|
13297
|
-
|
|
13298
|
-
result // $ExpectType string
|
|
13299
|
-
})
|
|
13300
|
-
})
|
|
13301
|
-
```
|
|
13302
|
-
|
|
13303
|
-
</details>
|
|
13304
|
-
|
|
13305
|
-
<details>
|
|
13306
|
-
|
|
13307
|
-
<summary>Lodash is fastest. Rambda is 60.48% slower and Ramda is 77.1% slower</summary>
|
|
13308
|
-
|
|
13309
|
-
```text
|
|
13310
|
-
const R = require('../../dist/rambda.js')
|
|
13311
|
-
|
|
13312
|
-
const fn = (acc, value) => acc + value
|
|
13313
|
-
const holder = [ 1, 2, 3 ]
|
|
13314
|
-
const acc = ''
|
|
13315
|
-
|
|
13316
|
-
const reduce = [
|
|
13317
|
-
{
|
|
13318
|
-
label : 'Rambda',
|
|
13319
|
-
fn : () => {
|
|
13320
|
-
R.reduce(
|
|
13321
|
-
fn, acc, holder
|
|
13322
|
-
)
|
|
13323
|
-
},
|
|
13324
|
-
},
|
|
13325
|
-
{
|
|
13326
|
-
label : 'Ramda',
|
|
13327
|
-
fn : () => {
|
|
13328
|
-
Ramda.reduce(
|
|
13329
|
-
fn, acc, holder
|
|
13330
|
-
)
|
|
13331
|
-
},
|
|
13332
|
-
},
|
|
13333
|
-
{
|
|
13334
|
-
label : 'Lodash',
|
|
13335
|
-
fn : () => {
|
|
13336
|
-
_.reduce(
|
|
13337
|
-
holder, fn, acc
|
|
13338
|
-
)
|
|
13339
|
-
},
|
|
13340
|
-
},
|
|
13341
|
-
]
|
|
13342
|
-
```
|
|
13343
|
-
|
|
13344
|
-
</details>
|
|
13345
|
-
|
|
13346
13291
|
[](#reduce)
|
|
13347
13292
|
|
|
13348
13293
|
### reject
|
|
@@ -16742,8 +16687,6 @@ export function uniq(list){
|
|
|
16742
16687
|
<summary><strong>Tests</strong></summary>
|
|
16743
16688
|
|
|
16744
16689
|
```javascript
|
|
16745
|
-
import { uniq as uniqRamda } from 'ramda'
|
|
16746
|
-
|
|
16747
16690
|
import { uniq } from './uniq.js'
|
|
16748
16691
|
|
|
16749
16692
|
test('happy', () => {
|
|
@@ -16794,7 +16737,7 @@ describe('R.uniq', () => {
|
|
|
16794
16737
|
|
|
16795
16738
|
<details>
|
|
16796
16739
|
|
|
16797
|
-
<summary>Rambda is faster than Ramda with
|
|
16740
|
+
<summary>Rambda is faster than Ramda with 90.24%</summary>
|
|
16798
16741
|
|
|
16799
16742
|
```text
|
|
16800
16743
|
const R = require('../../dist/rambda.js')
|
|
@@ -16842,6 +16785,10 @@ const tests = [
|
|
|
16842
16785
|
|
|
16843
16786
|
[](#uniq)
|
|
16844
16787
|
|
|
16788
|
+
### uniqBy
|
|
16789
|
+
|
|
16790
|
+
[](#uniqBy)
|
|
16791
|
+
|
|
16845
16792
|
### uniqWith
|
|
16846
16793
|
|
|
16847
16794
|
```typescript
|
|
@@ -16961,7 +16908,7 @@ describe('R.uniqWith', () => {
|
|
|
16961
16908
|
|
|
16962
16909
|
<details>
|
|
16963
16910
|
|
|
16964
|
-
<summary>Rambda is slower than Ramda with
|
|
16911
|
+
<summary>Rambda is slower than Ramda with 18.09%</summary>
|
|
16965
16912
|
|
|
16966
16913
|
```text
|
|
16967
16914
|
const R = require('../../dist/rambda.js')
|
|
@@ -18413,9 +18360,27 @@ describe('R.zipWith', () => {
|
|
|
18413
18360
|
|
|
18414
18361
|
## ❯ CHANGELOG
|
|
18415
18362
|
|
|
18363
|
+
7.2.0
|
|
18364
|
+
|
|
18365
|
+
- Wrong `R.update` if index is `-1` - [PR #593](https://github.com/selfrefactor/rambda/pull/593)
|
|
18366
|
+
|
|
18367
|
+
- Wrong curried typings in `R.anyPass` - [Issue #642](https://github.com/selfrefactor/rambda/issues/642)
|
|
18368
|
+
|
|
18369
|
+
- `R.modifyPath` not exported - [Issue #640](https://github.com/selfrefactor/rambda/issues/640)
|
|
18370
|
+
|
|
18371
|
+
- Add new method `R.uniqBy`. Implementation is coming from [Ramda MR#2641](https://github.com/ramda/ramda/pull/2641)
|
|
18372
|
+
|
|
18373
|
+
- Apply the following changes from `@types/rambda`:
|
|
18374
|
+
|
|
18375
|
+
-- [https://github.com/DefinitelyTyped/DefinitelyTyped/commit/bab47272d52fc7bb81e85da36dbe9c905a04d067](add `AnyFunction` and `AnyConstructor`)
|
|
18376
|
+
|
|
18377
|
+
-- Improve `R.ifElse` typings - https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59291
|
|
18378
|
+
|
|
18379
|
+
-- Make `R.propEq` safe for `null/undefined` arguments - https://github.com/ramda/ramda/pull/2594/files
|
|
18380
|
+
|
|
18416
18381
|
7.1.4
|
|
18417
18382
|
|
|
18418
|
-
`R.mergeRight` not found on `Deno` import - [Issue #633](https://github.com/selfrefactor/rambda/issues/633)
|
|
18383
|
+
- `R.mergeRight` not found on `Deno` import - [Issue #633](https://github.com/selfrefactor/rambda/issues/633)
|
|
18419
18384
|
|
|
18420
18385
|
7.1.0
|
|
18421
18386
|
|
|
@@ -18453,7 +18418,7 @@ Rambda doesn't work with `pnpm` due to wrong export configuration - [Issue #619]
|
|
|
18453
18418
|
|
|
18454
18419
|
7.0.0
|
|
18455
18420
|
|
|
18456
|
-
-
|
|
18421
|
+
- Breaking change - sync `R.compose`/`R.pipe` with `@types/ramda`. That is significant change so as safeguard, it will lead a major bump. Important - this lead to raising required Typescript version to `4.2.2`. In other words, to use `Rambda` you'll need Typescript version `4.2.2` or newer.
|
|
18457
18422
|
|
|
18458
18423
|
Related commit in `@types/ramda` - https://github.com/DefinitelyTyped/DefinitelyTyped/commit/286eff4f76d41eb8f091e7437eabd8a60d97fc1f#diff-4f74803fa83a81e47cb17a7d8a4e46a7e451f4d9e5ce2f1bd7a70a72d91f4bc1
|
|
18459
18424
|
|
|
@@ -18485,7 +18450,7 @@ There are several other changes in `@types/ramda` as stated in [this comment](ht
|
|
|
18485
18450
|
|
|
18486
18451
|
-- R.toUpper
|
|
18487
18452
|
|
|
18488
|
-
- One more reason for the
|
|
18453
|
+
- One more reason for the breaking change is changing of export declarations in `package.json` based on [this blog post](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#packagejson-exports-imports-and-self-referencing) and [this merged Ramda's PR](https://github.com/ramda/ramda/pull/2999). This also led to renaming of `babel.config.js` to `babel.config.cjs`.
|
|
18489
18454
|
|
|
18490
18455
|
- Add `R.apply`, `R.bind` and `R.unapply`
|
|
18491
18456
|
|
|
@@ -19153,6 +19118,8 @@ Approve [PR #266](https://github.com/selfrefactor/rambda/pull/266) that adds `R.
|
|
|
19153
19118
|
|
|
19154
19119
|
- [Awesome functional Javascript programming libraries](https://github.com/stoeffel/awesome-fp-js#libraries)
|
|
19155
19120
|
|
|
19121
|
+
- [Overview of Rambda pros/cons](https://mobily.github.io/ts-belt/docs/#rambda-%EF%B8%8F)
|
|
19122
|
+
|
|
19156
19123
|
> Links to Rambda
|
|
19157
19124
|
|
|
19158
19125
|
- [https://mailchi.mp/webtoolsweekly/web-tools-280](Web Tools Weekly)
|