mirror of
https://github.com/toss/es-toolkit.git
synced 2024-11-24 11:45:26 +03:00
feat(isMatch, matches): Add isMatch & matches in compat
This commit is contained in:
parent
e262fa1b13
commit
c350b23ee2
12
benchmarks/performance/isMatch.bench.ts
Normal file
12
benchmarks/performance/isMatch.bench.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { isMatch as isMatchToolkit } from 'es-toolkit/compat';
|
||||
import { isMatch as isMatchLodash } from 'lodash';
|
||||
|
||||
describe('isMatch', () => {
|
||||
bench('es-toolkit/isMatch', () => {
|
||||
isMatchToolkit({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }, { a: { b: { c: 1 } } })
|
||||
});
|
||||
bench('lodash/isMatch', () => {
|
||||
isMatchLodash({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }, { a: { b: { c: 1 } } })
|
||||
});
|
||||
});
|
14
benchmarks/performance/matches.bench.ts
Normal file
14
benchmarks/performance/matches.bench.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { matches as matchesToolkit } from 'es-toolkit/compat';
|
||||
import { matches as matchesLodash } from 'lodash';
|
||||
|
||||
describe('matches', () => {
|
||||
bench('es-toolkit/matches', () => {
|
||||
const isMatch = matchesToolkit({ a: { b: { c: 1 } } })
|
||||
isMatch({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 });
|
||||
});
|
||||
bench('lodash/matches', () => {
|
||||
const isMatch = matchesLodash({ a: { b: { c: 1 } } });
|
||||
isMatch({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 });
|
||||
});
|
||||
});
|
@ -159,6 +159,8 @@ function sidebar(): DefaultTheme.Sidebar {
|
||||
{ text: 'isLength', link: '/reference/predicate/isLength' },
|
||||
{ text: 'isPlainObject', link: '/reference/predicate/isPlainObject' },
|
||||
{ text: 'isPrimitive', link: '/reference/predicate/isPrimitive' },
|
||||
{ text: 'isMatch (compat)', link: '/reference/compat/predicate/isMatch' },
|
||||
{ text: 'matches (compat)', link: '/reference/compat/predicate/matches' },
|
||||
{ text: 'isNil', link: '/reference/predicate/isNil' },
|
||||
{ text: 'isNotNil', link: '/reference/predicate/isNotNil' },
|
||||
{ text: 'isNull', link: '/reference/predicate/isNull' },
|
||||
|
@ -170,6 +170,8 @@ function sidebar(): DefaultTheme.Sidebar {
|
||||
{ text: 'isLength', link: '/ko/reference/predicate/isLength' },
|
||||
{ text: 'isPlainObject', link: '/ko/reference/predicate/isPlainObject' },
|
||||
{ text: 'isPrimitive', link: '/ko/reference/predicate/isPrimitive' },
|
||||
{ text: 'isMatch (호환성)', link: '/ko/reference/compat/predicate/isMatch' },
|
||||
{ text: 'matches (호환성)', link: '/ko/reference/compat/predicate/matches' },
|
||||
{ text: 'isNil', link: '/ko/reference/predicate/isNil' },
|
||||
{ text: 'isNotNil', link: '/ko/reference/predicate/isNotNil' },
|
||||
{ text: 'isNull', link: '/ko/reference/predicate/isNull' },
|
||||
|
@ -155,6 +155,8 @@ function sidebar(): DefaultTheme.Sidebar {
|
||||
{ text: 'isLength', link: '/zh_hans/reference/predicate/isLength' },
|
||||
{ text: 'isPlainObject', link: '/zh_hans/reference/predicate/isPlainObject' },
|
||||
{ text: 'isPrimitive', link: '/zh_hans/reference/predicate/isPrimitive' },
|
||||
{ text: 'isMatch (兼容性)', link: '/zh_hans/reference/compat/predicate/isMatch' },
|
||||
{ text: 'matches (兼容性)', link: '/zh_hans/reference/compat/predicate/matches' },
|
||||
{ text: 'isNil', link: '/zh_hans/reference/predicate/isNil' },
|
||||
{ text: 'isNotNil', link: '/zh_hans/reference/predicate/isNotNil' },
|
||||
{ text: 'isNull', link: '/zh_hans/reference/predicate/isNull' },
|
||||
|
54
docs/ko/reference/compat/predicate/isMatch.md
Normal file
54
docs/ko/reference/compat/predicate/isMatch.md
Normal file
@ -0,0 +1,54 @@
|
||||
# isMatch
|
||||
|
||||
::: info
|
||||
이 함수는 [lodash와 완전히 호환](../../../compatibility.md)돼요. `es-toolkit/compat` 라이브러리에서 쓸 수 있어요.
|
||||
:::
|
||||
|
||||
`target`이 `source`의 모양 및 값과 일치하는지 확인해요. 객체, 배열, `Map`, `Set`의 깊은 비교를 지원해요.
|
||||
|
||||
## 인터페이스
|
||||
|
||||
```typescript
|
||||
function isMatch(target: unknown, source: unknown): boolean;
|
||||
```
|
||||
|
||||
## 파라미터
|
||||
|
||||
- `target` (`unknown`): 모양과 값이 일치하는지 확인할 값.
|
||||
- `source` (`unknown`): 확인할 모양과 값을 가진 객체.
|
||||
|
||||
## 반환 값
|
||||
|
||||
- (`boolean`): `target`이 `source`의 모양 및 값과 일치하면 `true`. 아니면 `false`.
|
||||
|
||||
## 예시
|
||||
|
||||
### 객체 일치
|
||||
|
||||
```typescript
|
||||
isMatch({ a: 1, b: 2 }, { a: 1 }); // true
|
||||
```
|
||||
|
||||
### 배열 일치
|
||||
|
||||
```typescript
|
||||
isMatch([1, 2, 3], [1, 2, 3]); // true
|
||||
isMatch([1, 2, 2, 3], [2, 2]); // true
|
||||
isMatch([1, 2, 3], [2, 2]); // false
|
||||
```
|
||||
|
||||
### `Map` 일치
|
||||
|
||||
```typescript
|
||||
const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
|
||||
const sourceMap = new Map([['key1', 'value1']]);
|
||||
isMatch(targetMap, sourceMap); // true
|
||||
```
|
||||
|
||||
### `Set` 일치
|
||||
|
||||
```javascript
|
||||
const targetSet = new Set([1, 2, 3]);
|
||||
const sourceSet = new Set([1, 2]);
|
||||
isMatch(targetSet, sourceSet); // true
|
||||
```
|
52
docs/ko/reference/compat/predicate/matches.md
Normal file
52
docs/ko/reference/compat/predicate/matches.md
Normal file
@ -0,0 +1,52 @@
|
||||
# matches
|
||||
|
||||
::: info
|
||||
이 함수는 [lodash와 완전히 호환](../../../compatibility.md)돼요. `es-toolkit/compat` 라이브러리에서 쓸 수 있어요.
|
||||
:::
|
||||
|
||||
`source`의 모양 및 값과 일치하는지 확인하는 함수를 만들어요.
|
||||
객체, 배열, `Map`, `Set`과의 깊은 비교를 지원해요.
|
||||
|
||||
이 함수의 동작은 [isMatch](./isMatch.md)와 동일하고, 호출하는 방법만 달라요.
|
||||
|
||||
## 인터페이스
|
||||
|
||||
```typescript
|
||||
function matches(source: unknown): (target: unknown) => boolean;
|
||||
```
|
||||
|
||||
## 파라미터
|
||||
|
||||
- `source` (`unknown`): 확인하는 함수가 참고할 객체.
|
||||
|
||||
## 반환 값
|
||||
|
||||
- (`(target: unknown) => boolean`): `source`의 모양 및 값과 일치하는지 확인하는 함수. `target`이 `source`과 일치하면 `true`, 아니면 `false`를 반환해요.
|
||||
|
||||
|
||||
## 예시
|
||||
|
||||
### 객체 일치
|
||||
|
||||
```typescript
|
||||
const matcher = matches({ a: 1, b: 2 });
|
||||
matcher({ a: 1, b: 2, c: 3 }); // true
|
||||
matcher({ a: 1, c: 3 }); // false
|
||||
```
|
||||
|
||||
### 배열 일치
|
||||
|
||||
```typescript
|
||||
const arrayMatcher = matches([1, 2, 3]);
|
||||
arrayMatcher([1, 2, 3, 4]); // true
|
||||
arrayMatcher([4, 5, 6]); // false
|
||||
```
|
||||
|
||||
### 중첩된 구조 일치
|
||||
|
||||
```typescript
|
||||
// Matching objects with nested structures
|
||||
const nestedMatcher = matches({ a: { b: 2 } });
|
||||
nestedMatcher({ a: { b: 2, c: 3 } }); // true
|
||||
nestedMatcher({ a: { c: 3 } }); // false
|
||||
```
|
55
docs/reference/compat/predicate/isMatch.md
Normal file
55
docs/reference/compat/predicate/isMatch.md
Normal file
@ -0,0 +1,55 @@
|
||||
# isMatch
|
||||
|
||||
::: info
|
||||
This function is fully compatible with lodash. You can find it in our [compatibility library](../../../compatibility.md), `es-toolkit/compat`.
|
||||
:::
|
||||
|
||||
Checks if the target matches the source by comparing their structures and values.
|
||||
This function supports deep comparison for objects, arrays, maps, and sets.
|
||||
|
||||
## Signature
|
||||
|
||||
```typescript
|
||||
function isMatch(target: unknown, source: unknown): boolean;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `target` (`unknown`): The target value to match against.
|
||||
- `source` (`unknown`): The source value to match with.
|
||||
|
||||
## Returns
|
||||
|
||||
- (`boolean`): Returns `true` if the target matches the source, otherwise `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic usage
|
||||
|
||||
```typescript
|
||||
isMatch({ a: 1, b: 2 }, { a: 1 }); // true
|
||||
```
|
||||
|
||||
### Matching arrays
|
||||
|
||||
```typescript
|
||||
isMatch([1, 2, 3], [1, 2, 3]); // true
|
||||
isMatch([1, 2, 2, 3], [2, 2]); // true
|
||||
isMatch([1, 2, 3], [2, 2]); // false
|
||||
```
|
||||
|
||||
### Matching maps
|
||||
|
||||
```typescript
|
||||
const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
|
||||
const sourceMap = new Map([['key1', 'value1']]);
|
||||
isMatch(targetMap, sourceMap); // true
|
||||
```
|
||||
|
||||
### Matching sets
|
||||
|
||||
```javascript
|
||||
const targetSet = new Set([1, 2, 3]);
|
||||
const sourceSet = new Set([1, 2]);
|
||||
isMatch(targetSet, sourceSet); // true
|
||||
```
|
51
docs/reference/compat/predicate/matches.md
Normal file
51
docs/reference/compat/predicate/matches.md
Normal file
@ -0,0 +1,51 @@
|
||||
# matches
|
||||
|
||||
::: info
|
||||
This function is fully compatible with lodash. You can find it in our [compatibility library](../../../compatibility.md), `es-toolkit/compat`.
|
||||
:::
|
||||
|
||||
Creates a function that performs a deep comparison between a given target and the source object.
|
||||
|
||||
This function produces the same results as the [isMatch](./isMatch.md) function, but provides for different ways to call it.
|
||||
|
||||
## Signature
|
||||
|
||||
```typescript
|
||||
function matches(source: unknown): (target: unknown) => boolean;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `source` (`unknown`): The source object to create the matcher from.
|
||||
|
||||
## Returns
|
||||
|
||||
- (`(target: unknown) => boolean`): Returns a function that takes a target object and returns `true` if the target matches the source, otherwise `false`.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic usage
|
||||
|
||||
```typescript
|
||||
const matcher = matches({ a: 1, b: 2 });
|
||||
matcher({ a: 1, b: 2, c: 3 }); // true
|
||||
matcher({ a: 1, c: 3 }); // false
|
||||
```
|
||||
|
||||
### Matching arrays
|
||||
|
||||
```typescript
|
||||
const arrayMatcher = matches([1, 2, 3]);
|
||||
arrayMatcher([1, 2, 3, 4]); // true
|
||||
arrayMatcher([4, 5, 6]); // false
|
||||
```
|
||||
|
||||
### Matching nested structures
|
||||
|
||||
```typescript
|
||||
// Matching objects with nested structures
|
||||
const nestedMatcher = matches({ a: { b: 2 } });
|
||||
nestedMatcher({ a: { b: 2, c: 3 } }); // true
|
||||
nestedMatcher({ a: { c: 3 } }); // false
|
||||
```
|
@ -1,5 +1,9 @@
|
||||
# isArray
|
||||
|
||||
::: info
|
||||
此函数与 lodash 完全兼容。您可以在我们的[兼容性库](../../../compatibility.md)中找到它,`es-toolkit/compat`。
|
||||
:::
|
||||
|
||||
检查给定的值是否为数组。
|
||||
|
||||
该函数测试提供的值是否为数组。
|
||||
|
55
docs/zh_hans/reference/compat/predicate/isMatch.md
Normal file
55
docs/zh_hans/reference/compat/predicate/isMatch.md
Normal file
@ -0,0 +1,55 @@
|
||||
# isMatch
|
||||
|
||||
::: info
|
||||
此函数与 lodash 完全兼容。您可以在我们的[兼容性库](../../../compatibility.md)中找到它,`es-toolkit/compat`。
|
||||
:::
|
||||
|
||||
检查目标是否与源匹配,方法是比较它们的结构和值。
|
||||
此函数支持对象、数组、映射和集合的深度比较。
|
||||
|
||||
## 签名
|
||||
|
||||
```typescript
|
||||
function isMatch(target: unknown, source: unknown): boolean;
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
- `target` (`unknown`): 要匹配的目标值。
|
||||
- `source` (`unknown`): 用于匹配的源值。
|
||||
|
||||
## 返回值
|
||||
|
||||
- (`boolean`): 如果目标与源匹配,则返回 `true`,否则返回 `false`。
|
||||
|
||||
## 示例
|
||||
|
||||
### 基本用法
|
||||
|
||||
```typescript
|
||||
isMatch({ a: 1, b: 2 }, { a: 1 }); // true
|
||||
```
|
||||
|
||||
### 匹配数组
|
||||
|
||||
```typescript
|
||||
isMatch([1, 2, 3], [1, 2, 3]); // true
|
||||
isMatch([1, 2, 2, 3], [2, 2]); // true
|
||||
isMatch([1, 2, 3], [2, 2]); // false
|
||||
```
|
||||
|
||||
### 匹配映射
|
||||
|
||||
```typescript
|
||||
const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
|
||||
const sourceMap = new Map([['key1', 'value1']]);
|
||||
isMatch(targetMap, sourceMap); // true
|
||||
```
|
||||
|
||||
### 匹配集合
|
||||
|
||||
```typescript
|
||||
const targetSet = new Set([1, 2, 3]);
|
||||
const sourceSet = new Set([1, 2]);
|
||||
isMatch(targetSet, sourceSet); // true
|
||||
```
|
50
docs/zh_hans/reference/compat/predicate/matches.md
Normal file
50
docs/zh_hans/reference/compat/predicate/matches.md
Normal file
@ -0,0 +1,50 @@
|
||||
# matches
|
||||
|
||||
::: info
|
||||
此函数与 lodash 完全兼容。您可以在我们的[兼容性库](../../../compatibility.md)中找到它,`es-toolkit/compat`。
|
||||
:::
|
||||
|
||||
创建一个函数来对给定的目标对象和源对象进行深度比较。
|
||||
|
||||
这个函数产生与[isMatch](./isMatch.md)函数相同的结果,但提供了不同的调用方式。
|
||||
|
||||
## 签名
|
||||
|
||||
```typescript
|
||||
function matches(source: unknown): (target: unknown) => boolean;
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
- `source` (`unknown`): 用于创建匹配器的源对象。
|
||||
|
||||
## 返回值
|
||||
|
||||
- (`(target: unknown) => boolean`): 返回一个函数,该函数接收一个目标对象,如果目标对象与源对象匹配则返回`true`,否则返回`false`。
|
||||
|
||||
## 示例
|
||||
|
||||
### 基本用法
|
||||
|
||||
```typescript
|
||||
const matcher = matches({ a: 1, b: 2 });
|
||||
matcher({ a: 1, b: 2, c: 3 }); // true
|
||||
matcher({ a: 1, c: 3 }); // false
|
||||
```
|
||||
|
||||
### 匹配数组
|
||||
|
||||
```typescript
|
||||
const arrayMatcher = matches([1, 2, 3]);
|
||||
arrayMatcher([1, 2, 3, 4]); // true
|
||||
arrayMatcher([4, 5, 6]); // false
|
||||
```
|
||||
|
||||
### 匹配嵌套结构
|
||||
|
||||
```typescript
|
||||
// Matching objects with nested structures
|
||||
const nestedMatcher = matches({ a: { b: 2 } });
|
||||
nestedMatcher({ a: { b: 2, c: 3 } }); // true
|
||||
nestedMatcher({ a: { c: 3 } }); // false
|
||||
```
|
28
src/compat/_internal/isArrayMatch.spec.ts
Normal file
28
src/compat/_internal/isArrayMatch.spec.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isArrayMatch } from './isArrayMatch';
|
||||
|
||||
describe('isArrayMatch', () => {
|
||||
it('can match arrays', () => {
|
||||
expect(isArrayMatch([1, 2, 3], [2, 3])).toBe(true);
|
||||
expect(isArrayMatch([1, 2, 3, 4, 5], [1, 3, 5])).toBe(true);
|
||||
expect(isArrayMatch([1, 2, 3, 4, 5], [0, 1])).toBe(false);
|
||||
});
|
||||
|
||||
it('can match arrays with duplicated values', () => {
|
||||
expect(isArrayMatch([2, 2], [2, 2])).toEqual(true);
|
||||
expect(isArrayMatch([1, 2], [2, 2])).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true if source is empty', () => {
|
||||
expect(isArrayMatch([1, 2, 3], [])).toBe(true);
|
||||
expect(isArrayMatch(1, [])).toBe(true);
|
||||
expect(isArrayMatch(new Map(), [])).toBe(true);
|
||||
expect(isArrayMatch(new Set(), [])).toBe(true);
|
||||
});
|
||||
|
||||
it('can match non-arrays', () => {
|
||||
expect(isArrayMatch(1, [2, 3])).toBe(false);
|
||||
expect(isArrayMatch(new Map(), [2, 3])).toBe(false);
|
||||
expect(isArrayMatch(new Set(), [2, 3])).toBe(false);
|
||||
});
|
||||
});
|
28
src/compat/_internal/isArrayMatch.ts
Normal file
28
src/compat/_internal/isArrayMatch.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { isMatch } from '../predicate/isMatch.ts';
|
||||
|
||||
export function isArrayMatch(target: unknown, source: readonly unknown[]) {
|
||||
if (source.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const countedIndex = new Set<number>();
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const sourceItem = source[i];
|
||||
const index = target.findIndex((targetItem, index) => {
|
||||
return isMatch(targetItem, sourceItem) && !countedIndex.has(index);
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
countedIndex.add(index);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
88
src/compat/_internal/isMapMatch.spec.ts
Normal file
88
src/compat/_internal/isMapMatch.spec.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isMapMatch } from './isMapMatch';
|
||||
|
||||
describe('isMapMatch', () => {
|
||||
it('can match maps', () => {
|
||||
expect(
|
||||
isMapMatch(
|
||||
new Map([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
]),
|
||||
new Map([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
])
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isMapMatch(
|
||||
new Map([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
['c', 3],
|
||||
]),
|
||||
new Map([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
])
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isMapMatch(
|
||||
new Map([['b', 2]]),
|
||||
new Map([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
])
|
||||
)
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isMapMatch(
|
||||
new Map([
|
||||
['a', 2],
|
||||
['b', 2],
|
||||
]),
|
||||
new Map([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
])
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if source is empty', () => {
|
||||
const map = new Map();
|
||||
|
||||
expect(
|
||||
isMapMatch(
|
||||
new Map([
|
||||
['a', 2],
|
||||
['b', 2],
|
||||
]),
|
||||
map
|
||||
)
|
||||
).toBe(true);
|
||||
expect(isMapMatch(1, map)).toBe(true);
|
||||
expect(isMapMatch('a', map)).toBe(true);
|
||||
expect(isMapMatch(new Set(), map)).toBe(true);
|
||||
expect(isMapMatch([1, 2, 3], map)).toBe(true);
|
||||
expect(isMapMatch({ a: 1, b: 2 }, map)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if source is not empty and targets that are not maps', () => {
|
||||
const map = new Map([
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
]);
|
||||
|
||||
expect(isMapMatch(1, map)).toBe(false);
|
||||
expect(isMapMatch('a', map)).toBe(false);
|
||||
expect(isMapMatch(new Set(), map)).toBe(false);
|
||||
expect(isMapMatch([1, 2, 3], map)).toBe(false);
|
||||
expect(isMapMatch({ a: 1, b: 2 }, map)).toBe(false);
|
||||
});
|
||||
});
|
19
src/compat/_internal/isMapMatch.ts
Normal file
19
src/compat/_internal/isMapMatch.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { isMatch } from '../predicate/isMatch.ts';
|
||||
|
||||
export function isMapMatch(target: unknown, source: Map<any, any>) {
|
||||
if (source.size === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(target instanceof Map)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [key, value] of source.entries()) {
|
||||
if (!isMatch(target.get(key), value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
32
src/compat/_internal/isSetMatch.spec.ts
Normal file
32
src/compat/_internal/isSetMatch.spec.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isMapMatch } from './isMapMatch';
|
||||
import { isSetMatch } from './isSetMatch';
|
||||
|
||||
describe('isSetMatch', () => {
|
||||
it('can match sets', () => {
|
||||
expect(isSetMatch(new Set([1, 2, 3]), new Set([1, 2, 3]))).toBe(true);
|
||||
expect(isSetMatch(new Set([1, 2, 3]), new Set([1, 2]))).toBe(true);
|
||||
expect(isSetMatch(new Set([1, 2]), new Set([1, 2, 3]))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if source is empty', () => {
|
||||
const set = new Set();
|
||||
|
||||
expect(isSetMatch(new Set([1, 2, 3]), set)).toBe(true);
|
||||
expect(isSetMatch(1, set)).toBe(true);
|
||||
expect(isSetMatch('a', set)).toBe(true);
|
||||
expect(isSetMatch(new Set(), set)).toBe(true);
|
||||
expect(isSetMatch([1, 2, 3], set)).toBe(true);
|
||||
expect(isSetMatch({ a: 1, b: 2 }, set)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if source is not empty and target is not a map', () => {
|
||||
const set = new Set([1, 2, 3]);
|
||||
|
||||
expect(isSetMatch(1, set)).toBe(false);
|
||||
expect(isSetMatch('a', set)).toBe(false);
|
||||
expect(isSetMatch(new Set(), set)).toBe(false);
|
||||
expect(isSetMatch([1, 2, 3], set)).toBe(false);
|
||||
expect(isSetMatch({ a: 1, b: 2 }, set)).toBe(false);
|
||||
});
|
||||
});
|
13
src/compat/_internal/isSetMatch.ts
Normal file
13
src/compat/_internal/isSetMatch.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { isArrayMatch } from './isArrayMatch.ts';
|
||||
|
||||
export function isSetMatch(target: unknown, source: Set<any>) {
|
||||
if (source.size === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(target instanceof Set)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isArrayMatch([...target], [...source]);
|
||||
}
|
1
src/compat/_internal/numberProto.ts
Normal file
1
src/compat/_internal/numberProto.ts
Normal file
@ -0,0 +1 @@
|
||||
export const numberProto: any = Number.prototype;
|
@ -37,6 +37,8 @@ export { set } from './object/set.ts';
|
||||
export { isPlainObject } from './predicate/isPlainObject.ts';
|
||||
export { isArray } from './predicate/isArray.ts';
|
||||
export { isTypedArray } from './predicate/isTypedArray.ts';
|
||||
export { isMatch } from './predicate/isMatch.ts';
|
||||
export { matches } from './predicate/matches.ts';
|
||||
|
||||
export { startsWith } from './string/startsWith.ts';
|
||||
export { endsWith } from './string/endsWith.ts';
|
||||
|
320
src/compat/predicate/isMatch.spec.ts
Normal file
320
src/compat/predicate/isMatch.spec.ts
Normal file
@ -0,0 +1,320 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { noop } from '../../function/noop';
|
||||
import { empties } from '../_internal/empties';
|
||||
import { stubTrue } from '../_internal/stubTrue';
|
||||
import { isMatch } from './isMatch';
|
||||
|
||||
describe('isMatch', () => {
|
||||
it(`should perform a deep comparison between \`source\` and \`object\``, () => {
|
||||
let object: any = { a: 1, b: 2, c: 3 };
|
||||
|
||||
expect(isMatch(object, { a: 1 })).toBe(true);
|
||||
expect(isMatch(object, { b: 2 })).toBe(true);
|
||||
expect(isMatch(object, { a: 1, c: 3 })).toBe(true);
|
||||
expect(isMatch(object, { c: 3, d: 4 })).toBe(false);
|
||||
expect(isMatch({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }, { a: { b: { c: 1 } } })).toBe(true);
|
||||
});
|
||||
|
||||
it(`should match inherited string keyed \`object\` properties`, () => {
|
||||
interface Foo {
|
||||
a: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
interface FooConstructor {
|
||||
new(): Foo;
|
||||
}
|
||||
|
||||
const Foo = function Foo(this: Foo) {
|
||||
this.a = 1;
|
||||
} as any as FooConstructor;
|
||||
|
||||
Foo.prototype.b = 2;
|
||||
|
||||
const object = { a: new Foo() };
|
||||
expect(isMatch(object, { a: { b: 2 } })).toBe(true);
|
||||
});
|
||||
|
||||
it(`should not match by inherited \`source\` properties`, () => {
|
||||
interface Foo {
|
||||
a: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
interface FooConstructor {
|
||||
new(): Foo;
|
||||
}
|
||||
|
||||
const Foo = function Foo(this: Foo) {
|
||||
this.a = 1;
|
||||
} as any as FooConstructor;
|
||||
|
||||
Foo.prototype.b = 2;
|
||||
|
||||
const objects = [{ a: 1 }, { a: 1, b: 2 }];
|
||||
const source = new Foo();
|
||||
const actual = objects.map(object => isMatch(object, source));
|
||||
const expected = objects.map(stubTrue);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should compare a variety of \`source\` property values`, () => {
|
||||
const object1 = { a: false, b: true, c: '3', d: 4, e: [5], f: { g: 6 } };
|
||||
const object2 = { a: 0, b: 1, c: 3, d: '4', e: ['5'], f: { g: '6' } };
|
||||
|
||||
expect(isMatch(object1, object1)).toBe(true);
|
||||
expect(isMatch(object2, object1)).toBe(false);
|
||||
});
|
||||
|
||||
it(`should match \`-0\` as \`0\``, () => {
|
||||
const object1 = { a: -0 };
|
||||
const object2 = { a: 0 };
|
||||
|
||||
expect(isMatch(object2, object1)).toBe(true);
|
||||
expect(isMatch(object1, object2)).toBe(true);
|
||||
});
|
||||
|
||||
it(`should compare functions by reference`, () => {
|
||||
const object1 = { a: noop };
|
||||
const object2 = { a: () => { } };
|
||||
const object3 = { a: {} };
|
||||
|
||||
expect(isMatch(object1, object1)).toBe(true);
|
||||
expect(isMatch(object2, object1)).toBe(false);
|
||||
expect(isMatch(object3, object1)).toBe(false);
|
||||
});
|
||||
|
||||
it(`should work with a function for \`object\``, () => {
|
||||
function Foo() { }
|
||||
Foo.a = { b: 2, c: 3 };
|
||||
|
||||
expect(isMatch(Foo, { a: { b: 2 } })).toBe(true);
|
||||
});
|
||||
|
||||
it(`should work with a function for \`source\``, () => {
|
||||
function Foo() { }
|
||||
Foo.a = 1;
|
||||
Foo.b = function () { };
|
||||
Foo.c = 3;
|
||||
|
||||
const objects = [{ a: 1 }, { a: 1, b: Foo.b, c: 3 }];
|
||||
const actual = objects.map(object => isMatch(object, Foo));
|
||||
|
||||
expect(actual).toEqual([false, true]);
|
||||
});
|
||||
|
||||
it(`should work with a non-plain \`object\``, () => {
|
||||
interface Foo {
|
||||
a: number;
|
||||
b: number;
|
||||
c: number;
|
||||
}
|
||||
|
||||
interface FooConstructor {
|
||||
new(arg: Partial<Foo>): Foo;
|
||||
}
|
||||
|
||||
const Foo = function Foo(this: Foo, object: Partial<Foo>) {
|
||||
Object.assign(this, object);
|
||||
} as any as FooConstructor;
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
const object = new Foo({ a: new Foo({ b: 2, c: 3 }) });
|
||||
|
||||
expect(isMatch(object, { a: { b: 2 } })).toBe(true);
|
||||
});
|
||||
|
||||
it(`should partial match arrays`, () => {
|
||||
const objects = [{ a: ['b'] }, { a: ['c', 'd'] }];
|
||||
let actual = objects.filter(x => isMatch(x, { a: ['d'] }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
|
||||
actual = objects.filter(x => isMatch(x, { a: ['b', 'd'] }));
|
||||
expect(actual).toEqual([]);
|
||||
|
||||
actual = objects.filter(x => isMatch(x, { a: ['d', 'b'] }));
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it(`should partial match arrays with duplicate values`, () => {
|
||||
const objects = [{ a: [1, 2] }, { a: [2, 2] }];
|
||||
const actual = objects.filter(x => isMatch(x, { a: [2, 2] }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
});
|
||||
|
||||
it('should partial match arrays of objects', () => {
|
||||
const objects = [
|
||||
{
|
||||
a: [
|
||||
{ b: 1, c: 2 },
|
||||
{ b: 4, c: 5, d: 6 },
|
||||
],
|
||||
},
|
||||
{
|
||||
a: [
|
||||
{ b: 1, c: 2 },
|
||||
{ b: 4, c: 6, d: 7 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const actual = objects.filter(x => isMatch(x, { a: [{ b: 1 }, { b: 4, c: 5 }] }));
|
||||
expect(actual).toEqual([objects[0]]);
|
||||
});
|
||||
|
||||
it(`should partial match maps`, () => {
|
||||
const objects = [{ a: new Map() }, { a: new Map() }];
|
||||
objects[0].a.set('a', 1);
|
||||
objects[1].a.set('a', 1);
|
||||
objects[1].a.set('b', 2);
|
||||
|
||||
const map = new Map();
|
||||
map.set('b', 2);
|
||||
let actual = objects.filter(x => isMatch(x, { a: map }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
|
||||
map.delete('b');
|
||||
actual = objects.filter(x => isMatch(x, { a: map }));
|
||||
|
||||
expect(actual).toEqual(objects);
|
||||
|
||||
map.set('c', 3);
|
||||
actual = objects.filter(x => isMatch(x, { a: map }));
|
||||
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it(`should partial match sets`, () => {
|
||||
const objects = [{ a: new Set() }, { a: new Set() }];
|
||||
objects[0].a.add(1);
|
||||
objects[1].a.add(1);
|
||||
objects[1].a.add(2);
|
||||
|
||||
const set = new Set();
|
||||
set.add(2);
|
||||
let actual = objects.filter(x => isMatch(x, { a: set }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
|
||||
set.delete(2);
|
||||
actual = objects.filter(x => isMatch(x, { a: set }));
|
||||
|
||||
expect(actual).toEqual(objects);
|
||||
|
||||
set.add(3);
|
||||
actual = objects.filter(x => isMatch(x, { a: set }));
|
||||
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it(`should match \`undefined\` values`, () => {
|
||||
const objects1 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }];
|
||||
const actual1 = objects1.map(x => isMatch(x, { b: undefined }));
|
||||
const expected1 = [false, false, true];
|
||||
|
||||
expect(actual1).toEqual(expected1);
|
||||
|
||||
const objects2 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }];
|
||||
const actual2 = objects2.map(x => isMatch(x, { a: 1, b: undefined }));
|
||||
const expected2 = [false, false, true];
|
||||
|
||||
expect(actual2).toEqual(expected2);
|
||||
|
||||
const objects3 = [{ a: { b: 2 } }, { a: { b: 2, c: 3 } }, { a: { b: 2, c: undefined } }];
|
||||
const actual3 = objects3.map(x => isMatch(x, { a: { c: undefined } }));
|
||||
const expected3 = [false, false, true];
|
||||
|
||||
expect(actual3).toEqual(expected3);
|
||||
});
|
||||
|
||||
it(`should match \`undefined\` values on primitives`, () => {
|
||||
const numberProto = Number.prototype;
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
numberProto.a = 1;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
numberProto.b = undefined;
|
||||
|
||||
try {
|
||||
expect(isMatch(1, { b: undefined })).toBe(true);
|
||||
} catch (e: any) {
|
||||
expect(false, e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
expect(isMatch(1, { a: 1, b: undefined })).toBe(true);
|
||||
} catch (e: any) {
|
||||
expect(false, e.message);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
numberProto.a = { b: 1, c: undefined };
|
||||
try {
|
||||
expect(isMatch(1, { a: { c: undefined } })).toBe(true);
|
||||
} catch (e: any) {
|
||||
expect(false, e.message);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete numberProto.a;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete numberProto.b;
|
||||
});
|
||||
|
||||
it(`should return \`false\` when \`object\` is nullish`, () => {
|
||||
const values = [, null, undefined];
|
||||
const expected = values.map(() => false);
|
||||
|
||||
const actual = values.map((value, index) => {
|
||||
try {
|
||||
return index ? isMatch(value, { a: 1 }) : isMatch(undefined, { a: 1 });
|
||||
} catch (e: any) { }
|
||||
});
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should return \`true\` when comparing an empty \`source\``, () => {
|
||||
const object = { a: 1 };
|
||||
const expected = empties.map(stubTrue);
|
||||
|
||||
const actual = empties.map(value => {
|
||||
return isMatch(object, value);
|
||||
});
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should return \`true\` when comparing an empty \`source\` to a nullish \`object\``, () => {
|
||||
const values = [, null, undefined];
|
||||
const expected = values.map(stubTrue);
|
||||
|
||||
const actual = values.map((value, index) => {
|
||||
try {
|
||||
return index ? isMatch(value, {}) : isMatch(undefined, {});
|
||||
} catch (e: any) { }
|
||||
});
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should return \`true\` when comparing a \`source\` of empty arrays and objects`, () => {
|
||||
const objects = [
|
||||
{ a: [1], b: { c: 1 } },
|
||||
{ a: [2, 3], b: { d: 2 } },
|
||||
];
|
||||
const actual = objects.filter(x => isMatch(x, { a: [], b: {} }));
|
||||
|
||||
expect(actual).toEqual(objects);
|
||||
});
|
||||
});
|
99
src/compat/predicate/isMatch.ts
Normal file
99
src/compat/predicate/isMatch.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { isPrimitive } from '../../predicate';
|
||||
import { isArrayMatch } from '../_internal/isArrayMatch';
|
||||
import { isMapMatch } from '../_internal/isMapMatch';
|
||||
import { isSetMatch } from '../_internal/isSetMatch';
|
||||
|
||||
/**
|
||||
* Checks if the target matches the source by comparing their structures and values.
|
||||
* This function supports deep comparison for objects, arrays, maps, and sets.
|
||||
*
|
||||
* @param {unknown} target - The target value to match against.
|
||||
* @param {unknown} source - The source value to match with.
|
||||
* @returns {boolean} - Returns `true` if the target matches the source, otherwise `false`.
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* isMatch({ a: 1, b: 2 }, { a: 1 }); // true
|
||||
*
|
||||
* @example
|
||||
* // Matching arrays
|
||||
* isMatch([1, 2, 3], [1, 2, 3]); // true
|
||||
*
|
||||
* @example
|
||||
* // Matching maps
|
||||
* const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
|
||||
* const sourceMap = new Map([['key1', 'value1']]);
|
||||
* isMatch(targetMap, sourceMap); // true
|
||||
*
|
||||
* @example
|
||||
* // Matching sets
|
||||
* const targetSet = new Set([1, 2, 3]);
|
||||
* const sourceSet = new Set([1, 2]);
|
||||
* isMatch(targetSet, sourceSet); // true
|
||||
*/
|
||||
export function isMatch(target: unknown, source: unknown);
|
||||
export function isMatch(target: any, source: any) {
|
||||
if (source === target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (typeof source) {
|
||||
case 'object': {
|
||||
if (source == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
source = source ?? {};
|
||||
|
||||
const keys = Object.keys(source as any);
|
||||
|
||||
if (target == null) {
|
||||
if (keys.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(source)) {
|
||||
return isArrayMatch(target, source);
|
||||
}
|
||||
|
||||
if (source instanceof Map) {
|
||||
return isMapMatch(target, source);
|
||||
}
|
||||
|
||||
if (source instanceof Set) {
|
||||
return isSetMatch(target, source);
|
||||
}
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!isPrimitive(target) && !(key in target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source[key] === undefined && target[key] !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isMatch(target[key], source[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case 'function': {
|
||||
if (Object.keys(source).length > 0) {
|
||||
return isMatch(target, { ...source });
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
return !source;
|
||||
}
|
||||
}
|
||||
}
|
375
src/compat/predicate/matches.spec.ts
Normal file
375
src/compat/predicate/matches.spec.ts
Normal file
@ -0,0 +1,375 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { noop } from '../../function/noop';
|
||||
import { empties } from '../_internal/empties';
|
||||
import { stubTrue } from '../_internal/stubTrue';
|
||||
import { matches } from './matches';
|
||||
|
||||
describe('matches', () => {
|
||||
it(`should perform a deep comparison between \`source\` and \`object\``, () => {
|
||||
let object: any = { a: 1, b: 2, c: 3 };
|
||||
|
||||
const isMatch1 = matches({ a: 1 });
|
||||
expect(isMatch1(object)).toBe(true);
|
||||
|
||||
const isMatch2 = matches({ b: 2 });
|
||||
expect(isMatch2(object)).toBe(true);
|
||||
|
||||
const isMatch3 = matches({ a: 1, c: 3 });
|
||||
expect(isMatch3(object)).toBe(true);
|
||||
|
||||
const isMatch4 = matches({ c: 3, d: 4 });
|
||||
expect(isMatch4(object)).toBe(false);
|
||||
|
||||
const isMatch5 = matches({ a: { b: { c: 1 } } });
|
||||
expect(isMatch5({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 })).toBe(true);
|
||||
});
|
||||
|
||||
it(`should match inherited string keyed \`object\` properties`, () => {
|
||||
interface Foo {
|
||||
a: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
interface FooConstructor {
|
||||
new (): Foo;
|
||||
}
|
||||
|
||||
const Foo = function Foo(this: Foo) {
|
||||
this.a = 1;
|
||||
} as any as FooConstructor;
|
||||
|
||||
Foo.prototype.b = 2;
|
||||
|
||||
const object = { a: new Foo() };
|
||||
const isMatch = matches({ a: { b: 2 } });
|
||||
|
||||
expect(isMatch(object)).toBe(true);
|
||||
});
|
||||
|
||||
it(`should not match by inherited \`source\` properties`, () => {
|
||||
interface Foo {
|
||||
a: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
interface FooConstructor {
|
||||
new (): Foo;
|
||||
}
|
||||
|
||||
const Foo = function Foo(this: Foo) {
|
||||
this.a = 1;
|
||||
} as any as FooConstructor;
|
||||
|
||||
Foo.prototype.b = 2;
|
||||
|
||||
const objects = [{ a: 1 }, { a: 1, b: 2 }];
|
||||
const source = new Foo();
|
||||
const actual = objects.map(matches(source));
|
||||
const expected = objects.map(stubTrue);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should compare a variety of \`source\` property values`, () => {
|
||||
const object1 = { a: false, b: true, c: '3', d: 4, e: [5], f: { g: 6 } };
|
||||
const object2 = { a: 0, b: 1, c: 3, d: '4', e: ['5'], f: { g: '6' } };
|
||||
|
||||
const isMatch = matches(object1);
|
||||
|
||||
expect(isMatch(object1)).toBe(true);
|
||||
expect(isMatch(object2)).toBe(false);
|
||||
});
|
||||
|
||||
it(`should match \`-0\` as \`0\``, () => {
|
||||
const object1 = { a: -0 };
|
||||
const object2 = { a: 0 };
|
||||
|
||||
const isMatch1 = matches(object1);
|
||||
const isMatch2 = matches(object2);
|
||||
|
||||
expect(isMatch1(object2)).toBe(true);
|
||||
expect(isMatch2(object1)).toBe(true);
|
||||
});
|
||||
|
||||
it(`should compare functions by reference`, () => {
|
||||
const object1 = { a: noop };
|
||||
const object2 = { a: () => {} };
|
||||
const object3 = { a: {} };
|
||||
|
||||
const isMatch = matches(object1);
|
||||
|
||||
expect(isMatch(object1)).toBe(true);
|
||||
expect(isMatch(object2)).toBe(false);
|
||||
expect(isMatch(object3)).toBe(false);
|
||||
});
|
||||
|
||||
it(`should work with a function for \`object\``, () => {
|
||||
function Foo() {}
|
||||
Foo.a = { b: 2, c: 3 };
|
||||
|
||||
const isMatch = matches({ a: { b: 2 } });
|
||||
|
||||
expect(isMatch(Foo)).toBe(true);
|
||||
});
|
||||
|
||||
it(`should work with a function for \`source\``, () => {
|
||||
function Foo() {}
|
||||
Foo.a = 1;
|
||||
Foo.b = function () {};
|
||||
Foo.c = 3;
|
||||
|
||||
const objects = [{ a: 1 }, { a: 1, b: Foo.b, c: 3 }];
|
||||
const actual = objects.map(matches(Foo));
|
||||
|
||||
expect(actual).toEqual([false, true]);
|
||||
});
|
||||
|
||||
it(`should work with a non-plain \`object\``, () => {
|
||||
interface Foo {
|
||||
a: number;
|
||||
b: number;
|
||||
c: number;
|
||||
}
|
||||
|
||||
interface FooConstructor {
|
||||
new (arg: Partial<Foo>): Foo;
|
||||
}
|
||||
|
||||
const Foo = function Foo(this: Foo, object: Partial<Foo>) {
|
||||
Object.assign(this, object);
|
||||
} as any as FooConstructor;
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
const object = new Foo({ a: new Foo({ b: 2, c: 3 }) });
|
||||
|
||||
const isMatch = matches({ a: { b: 2 } });
|
||||
|
||||
expect(isMatch(object)).toBe(true);
|
||||
});
|
||||
|
||||
it(`should partial match arrays`, () => {
|
||||
const objects = [{ a: ['b'] }, { a: ['c', 'd'] }];
|
||||
let actual = objects.filter(matches({ a: ['d'] }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
|
||||
actual = objects.filter(matches({ a: ['b', 'd'] }));
|
||||
expect(actual).toEqual([]);
|
||||
|
||||
actual = objects.filter(matches({ a: ['d', 'b'] }));
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it(`should partial match arrays with duplicate values`, () => {
|
||||
const objects = [{ a: [1, 2] }, { a: [2, 2] }];
|
||||
const actual = objects.filter(matches({ a: [2, 2] }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
});
|
||||
|
||||
it('should partial match arrays of objects', () => {
|
||||
const objects = [
|
||||
{
|
||||
a: [
|
||||
{ b: 1, c: 2 },
|
||||
{ b: 4, c: 5, d: 6 },
|
||||
],
|
||||
},
|
||||
{
|
||||
a: [
|
||||
{ b: 1, c: 2 },
|
||||
{ b: 4, c: 6, d: 7 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const actual = objects.filter(matches({ a: [{ b: 1 }, { b: 4, c: 5 }] }));
|
||||
expect(actual).toEqual([objects[0]]);
|
||||
});
|
||||
|
||||
it(`should partial match maps`, () => {
|
||||
const objects = [{ a: new Map() }, { a: new Map() }];
|
||||
objects[0].a.set('a', 1);
|
||||
objects[1].a.set('a', 1);
|
||||
objects[1].a.set('b', 2);
|
||||
|
||||
const map = new Map();
|
||||
map.set('b', 2);
|
||||
let actual = objects.filter(matches({ a: map }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
|
||||
map.delete('b');
|
||||
actual = objects.filter(matches({ a: map }));
|
||||
|
||||
expect(actual).toEqual(objects);
|
||||
|
||||
map.set('c', 3);
|
||||
actual = objects.filter(matches({ a: map }));
|
||||
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it(`should partial match sets`, () => {
|
||||
const objects = [{ a: new Set() }, { a: new Set() }];
|
||||
objects[0].a.add(1);
|
||||
objects[1].a.add(1);
|
||||
objects[1].a.add(2);
|
||||
|
||||
const set = new Set();
|
||||
set.add(2);
|
||||
let actual = objects.filter(matches({ a: set }));
|
||||
|
||||
expect(actual).toEqual([objects[1]]);
|
||||
|
||||
set.delete(2);
|
||||
actual = objects.filter(matches({ a: set }));
|
||||
|
||||
expect(actual).toEqual(objects);
|
||||
|
||||
set.add(3);
|
||||
actual = objects.filter(matches({ a: set }));
|
||||
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it(`should match \`undefined\` values`, () => {
|
||||
const objects1 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }];
|
||||
const actual1 = objects1.map(matches({ b: undefined }));
|
||||
const expected1 = [false, false, true];
|
||||
|
||||
expect(actual1).toEqual(expected1);
|
||||
|
||||
const objects2 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }];
|
||||
const actual2 = objects2.map(matches({ a: 1, b: undefined }));
|
||||
const expected2 = [false, false, true];
|
||||
|
||||
expect(actual2).toEqual(expected2);
|
||||
|
||||
const objects3 = [{ a: { b: 2 } }, { a: { b: 2, c: 3 } }, { a: { b: 2, c: undefined } }];
|
||||
const actual3 = objects3.map(matches({ a: { c: undefined } }));
|
||||
const expected3 = [false, false, true];
|
||||
|
||||
expect(actual3).toEqual(expected3);
|
||||
});
|
||||
|
||||
it(`should match \`undefined\` values on primitives`, () => {
|
||||
const numberProto = Number.prototype;
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
numberProto.a = 1;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
numberProto.b = undefined;
|
||||
|
||||
try {
|
||||
const isMatch = matches({ b: undefined });
|
||||
expect(isMatch(1)).toBe(true);
|
||||
} catch (e: any) {
|
||||
expect(false, e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const isMatch = matches({ a: 1, b: undefined });
|
||||
expect(isMatch(1)).toBe(true);
|
||||
} catch (e: any) {
|
||||
expect(false, e.message);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
numberProto.a = { b: 1, c: undefined };
|
||||
try {
|
||||
const isMatch = matches({ a: { c: undefined } });
|
||||
expect(isMatch(1)).toBe(true);
|
||||
} catch (e: any) {
|
||||
expect(false, e.message);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete numberProto.a;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete numberProto.b;
|
||||
});
|
||||
|
||||
it(`should return \`false\` when \`object\` is nullish`, () => {
|
||||
const values = [, null, undefined];
|
||||
const expected = values.map(() => false);
|
||||
|
||||
const isMatch = matches({ a: 1 });
|
||||
|
||||
const actual = values.map((value, index) => {
|
||||
try {
|
||||
return index ? isMatch(value) : isMatch(undefined);
|
||||
} catch (e: any) {}
|
||||
});
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should return \`true\` when comparing an empty \`source\``, () => {
|
||||
const object = { a: 1 };
|
||||
const expected = empties.map(stubTrue);
|
||||
|
||||
const actual = empties.map(value => {
|
||||
const isMatch = matches(object);
|
||||
|
||||
return isMatch(object);
|
||||
});
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should return \`true\` when comparing an empty \`source\` to a nullish \`object\``, () => {
|
||||
const values = [, null, undefined];
|
||||
const expected = values.map(stubTrue);
|
||||
|
||||
const isMatch = matches({});
|
||||
|
||||
const actual = values.map((value, index) => {
|
||||
try {
|
||||
return index ? isMatch(value) : isMatch(undefined);
|
||||
} catch (e: any) {}
|
||||
});
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it(`should return \`true\` when comparing a \`source\` of empty arrays and objects`, () => {
|
||||
const objects = [
|
||||
{ a: [1], b: { c: 1 } },
|
||||
{ a: [2, 3], b: { d: 2 } },
|
||||
];
|
||||
const actual = objects.filter(matches({ a: [], b: {} }));
|
||||
|
||||
expect(actual).toEqual(objects);
|
||||
});
|
||||
|
||||
it('should not change behavior if `source` is modified', () => {
|
||||
const sources = [{ a: { b: 2, c: 3 } }, { a: 1, b: 2 }, { a: 1 }];
|
||||
|
||||
sources.forEach((source: any, index) => {
|
||||
const object = structuredClone(source);
|
||||
const isMatch = matches(source);
|
||||
|
||||
expect(isMatch(object)).toBe(true);
|
||||
|
||||
if (index) {
|
||||
source.a = 2;
|
||||
source.b = 1;
|
||||
source.c = 3;
|
||||
} else {
|
||||
source.a.b = 1;
|
||||
source.a.c = 2;
|
||||
source.a.d = 3;
|
||||
}
|
||||
|
||||
expect(isMatch(object)).toBe(true);
|
||||
expect(isMatch(source)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
34
src/compat/predicate/matches.ts
Normal file
34
src/compat/predicate/matches.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { cloneDeep } from '../../object/cloneDeep.ts';
|
||||
import { isMatch } from './isMatch.ts';
|
||||
|
||||
/**
|
||||
* Creates a function that performs a deep comparison between a given target and the source object.
|
||||
*
|
||||
* @param {unknown} source - The source object to create the matcher from.
|
||||
* @returns {(target: unknown) => boolean} - Returns a function that takes a target object and returns `true` if the target matches the source, otherwise `false`.
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* const matcher = matches({ a: 1, b: 2 });
|
||||
* matcher({ a: 1, b: 2, c: 3 }); // true
|
||||
* matcher({ a: 1, c: 3 }); // false
|
||||
*
|
||||
* @example
|
||||
* // Matching arrays
|
||||
* const arrayMatcher = matches([1, 2, 3]);
|
||||
* arrayMatcher([1, 2, 3, 4]); // true
|
||||
* arrayMatcher([4, 5, 6]); // false
|
||||
*
|
||||
* @example
|
||||
* // Matching objects with nested structures
|
||||
* const nestedMatcher = matches({ a: { b: 2 } });
|
||||
* nestedMatcher({ a: { b: 2, c: 3 } }); // true
|
||||
* nestedMatcher({ a: { c: 3 } }); // false
|
||||
*/
|
||||
export function matches(source: unknown): (target: unknown) => boolean {
|
||||
source = cloneDeep(source);
|
||||
|
||||
return (target?: unknown): boolean => {
|
||||
return isMatch(target, source);
|
||||
};
|
||||
}
|
@ -2,15 +2,6 @@ import { describe, expect, it } from 'vitest';
|
||||
import { cloneDeep } from './cloneDeep';
|
||||
|
||||
describe('cloneDeep', () => {
|
||||
//-------------------------------------------------------------------------------------
|
||||
// function
|
||||
//-------------------------------------------------------------------------------------
|
||||
it('not support function', () => {
|
||||
const func = () => {};
|
||||
const clonedFunc = cloneDeep(func);
|
||||
expect(clonedFunc).toBe(undefined);
|
||||
});
|
||||
|
||||
//-------------------------------------------------------------------------------------
|
||||
// primitive
|
||||
//-------------------------------------------------------------------------------------
|
||||
@ -167,6 +158,7 @@ describe('cloneDeep', () => {
|
||||
expect(b['#b']).toBe(undefined);
|
||||
expect(b).toEqual({
|
||||
props: { a: 'es-toolkit' },
|
||||
d,
|
||||
c: 2,
|
||||
});
|
||||
});
|
||||
|
@ -112,7 +112,7 @@ export function cloneDeep<T>(obj: T): Resolved<T> {
|
||||
}
|
||||
|
||||
if (obj instanceof Error) {
|
||||
const result = new (obj.constructor as { new (): Error })();
|
||||
const result = new (obj.constructor as { new(): Error })();
|
||||
result.message = obj.message;
|
||||
result.name = obj.name;
|
||||
result.stack = obj.stack;
|
||||
@ -127,10 +127,6 @@ export function cloneDeep<T>(obj: T): Resolved<T> {
|
||||
return result as Resolved<T>;
|
||||
}
|
||||
|
||||
if (typeof obj === 'function') {
|
||||
return void 0 as Resolved<T>;
|
||||
}
|
||||
|
||||
return obj as Resolved<T>;
|
||||
}
|
||||
|
||||
@ -141,12 +137,14 @@ function isPrimitive(value: unknown): value is Primitive {
|
||||
|
||||
// eslint-disable-next-line
|
||||
function cloneDeepHelper(obj: any, clonedObj: any): void {
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
||||
if ((descriptor?.writable || descriptor?.set) && typeof descriptor?.value !== 'function') {
|
||||
clonedObj[key] = cloneDeep(obj[key]);
|
||||
}
|
||||
const keys = Object.keys(obj);
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
||||
|
||||
if ((descriptor?.writable || descriptor?.set)) {
|
||||
clonedObj[key] = cloneDeep(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -188,82 +186,82 @@ type Equal<X, Y> = X extends Y ? (Y extends X ? true : false) : false;
|
||||
type ResolvedMain<T> = T extends [never]
|
||||
? never // (special trick for jsonable | null) type
|
||||
: ValueOf<T> extends boolean | number | bigint | string
|
||||
? ValueOf<T>
|
||||
: T extends (...args: any[]) => any
|
||||
? never
|
||||
: T extends object
|
||||
? ResolvedObject<T>
|
||||
: ValueOf<T>;
|
||||
? ValueOf<T>
|
||||
: T extends (...args: any[]) => any
|
||||
? never
|
||||
: T extends object
|
||||
? ResolvedObject<T>
|
||||
: ValueOf<T>;
|
||||
|
||||
type ResolvedObject<T extends object> =
|
||||
T extends Array<infer U>
|
||||
? IsTuple<T> extends true
|
||||
? ResolvedTuple<T>
|
||||
: Array<ResolvedMain<U>>
|
||||
: T extends Set<infer U>
|
||||
? Set<ResolvedMain<U>>
|
||||
: T extends Map<infer K, infer V>
|
||||
? Map<ResolvedMain<K>, ResolvedMain<V>>
|
||||
: T extends WeakSet<any> | WeakMap<any, any>
|
||||
? never
|
||||
: T extends
|
||||
| Date
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Uint16Array
|
||||
| Uint32Array
|
||||
| BigUint64Array
|
||||
| Int8Array
|
||||
| Int16Array
|
||||
| Int32Array
|
||||
| BigInt64Array
|
||||
| Float32Array
|
||||
| Float64Array
|
||||
| ArrayBuffer
|
||||
| SharedArrayBuffer
|
||||
| DataView
|
||||
| Blob
|
||||
| File
|
||||
? T
|
||||
: {
|
||||
[P in keyof T]: ResolvedMain<T[P]>;
|
||||
};
|
||||
? IsTuple<T> extends true
|
||||
? ResolvedTuple<T>
|
||||
: Array<ResolvedMain<U>>
|
||||
: T extends Set<infer U>
|
||||
? Set<ResolvedMain<U>>
|
||||
: T extends Map<infer K, infer V>
|
||||
? Map<ResolvedMain<K>, ResolvedMain<V>>
|
||||
: T extends WeakSet<any> | WeakMap<any, any>
|
||||
? never
|
||||
: T extends
|
||||
| Date
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Uint16Array
|
||||
| Uint32Array
|
||||
| BigUint64Array
|
||||
| Int8Array
|
||||
| Int16Array
|
||||
| Int32Array
|
||||
| BigInt64Array
|
||||
| Float32Array
|
||||
| Float64Array
|
||||
| ArrayBuffer
|
||||
| SharedArrayBuffer
|
||||
| DataView
|
||||
| Blob
|
||||
| File
|
||||
? T
|
||||
: {
|
||||
[P in keyof T]: ResolvedMain<T[P]>;
|
||||
};
|
||||
|
||||
type ResolvedTuple<T extends readonly any[]> = T extends []
|
||||
? []
|
||||
: T extends [infer F]
|
||||
? [ResolvedMain<F>]
|
||||
: T extends [infer F, ...infer Rest extends readonly any[]]
|
||||
? [ResolvedMain<F>, ...ResolvedTuple<Rest>]
|
||||
: T extends [(infer F)?]
|
||||
? [ResolvedMain<F>?]
|
||||
: T extends [(infer F)?, ...infer Rest extends readonly any[]]
|
||||
? [ResolvedMain<F>?, ...ResolvedTuple<Rest>]
|
||||
: [];
|
||||
? [ResolvedMain<F>]
|
||||
: T extends [infer F, ...infer Rest extends readonly any[]]
|
||||
? [ResolvedMain<F>, ...ResolvedTuple<Rest>]
|
||||
: T extends [(infer F)?]
|
||||
? [ResolvedMain<F>?]
|
||||
: T extends [(infer F)?, ...infer Rest extends readonly any[]]
|
||||
? [ResolvedMain<F>?, ...ResolvedTuple<Rest>]
|
||||
: [];
|
||||
|
||||
type IsTuple<T extends readonly any[] | { length: number }> = [T] extends [never]
|
||||
? false
|
||||
: T extends readonly any[]
|
||||
? number extends T['length']
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
? number extends T['length']
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
|
||||
type ValueOf<Instance> =
|
||||
IsValueOf<Instance, boolean> extends true
|
||||
? boolean
|
||||
: IsValueOf<Instance, number> extends true
|
||||
? number
|
||||
: IsValueOf<Instance, string> extends true
|
||||
? string
|
||||
: Instance;
|
||||
? boolean
|
||||
: IsValueOf<Instance, number> extends true
|
||||
? number
|
||||
: IsValueOf<Instance, string> extends true
|
||||
? string
|
||||
: Instance;
|
||||
|
||||
type IsValueOf<Instance, O extends IValueOf<any>> = Instance extends O
|
||||
? O extends IValueOf<infer Primitive>
|
||||
? Instance extends Primitive
|
||||
? false
|
||||
: true // not Primitive, but Object
|
||||
: false // cannot be
|
||||
? Instance extends Primitive
|
||||
? false
|
||||
: true // not Primitive, but Object
|
||||
: false // cannot be
|
||||
: false;
|
||||
|
||||
interface IValueOf<T> {
|
||||
|
Loading…
Reference in New Issue
Block a user