Skip to content

Commit

Permalink
feat(snakeCase): add snakeCase (#152)
Browse files Browse the repository at this point in the history
* feat(snakeCase): Add caseSplitPattern RegExp const

* feat(snakeCase): Add caseSplitPattern test code

* feat(snakeCase): Add snakeCase function

* feat(snakeCase): Add snakeCase test code

* feat(snakeCase): Add snakeCase docs

* feat(snakeCase): Add snakeCase benchmarks

* chore: Add string export

* fix(snakeCase): constants public api

* Update docs/ko/reference/string/snakeCase.md

* Update docs/ko/reference/string/snakeCase.md

* Update docs/reference/string/snakeCase.md

---------

Co-authored-by: Sojin Park <raon0211@gmail.com>
  • Loading branch information
haejunejung and raon0211 authored Jul 11, 2024
1 parent 883553b commit d48900f
Show file tree
Hide file tree
Showing 16 changed files with 234 additions and 1 deletion.
1 change: 1 addition & 0 deletions .scripts/postbuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ echo "export * from './dist/math';" > math.d.ts
echo "export * from './dist/object';" > object.d.ts
echo "export * from './dist/predicate';" > predicate.d.ts
echo "export * from './dist/promise';" > promise.d.ts
echo "export * from './dist/string';" > string.d.ts
15 changes: 15 additions & 0 deletions benchmarks/snakeCase.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { bench, describe } from 'vitest';
import { snakeCase as snakeCaseToolkit } from 'es-toolkit';
import { snakeCase as snakeCaseLodash } from 'lodash';

describe('snakeCase', () => {
bench('es-toolkit/snakeCase', () => {
const str = 'camleCase';
snakeCaseToolkit(str);
});

bench('lodash/snakeCase', () => {
const str = 'camelCase';
snakeCaseLodash(str);
});
});
4 changes: 4 additions & 0 deletions docs/.vitepress/en.mts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ function sidebar(): DefaultTheme.Sidebar {
text: 'Promise Utilities',
items: [{ text: 'delay', link: '/reference/promise/delay' }],
},
{
text: 'String Utilities',
items: [{ text: 'snakeCase', link: '/reference/string/snakeCase' }],
},
],
},
];
Expand Down
4 changes: 4 additions & 0 deletions docs/.vitepress/ko.mts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ function sidebar(): DefaultTheme.Sidebar {
text: 'Promise',
items: [{ text: 'delay', link: '/ko/reference/promise/delay' }],
},
{
text: '문자열',
items: [{ text: 'snakeCase', link: '/ko/reference/string/snakeCase' }],
},
],
},
];
Expand Down
1 change: 1 addition & 0 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Here are some of the features es-toolkit offers:
- **Object**: Tools for manipulating JavaScript objects, such as [pick](./reference/object/pick.md) and [omit](./reference/object/omit.md).
- **Predicate**: Type guard functions like [isNotNil](./reference/predicate/isNotNil.md).
- **Promise**: Asynchronous utilities like [delay](./reference/promise/delay.md).
- **String**: Utilties for string manipulation, such as [snakeCase](./reference/string/snakeCase.md)

## Links

Expand Down
1 change: 1 addition & 0 deletions docs/ko/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ es-toolkit이 제공하는 기능 목록은 다음과 같습니다.
- **객체**: [pick](./reference/object/pick.md)이나 [omit](./reference/object/omit.md)처럼 JavaScript 객체를 다루는 함수를 제공해요.
- **타입 가드**: [isNotNil](./reference/predicate/isNotNil.md)처럼 특정한 객체가 어떤 상태인지 검사하는 타입 가드 함수를 제공해요.
- **Promise**: [delay](./reference/promise/delay.md)와 같은 비동기 유틸리티 함수를 제공해요.
- **문자열**: [snakeCase](./reference/string/snakeCase.md)와 같이 문자열을 다루기 위한 다양한 함수를 제공해요.

## 링크

