Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(utils): isSubset #309

Merged
merged 20 commits into from
Jul 8, 2024
Merged

Conversation

beberiche
Copy link
Contributor

@beberiche beberiche commented Jul 6, 2024

Overview

Issue: #306

두번째 인자로 주어지는 배열의 모든 요소를 첫번째 인자의 배열이 완전히 포함하는지(부분집합) 에 대한 여부 boolean 를 반환합니다.

배열 요소의 타입이 참조형인 경우, 깊은 비교를 진행하며, iteratee 함수 인자를 정의하여 비교항목을 설정하는 것이 가능합니다.

고민점 1. (배열요소가 참조형인 경우 부분집합의 정의)

예를들어, [{1:1, 2:2}, {3:3, 4:4}][{2:2}] 의 경우에 대해 {2:2}{1:1, 2:2} 의 부분집합이라고 볼 수 있냐는 것 입니다.

결과적으로 저는 부분집합이 아니라고 판단했습니다. 참조형 역시 여러 값이 모인 하나의 집합이라고 볼 수 있으며, 객체 입장으로 바라보았을 때{2:2}, {1:1, 2:2} 는 전혀 다른 구조를 가지므로 부분집합 관계라 정의하기 어렵다고 해석했습니다.

고민점 2. (시간복잡도)

참조형 요소의 속성과 값이 서로 일치하는지 확인하기 위해서는 깊은 비교 가 필요합니다. 다만, 객체 내부의 객체, 또 내부의 객체 등 객체의 깊이가 깊을 수록, 시간복잡도는 한없이 증가한다는 문제가 있습니다.

이에 대해 크게 2가지는 해결안을 모색했습니다.

  • 깊이에 상관없이 참조형 요소의 모든 항목을 비교한다.
    • 장점 : 깊이에 상관없이 부분집합 여부를 확인할 수 있다.
    • 단점 : 객체의 수가 많고, 깊이가 깊다면 시간복잡도가 매우 커진다. O(N*M*d)
  • 깊이가 2보다 미만인 참조형 요소만 인자로 반영되도록 한다.
    • 장점 : 빠른 비교가 가능하다.
    • 단점 : 깊이가 2보다 큰 객체 요소는 부분집합 여부를 확인할 수 없다.

현재 구현된 isSubset() 함수는 깊이에 상관없이 참조형 요소의 모든 항목을 비교하는 형태로 로직을 구성했습니다.

이미 라이브러리 내에 deepEqual() 이 구현되어 있기도 했고, 이왕이면 대부분의 케이스를 반영하는 것이 라이브러리 구현에 적합하다고 판단했습니다.

단, 두 해결책 모두 장단점이 명확하기 때문에 이 부분은 메인테이너님의 의견도 한번 들어본 후에 다시 방향성을 고민해보려고 합니다.

close #306

PR Checklist

  • All tests pass.
  • All type checks pass.
  • I have read the Contributing Guide document.
    Contributing Guide

@beberiche beberiche requested a review from ssi02014 as a code owner July 6, 2024 13:01
Copy link

changeset-bot bot commented Jul 6, 2024

🦋 Changeset detected

Latest commit: 9308730

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@modern-kit/utils Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ssi02014 ssi02014 added feature 새로운 기능 추가 @modern-kit/utils @modern-kit/utils labels Jul 6, 2024
@ssi02014
Copy link
Contributor

ssi02014 commented Jul 6, 2024

@beberiche 우선 작업해주셔서 정말 감사합니다 :)
제가 말씀드릴 내용이 고민점1고민점2 함께 맞물려있는 것 같은데, 제 의견은 isSubset함수에서 참조형일 때 이것이 부분집합인지 판단하는 책임은 온전히 사용자에게 위임했으면 합니다.

이슈의 코멘트로 남긴 예제처럼 부분 집합 임을 결정하는건 id만 사용 할 수 있게 사용자가 직접 선택하게끔 하는 것입니다.

const arr1 = [{ id: 1, name: 'mj'}, { id: 2 }, {id: 3}];
const arr2 = [{ id: 1}, { id: 2, name: 'gromit' }];

isSubset(arr1, arr2, (item) => item.id); // true

{ id: 1, name: 'mj'}{ id: 1}를 같다고 판단할 수 있냐?의 문제는 결국 사용자가 직접 결정할 수 있기 때문에 더이상의 문제가 되지 않습니다.

또한, 이렇게 될 경우 우리는 참조형일 때 굳이 깊은 비교를 하지 않아도 되며, 따라서 현재의 시간복잡도에 대한 고민점도 해결이 가능합니다.

만약, 깊은 중첩 객체 혹은 배열의 케이스에는 JSON.strinify와 같은 함수를 이용 할 수 있습니다.

const arr1 = [{ id: 1, info: { address: 'seou' }}, { id: 2, info: { address: 'busan' }}];
const arr2 = [{ id: 2, info: { address: 'busan' }}];

isSubset(arr1, arr2, (item) => JSON.strinify(item));

또는 iteratee의 반환값을 특정해서 지정해도 됩니다. ex: (item) => ({ id: item.id, address: item.info.address}))
(JSON.stringify를 사용할지 위와 같이 사용할지도 결국 사용자의 결정입니다!!)
(사실 실제 개발을 할 때 이런 중첩 객체 혹은 중첩 배열에 대한 부분 집합 비교는 흔하지는 않을 거라고 생각이 들기도합니다 ..!)

