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(compat): Add map function #514

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions benchmarks/performance/map.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { bench, describe } from 'vitest';
import { map as mapToolkit } from 'es-toolkit/compat';
import { map as mapLodash } from 'lodash';

const generateArray = (length: number) => Array.from({ length }, (_, i) => i);
const generateObject = (length: number) => Object.fromEntries(generateArray(length).map(i => [`key${i}`, i]));

describe('map/array', () => {
const array = generateArray(10000);

bench('es-toolkit/map', () => {
mapToolkit(array, value => value * 2);
});

bench('lodash/map', () => {
mapLodash(array, value => value * 2);
});
});

describe('map/object', () => {
const obj = generateObject(10000);

bench('es-toolkit/map', () => {
mapToolkit(obj, value => value * 2);
});

bench('lodash/map', () => {
mapLodash(obj, value => value * 2);
});
});
47 changes: 47 additions & 0 deletions docs/ko/reference/compat/array/map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# map

::: info
이 함수는 호환성을 위한 `es-toolkit/compat` 에서만 가져올 수 있어요. 대체할 수 있는 네이티브 JavaScript API가 있거나, 아직 충분히 최적화되지 않았기 때문이에요.

`es-toolkit/compat`에서 이 함수를 가져오면, [lodash와 완전히 똑같이 동작](../../../compatibility.md)해요.
:::

컬렉션의 요소들을 순회하며 변환 함수(이터레이터)를 적용한 결과를 반환하는 배열을 생성해요.
배열, 객체, 기타 여러 타입의 컬렉션과 함께 사용할 수 있어 다양한 데이터 구조에서 호환돼요.
만약 이터레이터가 제공되지 않으면, 컬렉션의 원래 요소들을 반환해요.

## 인터페이스

```typescript
function map<T, U>(
collection: T[] | Record<string, T> | number | string | boolean | null | undefined,
iteratee?: ((value: T, key: string | number) => U) | keyof T | null
): U[];
```

### 파라미터
- `collection` (`T[] | Record<string, T> | number | string | boolean | null | undefined`): 순회할 컬렉션. 배열이나 객체, 원시 타입이 될 수 있어요. 컬렉션이 `null` 또는 `undefined`인 경우 빈 배열을 반환해요.
- `iteratee` (`(value: T, key: string | number) => U | keyof T | null`, 선택): 각 요소마다 호출되는 변환 함수에요. 함수가 제공되면, 해당 함수가 각 요소에 대해 변환 작업을 수행해요. 만약 이터레이터가 속성 키(예: `keyof T`)라면, 각 요소의 해당 속성 값이 반환돼요. 이터레이터가 제공되지 않으면, 원래 요소들이 그대로 반환돼요.

### 반환 값
(`U[]`): 변환된 값들로 이루어진 새로운 배열이에요.

### 예시

```typescript
// 변환 함수를 사용하는 경우
const array = [1, 2, 3];
map(array, (value) => value * 2); // => [2, 4, 6]

// 속성 키를 이터레이터로 사용하는 경우
const objects = [{a: 1}, {a: 2}, {a: 3}];
map(objects, 'a'); // => [1, 2, 3]

// 이터레이터가 없는 경우
const numbers = [1, 2, 3];
map(numbers); // => [1, 2, 3]

// 객체를 사용하는 경우
const obj = {a: 1, b: 2, c: 3};
map(obj, (value, key) => `${key}: ${value}`); // => ['a: 1', 'b: 2', 'c: 3']
```
47 changes: 47 additions & 0 deletions docs/reference/compat/array/map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# map

::: info
This function is only available in `es-toolkit/compat` for compatibility reasons. It either has alternative native JavaScript APIs or isn’t fully optimized yet.

When imported from `es-toolkit/compat`, it behaves exactly like lodash and provides the same functionalities, as detailed [here](../../../compatibility.md).
:::

Iterates over elements in a collection and applies a transformation function, returning a new array of results.
This method works with arrays, objects, and other types, ensuring compatibility across different types of collections.
If no iteratee is provided, the function returns the original elements of the collection.

## Signature

```typescript
function map<T, U>(
collection: T[] | Record<string, T> | number | string | boolean | null | undefined,
iteratee?: ((value: T, key: string | number) => U) | keyof T | null
): U[];
```

### Parameters
- `collection` (`T[] | Record<string, T> | number | string | boolean | null | undefined`): The collection to iterate over. It can be an array, an object, or a primitive type. If the collection is `null` or `undefined`, an empty array is returned.
- `iteratee` (`(value: T, key: string | number) => U | keyof T | null`, optional): The function invoked per iteration. If it's a function, it will be used to transform each element. If it's a property key (i.e., `keyof T`), the value of that property will be returned for each element. If no iteratee is provided, the function returns the original elements.

### Returns
(`U[]`): A new array of transformed values.

### Example