Expand Down
30 changes: 30 additions & 0 deletions docs/ko/reference/string/snakeCase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# snakeCase

문자열을 스네이크 표기법으로 변환해요.

스네이크 표기법은 여러 단어로 구성된 식별자에서 각 단어를 소문자로 작성하고, 단어 사이를 밑줄(\_)로 연결하는 명명 규칙이에요. 예를 들어서, `snake_case` 처럼 작성해요.

## 인터페이스

```typescript
function snakeCase(str: string): string;
```

### 파라미터

- `str` (`string`): 스네이크 케이스로 변환할 문자열이에요.

### 반환 값

(`string`) 스네이크 케이스로 변환된 문자열이에요.

## 예시

```typescript
import { snakeCase } from 'es-toolkit/string';

snakeCase('camelCase'); // returns 'camel_case'
snakeCase('some whitespace'); // returns 'some_whitespace'
snakeCase('hyphen-text'); // returns 'hyphen_text'
snakeCase('HTTPRequest'); // returns 'http_request'
```
30 changes: 30 additions & 0 deletions docs/reference/string/snakeCase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# snakeCase

Converts a string to snake case.

Snake case is the naming convention in which each word is written in lowercase and separated by an underscore (\_) character. For example, `snake_case`.

## Signature

```typescript
function snakeCase(str: string): string;
```

### Parameters

- `str` (`string`): The string that is to be changed to snake case.

### Returns

(`string`) The converted string to snake case.

## Examples

```typescript
import { snakeCase } from 'es-toolkit/string';

snakeCase('camelCase'); // returns 'camel_case'
snakeCase('some whitespace'); // returns 'some_whitespace'
snakeCase('hyphen-text'); // returns 'hyphen_text'
snakeCase('HTTPRequest'); // returns 'http_request'
```
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"./object": "./src/object/index.ts",
"./predicate": "./src/predicate/index.ts",
"./promise": "./src/promise/index.ts",
"./string": "./src/string/index.ts",
"./package.json": "./package.json"
},
"files": [
Expand Down Expand Up @@ -103,6 +104,16 @@
"default": "./dist/promise/index.js"
}
},
"./string": {
"import": {
"types": "./dist/string/index.d.mts",
"default": "./dist/string/index.mjs"
},
"require": {
"types": "./dist/string/index.d.ts",
"default": "./dist/string/index.js"
}
},
"./package.json": "./package.json"
}
},
Expand Down Expand Up @@ -140,4 +151,4 @@
"lint": "eslint ./src --ext .ts",
"format": "prettier --write ."
}
}
}
57 changes: 57 additions & 0 deletions src/constants/caseSplitPattern.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { CASE_SPLIT_PATTERN } from './caseSplitPattern';
describe('caseSplitPattern', () => {
it('should match camelCase', async () => {
const str = 'camelCase';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['camel', 'Case']);
});

it('should match snake_case', async () => {
const str = 'snake_case';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['snake', 'case']);
});

it('should match kebab-case', async () => {
const str = 'kebab-case';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['kebab', 'case']);
});

it('should handle mixed formats', async () => {
const str = 'camelCase_snake_case-kebabCase';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['camel', 'Case', 'snake', 'case', 'kebab', 'Case']);
});

it('should match acronyms', async () => {
const str = 'HTTPRequest';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['HTTP', 'Request']);
});

it('should match special characters', async () => {
const str = 'special_characters@123';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['special', 'characters', '123']);
});

it('should handle leading and trailing whitespace', async () => {
const str = ' leading_and_trailing_whitespace ';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['leading', 'and', 'trailing', 'whitespace']);
});

it('should handle underscores', async () => {
const str = 'underscore_case_example';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['underscore', 'case', 'example']);
});

