├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .vuepress │ ├── config.ts │ ├── midash.ts │ └── public │ │ └── midash.svg ├── README.md ├── api.md └── zh │ ├── Readme.md │ └── api.md ├── package.json ├── pnpm-lock.yaml ├── src ├── assign.ts ├── async.ts ├── camelCase.ts ├── castArray.ts ├── chunk.ts ├── clone.ts ├── compact.ts ├── compose.ts ├── debounce.ts ├── defaults.ts ├── difference.ts ├── get.ts ├── groupBy.ts ├── head.ts ├── index.ts ├── intersection.ts ├── isArray.ts ├── isBoolean.ts ├── isEqual.ts ├── isObject.ts ├── isPlainObject.ts ├── isPrimitive.ts ├── isPromise.ts ├── isTypedArray.ts ├── kebabCase.ts ├── keyBy.ts ├── mapKeys.ts ├── max.ts ├── memoize.ts ├── merge.ts ├── min.ts ├── nth.ts ├── omit.ts ├── omitBy.ts ├── once.ts ├── pick.ts ├── pickBy.ts ├── property.ts ├── random.ts ├── range.ts ├── sample.ts ├── sampleSize.ts ├── shuffle.ts ├── snakeCase.ts ├── sum.ts ├── template.ts ├── throttle.ts ├── uniq.ts ├── uniqBy.ts ├── unzip.ts ├── words.ts └── zip.ts ├── test ├── assign.spec.ts ├── async.spec.ts ├── camelCase.spec.ts ├── castArray.spec.ts ├── chunk.spec.ts ├── clone.spec.ts ├── compact.spec.ts ├── compose.spec.ts ├── debounce.spec.ts ├── defaults.spec.ts ├── difference.spec.ts ├── get.spec.ts ├── groupBy.spec.ts ├── head.spec.ts ├── intersection.spec.ts ├── isArray.spec.ts ├── isBoolean.ts ├── isEqual.spec.ts ├── isObject.spec.ts ├── isPlainObject.spec.ts ├── isPromise.spec.ts ├── isTypedArray.spec.ts ├── kebabCase.spec.ts ├── keyBy.spec.ts ├── mapKeys.spec.ts ├── max.spec.ts ├── memoize.spec.ts ├── merge.spec.ts ├── min.spec.ts ├── nth.spec.ts ├── omit.spec.ts ├── omitBy.spec.ts ├── once.spec.ts ├── pick.spec.ts ├── pickBy.spec.ts ├── property.spec.ts ├── random.spec.ts ├── range.spec.ts ├── sample.spec.ts ├── sampleSize.spec.ts ├── shuffle.spec.ts ├── snakeCase.spec.ts ├── sum.spec.ts ├── template.spec.ts ├── throttle.spec.ts ├── uniq.spec.ts ├── uniqBy.spec.ts ├── unzip.spec.ts └── zip.spec.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.ts /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['16.x', '18.x'] 11 | os: [ubuntu-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Setup pnpm 23 | run: corepack enable 24 | 25 | - name: Install deps and build (with cache) 26 | run: pnpm i 27 | 28 | - name: Lint 29 | run: pnpm lint 30 | 31 | - name: Test 32 | run: pnpm test -- run --coverage 33 | 34 | - name: Build 35 | run: pnpm build 36 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - uses: andresz1/size-limit-action@v1.7.0 12 | with: 13 | github_token: ${{ secrets.GITHUB_TOKEN }} 14 | package_manager: pnpm 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | package-lock.json 6 | .cache 7 | .temp 8 | .idea 9 | coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 shfshanyue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Midash 2 | 3 | [![Npm Version](https://badgen.net/npm/v/midash)](https://npmjs.com/package/midash) 4 | ![Node Version](https://badgen.net/npm/node/midash) 5 | ![Type Support](https://badgen.net/npm/types/midash) 6 | ![Tree Shaking Support](https://badgen.net/bundlephobia/tree-shaking/midash) 7 | ![Npm Month Downloads](https://badgen.net/npm/dw/midash) 8 | [![Gzip Size](https://img.badgesize.io/https://unpkg.com/midash/dist/index.esm.js?compression=gzip)](https://unpkg.com/midash/dist/index.esm.js) 9 | 10 | An alternative to `lodash` with the same API, plus additional async utilities. 11 | 12 | + 🔨 High frequency API 13 | + 🕒 Familiar lodash API 14 | + 💪 Support Tree Shaking 15 | + 👫 Support Typescript 16 | + 🔥 Smaller Size (with ES6+ API) 17 | + 📦 2.5kb mini library 18 | + 🚀 Additional async utilities 19 | 20 | ## API 21 | 22 | [Documentation](https://midash.devtool.tech/) [中文文档](https://midash.devtool.tech/zh/api.html) 23 | 24 | ## Installation 25 | 26 | ``` bash 27 | # yarn 28 | $ yarn add midash 29 | # pnpm 30 | $ pnpm i midash 31 | ``` 32 | 33 | ## Usage 34 | 35 | ``` js 36 | import { sum } from 'midash' 37 | 38 | sum([1, 3, 5, 7, 9]) 39 | ``` 40 | 41 | ## Async Utilities 42 | 43 | Midash provides several async utilities: 44 | 45 | - `sleep(ms)`: Pause execution for a specified number of milliseconds. 46 | - `retry(fn, options)`: Retry a function multiple times with customizable options. 47 | - `map(iterable, mapper, options)`: Asynchronously map over an iterable with concurrency control. 48 | - `filter(iterable, filterer, options)`: Asynchronously filter an iterable with concurrency control. 49 | 50 | Example usage: 51 | 52 | ```js 53 | import { sleep, retry, map, filter } from 'midash' 54 | 55 | // Sleep for 1 second 56 | await sleep(1000) 57 | 58 | // Retry a function up to 3 times 59 | const result = await retry(async () => { 60 | // Your async operation here 61 | }, { times: 3 }) 62 | 63 | // Asynchronously map over an array with a concurrency of 2 64 | const mappedResults = await map([1, 2, 3, 4], async (num) => { 65 | await sleep(100) 66 | return num * 2 67 | }, { concurrency: 2 }) 68 | 69 | // Asynchronously filter an array 70 | const filteredResults = await filter([1, 2, 3, 4, 5], async (num) => { 71 | await sleep(100) 72 | return num % 2 === 0 73 | }) 74 | ``` 75 | 76 | These async utilities make Midash a powerful choice for both synchronous and asynchronous operations in modern JavaScript applications. -------------------------------------------------------------------------------- /docs/.vuepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defaultTheme } from '@vuepress/theme-default' 2 | import { path } from '@vuepress/utils' 3 | 4 | export default { 5 | head: [ 6 | [ 7 | 'link', 8 | { 9 | rel: 'icon', 10 | type: 'image/svg+xml', 11 | sizes: '16x16', 12 | href: `/midash.svg`, 13 | }, 14 | ], 15 | ], 16 | locales: { 17 | '/': { 18 | lang: 'en-US', 19 | title: 'midash', 20 | description: 'An alternative to lodash with the same API.', 21 | }, 22 | '/zh/': { 23 | lang: 'zh-CN', 24 | title: 'midash', 25 | description: '与 lodash 拥有相似 API,基于 ES6+,体积更小的工具函数库', 26 | }, 27 | }, 28 | theme: defaultTheme({ 29 | logo: '/midash.svg', 30 | home: '/', 31 | docsRepo: 'shfshanyue/midash', 32 | repo: 'shfshanyue/midash', 33 | locales: { 34 | /** 35 | * English locale config 36 | * 37 | * As the default locale of @vuepress/theme-default is English, 38 | * we don't need to set all of the locale fields 39 | */ 40 | '/': { 41 | // navbar 42 | navbar: [ 43 | { 44 | text: 'Home', 45 | link: '/', 46 | }, 47 | { 48 | text: 'API Documentation', 49 | link: '/api', 50 | }, 51 | ], 52 | editLinkText: 'Edit this page on GitHub', 53 | }, 54 | 55 | /** 56 | * Chinese locale config 57 | */ 58 | '/zh/': { 59 | // navbar 60 | navbar: [ 61 | { 62 | text: '首页', 63 | link: '/', 64 | }, 65 | { 66 | text: 'API 文档', 67 | link: '/zh/api', 68 | }, 69 | ], 70 | selectLanguageName: '简体中文', 71 | selectLanguageText: '选择语言', 72 | selectLanguageAriaLabel: '选择语言', 73 | editLinkText: '在 GitHub 上编辑此页', 74 | lastUpdatedText: '上次更新', 75 | contributorsText: '贡献者', 76 | tip: '提示', 77 | warning: '注意', 78 | danger: '警告', 79 | // 404 page 80 | notFound: [ 81 | '这里什么都没有', 82 | '我们怎么到这来了?', 83 | '这是一个 404 页面', 84 | '看起来我们进入了错误的链接', 85 | ], 86 | backToHome: '返回首页', 87 | // a11y 88 | openInNewWindow: '在新窗口打开', 89 | toggleColorMode: '切换颜色模式', 90 | toggleSidebar: '切换侧边栏', 91 | }, 92 | }, 93 | }), 94 | plugins: [ 95 | { 96 | name: 'midash API', 97 | clientConfigFile: path.resolve(__dirname, './midash.ts'), 98 | }, 99 | ], 100 | } 101 | -------------------------------------------------------------------------------- /docs/.vuepress/midash.ts: -------------------------------------------------------------------------------- 1 | import { defineClientConfig } from '@vuepress/client' 2 | import pkg from '../../package.json' 3 | 4 | export default defineClientConfig({ 5 | async enhance(context) { 6 | import('../..').then((o) => { 7 | console.log( 8 | 'You can try midash API using variable %c_%c or %cmidash%c in browser devtools.', 9 | 'font-style: italic; color: red; font-size: 1.5em;', 'font-style: normal', 10 | 'font-style: italic; color: red; font-size: 1.5em;', 'font-style: normal', 11 | ) 12 | console.log( 13 | `Version: %cmidash v${pkg.version}%c`, 14 | 'font-style: italic; color: red; font-size: 1.5em;', 'font-style: normal', 15 | ) 16 | globalThis.midash = o 17 | globalThis._ = o 18 | }) 19 | 20 | // context.app.use(context.router) 21 | // if (globalThis.navigator?.language?.includes('zh')) { 22 | // context.router.push('/zh/') 23 | // } 24 | }, 25 | }) -------------------------------------------------------------------------------- /docs/.vuepress/public/midash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | --- 4 | 5 | # midash 6 | 7 | [![Npm Version](https://badgen.net/npm/v/midash)](https://npmjs.com/package/midash) 8 | ![Node Version](https://badgen.net/npm/node/midash) 9 | ![Type Support](https://badgen.net/npm/types/midash) 10 | ![Tree Shaking Support](https://badgen.net/bundlephobia/tree-shaking/midash) 11 | ![Npm Month Downloads](https://badgen.net/npm/dw/midash) 12 | ![Gzip Size](https://badgen.net/bundlephobia/minzip/midash) 13 | 14 | An alternative to `lodash` with the same API, plus additional async utilities. 15 | 16 | + 🔨 High frequency API 17 | + 🕒 Familiar lodash API 18 | + 💪 Support Tree Shaking 19 | + 👫 Support Typescript 20 | + 🔥 Smaller Size (with ES6+ API) 21 | + 📦 2.5kb mini library 22 | + 🚀 Additional async utilities 23 | 24 | ## API 25 | 26 | [Documentation](https://midash.devtool.tech/) [中文文档](https://midash.devtool.tech/zh/api.html) 27 | 28 | ## Installation 29 | 30 | ``` bash 31 | # yarn 32 | $ yarn add midash 33 | # pnpm 34 | $ pnpm i midash 35 | ``` 36 | 37 | ``` js 38 | import { sum } from 'midash' 39 | 40 | sum([1, 3, 5, 7, 9]) 41 | ``` 42 | 43 | ## Async Utilities 44 | 45 | Midash provides several async utilities: 46 | 47 | - `sleep(ms)`: Pause execution for a specified number of milliseconds. 48 | - `retry(fn, options)`: Retry a function multiple times with customizable options. 49 | - `map(iterable, mapper, options)`: Asynchronously map over an iterable with concurrency control. 50 | - `filter(iterable, filterer, options)`: Asynchronously filter an iterable with concurrency control. 51 | 52 | Example usage: 53 | 54 | ```js 55 | import { sleep, retry, map, filter } from 'midash' 56 | 57 | // Sleep for 1 second 58 | await sleep(1000) 59 | 60 | // Retry a function up to 3 times 61 | const result = await retry(async () => { 62 | // Your async operation here 63 | }, { times: 3 }) 64 | 65 | // Asynchronously map over an array with a concurrency of 2 66 | const mappedResults = await map([1, 2, 3, 4], async (num) => { 67 | await sleep(100) 68 | return num * 2 69 | }, { concurrency: 2 }) 70 | 71 | // Asynchronously filter an array 72 | const filteredResults = await filter([1, 2, 3, 4, 5], async (num) => { 73 | await sleep(100) 74 | return num % 2 === 0 75 | }) 76 | ``` -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # midash API 2 | 3 | ## Object 4 | 5 | ### get 6 | 7 | Get the attribute of object deeply. 8 | 9 | ``` js 10 | const object = { a: [{ b: 3 }] } 11 | 12 | // => 3 13 | _.get(object, 'a[0].b') 14 | 15 | // => 3 16 | _.get(object, ['a', '0', 'b']) 17 | 18 | // => 'default' 19 | _.get(object, 'a.b.c', 'default') 20 | ``` 21 | 22 | ### omit 23 | 24 | Ignore attributes of object and return new object. 25 | 26 | ::: warning 27 | `_.omit(object, 'a', 'b')` can't work well in midash. use `_.omit(object, ['a', 'b'])` instead. 28 | ::: 29 | 30 | ``` js 31 | const object = { 32 | a: 3, 33 | b: 4, 34 | c: 5 35 | } 36 | 37 | //=> { c: 5 } 38 | _.omit(object, ['a', 'b']) 39 | ``` 40 | 41 | ### omitBy 42 | 43 | Ignore attributes of object by function and return new object. 44 | 45 | ``` js 46 | const object = { 47 | a: 3, 48 | b: 4, 49 | c: 5 50 | } 51 | 52 | // omit by value 53 | //=> { b:4, c: 5 } 54 | _.omitBy(object, value => value === 3) 55 | 56 | // omit by key 57 | //=> { b:4, c: 5 } 58 | _.omitBy(object, (value, key) => key === 'a') 59 | ``` 60 | 61 | ### pick 62 | 63 | Pick attributes of object by function and return new object. 64 | 65 | ::: warning 66 | `_.pick(object, 'a', 'b')` can't work well in midash, use `_.pick(object, ['a', 'b'])` instead. 67 | ::: 68 | 69 | ``` js 70 | const object = { 71 | a: 3, 72 | b: 4, 73 | c: undefined 74 | } 75 | 76 | //=> { a: 3, b: 4 } 77 | _.pick(object, ['a', 'b']) 78 | 79 | //=> {} 80 | _.pick(object, ['z']) 81 | 82 | //=> { c: undefined } 83 | _.pick(object, ['c']) 84 | ``` 85 | 86 | ### pickBy 87 | 88 | Pick attributes of object by function and return new object. 89 | 90 | ``` js 91 | const object = { 92 | a: 3, 93 | b: 4, 94 | } 95 | 96 | //=> { a: 3 } 97 | _.pickBy(object, value => value === 3) 98 | 99 | //=> { a: 3 } 100 | _.pickBy(object, (value, key) => key === 'a') 101 | ``` 102 | 103 | ### defaults 104 | 105 | Assigns own enumerable properties of source objects to the destination object for all destination properties that resolve to undefined. 106 | 107 | ``` js 108 | //=> { mode: 'development', sourcemap: true, devtool: true } 109 | _.defaults({ 110 | mode: 'development', 111 | sourcemap: true 112 | }, { 113 | mode: 'production', 114 | devtool: true 115 | }) 116 | ``` 117 | 118 | ### clone 119 | 120 | Creates a shallow clone of an object. 121 | 122 | ``` js 123 | const o = { a: { aa: 3 }, b: 4 } 124 | 125 | //=> true 126 | _.clone(o).a === o.a 127 | ``` 128 | 129 | ### merge 130 | 131 | Merges one or more objects into first object recursively and returns new object. 132 | 133 | ``` js 134 | //=> { a: 4, b: 2 } 135 | _.merge({ a: 1 }, { b: 2 }, { a: 3 }, { a: 4 }) 136 | ``` 137 | 138 | ### assign 139 | 140 | Assigns own enumerable string keyed properties of source objects to the destination object. Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources. 141 | 142 | ``` js 143 | // => { a: 1, b: 3, c: 5, d: 6 } 144 | _.assign({ a: 1, b: 2 }, { b: 3, c: 4 }, { c: 5, d: 6 }) 145 | ``` 146 | 147 | ### mapKeys 148 | 149 | Transform the keys of an object with a function and returns a new object. 150 | 151 | ``` js 152 | //=> { a3: 3, b4: 4 } 153 | _.mapKeys({ a: 3, b: 4 }, (v, k) => `${k}${v}`) 154 | ``` 155 | 156 | ## Array 157 | 158 | ### chunk 159 | 160 | Get an array of elements split into chunk by size. 161 | 162 | ``` js 163 | //=> [[0, 1, 2], [3, 4, 5]] 164 | _.chunk([0, 1, 2, 3, 4, 5], 3) 165 | 166 | //=> [[0], [1], [2]] 167 | _.chunk([0, 1, 2]) 168 | 169 | //=> [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']] 170 | _.chunk('abcdefghi', 3) 171 | ``` 172 | 173 | ### sample 174 | 175 | Get a random element from an array. 176 | 177 | ``` js 178 | // get a random element from [0, 3, 6, 10] 179 | _.sample([0, 3, 6, 10]) 180 | 181 | //=> undefined 182 | _.sample([]) 183 | ``` 184 | 185 | ### sampleSize 186 | 187 | Get `n` random element from an array. 188 | 189 | ``` js 190 | //=> Maybe [1, 2] 191 | _.sampleSize([1, 2, 3], 2) 192 | 193 | //=> [1, 2, 3] 194 | _.sampleSize([1, 2, 3], 4) 195 | ``` 196 | 197 | ### shuffle 198 | 199 | Creates an array of shuffled values. 200 | 201 | ``` js 202 | //=> [2, 3, 1] (any random order) 203 | _.shuffle([1, 2, 3]) 204 | ``` 205 | 206 | ### difference/differenceBy 207 | 208 | Creates an array of array values not included in the other given arrays. 209 | 210 | ::: tip 211 | In `midash`, `differenceBy` is an alias of `difference`. 212 | ::: 213 | 214 | ``` js 215 | //=> [2, 4] 216 | _.difference([1, 2, 3, 4], [1, 3, 5]) 217 | 218 | //=> [{ a: 4 }] 219 | _.differenceBy([{ a: 3 }, { a: 4 }], [{ a: 3 }], x => x.a) 220 | ``` 221 | 222 | ### intersection 223 | 224 | Creates an array of unique values that are included in all given arrays. 225 | 226 | ``` js 227 | //=> [2] 228 | _.intersection([1, 2], [2, 3]) 229 | 230 | //=> [{ id: 1 }] 231 | _.intersection([{ id: 1 }, { id: 2 }], [{ id: 1 }, { id: 3 }], item => item.id) 232 | ``` 233 | 234 | ### uniq 235 | 236 | Creates a duplicate-free version of an array. 237 | 238 | ``` js 239 | //=> [1, 2, 3] 240 | _.uniq([1, 2, 3, 1, 2]) 241 | ``` 242 | 243 | ### uniqBy 244 | 245 | Creates a duplicate-free version of an array using a function for comparison. 246 | 247 | ``` js 248 | //=> [{ id: 1 }, { id: 2 }] 249 | _.uniqBy([{ id: 1 }, { id: 2 }, { id: 1 }], item => item.id) 250 | ``` 251 | 252 | ### keyBy 253 | 254 | Creates an object composed of keys generated from the results of running each element of collection through iteratee. 255 | 256 | ``` js 257 | const list = [ 258 | { id: 1, name: 'hello' }, 259 | { id: 2, name: 'world' }, 260 | ] 261 | 262 | //=> { '1': { id: 1, name: 'hello' }, '2': { id: 2, name: 'world' } } 263 | _.keyBy(list, x => x.id) 264 | ``` 265 | 266 | ### groupBy 267 | 268 | Creates an object composed of keys generated from the results of running each element of collection through iteratee. The corresponding value of each key is an array of elements responsible for generating the key. 269 | 270 | ``` js 271 | //=> { '3': ['one', 'two'], '5': ['three'] } 272 | _.groupBy(['one', 'two', 'three'], x => x.length) 273 | ``` 274 | 275 | ### zip 276 | 277 | Creates an array of grouped elements, the first of which contains the first elements of the given arrays, the second of which contains the second elements of the given arrays, and so on. 278 | 279 | ``` js 280 | // => [[1, 'a', true],[2, 'b', false],[3, 'c', undefined]]; 281 | _.zip([1, 2, 3], ['a', 'b', 'c'], [true, false]); 282 | 283 | // => [[undefined, 1], [undefined, 2], [undefined, 3]]; 284 | _.zip([],[1, 2, 3]) 285 | 286 | // => [[1, 'a', undefined], [2, 'b', undefined],[undefined, 'c', undefined]]; 287 | _.zip([1, 2], ['a', 'b', 'c'], []) 288 | ``` 289 | 290 | ### unzip 291 | 292 | This method is like _.zip except that it accepts an array of grouped elements and creates an array regrouping the elements to their pre-zip configuration. 293 | 294 | ``` js 295 | // => [[1, 2, 3], ['a', 'b', 'c'], [true, false, true]] 296 | _.unzip([[1, 'a', true], [2, 'b', false], [3, 'c', true]]) 297 | 298 | // => [] 299 | _.unzip([]) 300 | 301 | // => [[1, 2, 4], ['a', 'b', 'c'], [undefined, 3, undefined]] 302 | _.unzip([[1, 'a'], [2, 'b', 3], [4, 'c']]) 303 | ``` 304 | 305 | ### compact 306 | 307 | Removes all falsey values from an array. 308 | 309 | ``` js 310 | // => [1, 2, 3] 311 | _.compact([0, 1, false, 2, '', 3]) 312 | ``` 313 | 314 | ### head 315 | 316 | Gets the first element of array. 317 | 318 | ``` js 319 | // => 1 320 | _.head([1, 2, 3]) 321 | 322 | // => undefined 323 | _.head([]) 324 | ``` 325 | 326 | ### nth 327 | 328 | Gets the element at index n of array. If n is negative, the nth element from the end is returned. 329 | 330 | ``` js 331 | // => 2 332 | _.nth([1, 2, 3], 1) 333 | 334 | // => 3 335 | _.nth([1, 2, 3], -1) 336 | ``` 337 | 338 | ## String 339 | 340 | ### camelCase 341 | 342 | Converts string to camel case. 343 | 344 | ``` js 345 | // => 'fooBar' 346 | _.camelCase('foo_bar') 347 | 348 | // => 'fooBar' 349 | _.camelCase('foo-bar') 350 | 351 | // => 'fooBar' 352 | _.camelCase('Foo Bar') 353 | ``` 354 | 355 | ### snakeCase 356 | 357 | Converts string to snake case. 358 | 359 | ``` js 360 | // => 'foo_bar' 361 | _.snakeCase('fooBar') 362 | 363 | // => 'foo_bar' 364 | _.snakeCase('foo-bar') 365 | 366 | // => 'foo_bar' 367 | _.snakeCase('Foo Bar') 368 | ``` 369 | 370 | ### kebabCase 371 | 372 | Converts string to kebab case. 373 | 374 | ``` js 375 | // => 'foo-bar' 376 | _.kebabCase('fooBar') 377 | 378 | // => 'foo-bar' 379 | _.kebabCase('foo_bar') 380 | 381 | // => 'foo-bar' 382 | _.kebabCase('Foo Bar') 383 | ``` 384 | 385 | ### words 386 | 387 | Splits string into an array of its words. 388 | 389 | ``` js 390 | // => ['foo', 'bar'] 391 | _.words('foo bar') 392 | 393 | // => ['foo', 'bar'] 394 | _.words('foo-bar', /[^, -]+/g) 395 | ``` 396 | 397 | ### template 398 | 399 | Creates a compiled template function that can interpolate data properties. 400 | 401 | ``` js 402 | // => 'Hello Fred!' 403 | _.template('Hello ${name}!')({ name: 'Fred' }) 404 | ``` 405 | 406 | ## Number 407 | 408 | ### random 409 | 410 | Gets a random integer between min and max (inclusive). 411 | 412 | ``` js 413 | // an integer between 10 and 20, includes 10 and 20 414 | _.random(10, 20) 415 | 416 | // an integer between 0 and 20 417 | _.random(20) 418 | 419 | // an integer between 0 and 1 420 | _.random() 421 | ``` 422 | 423 | ### range 424 | 425 | Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end. 426 | 427 | ``` js 428 | //=> [0, 1, 2, 3] 429 | _.range(4) 430 | 431 | //=> [0, -1, -2, -3] 432 | _.range(-4) 433 | 434 | //=> [1, 2, 3, 4] 435 | _.range(1, 5) 436 | 437 | //=> [5, 4, 3, 2] 438 | _.range(5, 1) 439 | 440 | //=> [0, -1, -2, -3] 441 | _.range(0, -4, -1) 442 | ``` 443 | 444 | ## Lang 445 | 446 | ### castArray 447 | 448 | Casts value as an array if it's not one. 449 | 450 | ``` js 451 | _.castArray(1); 452 | // => [1] 453 | 454 | _.castArray({ 'a': 1 }); 455 | // => [{ 'a': 1 }] 456 | 457 | _.castArray('abc'); 458 | // => ['abc'] 459 | 460 | _.castArray(null); 461 | // => [null] 462 | 463 | _.castArray(undefined); 464 | // => [undefined] 465 | 466 | _.castArray(); 467 | // => [] 468 | 469 | const array = [1, 2, 3]; 470 | console.log(_.castArray(array) === array); 471 | // => true 472 | ``` 473 | 474 | ### isArray 475 | 476 | Checks if value is classified as an Array object. 477 | 478 | ``` js 479 | //=> true 480 | _.isArray([]) 481 | 482 | //=> false 483 | _.isArray({}) 484 | ``` 485 | 486 | ### isBoolean 487 | 488 | Checks if value is classified as a boolean primitive. 489 | 490 | ``` js 491 | //=> true 492 | _.isBoolean(false) 493 | 494 | //=> true 495 | _.isBoolean(true) 496 | 497 | //=> false 498 | _.isBoolean(null) 499 | ``` 500 | 501 | ### isObject 502 | 503 | Checks if value is the language type of Object. 504 | 505 | ``` js 506 | //=> true 507 | _.isObject({}) 508 | 509 | //=> true 510 | _.isObject([]) 511 | 512 | //=> true 513 | _.isObject(x => {}) 514 | 515 | //=> false 516 | _.isObject(null) 517 | ``` 518 | 519 | ### isPlainObject 520 | 521 | Checks if value is a plain object. 522 | 523 | ``` js 524 | //=> true 525 | _.isPlainObject({}) 526 | 527 | //=> true 528 | _.isPlainObject(Object.create(null)) 529 | 530 | //=> false 531 | _.isPlainObject([]) 532 | 533 | //=> false 534 | _.isPlainObject(new Date()) 535 | ``` 536 | 537 | ### isPromise 538 | 539 | Checks if value is a Promise. 540 | 541 | ``` js 542 | //=> true 543 | _.isPromise(Promise.resolve()) 544 | 545 | //=> false 546 | _.isPromise({}) 547 | ``` 548 | 549 | ### isPrimitive 550 | 551 | Checks if value is primitive. 552 | 553 | ``` js 554 | //=> true 555 | _.isPrimitive(null) 556 | 557 | //=> true 558 | _.isPrimitive(undefined) 559 | 560 | //=> true 561 | _.isPrimitive(1) 562 | 563 | //=> true 564 | _.isPrimitive('string') 565 | 566 | //=> true 567 | _.isPrimitive(true) 568 | 569 | //=> true 570 | _.isPrimitive(Symbol()) 571 | 572 | //=> false 573 | _.isPrimitive({}) 574 | 575 | //=> false 576 | _.isPrimitive([]) 577 | ``` 578 | 579 | ### isTypedArray 580 | 581 | Checks if value is classified as a typed array. 582 | 583 | ``` js 584 | //=> true 585 | _.isTypedArray(new Uint8Array([1, 2, 3])) 586 | 587 | //=> false 588 | _.isTypedArray([]) 589 | ``` 590 | 591 | ### isEqual 592 | 593 | Performs a deep comparison between two values to determine if they are equivalent. 594 | 595 | ``` js 596 | //=> true 597 | _.isEqual([1, 2, 3], [1, 2, 3]) 598 | 599 | //=> false 600 | _.isEqual([1, 2, 3], [1, 2, 4]) 601 | 602 | //=> true 603 | _.isEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) 604 | ``` 605 | 606 | ## Function 607 | 608 | ### compose/flowRight 609 | 610 | Composes functions from right to left. The rightmost function can take multiple arguments, the remaining functions must be unary. 611 | 612 | ::: tip 613 | In `midash`, `flowRight` is an alias of `compose`. 614 | ::: 615 | 616 | ``` js 617 | const double = x => x * 2 618 | const square = x => x * x 619 | 620 | //=> 200 621 | _.compose(double, square)(10) 622 | _.flowRight(double, square)(10) 623 | ``` 624 | 625 | ### property 626 | 627 | Creates a function that returns the value at path of a given object. 628 | 629 | ``` js 630 | const objects = [ 631 | { 'a': { 'b': 2 } }, 632 | { 'a': { 'b': 1 } } 633 | ]; 634 | // => [2, 1] 635 | _.map(objects, _.property('a.b')); 636 | 637 | // => [1, 2] 638 | _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b'); 639 | ``` 640 | 641 | ### once 642 | 643 | Creates a function that is restricted to be called only once. Repeat calls to the function return the value of the first invocation. 644 | 645 | ``` js 646 | // `initialize` can only call `createApplication` once. 647 | const initialize = _.once(createApplication); 648 | initialize(); 649 | initialize(); // No effect 650 | ``` 651 | 652 | ### memoize 653 | 654 | Creates a function that memoizes the result of func. 655 | 656 | ``` js 657 | const object = { 'a': 1, 'b': 2 }; 658 | const other = { 'c': 3, 'd': 4 }; 659 | 660 | // => [1, 2] 661 | const values = _.memoize(Object.values); 662 | values(object); 663 | 664 | // => [3, 4] 665 | values(other); 666 | 667 | object.a = 2; 668 | // => [1, 2] (cached result) 669 | values(object); 670 | ``` 671 | 672 | ### debounce 673 | 674 | Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked. 675 | 676 | ``` js 677 | // Create a debounced function that will only invoke updateChart 678 | // after waiting at least 200ms from the last time it was called 679 | const debouncedUpdate = _.debounce(updateChart, 200); 680 | 681 | // Call it multiple times 682 | window.addEventListener('resize', debouncedUpdate); 683 | ``` 684 | 685 | ### throttle 686 | 687 | Creates a throttled function that only invokes func at most once per every wait milliseconds. 688 | 689 | ``` js 690 | // Create a throttled function that only invokes saveInput 691 | // at most once every 500ms 692 | const throttledSave = _.throttle(saveInput, 500); 693 | 694 | // Call it multiple times 695 | inputField.addEventListener('input', throttledSave); 696 | ``` 697 | 698 | ## Math 699 | 700 | ### sum 701 | 702 | Computes the sum of the values in array. 703 | 704 | ``` js 705 | // => 6 706 | _.sum([1, 2, 3]) 707 | 708 | // => 0 709 | _.sum([]) 710 | ``` 711 | 712 | ### max 713 | 714 | Gets the maximum value of collection. If collection is empty or falsey, undefined is returned. 715 | 716 | ::: tip 717 | In `midash`, `maxBy` is an alias of `max`. 718 | ::: 719 | 720 | ``` js 721 | // => 5 722 | _.max([-5, -3, 0, 3, 5]) 723 | 724 | // => { a: 4 } 725 | _.maxBy([ 726 | { a: 3 }, 727 | { a: 4 } 728 | ], x => x.a) 729 | ``` 730 | 731 | ### min 732 | 733 | Gets the minimum value of collection. If collection is empty or falsey, undefined is returned. 734 | 735 | ::: tip 736 | In `midash`, `minBy` is an alias of `min`. 737 | ::: 738 | 739 | ``` js 740 | // => -5 741 | _.min([-5, -3, 0, 3, 5]) 742 | 743 | // => { a: 3 } 744 | _.minBy([ 745 | { a: 3 }, 746 | { a: 4 } 747 | ], x => x.a) 748 | ``` 749 | 750 | ## Async 751 | 752 | ### sleep 753 | 754 | Creates a Promise that resolves after the specified milliseconds. 755 | 756 | ``` js 757 | // Pause execution for 1 second 758 | await _.sleep(1000); 759 | console.log('This logs after 1 second'); 760 | ``` 761 | 762 | ### retry 763 | 764 | Attempts to execute a function multiple times until it succeeds. 765 | 766 | ``` js 767 | // Retry fetching data up to 3 times 768 | const data = await _.retry(async () => { 769 | const response = await fetch('https://api.example.com/data'); 770 | if (!response.ok) throw new Error('Failed to fetch'); 771 | return response.json(); 772 | }, { times: 3 }); 773 | ``` 774 | 775 | ### map 776 | 777 | Asynchronously maps over an array with concurrency control. 778 | 779 | ``` js 780 | // Process 2 items at a time 781 | const results = await _.map([1, 2, 3, 4, 5], async (num) => { 782 | await _.sleep(100); 783 | return num * 2; 784 | }, { concurrency: 2 }); 785 | // => [2, 4, 6, 8, 10] 786 | ``` 787 | 788 | ### filter 789 | 790 | Asynchronously filters an array with concurrency control. 791 | 792 | ``` js 793 | // Keep only even numbers, processing 3 at a time 794 | const evens = await _.filter([1, 2, 3, 4, 5, 6], async (num) => { 795 | await _.sleep(100); 796 | return num % 2 === 0; 797 | }, { concurrency: 3 }); 798 | // => [2, 4, 6] 799 | ``` -------------------------------------------------------------------------------- /docs/zh/Readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | --- 4 | 5 | # midash 6 | 7 | 与 lodash 拥有相似 API,基于 ES6+,体积更小的工具函数库,附带额外的异步工具函数。 8 | 9 | + 🔨 高频使用 API 10 | + 🕒 熟悉的 lodash API 11 | + 💪 支持 Tree Shaking 12 | + 👫 支持 Typescript 13 | + 🔥 体积更小 (基于 ES6+) 14 | + 📦 仅 2.5kb 的迷你库 15 | + 🚀 提供异步工具函数 16 | 17 | ## 安装 18 | 19 | ``` bash 20 | # yarn 21 | $ yarn add midash 22 | # pnpm 23 | $ pnpm i midash 24 | ``` 25 | 26 | ``` js 27 | import { sum } from 'midash' 28 | 29 | sum([1, 3, 5, 7, 9]) 30 | ``` 31 | 32 | ## 异步工具函数 33 | 34 | midash 提供了几个有用的异步工具函数: 35 | 36 | - `sleep(ms)`: 暂停执行指定的毫秒数。 37 | - `retry(fn, options)`: 多次尝试执行函数,直到成功。 38 | - `map(iterable, mapper, options)`: 异步映射可迭代对象,可控制并发数。 39 | - `filter(iterable, filterer, options)`: 异步过滤可迭代对象,可控制并发数。 40 | 41 | 使用示例: 42 | 43 | ```js 44 | import { sleep, retry, map, filter } from 'midash' 45 | 46 | // 暂停1秒 47 | await sleep(1000) 48 | 49 | // 最多尝试3次 50 | const result = await retry(async () => { 51 | // 你的异步操作 52 | }, { times: 3 }) 53 | 54 | // 异步映射数组,并发数为2 55 | const mappedResults = await map([1, 2, 3, 4], async (num) => { 56 | await sleep(100) 57 | return num * 2 58 | }, { concurrency: 2 }) 59 | 60 | // 异步过滤数组 61 | const filteredResults = await filter([1, 2, 3, 4, 5], async (num) => { 62 | await sleep(100) 63 | return num % 2 === 0 64 | }) 65 | ``` -------------------------------------------------------------------------------- /docs/zh/api.md: -------------------------------------------------------------------------------- 1 | # midash API 2 | 3 | ## Object 4 | 5 | ### get 6 | 7 | 获取 `object` 的嵌套属性。 8 | 9 | ``` js 10 | const object = { a: [{ b: 3 }] } 11 | 12 | // => 3 13 | _.get(object, 'a[0].b') 14 | 15 | // => 3 16 | _.get(object, ['a', '0', 'b']) 17 | 18 | // => 'default' 19 | _.get(object, 'a.b.c', 'default') 20 | ``` 21 | 22 | ### omit 23 | 24 | 忽略 `object` 的某些属性,并返回新的 `object`。 25 | 26 | ::: warning 27 | 在 `midash` 中无法使用 `_.omit(object, 'a', 'b')`,请使用 `_.omit(object, ['a', 'b'])` 替代。 28 | ::: 29 | 30 | ``` js 31 | const object = { 32 | a: 3, 33 | b: 4, 34 | c: 5 35 | } 36 | 37 | //=> { c: 5 } 38 | _.omit(object, ['a', 'b']) 39 | 40 | ``` 41 | 42 | ### omitBy 43 | 44 | 通过函数忽略 `object` 的某些属性,并返回新的 `object`。 45 | 46 | ``` js 47 | const object = { 48 | a: 3, 49 | b: 4, 50 | c: 5 51 | } 52 | 53 | // 通过 value 进行筛选忽略 54 | //=> { b:4, c: 5 } 55 | _.omitBy(object, value => value === 3) 56 | 57 | // 通过 key 进行筛选忽略 58 | //=> { b:4, c: 5 } 59 | _.omitBy(object, (value, key) => key === 'a') 60 | ``` 61 | 62 | ### pick 63 | 64 | 选择 `object` 的某些属性,并返回新的 `object`。 65 | 66 | ::: warning 67 | 在 `midash` 中无法使用 `_.pick(object, 'a', 'b')`,请使用 `_.pick(object, ['a', 'b'])` 替代。 68 | ::: 69 | 70 | ``` js 71 | const object = { 72 | a: 3, 73 | b: 4, 74 | c: undefined 75 | } 76 | 77 | //=> { a: 3, b: 4 } 78 | _.pick(object, ['a', 'b']) 79 | 80 | //=> {} 81 | _.pick(object, ['z']) 82 | 83 | //=> { c: undefined } 84 | _.pick(object, ['c']) 85 | ``` 86 | 87 | ### pickBy 88 | 89 | 通过函数选择 `object` 的某些属性,并返回新的 `object`。 90 | 91 | ``` js 92 | const object = { 93 | a: 3, 94 | b: 4, 95 | } 96 | 97 | //=> { a: 3 } 98 | _.pickBy(object, value => value === 3) 99 | 100 | //=> { a: 3 } 101 | _.pickBy(object, (value, key) => key === 'a') 102 | ``` 103 | 104 | ### defaults 105 | 106 | 将来源对象中的所有可枚举属性分配到目标对象上,但仅在目标对象的属性为 undefined 时才分配。 107 | 108 | ``` js 109 | //=> { mode: 'development', sourcemap: true, devtool: true } 110 | _.defaults({ 111 | mode: 'development', 112 | sourcemap: true 113 | }, { 114 | mode: 'production', 115 | devtool: true 116 | }) 117 | ``` 118 | 119 | ### clone 120 | 121 | 浅拷贝对象属性 122 | 123 | ``` js 124 | const o = { a: { aa: 3 }, b: 4 } 125 | 126 | //=> true 127 | _.clone(o).a === o.a 128 | ``` 129 | 130 | ### cloneDeep 131 | 132 | 递归拷贝对象属性 133 | 134 | ``` js 135 | const o = { a: { aa: 3 }, b: 4 } 136 | 137 | //=> false 138 | _.cloneDeep(o).a === o.a 139 | ``` 140 | 141 | ### merge 142 | 143 | 递归合并所有对象的属性至第一参数对象中,并返回新的对象。 144 | 145 | ``` js 146 | //=> { a: 4, b: 2 } 147 | _.merge({ a: 1 }, { b: 2 }, { a: 3 }, { a: 4 }) 148 | ``` 149 | 150 | ### assign 151 | 152 | 分配来源对象的可枚举属性到目标对象上。 来源对象的应用规则是从左到右,随后的下一个对象的属性会覆盖上一个对象的属性。 153 | 154 | ``` js 155 | // => { a: 1, b: 3, c: 5, d: 6 } 156 | _.assign({ a: 1, b: 2 }, { b: 3, c: 4 }, { c: 5, d: 6 }) 157 | ``` 158 | 159 | ### mapKeys 160 | 161 | 使用函数转换对象的键,并返回新对象。 162 | 163 | ``` js 164 | //=> { a3: 3, b4: 4 } 165 | _.mapKeys({ a: 3, b: 4 }, (v, k) => `${k}${v}`) 166 | ``` 167 | 168 | ## Array 169 | 170 | ### chunk 171 | 172 | 对数组按照 `size` 进行分组。 173 | 174 | ``` js 175 | //=> [[0, 1, 2], [3, 4, 5]] 176 | _.chunk([0, 1, 2, 3, 4, 5], 3) 177 | 178 | //=> [[0], [1], [2]] 179 | _.chunk([0, 1, 2]) 180 | 181 | //=> [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']] 182 | _.chunk('abcdefghi', 3) 183 | ``` 184 | 185 | ### sample 186 | 187 | 从一个数组中随机获取值。 188 | 189 | ``` js 190 | // get a random element from [0, 3, 6, 10] 191 | _.sample([0, 3, 6, 10]) 192 | 193 | //=> undefined 194 | _.sample([]) 195 | ``` 196 | 197 | ### sampleSize 198 | 199 | 从一个数组中随机获取 `n` 个值。 200 | 201 | ``` js 202 | //=> Maybe [1, 2] 203 | _.sampleSize([1, 2, 3], 2) 204 | 205 | //=> [1, 2, 3] 206 | _.sampleSize([1, 2, 3], 4) 207 | ``` 208 | 209 | ### shuffle 210 | 211 | 创建一个被打乱元素顺序的数组。 212 | 213 | ``` js 214 | //=> [2, 3, 1](随机顺序) 215 | _.shuffle([1, 2, 3]) 216 | ``` 217 | 218 | ### difference/differenceBy 219 | 220 | 创建一个不包含在其他给定数组中的数组值的新数组。 221 | 222 | ::: tip 223 | 在 `midash` 中,`differenceBy` 是 `difference` 的别名。 224 | ::: 225 | 226 | ``` js 227 | //=> [2, 4] 228 | _.difference([1, 2, 3, 4], [1, 3, 5]) 229 | 230 | //=> [{ a: 4 }] 231 | _.differenceBy([{ a: 3 }, { a: 4 }], [{ a: 3 }], x => x.a) 232 | ``` 233 | 234 | ### intersection 235 | 236 | 创建一个包含所有给定数组中共有元素的新数组。 237 | 238 | ``` js 239 | //=> [2] 240 | _.intersection([1, 2], [2, 3]) 241 | 242 | //=> [{ id: 1 }] 243 | _.intersection([{ id: 1 }, { id: 2 }], [{ id: 1 }, { id: 3 }], item => item.id) 244 | ``` 245 | 246 | ### uniq 247 | 248 | 创建一个去重后的数组。 249 | 250 | ``` js 251 | //=> [1, 2, 3] 252 | _.uniq([1, 2, 3, 1, 2]) 253 | ``` 254 | 255 | ### uniqBy 256 | 257 | 使用迭代函数的返回值来进行去重。 258 | 259 | ``` js 260 | //=> [{ id: 1 }, { id: 2 }] 261 | _.uniqBy([{ id: 1 }, { id: 2 }, { id: 1 }], item => item.id) 262 | ``` 263 | 264 | ### keyBy 265 | 266 | 根据条件生成键,并对数组转化为对象。 267 | 268 | ``` js 269 | const list = [ 270 | { id: 1, name: 'hello' }, 271 | { id: 2, name: 'world' }, 272 | ] 273 | 274 | //=> { '1': { id: 1, name: 'hello' }, '2': { id: 2, name: 'world' } } 275 | _.keyBy(list, x => x.id) 276 | ``` 277 | 278 | ### groupBy 279 | 280 | 根据条件对数组进行分组。 281 | 282 | ``` js 283 | //=> { '3': ['one', 'two'], '5': ['three'] } 284 | _.groupBy(['one', 'two', 'three'], x => x.length) 285 | ``` 286 | 287 | ### zip 288 | 289 | 创建一个分组元素的数组,数组的第一个元素包含所有给定数组的第一个元素,数组的第二个元素包含所有给定数组的第二个元素,以此类推。 290 | 291 | ``` js 292 | // => [[1, 'a', true],[2, 'b', false],[3, 'c', undefined]]; 293 | _.zip([1, 2, 3], ['a', 'b', 'c'], [true, false]); 294 | 295 | // => [[undefined, 1], [undefined, 2], [undefined, 3]]; 296 | _.zip([],[1, 2, 3]) 297 | 298 | // => [[1, 'a', undefined], [2, 'b', undefined],[undefined, 'c', undefined]]; 299 | _.zip([1, 2], ['a', 'b', 'c'], []) 300 | ``` 301 | 302 | ### unzip 303 | 304 | 这个方法类似于_.zip,除了它接收分组元素的数组,并且创建一个数组,分组元素到打包前的结构。 305 | 306 | ``` js 307 | // => [[1, 2, 3], ['a', 'b', 'c'], [true, false, true]] 308 | _.unzip([[1, 'a', true], [2, 'b', false], [3, 'c', true]]) 309 | 310 | // => [] 311 | _.unzip([]) 312 | 313 | // => [[1, 2, 4], ['a', 'b', 'c'], [undefined, 3, undefined]] 314 | _.unzip([[1, 'a'], [2, 'b', 3], [4, 'c']]) 315 | ``` 316 | 317 | ### compact 318 | 319 | 过滤掉数组中的假值( `false`、`undefined`、`null`、`''`、`0`), 并返回新数组。 320 | 321 | ``` js 322 | // => [1, 2, 3] 323 | _.compact([0, 1, false, 2, '', 3]) 324 | ``` 325 | 326 | ### head 327 | 328 | 获取数组中的第一个元素。 329 | 330 | ``` js 331 | // => 1 332 | _.head([1, 2, 3]) 333 | 334 | // => undefined 335 | _.head([]) 336 | ``` 337 | 338 | ### nth 339 | 340 | 获取数组中指定索引的元素。如果索引为负数,则从末尾开始计数。 341 | 342 | ``` js 343 | // => 2 344 | _.nth([1, 2, 3], 1) 345 | 346 | // => 3 347 | _.nth([1, 2, 3], -1) 348 | ``` 349 | 350 | ## String 351 | 352 | ### camelCase 353 | 354 | 将字符串转换为驼峰式。 355 | 356 | ``` js 357 | // => 'fooBar' 358 | _.camelCase('foo_bar') 359 | 360 | // => 'fooBar' 361 | _.camelCase('foo-bar') 362 | 363 | // => 'fooBar' 364 | _.camelCase('Foo Bar') 365 | ``` 366 | 367 | ### snakeCase 368 | 369 | 将字符串转换为下划线式。 370 | 371 | ``` js 372 | // => 'foo_bar' 373 | _.snakeCase('fooBar') 374 | 375 | // => 'foo_bar' 376 | _.snakeCase('foo-bar') 377 | 378 | // => 'foo_bar' 379 | _.snakeCase('Foo Bar') 380 | ``` 381 | 382 | ### kebabCase 383 | 384 | 将字符串转换为短横线连接式。 385 | 386 | ``` js 387 | // => 'foo-bar' 388 | _.kebabCase('fooBar') 389 | 390 | // => 'foo-bar' 391 | _.kebabCase('foo_bar') 392 | 393 | // => 'foo-bar' 394 | _.kebabCase('Foo Bar') 395 | ``` 396 | 397 | ### words 398 | 399 | 将字符串分割成单词数组。 400 | 401 | ``` js 402 | // => ['foo', 'bar'] 403 | _.words('foo bar') 404 | 405 | // => ['foo', 'bar'] 406 | _.words('foo-bar', /[^, -]+/g) 407 | ``` 408 | 409 | ### template 410 | 411 | 创建一个预编译模板函数,可以插入数据属性。 412 | 413 | ``` js 414 | // => 'Hello Fred!' 415 | _.template('Hello ${name}!')({ name: 'Fred' }) 416 | ``` 417 | 418 | ## Number 419 | 420 | ### random 421 | 422 | 获取一个随机整数。 423 | 424 | ``` js 425 | // 10 到 20 之间的一个随机整数,闭区间,包括 10 与 20 426 | _.random(10, 20) 427 | 428 | // 0 到 20 之间的一个随机整数 429 | _.random(20) 430 | 431 | // 0 到 1 之间的一个随机整数 432 | _.random() 433 | ``` 434 | 435 | ### range 436 | 437 | 创建一个包含从 start 到 end(不包括end)之间步长为 step 的数字的数组。 438 | 439 | ``` js 440 | //=> [0, 1, 2, 3] 441 | _.range(4) 442 | 443 | //=> [0, -1, -2, -3] 444 | _.range(-4) 445 | 446 | //=> [1, 2, 3, 4] 447 | _.range(1, 5) 448 | 449 | //=> [5, 4, 3, 2] 450 | _.range(5, 1) 451 | 452 | //=> [0, -1, -2, -3] 453 | _.range(0, -4, -1) 454 | ``` 455 | 456 | ## Lang 457 | 458 | ### castArray 459 | 460 | 如果 value 不是数组, 那么强制转为数组。 461 | 462 | ``` js 463 | _.castArray(1); 464 | // => [1] 465 | 466 | _.castArray({ 'a': 1 }); 467 | // => [{ 'a': 1 }] 468 | 469 | _.castArray('abc'); 470 | // => ['abc'] 471 | 472 | _.castArray(null); 473 | // => [null] 474 | 475 | _.castArray(undefined); 476 | // => [undefined] 477 | 478 | _.castArray(); 479 | // => [] 480 | 481 | const array = [1, 2, 3]; 482 | console.log(_.castArray(array) === array); 483 | // => true 484 | ``` 485 | 486 | ### isArray 487 | 488 | 检查值是否为 Array 类型。 489 | 490 | ``` js 491 | //=> true 492 | _.isArray([]) 493 | 494 | //=> false 495 | _.isArray({}) 496 | ``` 497 | 498 | ### isBoolean 499 | 500 | 检查值是否为布尔原始类型。 501 | 502 | ``` js 503 | //=> true 504 | _.isBoolean(false) 505 | 506 | //=> true 507 | _.isBoolean(true) 508 | 509 | //=> false 510 | _.isBoolean(null) 511 | ``` 512 | 513 | ### isObject 514 | 515 | 检查值是否为 Object 类型。 516 | 517 | ``` js 518 | //=> true 519 | _.isObject({}) 520 | 521 | //=> true 522 | _.isObject([]) 523 | 524 | //=> true 525 | _.isObject(x => {}) 526 | 527 | //=> false 528 | _.isObject(null) 529 | ``` 530 | 531 | ### isPlainObject 532 | 533 | 检查值是否为普通对象。 534 | 535 | ``` js 536 | //=> true 537 | _.isPlainObject({}) 538 | 539 | //=> true 540 | _.isPlainObject(Object.create(null)) 541 | 542 | //=> false 543 | _.isPlainObject([]) 544 | 545 | //=> false 546 | _.isPlainObject(new Date()) 547 | ``` 548 | 549 | ### isPromise 550 | 551 | 检查值是否为 Promise 对象。 552 | 553 | ``` js 554 | //=> true 555 | _.isPromise(Promise.resolve()) 556 | 557 | //=> false 558 | _.isPromise({}) 559 | ``` 560 | 561 | ### isPrimitive 562 | 563 | 检查值是否为原始类型。 564 | 565 | ``` js 566 | //=> true 567 | _.isPrimitive(null) 568 | 569 | //=> true 570 | _.isPrimitive(undefined) 571 | 572 | //=> true 573 | _.isPrimitive(1) 574 | 575 | //=> true 576 | _.isPrimitive('string') 577 | 578 | //=> true 579 | _.isPrimitive(true) 580 | 581 | //=> true 582 | _.isPrimitive(Symbol()) 583 | 584 | //=> false 585 | _.isPrimitive({}) 586 | 587 | //=> false 588 | _.isPrimitive([]) 589 | ``` 590 | 591 | ### isTypedArray 592 | 593 | 检查值是否为类型化数组。 594 | 595 | ``` js 596 | //=> true 597 | _.isTypedArray(new Uint8Array([1, 2, 3])) 598 | 599 | //=> false 600 | _.isTypedArray([]) 601 | ``` 602 | 603 | ### isEqual 604 | 605 | 执行深度比较两个值是否相等。 606 | 607 | ``` js 608 | //=> true 609 | _.isEqual([1, 2, 3], [1, 2, 3]) 610 | 611 | //=> false 612 | _.isEqual([1, 2, 3], [1, 2, 4]) 613 | 614 | //=> true 615 | _.isEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) 616 | ``` 617 | 618 | ## Function 619 | 620 | ### compose/flowRight 621 | 622 | 从右至左执行函数,并将上一个函数的返回值作为下一个函数的参数。 623 | 624 | ::: tip 625 | 在 `midash` 中,`flowRight` 是 `compose` 的别名。 626 | ::: 627 | 628 | ``` js 629 | const double = x => x * 2 630 | const square = x => x * x 631 | 632 | //=> 200 633 | _.compose(double, square)(10) 634 | _.flowRight(double, square)(10) 635 | ``` 636 | 637 | ### property 638 | 639 | 创建一个返回给定对象的 path 的值的函数。 640 | 641 | ``` js 642 | const objects = [ 643 | { 'a': { 'b': 2 } }, 644 | { 'a': { 'b': 1 } } 645 | ]; 646 | // => [2, 1] 647 | _.map(objects, _.property('a.b')); 648 | 649 | // => [1, 2] 650 | _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b'); 651 | ``` 652 | 653 | ### once 654 | 655 | 创建一个只能调用一次的函数。重复调用将返回第一次调用的结果。 656 | 657 | ``` js 658 | // `initialize` 只能调用 `createApplication` 一次。 659 | const initialize = _.once(createApplication); 660 | initialize(); 661 | initialize(); // 无效 662 | ``` 663 | 664 | ### memoize 665 | 666 | 创建一个会缓存 func 结果的函数。 667 | 668 | ``` js 669 | const object = { 'a': 1, 'b': 2 }; 670 | const other = { 'c': 3, 'd': 4 }; 671 | 672 | // => [1, 2] 673 | const values = _.memoize(Object.values); 674 | values(object); 675 | 676 | // => [3, 4] 677 | values(other); 678 | 679 | object.a = 2; 680 | // => [1, 2] (缓存结果) 681 | values(object); 682 | ``` 683 | 684 | ### debounce 685 | 686 | 创建一个防抖函数,该函数会在调用结束后的指定毫秒后才执行。 687 | 688 | ``` js 689 | // 创建一个防抖函数,只有在上次调用后至少200ms才会执行updateChart 690 | const debouncedUpdate = _.debounce(updateChart, 200); 691 | 692 | // 多次调用 693 | window.addEventListener('resize', debouncedUpdate); 694 | ``` 695 | 696 | ### throttle 697 | 698 | 创建一个节流函数,该函数最多每隔 wait 毫秒执行一次。 699 | 700 | ``` js 701 | // 创建一个节流函数,每500ms最多执行saveInput一次 702 | const throttledSave = _.throttle(saveInput, 500); 703 | 704 | // 多次调用 705 | inputField.addEventListener('input', throttledSave); 706 | ``` 707 | 708 | ## Math 709 | 710 | ### sum 711 | 712 | 计算数组中所有值的总和。 713 | 714 | ``` js 715 | // => 6 716 | _.sum([1, 2, 3]) 717 | 718 | // => 0 719 | _.sum([]) 720 | ``` 721 | 722 | ### max 723 | 724 | 获取数字数组中最大的值。如果数组为空或者不存在,则返回 `undefined`。 725 | 726 | ::: tip 727 | 在 `midash` 中,`maxBy` 是 `max` 的别名。 728 | ::: 729 | 730 | ``` js 731 | // => 5 732 | _.max([-5, -3, 0, 3, 5]) 733 | 734 | // => { a: 4 } 735 | _.maxBy([ 736 | { a: 3 }, 737 | { a: 4 } 738 | ], x => x.a) 739 | ``` 740 | 741 | ### min 742 | 743 | 获取数字数组中最小的值。如果数组为空或者不存在,则返回 `undefined`。 744 | 745 | ::: tip 746 | 在 `midash` 中,`minBy` 是 `min` 的别名。 747 | ::: 748 | 749 | ``` js 750 | // => -5 751 | _.min([-5, -3, 0, 3, 5]) 752 | 753 | // => { a: 3 } 754 | _.minBy([ 755 | { a: 3 }, 756 | { a: 4 } 757 | ], x => x.a) 758 | ``` 759 | 760 | ## Async 761 | 762 | ### sleep 763 | 764 | 创建一个在指定毫秒后解析的 Promise。 765 | 766 | ``` js 767 | // 暂停执行1秒 768 | await _.sleep(1000); 769 | console.log('1秒后输出'); 770 | ``` 771 | 772 | ### retry 773 | 774 | 尝试多次执行一个函数,直到成功。 775 | 776 | ``` js 777 | // 最多尝试3次获取数据 778 | const data = await _.retry(async () => { 779 | const response = await fetch('https://api.example.com/data'); 780 | if (!response.ok) throw new Error('获取失败'); 781 | return response.json(); 782 | }, { times: 3 }); 783 | ``` 784 | 785 | ### map 786 | 787 | 异步映射数组,可控制并发数量。 788 | 789 | ``` js 790 | // 一次处理2个项目 791 | const results = await _.map([1, 2, 3, 4, 5], async (num) => { 792 | await _.sleep(100); 793 | return num * 2; 794 | }, { concurrency: 2 }); 795 | // => [2, 4, 6, 8, 10] 796 | ``` 797 | 798 | ### filter 799 | 800 | 异步过滤数组,可控制并发数量。 801 | 802 | ``` js 803 | // 仅保留偶数,一次处理3个项目 804 | const evens = await _.filter([1, 2, 3, 4, 5, 6], async (num) => { 805 | await _.sleep(100); 806 | return num % 2 === 0; 807 | }, { concurrency: 3 }); 808 | // => [2, 4, 6] 809 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midash", 3 | "description": "An alternative to lodash with the same API.", 4 | "version": "1.0.3", 5 | "sideEffects": false, 6 | "license": "MIT", 7 | "exports": { 8 | ".": { 9 | "require": { 10 | "types": "./dist/index.d.ts", 11 | "default": "./dist/index.cjs.js" 12 | }, 13 | "import": { 14 | "types": "./dist/index.d.mts", 15 | "default": "./dist/index.esm.js" 16 | } 17 | } 18 | }, 19 | "main": "./dist/index.cjs.js", 20 | "module": "./dist/index.esm.js", 21 | "typings": "dist/index.d.ts", 22 | "files": [ 23 | "dist", 24 | "src" 25 | ], 26 | "engines": { 27 | "node": ">=14" 28 | }, 29 | "scripts": { 30 | "build": "tsup", 31 | "test": "vitest", 32 | "lint": "eslint src/**/*.ts", 33 | "prepare": "pnpm run build", 34 | "size": "size-limit", 35 | "analyze": "size-limit --why", 36 | "release": "release-it", 37 | "docs:dev": "vuepress dev docs", 38 | "docs:build": "vuepress build docs" 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "pre-commit": "pnpm lint" 43 | } 44 | }, 45 | "prettier": { 46 | "printWidth": 80, 47 | "semi": false, 48 | "singleQuote": true, 49 | "trailingComma": "es5", 50 | "arrowParens": "avoid" 51 | }, 52 | "eslintConfig": { 53 | "extends": [ 54 | "prettier", 55 | "plugin:prettier/recommended" 56 | ], 57 | "rules": { 58 | "dot-notation": "off" 59 | }, 60 | "parser": "@typescript-eslint/parser", 61 | "parserOptions": { 62 | "ecmaVersion": "latest", 63 | "sourceType": "module" 64 | } 65 | }, 66 | "author": "shfshanyue", 67 | "repository": "shfshanyue/midash", 68 | "size-limit": [ 69 | { 70 | "path": "dist/index.cjs.js", 71 | "limit": "20 KB" 72 | }, 73 | { 74 | "path": "dist/index.esm.js", 75 | "limit": "20 KB" 76 | } 77 | ], 78 | "devDependencies": { 79 | "@types/node": "^20.8.10", 80 | "@typescript-eslint/parser": "^6.9.1", 81 | "@vitest/coverage-v8": "^0.34.6", 82 | "@vuepress/client": "2.0.0-beta.66", 83 | "@vuepress/theme-default": "2.0.0-beta.66", 84 | "@vuepress/utils": "2.0.0-beta.66", 85 | "eslint": "^8.52.0", 86 | "eslint-config-prettier": "^9.0.0", 87 | "eslint-plugin-prettier": "^5.0.1", 88 | "husky": "^8.0.3", 89 | "microbundle": "^0.15.1", 90 | "release-it": "^16.2.1", 91 | "size-limit": "^10.0.2", 92 | "time-span": "^5.1.0", 93 | "tslib": "^2.6.2", 94 | "tsup": "^7.2.0", 95 | "typescript": "^5.2.2", 96 | "vitest": "^0.34.6", 97 | "vue": "^3.3.7", 98 | "vuepress": "2.0.0-beta.66" 99 | }, 100 | "release-it": { 101 | "git": { 102 | "push": true, 103 | "commit": true, 104 | "tag": true, 105 | "requireCommits": false, 106 | "requireCleanWorkingDir": false 107 | }, 108 | "github": { 109 | "release": true, 110 | "draft": true 111 | }, 112 | "npm": { 113 | "publish": true, 114 | "ignoreVersion": false 115 | }, 116 | "hooks": { 117 | "before:init": "CI=true pnpm test && pnpm run build", 118 | "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}." 119 | } 120 | }, 121 | "keywords": [ 122 | "utils", 123 | "utilities", 124 | "toolkit", 125 | "pure", 126 | "functional" 127 | ], 128 | "packageManager": "pnpm@8.5.1" 129 | } 130 | -------------------------------------------------------------------------------- /src/assign.ts: -------------------------------------------------------------------------------- 1 | export function assign(target: T, source: U): T & U 2 | export function assign( 3 | target: T, 4 | source1: U, 5 | source2: V 6 | ): T & U & V 7 | export function assign( 8 | target: T, 9 | source1: U, 10 | source2: V, 11 | source3: W 12 | ): T & U & V & W 13 | 14 | export function assign(target: object, ...sources: any[]): any { 15 | if (target === null || target === undefined) { 16 | target = {} 17 | } 18 | return Object.assign(target, ...sources) 19 | } 20 | -------------------------------------------------------------------------------- /src/async.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number) { 2 | return new Promise(resolve => { 3 | setTimeout(resolve, ms) 4 | }) 5 | } 6 | 7 | interface RetryOptions { 8 | readonly times?: number 9 | readonly onFailedAttempt?: (error: Error) => void | Promise 10 | } 11 | 12 | export class AbortError extends Error { 13 | originalError: Error 14 | 15 | constructor(message: string | Error) { 16 | super() 17 | 18 | if (message instanceof Error) { 19 | this.originalError = message 20 | ;({ message } = message) 21 | } else { 22 | this.originalError = new Error(message) 23 | this.originalError.stack = this.stack 24 | } 25 | 26 | this.name = 'AbortError' 27 | this.message = message 28 | } 29 | } 30 | 31 | export async function retry( 32 | run: (attemptCount: number) => Promise | T, 33 | { times = 10, onFailedAttempt = () => {} }: RetryOptions = {} 34 | ) { 35 | let count = 1 36 | async function exec(): Promise { 37 | try { 38 | const result = await run(count) 39 | return result 40 | } catch (e) { 41 | if (count >= times || e instanceof AbortError) { 42 | throw e 43 | } 44 | count++ 45 | await onFailedAttempt(e as Error) 46 | return exec() 47 | } 48 | } 49 | return exec() 50 | } 51 | 52 | interface MapOptions { 53 | readonly concurrency?: number 54 | readonly settled?: boolean 55 | } 56 | 57 | export type Mapper = ( 58 | element: Element, 59 | index: number 60 | ) => NewElement | Promise 61 | 62 | export function map( 63 | it: Iterable, 64 | mapper: Mapper, 65 | { concurrency = Infinity }: MapOptions = {} 66 | ): Promise { 67 | const list = Array.from(it) 68 | return new Promise(resolve => { 69 | let currentIndex = 0 70 | let result: NewElement[] = [] 71 | let resolveCount = 0 72 | let len = list.length 73 | function next() { 74 | const index = currentIndex 75 | currentIndex++ 76 | Promise.resolve(list[index]) 77 | .then(o => mapper(o, index)) 78 | .then(o => { 79 | result[index] = o 80 | resolveCount++ 81 | if (resolveCount === len) { 82 | resolve(result) 83 | } 84 | if (currentIndex < len) { 85 | next() 86 | } 87 | }) 88 | } 89 | for (let i = 0; i < concurrency && i < len; i++) { 90 | next() 91 | } 92 | }) 93 | } 94 | 95 | export async function filter( 96 | it: Iterable, 97 | filterer: (item: Element, index: number) => boolean | Promise, 98 | options?: MapOptions 99 | ): Promise { 100 | const list = await map( 101 | it, 102 | async (item, index) => { 103 | const bool = await filterer(item, index) 104 | return [item, bool] 105 | }, 106 | options 107 | ) 108 | return list.filter(([_, bool]) => bool).map(([item]) => item) 109 | } 110 | -------------------------------------------------------------------------------- /src/camelCase.ts: -------------------------------------------------------------------------------- 1 | import { words } from './words' 2 | 3 | export function camelCase(str: string) { 4 | return words(str) 5 | .map((word, i) => (i ? word[0].toUpperCase() + word.slice(1) : word)) 6 | .join('') 7 | } 8 | -------------------------------------------------------------------------------- /src/castArray.ts: -------------------------------------------------------------------------------- 1 | export function castArray(...args: T[]): T[] { 2 | if (!args.length) { 3 | return [] 4 | } 5 | const value = args[0] 6 | return Array.isArray(value) ? value : [value] 7 | } 8 | -------------------------------------------------------------------------------- /src/chunk.ts: -------------------------------------------------------------------------------- 1 | export function chunk(list: ArrayLike, size: number = 1): T[][] { 2 | const l: T[][] = [] 3 | for (let i = 0; i < list.length; i++) { 4 | const index = Math.floor(i / Math.floor(size)) 5 | l[index] = l[index] || [] 6 | l[index].push(list[i]) 7 | } 8 | return l 9 | } 10 | -------------------------------------------------------------------------------- /src/clone.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from './isArray' 2 | import { isPlainObject } from './isPlainObject' 3 | import { isPrimitive } from './isPrimitive' 4 | 5 | export function clone(obj: T): T { 6 | if (isPrimitive(obj)) { 7 | return obj 8 | } 9 | if (isArray(obj)) { 10 | return [...(obj as any)] as T 11 | } 12 | if (isPlainObject(obj)) { 13 | return { 14 | ...obj, 15 | } 16 | } 17 | // TODO: Date/Buffer/Regexp 18 | return obj 19 | } 20 | 21 | export function cloneDeep(obj: T): T { 22 | if (!isArray(obj) && !isPlainObject(obj)) { 23 | return clone(obj) 24 | } 25 | if (isArray(obj)) { 26 | return (obj as any).map((x: any) => cloneDeep(x)) 27 | } 28 | return Object.keys(obj).reduce((acc, key) => { 29 | let value = (obj as any)[key] 30 | if (!isPrimitive(value)) { 31 | value = cloneDeep(value) 32 | } 33 | return { 34 | ...acc, 35 | [key]: value, 36 | } 37 | }, {} as T) 38 | } 39 | -------------------------------------------------------------------------------- /src/compact.ts: -------------------------------------------------------------------------------- 1 | export type FalselyValue = false | 0 | '' | null | undefined 2 | 3 | export const compact = (arr: (T | FalselyValue)[]) => { 4 | return arr.filter((item): item is T => !!item) 5 | } 6 | -------------------------------------------------------------------------------- /src/compose.ts: -------------------------------------------------------------------------------- 1 | export function compose(...funcs: Function[]): (...args: any[]) => T 2 | 3 | export function compose(...funcs: Function[]) { 4 | return funcs.reduce( 5 | (a, b) => 6 | (...args: any) => 7 | a(b(...args)), 8 | (x: any) => x 9 | ) 10 | } 11 | 12 | export { compose as flowRight } 13 | -------------------------------------------------------------------------------- /src/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce( 2 | f: (...args: T) => any, 3 | wait: number 4 | ) { 5 | let timer: number 6 | return (...args: T) => { 7 | clearTimeout(timer) 8 | timer = setTimeout(() => { 9 | f(...args) 10 | }, wait) as any as number 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | export function defaults( 2 | obj: Record, 3 | ...sources: Record[] 4 | ) { 5 | return sources.reduce((obj, source) => { 6 | for (const key of Object.keys(source)) { 7 | if (obj[key] === undefined) { 8 | obj[key] = source[key] 9 | } 10 | } 11 | return obj 12 | }, obj) 13 | } 14 | -------------------------------------------------------------------------------- /src/difference.ts: -------------------------------------------------------------------------------- 1 | export const difference = ( 2 | list: T[], 3 | values: T[] = [], 4 | by: (arg: T) => keyof any = x => x as number 5 | ): T[] => { 6 | if (!list || !list.length) return [] 7 | if (!values.length) return [...list] 8 | const map: Record = values.reduce( 9 | (acc, item) => ({ 10 | ...acc, 11 | [by(item)]: true, 12 | }), 13 | {} 14 | ) 15 | return list.filter(a => !map[by(a)]) 16 | } 17 | 18 | export { difference as differenceBy } 19 | -------------------------------------------------------------------------------- /src/get.ts: -------------------------------------------------------------------------------- 1 | export function get( 2 | object: TObject, 3 | path: TKey | [TKey] 4 | ): TObject[TKey] 5 | export function get( 6 | object: TObject | null | undefined, 7 | path: TKey | [TKey] 8 | ): TObject[TKey] | undefined 9 | export function get< 10 | TObject extends object, 11 | TKey extends keyof TObject, 12 | TDefault, 13 | >( 14 | object: TObject | null | undefined, 15 | path: TKey | [TKey], 16 | defaultValue: TDefault 17 | ): Exclude | TDefault 18 | export function get< 19 | TObject extends object, 20 | TKey1 extends keyof TObject, 21 | TKey2 extends keyof TObject[TKey1], 22 | >(object: TObject, path: [TKey1, TKey2]): TObject[TKey1][TKey2] 23 | export function get< 24 | TObject extends object, 25 | TKey1 extends keyof TObject, 26 | TKey2 extends keyof TObject[TKey1], 27 | >( 28 | object: TObject | null | undefined, 29 | path: [TKey1, TKey2] 30 | ): TObject[TKey1][TKey2] | undefined 31 | export function get< 32 | TObject extends object, 33 | TKey1 extends keyof TObject, 34 | TKey2 extends keyof TObject[TKey1], 35 | TDefault, 36 | >( 37 | object: TObject | null | undefined, 38 | path: [TKey1, TKey2], 39 | defaultValue: TDefault 40 | ): Exclude | TDefault 41 | export function get< 42 | TObject extends object, 43 | TKey1 extends keyof TObject, 44 | TKey2 extends keyof TObject[TKey1], 45 | TKey3 extends keyof TObject[TKey1][TKey2], 46 | >(object: TObject, path: [TKey1, TKey2, TKey3]): TObject[TKey1][TKey2][TKey3] 47 | export function get< 48 | TObject extends object, 49 | TKey1 extends keyof TObject, 50 | TKey2 extends keyof TObject[TKey1], 51 | TKey3 extends keyof TObject[TKey1][TKey2], 52 | >( 53 | object: TObject | null | undefined, 54 | path: [TKey1, TKey2, TKey3] 55 | ): TObject[TKey1][TKey2][TKey3] | undefined 56 | export function get< 57 | TObject extends object, 58 | TKey1 extends keyof TObject, 59 | TKey2 extends keyof TObject[TKey1], 60 | TKey3 extends keyof TObject[TKey1][TKey2], 61 | TDefault, 62 | >( 63 | object: TObject | null | undefined, 64 | path: [TKey1, TKey2, TKey3], 65 | defaultValue: TDefault 66 | ): Exclude | TDefault 67 | export function get< 68 | TObject extends object, 69 | TKey1 extends keyof TObject, 70 | TKey2 extends keyof TObject[TKey1], 71 | TKey3 extends keyof TObject[TKey1][TKey2], 72 | TKey4 extends keyof TObject[TKey1][TKey2][TKey3], 73 | >( 74 | object: TObject, 75 | path: [TKey1, TKey2, TKey3, TKey4] 76 | ): TObject[TKey1][TKey2][TKey3][TKey4] 77 | export function get< 78 | TObject extends object, 79 | TKey1 extends keyof TObject, 80 | TKey2 extends keyof TObject[TKey1], 81 | TKey3 extends keyof TObject[TKey1][TKey2], 82 | TKey4 extends keyof TObject[TKey1][TKey2][TKey3], 83 | >( 84 | object: TObject | null | undefined, 85 | path: [TKey1, TKey2, TKey3, TKey4] 86 | ): TObject[TKey1][TKey2][TKey3][TKey4] | undefined 87 | export function get< 88 | TObject extends object, 89 | TKey1 extends keyof TObject, 90 | TKey2 extends keyof TObject[TKey1], 91 | TKey3 extends keyof TObject[TKey1][TKey2], 92 | TKey4 extends keyof TObject[TKey1][TKey2][TKey3], 93 | TDefault, 94 | >( 95 | object: TObject | null | undefined, 96 | path: [TKey1, TKey2, TKey3, TKey4], 97 | defaultValue: TDefault 98 | ): Exclude | TDefault 99 | export function get( 100 | object: null | undefined, 101 | path: string | number | string[] | number[] 102 | ): undefined 103 | export function get( 104 | object: object, 105 | path: string | string[] | number[], 106 | defaultValue?: any 107 | ): any 108 | 109 | export function get( 110 | obj: Record | null | undefined, 111 | path: string | number | string[] | number[], 112 | defaultValue?: any 113 | ) { 114 | const paths = Array.isArray(path) 115 | ? path 116 | : String(path) 117 | .replace(/\[(\w+)\]/g, '.$1') 118 | .replace(/\["(\w+)"\]/g, '.$1') 119 | .replace(/\['(\w+)'\]/g, '.$1') 120 | .split('.') 121 | let result = obj 122 | for (const p of paths) { 123 | result = result?.[p] 124 | if (result === undefined) { 125 | return defaultValue 126 | } 127 | } 128 | return result 129 | } 130 | -------------------------------------------------------------------------------- /src/groupBy.ts: -------------------------------------------------------------------------------- 1 | export function groupBy( 2 | list: T[], 3 | by: (value: T) => unknown 4 | ): Record { 5 | if (!list?.length) { 6 | return {} 7 | } 8 | return list.reduce( 9 | (acc, x) => { 10 | const key = String(by(x)) 11 | if (acc[key]) { 12 | acc[key].push(x) 13 | } else { 14 | acc[key] = [x] 15 | } 16 | return acc 17 | }, 18 | {} as Record 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/head.ts: -------------------------------------------------------------------------------- 1 | export function head(list?: T[]): T | undefined { 2 | return list?.[0] 3 | } 4 | 5 | export function tail(list?: T[]): T[] { 6 | return list?.slice(1) ?? [] 7 | } 8 | 9 | export function take(list?: T[], n = 1): T[] { 10 | return list?.slice(0, n) ?? [] 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sample' 2 | export * from './sampleSize' 3 | export * from './shuffle' 4 | export * from './uniq' 5 | export * from './uniqBy' 6 | export * from './omitBy' 7 | export * from './pickBy' 8 | export * from './pick' 9 | export * from './omit' 10 | export * from './groupBy' 11 | export * from './keyBy' 12 | export * from './chunk' 13 | export * from './get' 14 | export * from './camelCase' 15 | export * from './snakeCase' 16 | export * from './kebabCase' 17 | export * from './defaults' 18 | export * from './random' 19 | export * from './merge' 20 | export * from './isArray' 21 | export * from './isBoolean' 22 | export * from './isObject' 23 | export * from './isPromise' 24 | export * from './isPrimitive' 25 | export * from './isTypedArray' 26 | export * from './isPlainObject' 27 | export * from './isEqual' 28 | export * from './range' 29 | export * from './sum' 30 | export * from './max' 31 | export * from './min' 32 | export * from './clone' 33 | export * from './compose' 34 | export * from './difference' 35 | export * from './async' 36 | export * from './nth' 37 | export * from './intersection' 38 | export * from './debounce' 39 | export * from './head' 40 | export * from './compact' 41 | export * from './zip' 42 | export * from './unzip' 43 | export * from './assign' 44 | export * from './mapKeys' 45 | export * from './mapKeys' 46 | export * from './castArray' 47 | export * from './throttle' 48 | export * from './property' 49 | export * from './once' 50 | export * from './memoize' 51 | export * from './template' 52 | -------------------------------------------------------------------------------- /src/intersection.ts: -------------------------------------------------------------------------------- 1 | type by = (item: T) => number | string 2 | 3 | export function intersection(...args: Array>): T[] { 4 | if (!args || !args.length) return [] 5 | if (args.slice(0, args.length - 1).find(item => typeof item === 'function')) 6 | return [] 7 | const list: T[][] = args.filter(Array.isArray) 8 | const by: by = 9 | typeof args[args.length - 1] === 'function' 10 | ? (args[args.length - 1] as by) 11 | : x => x as number 12 | return list?.reduce((a, b) => { 13 | const setB = new Set(b.map(by)) 14 | return a.filter(c => setB.has(by(c))) 15 | }) 16 | } 17 | 18 | export { intersection as intersectionBy } 19 | -------------------------------------------------------------------------------- /src/isArray.ts: -------------------------------------------------------------------------------- 1 | export function isArray(value?: any): value is any[] { 2 | return Array.isArray(value) 3 | } 4 | -------------------------------------------------------------------------------- /src/isBoolean.ts: -------------------------------------------------------------------------------- 1 | export function isBoolean(value: any): value is boolean { 2 | return value === false || value === true 3 | } 4 | -------------------------------------------------------------------------------- /src/isEqual.ts: -------------------------------------------------------------------------------- 1 | import { isTypedArray } from './isTypedArray' 2 | 3 | export function isEqual(value: any, other: any): boolean { 4 | if (value === other) { 5 | return true 6 | } 7 | 8 | if ( 9 | !(value && other && typeof value === 'object' && typeof other === 'object') 10 | ) { 11 | // isNaN 12 | return value !== other && other !== other 13 | } 14 | 15 | if (value.constructor !== other.constructor) { 16 | return false 17 | } 18 | 19 | // 因为二者 constructor 相同,因此只需要判断 value 的数据类型 20 | if (Array.isArray(value)) { 21 | if (value.length !== other.length) { 22 | return false 23 | } 24 | 25 | return value.every((x, i) => { 26 | return isEqual(x, other[i]) 27 | }) 28 | } 29 | 30 | if (isTypedArray(value)) { 31 | if (value.length !== other.length) { 32 | return false 33 | } 34 | 35 | return value.every((x, i) => x === other[i]) 36 | } 37 | 38 | if (value.constructor === RegExp) { 39 | return value.source === other.source && value.flags === other.flags 40 | } 41 | 42 | if (value instanceof Map) { 43 | if (value.size !== other.size) { 44 | return false 45 | } 46 | for (const x of value.keys()) { 47 | if (!other.has(x)) { 48 | return false 49 | } 50 | } 51 | for (const [k, v] of Object.entries(value)) { 52 | if (!isEqual(v, other.get(k))) { 53 | return false 54 | } 55 | } 56 | return true 57 | } 58 | 59 | if (value instanceof Set) { 60 | if (value.size !== other.size) { 61 | return false 62 | } 63 | for (const x of value.keys()) { 64 | if (!other.has(x)) { 65 | return false 66 | } 67 | } 68 | return true 69 | } 70 | 71 | const keys = Object.keys(value) 72 | const otherKeys = Object.keys(other) 73 | 74 | if (keys.length !== otherKeys.length) { 75 | return false 76 | } 77 | 78 | for (const key of keys) { 79 | if (!Object.prototype.hasOwnProperty.call(other, key)) { 80 | return false 81 | } 82 | } 83 | 84 | for (const key of keys) { 85 | if (!isEqual(value[key], other[key])) { 86 | return false 87 | } 88 | } 89 | 90 | return true 91 | } 92 | -------------------------------------------------------------------------------- /src/isObject.ts: -------------------------------------------------------------------------------- 1 | export function isObject(value?: any): value is object { 2 | return ( 3 | (typeof value === 'object' || typeof value === 'function') && value !== null 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/isPlainObject.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from './isObject' 2 | 3 | export function isPlainObject(value?: any) { 4 | if (!isObject(value)) { 5 | return false 6 | } 7 | 8 | // if Object.create(null) 9 | if (Object.getPrototypeOf(value) === null) { 10 | return true 11 | } 12 | 13 | let proto = value 14 | while (Object.getPrototypeOf(proto) !== null) { 15 | proto = Object.getPrototypeOf(proto) 16 | } 17 | 18 | return Object.getPrototypeOf(value) === proto 19 | } 20 | -------------------------------------------------------------------------------- /src/isPrimitive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from './isObject' 2 | 3 | export function isPrimitive(value?: any) { 4 | return !isObject(value) 5 | } 6 | -------------------------------------------------------------------------------- /src/isPromise.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from './isObject' 2 | 3 | export function isPromise(value?: any): value is Promise { 4 | return isObject(value) && typeof (value as any).then === 'function' 5 | } 6 | -------------------------------------------------------------------------------- /src/isTypedArray.ts: -------------------------------------------------------------------------------- 1 | type TypedArray = 2 | | Uint8Array 3 | | Uint8ClampedArray 4 | | Uint16Array 5 | | Uint32Array 6 | | Int8Array 7 | | Int16Array 8 | | Int32Array 9 | | BigUint64Array 10 | | BigInt64Array 11 | | Float32Array 12 | | Float64Array 13 | 14 | export function isTypedArray( 15 | value?: any 16 | ): value is T { 17 | return value.length !== undefined && ArrayBuffer.isView(value) 18 | } 19 | -------------------------------------------------------------------------------- /src/kebabCase.ts: -------------------------------------------------------------------------------- 1 | import { words } from './words' 2 | 3 | export function kebabCase(str: string) { 4 | return words(str).join('-') 5 | } 6 | -------------------------------------------------------------------------------- /src/keyBy.ts: -------------------------------------------------------------------------------- 1 | export function keyBy( 2 | list: T[], 3 | by: (value: T) => unknown 4 | ): Record { 5 | if (!list?.length) { 6 | return {} 7 | } 8 | return list.reduce( 9 | (acc, x) => { 10 | const key = String(by(x)) 11 | acc[key] = x 12 | return acc 13 | }, 14 | {} as Record 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/mapKeys.ts: -------------------------------------------------------------------------------- 1 | export function mapKeys< 2 | T, 3 | K extends string | number | symbol, 4 | KNew extends string | number | symbol, 5 | >( 6 | obj: Record, 7 | iteratee: (value: T, key: string) => KNew = value => value as unknown as KNew 8 | ): Record { 9 | return Object.entries(obj).reduce( 10 | (acc, [key, value]) => { 11 | return { 12 | ...acc, 13 | [iteratee(value, key)]: value, 14 | } 15 | }, 16 | {} as Record 17 | ) 18 | } 19 | 20 | export function mapValues( 21 | obj: Record, 22 | iteratee: (value: T, key: string) => TNew = value => value as unknown as TNew 23 | ): Record { 24 | return Object.entries(obj).reduce( 25 | (acc, [key, value]) => { 26 | return { 27 | ...acc, 28 | [key]: iteratee(value, key), 29 | } 30 | }, 31 | {} as Record 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/max.ts: -------------------------------------------------------------------------------- 1 | export function max( 2 | list?: T[], 3 | by: (value: T) => number | string = (x: T) => x as number 4 | ): T | undefined { 5 | if (!list || !list.length) { 6 | return 7 | } 8 | return list.reduce((x, y) => (by(x) > by(y) ? x : y)) 9 | } 10 | 11 | export { max as maxBy } 12 | -------------------------------------------------------------------------------- /src/memoize.ts: -------------------------------------------------------------------------------- 1 | export function memoize(fn: (...args: T) => any) { 2 | let cache: { [key: string]: any } = {} 3 | return (...args: T) => { 4 | let key = args[0] 5 | if (cache[key] !== undefined) { 6 | return cache[key] 7 | } else { 8 | let result = fn(...args) 9 | cache[key] = result 10 | return result 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/merge.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from './isPlainObject' 2 | import { isArray } from './isArray' 3 | import { cloneDeep } from './clone' 4 | 5 | const isArrayOrPlainObject = (o?: any) => isPlainObject(o) || isArray(o) 6 | 7 | export function merge( 8 | object: TObject, 9 | source: TSource 10 | ): TObject & TSource 11 | export function merge( 12 | object: TObject, 13 | source1: TSource1, 14 | source2: TSource2 15 | ): TObject & TSource1 & TSource2 16 | export function merge( 17 | object: TObject, 18 | source1: TSource1, 19 | source2: TSource2, 20 | source3: TSource3 21 | ): TObject & TSource1 & TSource2 & TSource3 22 | export function merge(object: any, ...sources: any[]): any { 23 | if ( 24 | sources.length === 1 && 25 | (!isArrayOrPlainObject(sources[0]) || !isArrayOrPlainObject(object)) 26 | ) { 27 | if (sources[0] === undefined) { 28 | return cloneDeep(object) 29 | } 30 | return cloneDeep(sources[0]) 31 | } 32 | for (const source of sources) { 33 | if (!isArrayOrPlainObject(source)) { 34 | object = merge(object, source) 35 | } else { 36 | for (const key of Object.keys(source)) { 37 | ;(object as any)[key] = merge((object as any)[key], source[key]) 38 | } 39 | } 40 | } 41 | return object 42 | } 43 | -------------------------------------------------------------------------------- /src/min.ts: -------------------------------------------------------------------------------- 1 | export function min( 2 | list?: T[], 3 | by: (value: T) => number | string = (x: T) => x as number 4 | ): T | undefined { 5 | if (!list || !list.length) { 6 | return 7 | } 8 | return list.reduce((x, y) => (by(x) < by(y) ? x : y)) 9 | } 10 | 11 | export { min as minBy } 12 | -------------------------------------------------------------------------------- /src/nth.ts: -------------------------------------------------------------------------------- 1 | export function nth(list: T[], n: number = 0): undefined | T { 2 | return n >= 0 ? list[n] : list[list.length + n] 3 | } 4 | 5 | export { nth as at } 6 | -------------------------------------------------------------------------------- /src/omit.ts: -------------------------------------------------------------------------------- 1 | export function omit( 2 | object: T, 3 | paths?: K[] 4 | ): Omit 5 | 6 | export function omit(obj: T, paths?: string[]): Partial 7 | 8 | export function omit(obj: object, paths: string[] = []) { 9 | if (!obj) { 10 | return {} 11 | } 12 | return paths.reduce( 13 | (acc, key) => { 14 | delete acc[key] 15 | return acc 16 | }, 17 | { ...obj } as any 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/omitBy.ts: -------------------------------------------------------------------------------- 1 | export function omitBy( 2 | obj: T, 3 | predicate: (value: T[keyof T], key: string) => unknown 4 | ): Partial 5 | 6 | export function omitBy( 7 | obj: Record, 8 | predicate: (value: T, key: string) => unknown = () => true 9 | ): Record { 10 | if (!obj) { 11 | return {} 12 | } 13 | return Object.fromEntries( 14 | Object.entries(obj).filter( 15 | ([key, value]) => !Boolean(predicate(value, key)) 16 | ) 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/once.ts: -------------------------------------------------------------------------------- 1 | export function once(fn: (...args: T) => any) { 2 | let called = false 3 | return (...args: T) => { 4 | if (!called) { 5 | called = true 6 | return fn(...args) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/pick.ts: -------------------------------------------------------------------------------- 1 | export function pick( 2 | object: T, 3 | paths?: K[] 4 | ): Pick 5 | 6 | export function pick(obj: T, paths?: string[]): Partial 7 | 8 | export function pick(obj: object, paths: string[] = []) { 9 | if (!obj) { 10 | return {} 11 | } 12 | return paths.reduce((acc, key) => { 13 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 14 | acc[key] = (obj as any)[key] 15 | } 16 | return acc 17 | }, {} as any) 18 | } 19 | -------------------------------------------------------------------------------- /src/pickBy.ts: -------------------------------------------------------------------------------- 1 | export function pickBy( 2 | obj: Record, 3 | predicate: (value: T, key: string) => unknown = value => Boolean(value) 4 | ) { 5 | if (!obj) { 6 | return {} 7 | } 8 | return Object.fromEntries( 9 | Object.entries(obj).filter(([key, value]) => Boolean(predicate(value, key))) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/property.ts: -------------------------------------------------------------------------------- 1 | import { get } from './get' 2 | 3 | export const property = (path: string | string[] | number[]) => (obj: any) => 4 | get(obj, path) 5 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | export function random(lower?: number, upper?: number) { 2 | if (lower === undefined) { 3 | ;[lower, upper] = [0, 1] 4 | } 5 | if (upper === undefined) { 6 | ;[lower, upper] = [0, lower] 7 | } 8 | return Math.floor(Math.random() * (upper - lower + 1)) + lower 9 | } 10 | -------------------------------------------------------------------------------- /src/range.ts: -------------------------------------------------------------------------------- 1 | export function range(start: number, end?: number, step?: number): number[] { 2 | if (end === undefined) { 3 | end = start 4 | start = 0 5 | } 6 | 7 | if (step === 0) { 8 | return Array(Math.ceil(end - start)).fill(start) 9 | } 10 | 11 | step = step || (start > end ? -1 : 1) 12 | 13 | const r = [] 14 | for (let i = start; start > end ? i > end : i < end; i += step) { 15 | r.push(i) 16 | } 17 | 18 | return r 19 | } 20 | -------------------------------------------------------------------------------- /src/sample.ts: -------------------------------------------------------------------------------- 1 | import { random } from './random' 2 | 3 | export function sample(list: T[] = []) { 4 | const len = list.length 5 | return len ? list[random(len - 1)] : undefined 6 | } 7 | -------------------------------------------------------------------------------- /src/sampleSize.ts: -------------------------------------------------------------------------------- 1 | import { random } from './random' 2 | 3 | export function sampleSize(list: T[], n: number = 1) { 4 | if (n <= 0 || Number.isNaN(n) || !list?.length) { 5 | return [] 6 | } 7 | const result = [...list] 8 | const len = result.length 9 | n = n > len ? len : n 10 | for (let i = len - 1; i >= len - n; i--) { 11 | const rand = random(i) 12 | ;[result[i], result[rand]] = [result[rand], result[i]] 13 | } 14 | return result.slice(-n) 15 | } 16 | -------------------------------------------------------------------------------- /src/shuffle.ts: -------------------------------------------------------------------------------- 1 | import { sampleSize } from './sampleSize' 2 | 3 | export function shuffle(list: T[]) { 4 | return sampleSize(list, list.length) 5 | } 6 | -------------------------------------------------------------------------------- /src/snakeCase.ts: -------------------------------------------------------------------------------- 1 | import { words } from './words' 2 | 3 | export function snakeCase(str: string) { 4 | return words(str).join('_') 5 | } 6 | -------------------------------------------------------------------------------- /src/sum.ts: -------------------------------------------------------------------------------- 1 | export function sum(list?: number[]) { 2 | return (list || []).reduce((x, y) => x + y, 0) 3 | } 4 | -------------------------------------------------------------------------------- /src/template.ts: -------------------------------------------------------------------------------- 1 | export const template = ( 2 | str: string, 3 | data: Record, 4 | regex = /\{\{(.+?)\}\}/g 5 | ) => { 6 | return Array.from(str.matchAll(regex)).reduce((acc, match) => { 7 | return acc.replace(match[0], data[match[1].trim()]) 8 | }, str) 9 | } 10 | -------------------------------------------------------------------------------- /src/throttle.ts: -------------------------------------------------------------------------------- 1 | export function throttle( 2 | f: (...args: T) => any, 3 | wait: number 4 | ) { 5 | let timer: number | null 6 | return (...args: T) => { 7 | if (timer) { 8 | return 9 | } 10 | timer = setTimeout(() => { 11 | f(...args) 12 | timer = null 13 | }, wait) as any as number 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/uniq.ts: -------------------------------------------------------------------------------- 1 | export function uniq(list: T[]): T[] { 2 | return [...new Set(list)] 3 | } 4 | -------------------------------------------------------------------------------- /src/uniqBy.ts: -------------------------------------------------------------------------------- 1 | export function uniqBy(list: T[], by: (value: T) => unknown): T[] { 2 | const uniqMap: { [key: string]: T } = {} 3 | for (let item of list ?? []) { 4 | const key = by(item) as unknown as string 5 | if (!uniqMap[key]) { 6 | uniqMap[key] = item 7 | } 8 | } 9 | return Object.values(uniqMap) 10 | } 11 | -------------------------------------------------------------------------------- /src/unzip.ts: -------------------------------------------------------------------------------- 1 | export function unzip(data: any[][]): any[][] { 2 | const maxLength = Math.max(...data.map(row => row.length)) 3 | return Array.from({ length: maxLength }, (_, i) => data.map(row => row[i])) 4 | } 5 | -------------------------------------------------------------------------------- /src/words.ts: -------------------------------------------------------------------------------- 1 | export function words(str: string) { 2 | return str 3 | .replace(/[A-Z]+/g, x => `.${x.toLowerCase()}`) 4 | .replace(/[^\w]+/g, '.') 5 | .split('.') 6 | .filter(Boolean) 7 | } 8 | -------------------------------------------------------------------------------- /src/zip.ts: -------------------------------------------------------------------------------- 1 | export function zip(...arrays: any[][]): any[][] { 2 | const maxLength = Math.max(...arrays.map(arr => arr.length)) 3 | return Array.from({ length: maxLength }, (_, i) => arrays.map(arr => arr[i])) 4 | } 5 | -------------------------------------------------------------------------------- /test/assign.spec.ts: -------------------------------------------------------------------------------- 1 | import { assign } from '../src' 2 | 3 | describe('assign', () => { 4 | it('handles both null input', () => { 5 | const result = assign(null, null) 6 | expect(result).toEqual({}) 7 | }) 8 | 9 | it('handles null first input', () => { 10 | const result = assign({ a: '1' }, null) 11 | expect(result).toEqual({ a: '1' }) 12 | }) 13 | 14 | it('handles null last input', () => { 15 | const result = assign(null, { a: '1' }) 16 | expect(result).toEqual({ a: '1' }) 17 | }) 18 | 19 | it('correctly assign a with values from b', () => { 20 | const target = { a: 2, b: 2 } 21 | const result = assign(target, {}) 22 | expect(result).toEqual(target) 23 | }) 24 | 25 | it('handles target have unique value', () => { 26 | const source = { a: 3, b: 5 } 27 | const result = assign({}, source) 28 | expect(result).toEqual(source) 29 | }) 30 | 31 | it('handles source have unique value', () => { 32 | const target = { a: 2, b: 2 } 33 | const source = { a: 3, b: 5 } 34 | const result = assign(target, source) 35 | expect(result).toEqual(source) 36 | }) 37 | 38 | it('should merge nested properties correctly', () => { 39 | const target = { a: { b: 2, c: 3 } } 40 | const source1 = { a: { c: 4, d: 5 } } 41 | const source2 = { a: { d: 6, e: 7 } } 42 | const originTarget = { ...target } 43 | const result = assign(target, source1, source2) 44 | 45 | expect(result).toEqual({ a: { d: 6, e: 7 } }) 46 | expect(result).not.toBe(originTarget) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/async.spec.ts: -------------------------------------------------------------------------------- 1 | import { retry, AbortError, filter, map, sleep } from '../src' 2 | import timeSpan from 'time-span' 3 | 4 | describe('async filter', function () { 5 | it('expect work', async () => { 6 | const r1 = await filter([Promise.resolve(1), 2, 3, 4], (x: any) => 7 | Boolean(x % 2) 8 | ) 9 | const r2 = await filter([Promise.resolve(1), 2, 3, 4], (x: any) => 10 | Promise.resolve(Boolean(x % 2)) 11 | ) 12 | 13 | expect(r1).toStrictEqual([1, 3]) 14 | expect(r2).toStrictEqual([1, 3]) 15 | }) 16 | }) 17 | 18 | describe('async map', function () { 19 | const input = [ 20 | Promise.resolve(1), 21 | Promise.resolve(2), 22 | Promise.resolve(3), 23 | 4, 24 | 5, 25 | 6, 26 | ] 27 | 28 | const addOne = async (n: number | Promise) => { 29 | n = await n 30 | return n + 1 31 | } 32 | 33 | it('expect work', async () => { 34 | const r = await map(input, x => addOne(x)) 35 | expect(r).toStrictEqual([2, 3, 4, 5, 6, 7]) 36 | }) 37 | 38 | it('expect work with concurrency', async () => { 39 | const ts = timeSpan() 40 | const r = await map( 41 | input, 42 | async x => { 43 | await sleep(300) 44 | return addOne(x) 45 | }, 46 | { concurrency: 1 } 47 | ) 48 | const time = ts() 49 | expect(r).toStrictEqual([2, 3, 4, 5, 6, 7]) 50 | expect(time).toBeGreaterThan(1800) 51 | expect(time).toBeLessThan(2000) 52 | }) 53 | 54 | it('expect work with concurrency 2', async () => { 55 | const ts = timeSpan() 56 | const r = await map( 57 | input, 58 | async x => { 59 | await sleep(300) 60 | return addOne(x) 61 | }, 62 | { concurrency: 2 } 63 | ) 64 | const time = ts() 65 | expect(r).toStrictEqual([2, 3, 4, 5, 6, 7]) 66 | expect(time).toBeGreaterThan(900) 67 | expect(time).toBeLessThan(1000) 68 | }) 69 | 70 | it('expect work with concurrency 3', async () => { 71 | const ts = timeSpan() 72 | const r = await map( 73 | input, 74 | async (x, i) => { 75 | await sleep(i * 100) 76 | return addOne(x) 77 | }, 78 | { concurrency: 2 } 79 | ) 80 | const time = ts() 81 | expect(r).toStrictEqual([2, 3, 4, 5, 6, 7]) 82 | expect(time).toBeGreaterThan(900) 83 | expect(time).toBeLessThan(1000) 84 | }) 85 | }) 86 | 87 | describe('async retry', function () { 88 | it('expect work', async () => { 89 | let i = 0 90 | const result = 100 91 | 92 | const data = await retry( 93 | async attemptNumber => { 94 | i++ 95 | return attemptNumber === 3 ? result : Promise.reject(new Error('error')) 96 | }, 97 | { 98 | times: 3, 99 | } 100 | ) 101 | 102 | expect(data).toStrictEqual(100) 103 | expect(i).toStrictEqual(3) 104 | }) 105 | 106 | it('abort', async () => { 107 | let i = 0 108 | const err = new AbortError('hello') 109 | 110 | try { 111 | await retry( 112 | async () => { 113 | i++ 114 | return Promise.reject(err) 115 | }, 116 | { 117 | times: 3, 118 | } 119 | ) 120 | } catch (e) { 121 | expect(e).toStrictEqual(err) 122 | } 123 | 124 | expect(i).toStrictEqual(1) 125 | }) 126 | 127 | it('onFailedAttempt can return a promise to add a delay', async () => { 128 | const waitFor = 1000 129 | const start = Date.now() 130 | const result = 100 131 | let isCalled: boolean 132 | 133 | const r = await retry( 134 | async () => { 135 | if (isCalled) { 136 | return result 137 | } 138 | 139 | isCalled = true 140 | 141 | throw new Error('error') 142 | }, 143 | { 144 | onFailedAttempt: async () => { 145 | await sleep(waitFor) 146 | }, 147 | } 148 | ) 149 | 150 | expect(Date.now()).toBeGreaterThanOrEqual(start + waitFor) 151 | expect(r).toEqual(result) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /test/camelCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from '../src' 2 | 3 | describe('camelCase', function () { 4 | it('should work', function () { 5 | expect(camelCase('foo')).toEqual('foo') 6 | expect(camelCase('foo-bar')).toEqual('fooBar') 7 | expect(camelCase('foo--bar')).toEqual('fooBar') 8 | expect(camelCase('--foo--bar')).toEqual('fooBar') 9 | expect(camelCase('FOO-BAR')).toEqual('fooBar') 10 | expect(camelCase('-foo-bar-')).toEqual('fooBar') 11 | expect(camelCase('--foo--bar--')).toEqual('fooBar') 12 | expect(camelCase('foo.bar')).toEqual('fooBar') 13 | expect(camelCase('foo..bar')).toEqual('fooBar') 14 | expect(camelCase('..foo..bar..')).toEqual('fooBar') 15 | expect(camelCase(' foo bar ')).toEqual('fooBar') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/castArray.spec.ts: -------------------------------------------------------------------------------- 1 | import { castArray } from '../src' 2 | 3 | describe('castArray function', () => { 4 | it('should convert a non-array value to an array', () => { 5 | expect(castArray(42)).toEqual([42]) 6 | expect(castArray('hello')).toEqual(['hello']) 7 | expect(castArray(null)).toEqual([null]) 8 | }) 9 | 10 | it('should return the input array unchanged', () => { 11 | const inputArray = [1, 2, 3] 12 | expect(castArray(inputArray)).toBe(inputArray) 13 | }) 14 | 15 | it('should return an empty array if no arguments are provided', () => { 16 | expect(castArray()).toEqual([]) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/chunk.spec.ts: -------------------------------------------------------------------------------- 1 | import { chunk } from '../src' 2 | 3 | describe('chunk', function () { 4 | const array = [0, 1, 2, 3, 4, 5] 5 | 6 | it('should work', function () { 7 | const actual = chunk(array, 3) 8 | expect(actual).toEqual([ 9 | [0, 1, 2], 10 | [3, 4, 5], 11 | ]) 12 | }) 13 | 14 | it('should work with string', function () { 15 | const actual = chunk('abcdefghi', 3) 16 | expect(actual).toEqual([ 17 | ['a', 'b', 'c'], 18 | ['d', 'e', 'f'], 19 | ['g', 'h', 'i'], 20 | ]) 21 | }) 22 | 23 | it('should work with no size argument', function () { 24 | const actual = chunk(array) 25 | expect(actual).toEqual([[0], [1], [2], [3], [4], [5]]) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/clone.spec.ts: -------------------------------------------------------------------------------- 1 | import { clone, cloneDeep } from '../src' 2 | 3 | describe('clone', function () { 4 | it('should work', function () { 5 | expect(clone({ a: 3, b: 4 })).toEqual({ a: 3, b: 4 }) 6 | expect(clone([3, 4, 5])).toEqual([3, 4, 5]) 7 | expect(clone(3)).toEqual(3) 8 | }) 9 | it('should work with shallow clone', function () { 10 | const o = { a: { b: 4 } } 11 | expect(clone(o)).toEqual(o) 12 | expect(clone(o).a).toBe(o.a) 13 | expect(clone(o)).not.toBe(o) 14 | }) 15 | it('should work with date', function () { 16 | const d = new Date() 17 | expect(clone(d)).toBe(d) 18 | // expect(clone(d)).not.toBe(d) 19 | }) 20 | }) 21 | 22 | describe('cloneDeep', function () { 23 | it('should work', function () { 24 | const o = { a: { b: 4 } } 25 | expect(cloneDeep(o)).toEqual(o) 26 | expect(cloneDeep(o).a === o.a).toBe(false) 27 | expect(cloneDeep(o).a).not.toBe(o.a) 28 | expect(cloneDeep(o)).not.toBe(o) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/compact.spec.ts: -------------------------------------------------------------------------------- 1 | import { compact } from '../src' 2 | 3 | describe('compact', function () { 4 | it('should work', function () { 5 | expect(compact([1, 0, 2, false])).toEqual([1, 2]) 6 | 7 | expect(compact([{}, undefined, null, 3])).toEqual([{}, 3]) 8 | 9 | expect(compact(['a', 'b', 'c', '', 'd'])).toEqual(['a', 'b', 'c', 'd']) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/compose.spec.ts: -------------------------------------------------------------------------------- 1 | import { compose, flowRight } from '../src' 2 | 3 | describe('compose', () => { 4 | it('should work', () => { 5 | const double = (x: number) => x * 2 6 | const addFive = (x: number) => x + 5 7 | 8 | expect(compose(double)(5)).toBe(10) 9 | expect(compose(addFive, double)(5)).toBe(15) 10 | expect(compose(addFive, double, addFive, double)(5)).toBe(35) 11 | 12 | expect(compose).toBe(flowRight) 13 | }) 14 | 15 | it('shoul work with multiple arguments', () => { 16 | const square = (x: number) => x * x 17 | const add = (x: number, y: number) => x + y 18 | 19 | expect(compose(square, add)(1, 2)).toBe(9) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/debounce.spec.ts: -------------------------------------------------------------------------------- 1 | import { debounce, sleep } from '../src' 2 | 3 | describe('debounce', function () { 4 | it('only executes once when called rapidly', async function () { 5 | let num = 0 6 | let func = debounce(() => num++, 500) 7 | 8 | func() 9 | func() 10 | func() 11 | expect(num).toEqual(0) 12 | await sleep(610).then(() => expect(num).toEqual(1)) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/defaults.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaults } from '../src' 2 | 3 | describe('get', function () { 4 | it('should work', function () { 5 | expect(defaults({ a: 1 }, { b: 2 }, { a: 3 })).toEqual({ a: 1, b: 2 }) 6 | expect(defaults({ a: 1, b: 2 }, { b: 3 }, { c: 3 })).toEqual({ 7 | a: 1, 8 | b: 2, 9 | c: 3, 10 | }) 11 | expect(defaults({ a: null }, { a: 1 })).toEqual({ a: null }) 12 | expect(defaults({ a: undefined }, { a: 1 })).toEqual({ a: 1 }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/difference.spec.ts: -------------------------------------------------------------------------------- 1 | import { difference, differenceBy } from '../src' 2 | 3 | describe('max', function () { 4 | it('should work', function () { 5 | expect(difference([1, 2, 3, 4])).toEqual([1, 2, 3, 4]) 6 | expect(difference([1, 2, 3, 4], [])).toEqual([1, 2, 3, 4]) 7 | expect(difference([1, 2, 3, 4], [1, 3, 5])).toEqual([2, 4]) 8 | }) 9 | 10 | it('should work with differenceBy', function () { 11 | expect(difference).toBe(differenceBy) 12 | expect(differenceBy([{ a: 3 }, { a: 4 }], [], x => x.a)).toEqual([ 13 | { a: 3 }, 14 | { a: 4 }, 15 | ]) 16 | expect(differenceBy([{ a: 3 }, { a: 4 }], [{ a: 3 }], x => x.a)).toEqual([ 17 | { a: 4 }, 18 | ]) 19 | expect(differenceBy([3.5, 4.5], [3.8, 5.8], Math.floor)).toEqual([4.5]) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/get.spec.ts: -------------------------------------------------------------------------------- 1 | import { get } from '../src' 2 | 3 | describe('get', function () { 4 | const object = { a: 1, b: 2, c: 3, d: 4 } 5 | 6 | it('should work', function () { 7 | const a = get(object, ['a']) 8 | const b = get(object, 'b') 9 | 10 | expect(a).toEqual(1) 11 | expect(b).toEqual(2) 12 | }) 13 | 14 | it('should work when source is null or undefined', function () { 15 | const a = get(null, 'a') 16 | const b = get(undefined, 'a') 17 | 18 | expect(a).toEqual(undefined) 19 | expect(b).toEqual(undefined) 20 | }) 21 | 22 | it('should work with default value', function () { 23 | const actual = get(object, 'a.b', 404) 24 | 25 | expect(actual).toEqual(404) 26 | }) 27 | 28 | it('should work with array', function () { 29 | const actual = get({ a: { b: 3 } }, ['a', 'b']) 30 | 31 | expect(actual).toEqual(3) 32 | }) 33 | 34 | it('should work with null', function () { 35 | const actual = get({ a: null }, 'a', 404) 36 | 37 | expect(actual).toEqual(null) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/groupBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../src' 2 | 3 | describe('groupBy', function () { 4 | it('should work', function () { 5 | const actual = groupBy(['one', 'two', 'three'], x => x.length) 6 | expect(actual).toEqual({ '3': ['one', 'two'], '5': ['three'] }) 7 | }) 8 | 9 | it('should work with function', function () { 10 | const actual = groupBy([1.2, 3.4, 3.2], Math.floor) 11 | expect(actual).toEqual({ '1': [1.2], '3': [3.4, 3.2] }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/head.spec.ts: -------------------------------------------------------------------------------- 1 | import { head, tail, take } from '../src' 2 | 3 | describe('groupBy', function () { 4 | const list = [1, 2, 3, 4, 5] 5 | it('head should work', function () { 6 | expect(head()).toEqual(undefined) 7 | expect(head([])).toEqual(undefined) 8 | expect(head(list)).toEqual(1) 9 | }) 10 | 11 | it('tail should work', function () { 12 | expect(tail()).toEqual([]) 13 | expect(tail([])).toEqual([]) 14 | expect(tail(list)).toEqual([2, 3, 4, 5]) 15 | }) 16 | 17 | it('take should work', function () { 18 | expect(take()).toEqual([]) 19 | expect(take([])).toEqual([]) 20 | expect(take([1, 2, 3])).toEqual([1]) 21 | expect(take([1, 2, 3], 2)).toEqual([1, 2]) 22 | expect(take([1, 2, 3], 5)).toEqual([1, 2, 3]) 23 | expect(take([1, 2, 3], 0)).toEqual([]) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/intersection.spec.ts: -------------------------------------------------------------------------------- 1 | import { intersection } from '../src' 2 | import { intersectionBy } from '../src' 3 | 4 | describe('should work', function () { 5 | let list = [['a', 'b', 'c'], ['a', 'c'], ['a'], ['a', 'd']] 6 | it('should work', function () { 7 | expect(intersection()).toEqual([]) 8 | expect(intersection([])).toEqual([]) 9 | expect(intersection([2, 1], [4, 2], [1, 2])).toEqual([2]) 10 | expect(intersection([2, 1])).toEqual([2, 1]) 11 | expect(intersection(...list)).toEqual(['a']) 12 | }) 13 | 14 | it('should work with by', function () { 15 | expect(intersectionBy()).toEqual([]) 16 | expect(intersectionBy([])).toEqual([]) 17 | expect(intersectionBy([{ x: 1 }])).toEqual([{ x: 1 }]) 18 | expect(intersectionBy([2, 1], [4, 2], [1, 2], Math.floor)).toEqual([2]) 19 | expect(intersectionBy([{ x: 1 }], [{ x: 2 }, { x: 1 }], x => x.x)).toEqual([ 20 | { x: 1 }, 21 | ]) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/isArray.spec.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from '../src' 2 | 3 | describe('isObject', function () { 4 | it('should work', function () { 5 | expect(isArray([])).toBe(true) 6 | expect(isArray({ length: 3 })).toBe(false) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /test/isBoolean.ts: -------------------------------------------------------------------------------- 1 | import { isBoolean } from '../src' 2 | 3 | describe('isBoolean', function () { 4 | it('should work', function () { 5 | expect(isBoolean(true)).toBe(true) 6 | expect(isBoolean(false)).toBe(false) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /test/isEqual.spec.ts: -------------------------------------------------------------------------------- 1 | import { isEqual } from '../src' 2 | 3 | describe('isEqual', function () { 4 | it('should work', function () { 5 | expect(isEqual({ a: 3 }, { a: 3 })).toEqual(true) 6 | expect(isEqual([{ a: 3 }], [{ a: 3 }])).toEqual(true) 7 | expect(isEqual(new Map(), new Map())).toEqual(true) 8 | expect(isEqual(new Map([['a', 3]]), new Map([['a', 3]]))).toEqual(true) 9 | expect(isEqual(new Set(['a', 'b', 'c']), new Set(['a', 'b', 'c']))).toEqual( 10 | true 11 | ) 12 | }) 13 | 14 | it('should work with primitive value', function () { 15 | expect(isEqual(3, 3)).toEqual(true) 16 | expect(isEqual(NaN, NaN)).toEqual(true) 17 | expect(isEqual(3n, 3n)).toEqual(true) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/isObject.spec.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '../src' 2 | 3 | describe('isObject', function () { 4 | it('should work', function () { 5 | expect(isObject({})).toBe(true) 6 | expect(isObject([])).toBe(true) 7 | expect(isObject(() => {})).toBe(true) 8 | expect(isObject(new Date())).toBe(true) 9 | expect(isObject(3)).toBe(false) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/isPlainObject.spec.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from '../src' 2 | 3 | describe('isObject', function () { 4 | it('should work', function () { 5 | expect(isPlainObject({})).toBe(true) 6 | expect(isPlainObject(Object.create(null))).toBe(true) 7 | 8 | expect(isPlainObject([])).toBe(false) 9 | expect(isPlainObject(() => {})).toBe(false) 10 | expect(isPlainObject(new Date())).toBe(false) 11 | expect(isPlainObject(3)).toBe(false) 12 | expect(isPlainObject(Object.create({ a: 3 }))).toBe(false) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/isPromise.spec.ts: -------------------------------------------------------------------------------- 1 | import { isPromise } from '../src' 2 | 3 | describe('isPromise', function () { 4 | it('should work', function () { 5 | async function f() {} 6 | 7 | expect(isPromise(Promise.resolve())).toBe(true) 8 | expect(isPromise(f())).toBe(true) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/isTypedArray.spec.ts: -------------------------------------------------------------------------------- 1 | import { isTypedArray } from '../src' 2 | 3 | describe('isTypedArray', function () { 4 | it('should work', function () { 5 | expect(isTypedArray(new Uint16Array([3, 4, 5]))).toBe(true) 6 | expect(isTypedArray([])).toBe(false) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /test/kebabCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { kebabCase } from '../src' 2 | 3 | describe('kebabCase', function () { 4 | it('should work', function () { 5 | expect(kebabCase('foo')).toEqual('foo') 6 | expect(kebabCase('foo-bar')).toEqual('foo-bar') 7 | expect(kebabCase('foo--bar')).toEqual('foo-bar') 8 | expect(kebabCase('--foo--bar')).toEqual('foo-bar') 9 | expect(kebabCase('FOO-BAR')).toEqual('foo-bar') 10 | expect(kebabCase('-foo-bar-')).toEqual('foo-bar') 11 | expect(kebabCase('--foo--bar--')).toEqual('foo-bar') 12 | expect(kebabCase('foo.bar')).toEqual('foo-bar') 13 | expect(kebabCase('foo..bar')).toEqual('foo-bar') 14 | expect(kebabCase('..foo..bar..')).toEqual('foo-bar') 15 | expect(kebabCase(' foo bar ')).toEqual('foo-bar') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/keyBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { keyBy } from '../src' 2 | 3 | describe('groupBy', function () { 4 | const list = [ 5 | { id: 1, name: 'shuifeng' }, 6 | { id: 2, name: 'shanyue' }, 7 | ] 8 | 9 | it('should work', function () { 10 | const actual = keyBy(list, x => x.id) 11 | expect(actual).toEqual({ 12 | 1: { 13 | id: 1, 14 | name: 'shuifeng', 15 | }, 16 | 2: { 17 | id: 2, 18 | name: 'shanyue', 19 | }, 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/mapKeys.spec.ts: -------------------------------------------------------------------------------- 1 | import { mapKeys, mapValues } from '../src' 2 | 3 | describe('mapKeys', function () { 4 | it('should work', () => { 5 | expect(mapKeys({ a: 1, b: 2 }, String)).toStrictEqual({ 6 | 1: 1, 7 | 2: 2, 8 | }) 9 | 10 | expect(mapKeys({ a: 3 }, () => 'b')).toStrictEqual({ 11 | b: 3, 12 | }) 13 | 14 | expect(mapKeys({ a: 3, b: 4 }, String)).toStrictEqual({ 15 | 3: 3, 16 | 4: 4, 17 | }) 18 | }) 19 | 20 | it('should work with type', () => { 21 | type Input = { 22 | a: number 23 | b: number 24 | c?: number 25 | } 26 | const input: Input = { 27 | a: 3, 28 | b: 4, 29 | } 30 | // expect type same with Input 31 | const actual = mapKeys(input, String) 32 | expect(actual).toStrictEqual({ 33 | 3: 3, 34 | 4: 4, 35 | }) 36 | }) 37 | }) 38 | 39 | describe('mapValues', function () { 40 | it('should work', () => { 41 | expect(mapValues({ a: 3, b: 4 }, x => x + 1)).toStrictEqual({ 42 | a: 4, 43 | b: 5, 44 | }) 45 | 46 | expect(mapValues({ a: 1, b: 2 }, String)).toStrictEqual({ 47 | a: '1', 48 | b: '2', 49 | }) 50 | 51 | expect(mapValues({ a: 3 }, () => 'b')).toStrictEqual({ 52 | a: 'b', 53 | }) 54 | 55 | expect(mapValues({ a: 3, b: 4 })).toStrictEqual({ 56 | a: 3, 57 | b: 4, 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/max.spec.ts: -------------------------------------------------------------------------------- 1 | import { max, maxBy } from '../src' 2 | 3 | describe('max', function () { 4 | it('should work', function () { 5 | expect(max([-1, 0, 1])).toEqual(1) 6 | expect(max([1])).toEqual(1) 7 | expect(max([])).toEqual(undefined) 8 | expect(max()).toEqual(undefined) 9 | }) 10 | }) 11 | 12 | describe('maxBy', function () { 13 | it('should work', function () { 14 | expect( 15 | maxBy( 16 | [ 17 | { 18 | a: 3, 19 | }, 20 | { 21 | a: 4, 22 | }, 23 | ], 24 | x => x.a 25 | ) 26 | ).toEqual({ a: 4 }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/memoize.spec.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from '../src' 2 | 3 | describe('memorize', function () { 4 | it('memorize function should return cached result when input is same', () => { 5 | let count = 0 6 | const fn = (x: number): number => { 7 | count++ 8 | return x * x 9 | } 10 | const memoizedFunction = memoize(fn) 11 | 12 | const firstResult = memoizedFunction(5) // 输出 25,这次需要计算 13 | const secondResult = memoizedFunction(5) // 输出 25,这次直接从缓存中获取结果,不需要计算 14 | 15 | // 验证memoizedFunction(5)的结果是正确的 16 | expect(firstResult).toEqual(25) 17 | expect(secondResult).toEqual(25) 18 | 19 | // 验证fn只被调用了一次 20 | expect(count).toEqual(1) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/merge.spec.ts: -------------------------------------------------------------------------------- 1 | import { merge } from '../src' 2 | 3 | describe('merge', function () { 4 | it('should work', function () { 5 | expect( 6 | merge( 7 | { 8 | a: 3, 9 | }, 10 | { 11 | b: 4, 12 | } 13 | ) 14 | ).toEqual({ 15 | a: 3, 16 | b: 4, 17 | }) 18 | }) 19 | 20 | it('should work with array', function () { 21 | expect(merge(['a', 'b'], ['c'])).toEqual(['c', 'b']) 22 | }) 23 | 24 | // lodash.js Test Case 25 | // https://github.com/lodash/lodash/blob/master/test/merge.test.js#L8 26 | it('should merge `source` into `object`', function () { 27 | const names = { 28 | characters: [{ name: 'barney' }, { name: 'fred' }], 29 | } 30 | 31 | const ages = { 32 | characters: [{ age: 36 }, { age: 40 }], 33 | } 34 | 35 | const heights = { 36 | characters: [{ height: '5\'4"' }, { height: '5\'5"' }], 37 | } 38 | 39 | const expected = { 40 | characters: [ 41 | { name: 'barney', age: 36, height: '5\'4"' }, 42 | { name: 'fred', age: 40, height: '5\'5"' }, 43 | ], 44 | } 45 | 46 | expect(merge(names, ages, heights)).toEqual(expected) 47 | }) 48 | 49 | it('should work with more arguments', function () { 50 | const actual = merge({ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }) 51 | 52 | expect(actual).toEqual({ a: 4 }) 53 | }) 54 | 55 | it('should work with function', function () { 56 | const fn = function () {} 57 | 58 | const actual = merge({ a: fn }, { a: fn }, { a: {} }) 59 | 60 | expect(actual).toEqual({ a: {} }) 61 | }) 62 | 63 | it('should work with deepClone', function () { 64 | const o = { 65 | a: { b: 3 }, 66 | } 67 | 68 | expect(merge({}, o).a).toEqual(o.a) 69 | expect(merge({}, o).a).not.toBe(o.a) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/min.spec.ts: -------------------------------------------------------------------------------- 1 | import { min, minBy } from '../src' 2 | 3 | describe('min', function () { 4 | it('should work', function () { 5 | expect(min([-1, 0, 1])).toEqual(-1) 6 | expect(min([1])).toEqual(1) 7 | expect(min([])).toEqual(undefined) 8 | expect(min()).toEqual(undefined) 9 | }) 10 | }) 11 | 12 | describe('minBy', function () { 13 | it('should work', function () { 14 | expect( 15 | minBy( 16 | [ 17 | { 18 | a: 3, 19 | }, 20 | { 21 | a: 4, 22 | }, 23 | ], 24 | x => x.a 25 | ) 26 | ).toEqual({ a: 3 }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/nth.spec.ts: -------------------------------------------------------------------------------- 1 | import { nth } from '../src' 2 | 3 | describe('min', function () { 4 | const list = [1, 2, 3, 4, 5, 6] 5 | it('should work', function () { 6 | expect(nth(list, 3)).toEqual(4) 7 | expect(nth(list, -3)).toEqual(4) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/omit.spec.ts: -------------------------------------------------------------------------------- 1 | import { omit } from '../src' 2 | 3 | describe('omit', function () { 4 | const object = { a: 1, b: 2, c: 3, d: 4 } 5 | 6 | it('should work', function () { 7 | const actual = omit(object, ['a', 'c', 'z']) 8 | 9 | expect(actual).toEqual({ b: 2, d: 4 }) 10 | 11 | expect(omit(object)).toEqual(object) 12 | 13 | expect(omit(undefined)).toEqual({}) 14 | }) 15 | 16 | it('should work with second arguments', function () { 17 | const actual = omit(object) 18 | 19 | expect(actual).toEqual(object) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/omitBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { omitBy } from '../src' 2 | 3 | describe('omitBy', function () { 4 | const object = { a: 1, b: 2, c: 3, d: 4 } 5 | 6 | it('should work', function () { 7 | expect(omitBy(object, x => x === 3)).toEqual({ a: 1, b: 2, d: 4 }) 8 | expect(omitBy(object, undefined as unknown as () => boolean)).toEqual({}) 9 | }) 10 | 11 | it('should work with value', function () { 12 | const actual = omitBy(object, n => { 13 | return n === 1 || n === 3 14 | }) 15 | 16 | expect(actual).toEqual({ b: 2, d: 4 }) 17 | }) 18 | 19 | it('should work with key', function () { 20 | const actual = omitBy(object, (_, n) => { 21 | return n === 'a' || n === 'b' 22 | }) 23 | 24 | expect(actual).toEqual({ c: 3, d: 4 }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/once.spec.ts: -------------------------------------------------------------------------------- 1 | import { once } from '../src' 2 | 3 | describe('once', function () { 4 | it('once function should only allow the inner function to be called once', () => { 5 | let count = 0 6 | const fn = () => count++ 7 | const onceFn = once(fn) 8 | 9 | onceFn() 10 | onceFn() 11 | 12 | expect(count).toEqual(1) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/pick.spec.ts: -------------------------------------------------------------------------------- 1 | import { pick } from '../src' 2 | 3 | describe('pick', function () { 4 | const object = { a: 1, b: 2, c: 3, d: 4 } 5 | 6 | it('should work', function () { 7 | const actual = pick(object, ['a', 'c', 'z']) 8 | 9 | expect(actual).toEqual({ a: 1, c: 3 }) 10 | 11 | expect(pick({ a: undefined }, ['a'])).toStrictEqual({ a: undefined }) 12 | expect(pick({}, ['a'])).toStrictEqual({}) 13 | expect(pick(object)).toEqual({}) 14 | expect(pick(undefined)).toEqual({}) 15 | }) 16 | 17 | it('should work with second arguments', function () { 18 | const actual = pick(object) 19 | 20 | expect(actual).toEqual({}) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/pickBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { pickBy } from '../src' 2 | 3 | describe('pickBy', function () { 4 | const object = { a: 1, b: 2, c: 3, d: 4 } 5 | 6 | it('should work', function () { 7 | expect(pickBy(object, x => x === 3)).toEqual({ c: 3 }) 8 | expect(pickBy(object, undefined as unknown as () => boolean)).toEqual( 9 | object 10 | ) 11 | 12 | expect(pickBy({ a: undefined })).toEqual({}) 13 | }) 14 | 15 | it('should work with value', function () { 16 | const actual = pickBy(object, function (n) { 17 | return n === 1 || n === 3 18 | }) 19 | 20 | expect(actual).toEqual({ a: 1, c: 3 }) 21 | }) 22 | 23 | it('should work with key', function () { 24 | const actual = pickBy(object, function (_, n) { 25 | return n === 'a' || n === 'b' 26 | }) 27 | 28 | expect(actual).toEqual({ a: 1, b: 2 }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/property.spec.ts: -------------------------------------------------------------------------------- 1 | import { property } from '../src' 2 | 3 | interface Person { 4 | name: string 5 | age: number 6 | address: { 7 | city: string 8 | country: string 9 | } 10 | } 11 | 12 | describe('property function', () => { 13 | const objects = [{ a: { b: 2 } }, { a: { b: 1 } }] 14 | 15 | it('should return the correct property values', () => { 16 | const getB = property('a.b') 17 | expect(getB(objects[0])).toBe(2) 18 | expect(getB(objects[1])).toBe(1) 19 | }) 20 | 21 | it('should handle non-existing properties gracefully', () => { 22 | const getC = property('a.c') 23 | expect(getC(objects[0])).toBeUndefined() 24 | }) 25 | 26 | const person: Person = { 27 | name: 'John Doe', 28 | age: 30, 29 | address: { 30 | city: 'New York', 31 | country: 'USA', 32 | }, 33 | } 34 | 35 | it('should get the correct property value from an object', () => { 36 | const getName = property('name') 37 | const getCity = property('address.city') 38 | 39 | expect(getName(person)).toBe('John Doe') 40 | expect(getCity(person)).toBe('New York') 41 | }) 42 | 43 | it('should handle nested properties', () => { 44 | const getCountry = property('address.country') 45 | expect(getCountry(person)).toBe('USA') 46 | }) 47 | 48 | it('should return undefined for non-existing properties', () => { 49 | const getLastName = property('lastName') 50 | const getStreet = property('address.street') 51 | 52 | expect(getLastName(person)).toBeUndefined() 53 | expect(getStreet(person)).toBeUndefined() 54 | }) 55 | 56 | it('should handle non-object inputs', () => { 57 | const getLength = property('length') 58 | const getFirstChar = property('0') 59 | 60 | expect(getLength('hello')).toBe(5) 61 | expect(getFirstChar('hello')).toBe('h') 62 | }) 63 | 64 | it('should handle array inputs', () => { 65 | const getFirstElement = property('0') 66 | const getNestedElement = property('1.0') 67 | const getArrayElement = property(['1', '0']) 68 | expect(getFirstElement([1, 2, 3])).toBe(1) 69 | expect( 70 | getNestedElement([ 71 | [1, 2], 72 | [3, 4], 73 | ]) 74 | ).toBe(3) 75 | expect( 76 | getArrayElement([ 77 | [1, 2], 78 | [3, 4], 79 | ]) 80 | ).toBe(3) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/random.spec.ts: -------------------------------------------------------------------------------- 1 | import { random } from '../src' 2 | 3 | describe('sampleSize', function () { 4 | it('should work', function () { 5 | expect(random(10, 10)).toEqual(10) 6 | expect(random(0, 0)).toEqual(0) 7 | expect(random(0)).toEqual(0) 8 | expect(random(0, 1)).toBeGreaterThanOrEqual(0) 9 | expect(random(0, 1)).toBeLessThanOrEqual(1) 10 | expect(random()).toBeGreaterThanOrEqual(0) 11 | expect(random()).toBeLessThanOrEqual(1) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/range.spec.ts: -------------------------------------------------------------------------------- 1 | import { range } from '../src' 2 | 3 | describe('range', function () { 4 | it('should work', function () { 5 | expect(range(4)).toEqual([0, 1, 2, 3]) 6 | expect(range(-4)).toEqual([0, -1, -2, -3]) 7 | 8 | expect(range(1, 5)).toEqual([1, 2, 3, 4]) 9 | expect(range(5, 1)).toEqual([5, 4, 3, 2]) 10 | 11 | expect(range(0, -4, -1)).toEqual([0, -1, -2, -3]) 12 | expect(range(5, 1, -1)).toEqual([5, 4, 3, 2]) 13 | expect(range(0, 20, 5)).toEqual([0, 5, 10, 15]) 14 | 15 | expect(range(1, 5, 0)).toEqual([1, 1, 1, 1]) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/sample.spec.ts: -------------------------------------------------------------------------------- 1 | import { sample } from '../src' 2 | 3 | describe('sample', function () { 4 | const list = [1, 2, 3, 4, 5] 5 | 6 | it('should return a random element', function () { 7 | for (let i = 0; i < 10; i++) { 8 | const actual = sample(list) 9 | expect(list).toContain(actual) 10 | } 11 | }) 12 | 13 | it('should return `undefined` when sampling empty collections', function () { 14 | const actual = sample([]) 15 | expect(actual).toBe(undefined) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/sampleSize.spec.ts: -------------------------------------------------------------------------------- 1 | import { sampleSize } from '../src' 2 | 3 | describe('sampleSize', function () { 4 | const list = [1, 2, 3, 4, 5] 5 | 6 | it('should return an array of random elements', function () { 7 | const actual = sampleSize(list, 2) 8 | 9 | expect(actual).toHaveLength(2) 10 | expect(actual).toEqual(expect.arrayContaining(actual)) 11 | }) 12 | 13 | it('should contain elements of the collection', function () { 14 | const actual = sampleSize(list, list.length).sort() 15 | 16 | expect(actual).toStrictEqual(list) 17 | }) 18 | 19 | it('should return an empty array when `n` < `1` or `NaN`', function () { 20 | expect(sampleSize(list, 0)).toStrictEqual([]) 21 | expect(sampleSize(list, -1)).toStrictEqual([]) 22 | expect(sampleSize(list, NaN)).toStrictEqual([]) 23 | }) 24 | 25 | it('should return all elements when `n` >= `length`', function () { 26 | const actual = sampleSize(list, 10086).sort() 27 | 28 | expect(actual).toStrictEqual(list) 29 | }) 30 | 31 | it('should coerce `n` to an integer', function () { 32 | const actual = sampleSize(list, 2.3) 33 | 34 | expect(actual).toHaveLength(2) 35 | }) 36 | 37 | it('should return an empty array for empty collections', function () { 38 | const actual = sampleSize([]) 39 | 40 | expect(actual).toStrictEqual([]) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/shuffle.spec.ts: -------------------------------------------------------------------------------- 1 | import { shuffle } from '../src' 2 | 3 | describe('sampleSize', function () { 4 | const list = [1, 2, 3, 4, 5] 5 | 6 | it('should work', function () { 7 | const actual = shuffle(list) 8 | 9 | expect(actual).toHaveLength(5) 10 | expect(actual).toEqual(expect.arrayContaining(actual)) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/snakeCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { snakeCase } from '../src' 2 | 3 | describe('snakeCase', function () { 4 | it('should work', function () { 5 | expect(snakeCase('foo')).toEqual('foo') 6 | expect(snakeCase('foo-bar')).toEqual('foo_bar') 7 | expect(snakeCase('foo--bar')).toEqual('foo_bar') 8 | expect(snakeCase('--foo--bar')).toEqual('foo_bar') 9 | expect(snakeCase('FOO-BAR')).toEqual('foo_bar') 10 | expect(snakeCase('-foo-bar-')).toEqual('foo_bar') 11 | expect(snakeCase('--foo--bar--')).toEqual('foo_bar') 12 | expect(snakeCase('foo.bar')).toEqual('foo_bar') 13 | expect(snakeCase('foo..bar')).toEqual('foo_bar') 14 | expect(snakeCase('..foo..bar..')).toEqual('foo_bar') 15 | expect(snakeCase(' foo bar ')).toEqual('foo_bar') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/sum.spec.ts: -------------------------------------------------------------------------------- 1 | import { sum } from '../src' 2 | 3 | describe('range', function () { 4 | it('should work', function () { 5 | expect(sum()).toEqual(0) 6 | expect(sum([1])).toEqual(1) 7 | expect(sum([1, 2])).toEqual(3) 8 | expect(sum([1, 2, 3])).toEqual(6) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/template.spec.ts: -------------------------------------------------------------------------------- 1 | import { template } from '../src' 2 | 3 | describe('template function', () => { 4 | it('replaces all occurrences', () => { 5 | const tmp = ` 6 | Hello my name is {{name}}. I am a {{type}}. 7 | Not sure why I am {{reason}}. 8 | 9 | Thank You - {{name}} 10 | ` 11 | const data = { 12 | name: 'Ray', 13 | type: 'template', 14 | reason: 'so beautiful', 15 | } 16 | 17 | const result = template(tmp, data) 18 | const expected = ` 19 | Hello my name is ${data.name}. I am a ${data.type}. 20 | Not sure why I am ${data.reason}. 21 | 22 | Thank You - ${data.name} 23 | ` 24 | 25 | expect(result).toEqual(expected) 26 | }) 27 | 28 | it('replaces all occurrences given template', () => { 29 | const tmp = `Hello .` 30 | const data = { 31 | name: 'world', 32 | } 33 | 34 | const result = template(tmp, data, /<(.+?)>/g) 35 | expect(result).toEqual(`Hello ${data.name}.`) 36 | }) 37 | 38 | it('should replace multiple variables', () => { 39 | expect( 40 | template('{{ greeting }}, {{ name }}!', { 41 | greeting: 'hello', 42 | name: 'world', 43 | }) 44 | ).toBe('hello, world!') 45 | }) 46 | 47 | it('should ignore whitespace around variable names', () => { 48 | expect(template('hello, {{ name }}!', { name: 'world' })).toBe( 49 | 'hello, world!' 50 | ) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/throttle.spec.ts: -------------------------------------------------------------------------------- 1 | import { throttle, sleep } from '../src' 2 | 3 | describe('throttle', function () { 4 | it('should work', async function () { 5 | let num = 0 6 | let func = throttle(() => num++, 500) 7 | 8 | func() 9 | func() 10 | func() 11 | await sleep(600).then(() => expect(num).toEqual(1)) 12 | 13 | func() 14 | await sleep(600).then(() => expect(num).toEqual(2)) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/uniq.spec.ts: -------------------------------------------------------------------------------- 1 | import { uniq } from '../src' 2 | 3 | describe('sampleSize', function () { 4 | const list = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6] 5 | 6 | it('should work', function () { 7 | const actual = uniq(list) 8 | 9 | expect(actual).toEqual([1, 2, 3, 4, 5, 6]) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/uniqBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { uniqBy } from '../src' 2 | 3 | describe('sampleSize', function () { 4 | const list = [1.1, 2.5, 3.5, 4.1, 5.4, 1.5, 2.3, 3.7, 4.9, 5.3, 6.3] 5 | 6 | const testData = [ 7 | { id: 1, name: 'Tom' }, 8 | { id: 2, name: 'Jerry' }, 9 | { id: 1, name: 'Tom' }, 10 | { id: 3, name: 'Spike' }, 11 | { id: 2, name: 'Jerry' }, 12 | ] 13 | 14 | const testStrings = ['Tom', 'Jerry', 'Tom', 'Spike', 'Jerry'] 15 | 16 | it('should work', function () { 17 | const actual = uniqBy(list, Math.floor) 18 | 19 | expect(actual).toEqual([1.1, 2.5, 3.5, 4.1, 5.4, 6.3]) 20 | 21 | const actualObj = uniqBy(testData, item => item.id) 22 | 23 | expect(actualObj).toEqual([ 24 | { id: 1, name: 'Tom' }, 25 | { id: 2, name: 'Jerry' }, 26 | { id: 3, name: 'Spike' }, 27 | ]) 28 | 29 | const actualString = uniqBy(testStrings, item => item) 30 | 31 | expect(actualString).toEqual(['Tom', 'Jerry', 'Spike']) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/unzip.spec.ts: -------------------------------------------------------------------------------- 1 | import { unzip } from '../src' 2 | 3 | describe('unzip', () => { 4 | it('should correctly unzip an array of arrays', () => { 5 | const input = [ 6 | [1, 'a', true], 7 | [2, 'b', false], 8 | [3, 'c', true], 9 | ] 10 | const result = unzip(input) 11 | 12 | expect(result).toEqual([ 13 | [1, 2, 3], 14 | ['a', 'b', 'c'], 15 | [true, false, true], 16 | ]) 17 | }) 18 | 19 | it('should handle empty input', () => { 20 | const input: any[][] = [] 21 | const result = unzip(input) 22 | 23 | expect(result).toEqual([]) 24 | }) 25 | 26 | it('should handle arrays of different lengths', () => { 27 | const input = [ 28 | [1, 'a'], 29 | [2, 'b', 3], 30 | [4, 'c'], 31 | ] 32 | const result = unzip(input) 33 | 34 | expect(result).toEqual([ 35 | [1, 2, 4], 36 | ['a', 'b', 'c'], 37 | [undefined, 3, undefined], 38 | ]) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/zip.spec.ts: -------------------------------------------------------------------------------- 1 | import { zip } from '../src' 2 | 3 | describe('zip', () => { 4 | it('should zip arrays correctly', () => { 5 | const arr1 = [1, 2, 3] 6 | const arr2 = ['a', 'b', 'c'] 7 | const arr3 = [true, false] 8 | 9 | const zipped = zip(arr1, arr2, arr3) 10 | 11 | const expected = [ 12 | [1, 'a', true], 13 | [2, 'b', false], 14 | [3, 'c', undefined], 15 | ] 16 | 17 | expect(zipped).toEqual(expected) 18 | }) 19 | 20 | it('should handle empty arrays', () => { 21 | const emptyArray: number[] = [] 22 | const arr1 = [1, 2, 3] 23 | const zipped = zip(emptyArray, arr1) 24 | 25 | const expected = [ 26 | [undefined, 1], 27 | [undefined, 2], 28 | [undefined, 3], 29 | ] 30 | 31 | expect(zipped).toEqual(expected) 32 | }) 33 | 34 | it('should handle arrays of different lengths', () => { 35 | const arr1 = [1, 2] 36 | const arr2 = ['a', 'b', 'c'] 37 | const arr3: boolean[] = [] 38 | const zipped = zip(arr1, arr2, arr3) 39 | 40 | const expected = [ 41 | [1, 'a', undefined], 42 | [2, 'b', undefined], 43 | [undefined, 'c', undefined], 44 | ] 45 | 46 | expect(zipped).toEqual(expected) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types", "test"], 4 | "compilerOptions": { 5 | "rootDirs": ["src", "test", "types"], 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "target": "ES2020", 9 | "importHelpers": true, 10 | // output .d.ts declaration files for consumers 11 | "declaration": true, 12 | // output .js.map sourcemap files for consumers 13 | "sourceMap": true, 14 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 15 | // stricter type-checking for stronger correctness. Recommended by TS 16 | "strict": true, 17 | // linter checks for common issues 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | // use Node's module resolution algorithm, instead of the legacy TS one 24 | "moduleResolution": "node", 25 | // transpile JSX to React.createElement 26 | "jsx": "react", 27 | // interop between ESM and CJS modules. Recommended by TS 28 | "esModuleInterop": true, 29 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 30 | "skipLibCheck": true, 31 | // error out if import and file system have a casing mismatch. Recommended by TS 32 | "forceConsistentCasingInFileNames": true, 33 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 34 | "noEmit": true, 35 | "downlevelIteration": true, 36 | "types": ["vitest/globals"], 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: true, 8 | dts: true, 9 | format: ['cjs', 'esm'], 10 | outExtension({ format }) { 11 | return { 12 | js: `.${format}.js`, 13 | } 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | includeSource: ['test/*'], 6 | globals: true, 7 | coverage: { 8 | reporter: ['text'], // https://vitest.dev/guide/coverage.html#coverage-setup 9 | }, 10 | }, 11 | }) 12 | --------------------------------------------------------------------------------