제 의견을 정리하자면, 아래와 같습니다.

  1. 기본적으로 원시값에 대한 비교를 한다.
  2. 참조형과 같은 케이스는 iteratee를 통해 사용자가 직접 부분 집합을 판단할 수 있게 책임을 위임한다.

@beberiche
Copy link
Contributor Author

@ssi02014
너무 깊게 고민하고 있었나봅니다!
말씀해주신 수정사항에 맞춰 다시 올리겠습니다!

@beberiche
Copy link
Contributor Author

@ssi02014
코드 수정해서 다시 올렸습니다.

Copy link
Contributor

@ssi02014 ssi02014 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 면에서 검토하느라 리뷰를 늦은 밤에 남기는 점 양해부탁드립니다. 🙏

기존에 있던 difference 함수를 재사용해서 구현하는 방향으로 수정되면 좋을 것 같습니다.

저는 이런 방향성을 위해 간단하지만 difference의 성능 개선 작업을 진행하였습니다. (즉, 최신 main 브랜치와 싱크를 맞춰주세요!)

  • modern-kit의 difference는 lodash의 같은 기능인 differenceBy보다 약 1.8~2배가량 빠릅니다.

즉, 아래와 같이 변경을 요청드립니다 :)

const isSubset = <T, U>(
  superset: T[] | readonly T[],
  subset: T[] | readonly T[],
  iteratee?: (item: T) => U
) => {
  return difference(subset, superset, iteratee).length === 0;
};
  1. 부분집합을 검증하는 의미에서 superset, subset 으로 인자의 네이밍 변경
  2. superset, subset의 타입을 다른 함수들과 컨벤션을 맞추기 위해 T[] | readonly T[] 로 변경
  3. difference 재사용
  4. 문서테스트 코드 변수 네이밍 변경

성능 테스트

실제로 제안주신 코드(isSubsetDefault)와 difference를 활용한 isSubset 코드(isSubsetWithDifference)의 성능 차이는 크게 없습니다.

  • 성능 테스트 실행 할 때마다 결과가 다릅니다. 따라서 이정도 차이는 크게 고려하지 않아도 된다고 판단됩니다

스크린샷 2024-07-08 오전 12 52 46

스크린샷 2024-07-08 오전 12 54 06

스크린샷 2024-07-08 오전 12 54 27

Comment on lines 1 to 70
import { isSubset } from '.';

describe('isSubset', () => {
it('if the childArray is a subset of the parentArray', () => {
const parentArray = [1, 2, 3, 4];
const childArray1 = [1, 3];
const childArray2 = [1, 5];

expect(isSubset(parentArray, childArray1)).toBeTruthy();
expect(isSubset(parentArray, childArray2)).toBeFalsy();
});

it('if the type is dfferent between comparison elements', () => {
const parentArray = ['1', 2, 3, 4];
const childArray1 = ['1', 2, 3];
const childArray2 = [1, '2', 3];

expect(isSubset(parentArray, childArray1)).toBeTruthy();
expect(isSubset(parentArray, childArray2)).toBeFalsy();
expect(isSubset(parentArray, childArray2, (el) => Number(el))).toBeTruthy();
});

it('if elements type is array', () => {
const parentArray = [
[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
];
const childArray = [[0, 1, 7, 4, 9]];

expect(isSubset(parentArray, childArray)).toBeFalsy();
expect(isSubset(parentArray, childArray, (obj) => obj[2])).toBeTruthy(); // [2,7], [7];
expect(isSubset(parentArray, childArray, (obj) => obj[3])).toBeFalsy(); // [3,8], [4]
});

it('if elements type is reference', () => {
const parentArray = [
{
name: 'Peter',
age: 13,
},
{
name: 'Aimee',
age: 25,
},
];

const childArray1 = [
{
name: 'Aimee',
age: 25,
},
];

const childArray2 = [
{
name: 'Peter',
age: 15,
},
];

expect(isSubset(parentArray, childArray1)).toBeFalsy();
expect(
isSubset(parentArray, childArray1, (obj) => JSON.stringify(obj))
).toBeTruthy();
expect(
isSubset(parentArray, childArray2, (obj) => JSON.stringify(obj))
).toBeFalsy();
expect(isSubset(parentArray, childArray2, (obj) => obj.name)).toBeTruthy();
});
});
Copy link
Contributor

@ssi02014 ssi02014 Jul 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { isSubset } from '.';

describe('isSubset', () => {
  it('should correctly determine if the subset arrays are subsets of the superset array', () => {
    // 네이밍 변경
    const superset = [1, 2, 3, 4];
    const subset1 = [1, 3];
    const subset2 = [1, 5];
    
    // ...
  });

  it('should return the correct result if the types are different between comparison elements', () => {
    // ...
  });

  it('should handle elements of type array correctly', () => {
    expect(isSubset(superset, subset1, (arr) => arr[2])).toBeTruthy(); // (*) iteratee인자를 arr로 변경해주세요.
    // ...
  });

  it('should handle elements of type reference correctly', () => {
    // ...
  });
});

@beberiche
Copy link
Contributor Author

@ssi02014
피드백 사항에 맞게 업데이트하여 다시 올렸습니다.

Copy link
Contributor

@ssi02014 ssi02014 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작업하시느라 고생하셨습니다 🤗

@ssi02014 ssi02014 merged commit 7a94508 into modern-agile-team:main Jul 8, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature 새로운 기능 추가 @modern-kit/utils @modern-kit/utils
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature]: isSubset
2 participants