it('should handle single character words', async () => {
const str = 'aB';
const matches = str.match(CASE_SPLIT_PATTERN);
expect(matches).toEqual(['a', 'B']);
});
});
18 changes: 18 additions & 0 deletions src/constants/caseSplitPattern.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Regular expression pattern to split strings into words for various case conversions
*
* This pattern matchs sequences of characters in a string, considering the following case:
* - Sequences of two or more uppercase letters followed by an uppercase letter and lowercase letters or digits (for acronyms)
* - Sequences of one uppercase letter optionally followed by lowercase letters and digits
* - Single uppercase letters
* - Sequences of digis
*
* The resulting match can be used to convert camelCase, snake_case, kebab-case, and other mixed formats into
* a consistent format like snake case.
*
* @example
* const matches = 'camelCaseHTTPRequest'.match(CASE_SPLIT_PATTERN);
* // matchs: ['camel', 'Case', 'HTTP', 'Request']
*/

export const CASE_SPLIT_PATTERN = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g;
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CASE_SPLIT_PATTERN } from './caseSplitPattern';
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* - **Object**: Tools for manipulating JavaScript objects, such as [pick](https://es-toolkit.slash.page/reference/object/pick.html) and [omit](https://es-toolkit.slash.page/reference/object/omit.html).
* - **Predicate**: Type guard functions like [isNotNil](https://es-toolkit.slash.page/reference/predicate/isNotNil.html).
* - **Promise**: Asynchronous utilities like [delay](https://es-toolkit.slash.page/reference/promise/delay.html).
* - **String**: Utilities for string manipulation, such as [snakeCase](https://es-toolkit.slash.page/reference/string/snakeCase.html)
*
* If you want to know more about the project, please take a look at the
* following resources:
Expand All @@ -36,3 +37,4 @@ export * from './math/index.ts';
export * from './object/index.ts';
export * from './predicate/index.ts';
export * from './promise/index.ts';
export * from './string/index.ts'
1 change: 1 addition & 0 deletions src/string/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { snakeCase } from './snakeCase.ts';
36 changes: 36 additions & 0 deletions src/string/snakeCase.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { snakeCase } from './snakeCase';

describe('snakeCase', () => {
it('should change camel case to snake case', async () => {
expect(snakeCase('camelCase')).toEqual('camel_case');
});

it('should change space to underscore', async () => {
expect(snakeCase('some whitespace')).toEqual('some_whitespace');
});

it('should change hyphen to underscore', async () => {
expect(snakeCase('hyphen-text')).toEqual('hyphen_text');
});

it('should change Acronyms to small letter', async () => {
expect(snakeCase('HTTPRequest')).toEqual('http_request');
});

it('should handle leading and trailing whitepspace', async () => {
expect(snakeCase(' leading and trailing whitespace')).toEqual('leading_and_trailing_whitespace');
});

it('should handle special characters correctly', async () => {
expect(snakeCase('special@characters!')).toEqual('special_characters');
});

it('should handle strings that are already in snake_case', async () => {
expect(snakeCase('snake_case')).toEqual('snake_case');
});

it('should work with an empty string', async () => {
expect(snakeCase('')).toEqual('');
});
});
21 changes: 21 additions & 0 deletions src/string/snakeCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CASE_SPLIT_PATTERN } from '../constants';

/**
* Converts a string to snake case.
*
* Snake case is the naming convention in which each word is written in lowercase and separated by an underscore (_) character.
*
* @param {string} str - The string that is to be changed to snake case.
* @returns {string} - The converted string to snake case.
*
* @example
* const convertedStr1 = snakeCase('camelCase') // returns 'camel_case'
* const convertedStr2 = snakeCase('some whitespace') // returns 'some_whitespace'
* const convertedStr3 = snakeCase('hyphen-text') // returns 'hyphen_text'
* const convertedStr4 = snakeCase('HTTPRequest') // returns 'http_request'
*/

export const snakeCase = (str: string): string => {
const splitWords = str.match(CASE_SPLIT_PATTERN) || [];
return splitWords.map(word => word.toLowerCase()).join('_');
};

0 comments on commit d48900f

Please sign in to comment.