feat(isMatch, matches): Add isMatch & matches in compat

This commit is contained in:
raon0211 2024-07-25 11:36:14 +09:00
parent e262fa1b13
commit c350b23ee2
26 changed files with 1461 additions and 79 deletions

View 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 } } })
});
});

View 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 });
});
});

View File

@ -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' },

View File

@ -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' },

View File

@ -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' },

View 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
```

View 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
```

View 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
```

View 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
```

View File

@ -1,5 +1,9 @@
# isArray
::: info
此函数与 lodash 完全兼容。您可以在我们的[兼容性库](../../../compatibility.md)中找到它,`es-toolkit/compat`。
:::
检查给定的值是否为数组。
该函数测试提供的值是否为数组。

View 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
```

View 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
```

View 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);
});
});

View 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;
}

View 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);
});
});

View 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;
}

View 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);
});
});

View 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]);
}

View File

@ -0,0 +1 @@
export const numberProto: any = Number.prototype;

View File

@ -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';

View 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);
});
});

View 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;
}
}
}

View 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);
});
});
});

View 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);
};
}

View File

@ -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,
});
});

View File

@ -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> {