```typescript
// Using a transformation function
const array = [1, 2, 3];
map(array, (value) => value * 2); // => [2, 4, 6]

// Using a property key as the iteratee
const objects = [{a: 1}, {a: 2}, {a: 3}];
map(objects, 'a'); // => [1, 2, 3]

// No iteratee
const numbers = [1, 2, 3];
map(numbers); // => [1, 2, 3]

// Using an object
const obj = {a: 1, b: 2, c: 3};
map(obj, (value, key) => `${key}: ${value}`); // => ['a: 1', 'b: 2', 'c: 3']
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,4 @@
"format": "prettier --write .",
"transform": "jscodeshift -t ./.scripts/tests/transform-lodash-test.ts $0 && prettier --write $0"
}
}
}
77 changes: 77 additions & 0 deletions src/compat/array/map.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from 'vitest';
import { falsey } from '../_internal/falsey';
import { identity } from '../_internal/identity';
import { map } from '../index';

/**
* @see https://github.com/lodash/lodash/blob/6a2cc1dfcf7634fea70d1bc5bd22db453df67b42/test/map.spec.js
*/
describe('map', () => {
const array = [1, 2];

it('should map values in `collection` to a new array', () => {
const object = { a: 1, b: 2 };
const expected = ['1', '2'];

expect(map(array, String)).toEqual(expected);
expect(map(object, String)).toEqual(expected);
});

it('should work with `_.property` shorthands', () => {
const objects = [{ a: 'x' }, { a: 'y' }];
expect(map(objects, 'a')).toEqual(['x', 'y']);
});

it('should iterate over own string keyed properties of objects', () => {
class Foo {
static b = 2;
a: number;

constructor() {
this.a = 1;
}

[key: string]: unknown;
}

const actual = map(new Foo(), identity);
expect(actual).toEqual([1]);
});

it('should use `_.identity` when `iteratee` is nullish', () => {
const object = { a: 1, b: 2 };
const values = [null, undefined];
const expected = values.map(() => [1, 2]);

[array, object].forEach(collection => {
const actual = values.map((value, index) => (index ? map(collection, value) : map(collection)));

expect(actual).toEqual(expected);
});
});

it('should accept a falsey `collection`', () => {
const expected = falsey.map(() => []);

const actual = (falsey as any[]).map((collection, index) => {
try {
return index ? map(collection) : map(null);
} catch (e) {
return e;
}
});

expect(actual).toEqual(expected);
});

it('should treat number values for `collection` as empty', () => {
expect(map(1)).toEqual([]);
});

it('should work with objects with non-number length properties', () => {
const value = { value: 'x' };
const object = { length: { value: 'x' } };

expect(map(object, identity)).toEqual([value]);
});
});
60 changes: 60 additions & 0 deletions src/compat/array/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Maps each element in a collection to a new array of values using an iteratee.
*
* This function takes a collection (either an array or an object) and applies
* a mapping function (iteratee) to each element or value. It returns a new array
* of values generated by the iteratee. If no iteratee is provided, the identity
* function is used, meaning the values themselves are returned.
*
* @template T The type of elements or values in the collection.
* @template U The type of elements in the resulting array.
* @param {T[] | Record<string, T> | number | string | boolean | null | undefined} collection
* The collection to iterate over. It can be an array, an object, or a primitive.
* If the collection is null or undefined, an empty array is returned.
* @param {((value: T, key: string | number) => U) | keyof T | null} [iteratee=null]
* The function invoked per iteration. If a key of the collection elements is
* provided instead of a function, the value of that key will be returned for
* each element.
* @returns {U[]} A new array of values mapped by the iteratee or by the identity function.
*
* @example
* // Maps an array of numbers by multiplying each by 2
* map([1, 2, 3], n => n * 2);
* // Returns: [2, 4, 6]
*
* @example
* // Maps an object, extracting the values of the 'name' property
* map({ a: { name: 'John' }, b: { name: 'Jane' } }, 'name');
* // Returns: ['John', 'Jane']
*
* @example
* // If the collection is null or undefined, returns an empty array
* map(null);
* // Returns: []
*/
export function map<T, U>(
collection: T[] | Record<string, T> | number | string | boolean | null | undefined,
iteratee?: ((value: T, key: string | number) => U) | keyof T | null
): U[] {
if (collection == null || typeof collection !== 'object') {
return [];
}

const mapper: (value: T, key: string | number) => U =
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe you might check our some, indexOf function which similarly handles this. I think the documentation and implementation is better.

typeof iteratee === 'function'
? iteratee
: iteratee == null
? (item: T) => item as unknown as U
: (item: T) => item[iteratee as keyof T] as unknown as U;

if (Array.isArray(collection)) {
const length = collection.length;
const result: U[] = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = mapper(collection[i], i);
}
return result;
} else {
return Object.keys(collection).map(key => mapper(collection[key], key));
}
}
1 change: 1 addition & 0 deletions src/compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { size } from './array/size.ts';
export { some } from './array/some.ts';
export { zipObjectDeep } from './array/zipObjectDeep.ts';
export { head as first } from '../array/head.ts';
export { map } from './array/map.ts';

export { ary } from './function/ary.ts';
export { bind } from './function/bind.ts';
Expand Down