├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── array.d.ts ├── array.js ├── collection.d.ts ├── collection.js ├── fn.d.ts ├── fn.js ├── index.d.ts ├── index.js ├── lang.d.ts ├── lang.js ├── object.d.ts └── object.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── test ├── .eslintrc ├── array.spec.js ├── bundling │ ├── index.js │ └── rollup.config.js ├── collection.spec.js ├── fn.spec.js ├── index.spec.ts ├── integration │ ├── bundle.spec.cjs │ └── bundle.spec.js ├── lang.spec.js └── object.spec.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | test/bundling/bundled.js 2 | dist -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:bpmn-io/recommended" 3 | } -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | Build: 5 | 6 | strategy: 7 | matrix: 8 | os: [ ubuntu-latest ] 9 | node-version: [ 16, 20 ] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'npm' 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Build 24 | run: npm run all 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | test/bundling/bundled.js 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to [min-dash](https://github.com/bpmn-io/min-dash) are documented here. We use [semantic versioning](http://semver.org/) for releases. 4 | 5 | ## Unreleased 6 | 7 | ___Note:__ Yet to be released changes appear here._ 8 | 9 | ## 4.2.3 10 | 11 | * `FIX`: correct `flatten` type definitions ([#38](https://github.com/bpmn-io/min-dash/pull/38)) 12 | 13 | ## 4.2.2 14 | 15 | * `FIX`: gracefully handle `undefined` target in `has` 16 | * `FIX`: correct `findIndex` type definitions ([#36](https://github.com/bpmn-io/min-dash/issues/36)) 17 | 18 | ## 4.2.1 19 | 20 | * `FIX`: correct `isNil` and `isArray` type definitions ([#35](https://github.com/bpmn-io/min-dash/pull/35)) 21 | 22 | ## 4.2.0 23 | 24 | * `FEAT`: add `ESM` package exports ([#29](https://github.com/bpmn-io/min-dash/pull/29)) 25 | * `FIX`: correct various type definitions ([#33](https://github.com/bpmn-io/min-dash/pull/33)) 26 | * `FIX`: allow type definitions to be consumed in ESM setups ([#31](https://github.com/bpmn-io/min-dash/pull/31)) 27 | 28 | ## 4.1.1 29 | 30 | * `FIX`: correct `pick` and `omit` type definitions ([#26](https://github.com/bpmn-io/min-dash/issues/26)) 31 | 32 | ## 4.1.0 33 | 34 | * `FIX`: various type definition fixes ([#25](https://github.com/bpmn-io/min-dash/pull/25)) 35 | 36 | ## 4.0.0 37 | 38 | * `FEAT`: use ES2018 39 | 40 | ### Breaking changes 41 | 42 | * The library exposes now ES2018 code. You have to transpile it yourself to support older (ES5) syntax. 43 | 44 | ## 3.8.1 45 | 46 | * `FIX`: prevent prototype pollution via `set` ([#21](https://github.com/bpmn-io/min-dash/pull/21)) 47 | 48 | ## 3.8.0 49 | 50 | * `FEAT`: provide lodash-style `cancel` and `flush` on debounced function 51 | 52 | ## 3.7.0 53 | 54 | * `FEAT`: add `get` utility ([#19](https://github.com/bpmn-io/min-dash/pull/19)) 55 | 56 | ## 3.6.1 57 | 58 | * `FIX`: correct `set` handling of `0` keys ([#18](https://github.com/bpmn-io/min-dash/pull/18)) 59 | * `FIX`: correct `set` scaffolding on `null` values ([#18](https://github.com/bpmn-io/min-dash/pull/18)) 60 | 61 | ## 3.6.0 62 | 63 | * `FEAT`: add `set` utility ([#16](https://github.com/bpmn-io/min-dash/pull/16)) 64 | 65 | ## 3.5.2 66 | 67 | * `FIX`: prevent prototype pollution via `merge` 68 | 69 | ## 3.5.1 70 | 71 | * `FIX`: make `every` always return boolean value ([#14](https://github.com/bpmn-io/min-dash/pull/14)) 72 | 73 | ## 3.5.0 74 | 75 | * `FIX`: make `isFunction` detect async functions and generators 76 | * `FIX`: correct `bind` TypeScript definitions 77 | * `FIX`: match `forEach` implementation with documentation 78 | * `CHORE`: bump to `babel@7` 79 | 80 | ## 3.4.0 81 | 82 | * `CHORE`: make `debounce` work without `clearTimeout` ([#7](https://github.com/bpmn-io/min-dash/pull/7)) 83 | 84 | ## 3.3.0 85 | 86 | * `FEAT`: add `throttle(fn, interval)` util 87 | 88 | ## 3.2.0 89 | 90 | * `FEAT`: add `isNil` utility that checks for `undefined || null` 91 | * `FIX`: correct `isDefined` behavior 92 | * `FIX`: make `isUndefined` behavior 93 | 94 | ## 3.1.0 95 | 96 | * `FEAT`: add TypeScript definitions 97 | 98 | ## 3.0.0 99 | 100 | ### Breaking Changes 101 | 102 | * `FIX`: remove browser field again; it confuses modern module bundlers. This partially reverts `v2.4.0` 103 | 104 | ## 2.4.0 105 | 106 | * `CHORE`: add `browser` field 107 | 108 | ## 2.3.0 109 | 110 | * `FEAT`: add `omit(obj, properties)` util 111 | 112 | ## 2.2.0 113 | 114 | * `FEAT`: add `flatten(array)` util 115 | 116 | ## 2.1.0 117 | 118 | * `FEAT`: add `merge(target, ...sources)` util 119 | * `FEAT`: add `size(obj)` util 120 | * `FEAT`: add `has(obj, property)` util 121 | * `DOCS`: improve utils documentation 122 | 123 | ## 2.0.0 124 | 125 | ### Breaking Changes 126 | 127 | * `FEAT`: expose utilities via main export only ([`cb6ab757`](https://github.com/bpmn-io/min-dash/commit/cb6ab757fa07e8728ba6c7bd692f93a94afecceb)) 128 | 129 | ### Other Improvements 130 | 131 | * `CHORE`: generate ES, CJS and UMD bundles using rollup 132 | * `CHORE`: babelify results and don't require `Object.assign` polyfill 133 | 134 | ## ... 135 | 136 | Check `git log` for earlier history. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present camunda Services GmbH 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # min-dash 2 | 3 | [![CI](https://github.com/bpmn-io/min-dash/workflows/CI/badge.svg)](https://github.com/bpmn-io/min-dash/actions?query=workflow%3ACI) 4 | 5 | Minimal utility tool belt to be used with [bpmn.io](https://bpmn.io/) related libraries. 6 | 7 | 8 | ## Features 9 | 10 | * fine selection of [powerful utilities](./lib) on board 11 | * ES2015 compatible 12 | * complete bundle `< 2 kB` minified and gzipped 13 | * utilities optimized for speed (i.e. sorting and union only by key) 14 | 15 | 16 | ## How to use 17 | 18 | ```javascript 19 | import { 20 | find, 21 | sortBy, 22 | assign 23 | } from 'min-dash'; 24 | ``` 25 | 26 | Your favourite module bundler should apply tree-shaking to only include the components your application requires. If you're using CommonJS modules give [common-shake](https://github.com/indutny/common-shake) a try. 27 | 28 | 29 | ## Related 30 | 31 | * [1-liners](https://github.com/1-liners/1-liners) - a slightly more opinionated collection of useful utilities 32 | * [min-dom](https://github.com/bpmn-io/min-dom) - minimal DOM utility toolbelt 33 | * [tiny-svg](https://github.com/bpmn-io/tiny-svg) - tiny SVG utility toolbelt 34 | 35 | 36 | ## License 37 | 38 | MIT 39 | -------------------------------------------------------------------------------- /lib/array.d.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCollection } from './collection.js'; 2 | 3 | type Flatten = Type extends Array ? Item : Type; 4 | 5 | /** 6 | * Flatten array, one level deep. 7 | * 8 | * @param arr 9 | * 10 | * @return 11 | */ 12 | export function flatten(arr: ArrayCollection | null | undefined): Flatten[]; -------------------------------------------------------------------------------- /lib/array.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Flatten array, one level deep. 4 | * 5 | * @template T 6 | * 7 | * @param {T[][] | T[] | null} [arr] 8 | * 9 | * @return {T[]} 10 | */ 11 | export function flatten(arr) { 12 | return Array.prototype.concat.apply([], arr); 13 | } -------------------------------------------------------------------------------- /lib/collection.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Matcher = 3 | ((e: T) => boolean) | 4 | ((e: T, idx: number) => boolean) | 5 | ((e: T, key: string) => boolean) | 6 | any; 7 | 8 | export type Extractor = ((e: T) => U) | string | number; 9 | 10 | export type ArrayCollection = Array; 11 | export type StringKeyValueCollection = { [key: string]: T }; 12 | export type NumberKeyValueCollection = { [key: number]: T }; 13 | export type KeyValueCollection = StringKeyValueCollection | NumberKeyValueCollection; 14 | export type Collection = KeyValueCollection | ArrayCollection | null | undefined; 15 | 16 | /** 17 | * Find element in collection. 18 | * 19 | * @param collection 20 | * @param matcher 21 | * 22 | * @return 23 | */ 24 | export function find(collection: Collection, matcher: Matcher): T | undefined; 25 | 26 | /** 27 | * Find element index in collection. 28 | * 29 | * @param collection 30 | * @param matcher 31 | * 32 | * @return 33 | */ 34 | export function findIndex(collection: Collection, matcher: Matcher): number | string | undefined; 35 | 36 | /** 37 | * Find element in collection. 38 | * 39 | * @param collection 40 | * @param matcher 41 | * 42 | * @return result 43 | */ 44 | export function filter(collection: Collection, matcher: Matcher): T[]; 45 | 46 | /** 47 | * Iterate over collection; returning something 48 | * (non-undefined) will stop iteration. 49 | * 50 | * @param collection 51 | * @param iterator 52 | * 53 | * @return return result that stopped the iteration 54 | */ 55 | export function forEach(collection: Collection, iterator: (item: T, convertKey: any /* TODO */) => boolean | void): T; 56 | 57 | /** 58 | * Return collection without element. 59 | * 60 | * @param arr 61 | * @param matcher 62 | * 63 | * @return 64 | */ 65 | export function without(arr: T[], matcher: Matcher): T[]; 66 | 67 | /** 68 | * Reduce collection, returning a single result. 69 | * 70 | * @param collection 71 | * @param iterator 72 | * @param result 73 | * 74 | * @return result returned from last iterator 75 | */ 76 | export function reduce(collection: Collection, iterator: (result: V, entry: T, index: any) => V, result: V): V; 77 | 78 | /** 79 | * Return true if every element in the collection 80 | * matches the criteria. 81 | * 82 | * @param collection 83 | * @param matcher 84 | * 85 | * @return 86 | */ 87 | export function every(collection: Collection, matcher: Matcher): boolean; 88 | 89 | /** 90 | * Return true if some elements in the collection 91 | * match the criteria. 92 | * 93 | * @param collection 94 | * @param matcher 95 | * 96 | * @return 97 | */ 98 | export function some(collection: Collection, matcher: Matcher): boolean; 99 | 100 | /** 101 | * Transform a collection into another collection 102 | * by piping each member through the given fn. 103 | * 104 | * @param collection 105 | * @param fn 106 | * 107 | * @return transformed collection 108 | */ 109 | export function map(collection: Collection, fn: (value: T, key: number) => U): U[]; 110 | 111 | /** 112 | * Get the collections keys. 113 | * 114 | * @param collection 115 | * 116 | * @return 117 | */ 118 | export function keys(collection: Collection): T extends Array ? number[] : (keyof T)[]; 119 | 120 | /** 121 | * Shorthand for `keys(o).length`. 122 | * 123 | * @param collection 124 | * 125 | * @return 126 | */ 127 | export function size(collection: Collection): number; 128 | 129 | /** 130 | * Get the values in the collection. 131 | * 132 | * @param collection 133 | * 134 | * @return 135 | */ 136 | export function values(collection: Collection): T[]; 137 | 138 | /** 139 | * Group collection members by attribute. 140 | * 141 | * @param collection 142 | * @param extractor 143 | * 144 | * @return map with { attrValue => [ a, b, c ] } 145 | */ 146 | export function groupBy(collection: Collection, extractor: Extractor, grouped?: any): { [attrValue: string]: any[] }; 147 | 148 | export function uniqueBy(extractor: Extractor, ...collections: Collection[]): T[]; 149 | export function unionBy(extractor: Extractor, ...collections: Collection[]): T[]; 150 | 151 | /** 152 | * Sort collection by criteria. 153 | * 154 | * @param collection 155 | * @param extractor 156 | * 157 | * @return 158 | */ 159 | export function sortBy(collection: Collection, extractor: Extractor): T[]; 160 | 161 | /** 162 | * Create an object pattern matcher. 163 | * 164 | * @example 165 | * 166 | * const matcher = matchPattern({ id: 1 }); 167 | * 168 | * let element = find(elements, matcher); 169 | * 170 | * @param pattern 171 | * 172 | * @return matcherFn 173 | */ 174 | export function matchPattern(pattern: T): (e: any) => boolean; 175 | -------------------------------------------------------------------------------- /lib/collection.js: -------------------------------------------------------------------------------- 1 | import { 2 | isUndefined, 3 | ensureArray, 4 | isArray, 5 | isFunction, 6 | has 7 | } from './lang.js'; 8 | 9 | /** 10 | * @template T 11 | * @typedef { ( 12 | * ((e: T) => boolean) | 13 | * ((e: T, idx: number) => boolean) | 14 | * ((e: T, key: string) => boolean) | 15 | * string | 16 | * number 17 | * ) } Matcher 18 | */ 19 | 20 | /** 21 | * @template T 22 | * @template U 23 | * 24 | * @typedef { ( 25 | * ((e: T) => U) | string | number 26 | * ) } Extractor 27 | */ 28 | 29 | 30 | /** 31 | * @template T 32 | * @typedef { (val: T, key: any) => boolean } MatchFn 33 | */ 34 | 35 | /** 36 | * @template T 37 | * @typedef { T[] } ArrayCollection 38 | */ 39 | 40 | /** 41 | * @template T 42 | * @typedef { { [key: string]: T } } StringKeyValueCollection 43 | */ 44 | 45 | /** 46 | * @template T 47 | * @typedef { { [key: number]: T } } NumberKeyValueCollection 48 | */ 49 | 50 | /** 51 | * @template T 52 | * @typedef { StringKeyValueCollection | NumberKeyValueCollection } KeyValueCollection 53 | */ 54 | 55 | /** 56 | * @template T 57 | * @typedef { KeyValueCollection | ArrayCollection } Collection 58 | */ 59 | 60 | /** 61 | * Find element in collection. 62 | * 63 | * @template T 64 | * @param {Collection} collection 65 | * @param {Matcher} matcher 66 | * 67 | * @return {Object} 68 | */ 69 | export function find(collection, matcher) { 70 | 71 | const matchFn = toMatcher(matcher); 72 | 73 | let match; 74 | 75 | forEach(collection, function(val, key) { 76 | if (matchFn(val, key)) { 77 | match = val; 78 | 79 | return false; 80 | } 81 | }); 82 | 83 | return match; 84 | 85 | } 86 | 87 | 88 | /** 89 | * Find element index in collection. 90 | * 91 | * @template T 92 | * @param {Collection} collection 93 | * @param {Matcher} matcher 94 | * 95 | * @return {number | string | undefined} 96 | */ 97 | export function findIndex(collection, matcher) { 98 | 99 | const matchFn = toMatcher(matcher); 100 | 101 | let idx = isArray(collection) ? -1 : undefined; 102 | 103 | forEach(collection, function(val, key) { 104 | if (matchFn(val, key)) { 105 | idx = key; 106 | 107 | return false; 108 | } 109 | }); 110 | 111 | return idx; 112 | } 113 | 114 | 115 | /** 116 | * Filter elements in collection. 117 | * 118 | * @template T 119 | * @param {Collection} collection 120 | * @param {Matcher} matcher 121 | * 122 | * @return {T[]} result 123 | */ 124 | export function filter(collection, matcher) { 125 | 126 | const matchFn = toMatcher(matcher); 127 | 128 | let result = []; 129 | 130 | forEach(collection, function(val, key) { 131 | if (matchFn(val, key)) { 132 | result.push(val); 133 | } 134 | }); 135 | 136 | return result; 137 | } 138 | 139 | 140 | /** 141 | * Iterate over collection; returning something 142 | * (non-undefined) will stop iteration. 143 | * 144 | * @template T 145 | * @param {Collection} collection 146 | * @param { ((item: T, idx: number) => (boolean|void)) | ((item: T, key: string) => (boolean|void)) } iterator 147 | * 148 | * @return {T} return result that stopped the iteration 149 | */ 150 | export function forEach(collection, iterator) { 151 | 152 | let val, 153 | result; 154 | 155 | if (isUndefined(collection)) { 156 | return; 157 | } 158 | 159 | const convertKey = isArray(collection) ? toNum : identity; 160 | 161 | for (let key in collection) { 162 | 163 | if (has(collection, key)) { 164 | val = collection[key]; 165 | 166 | result = iterator(val, convertKey(key)); 167 | 168 | if (result === false) { 169 | return val; 170 | } 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Return collection without element. 177 | * 178 | * @template T 179 | * @param {ArrayCollection} arr 180 | * @param {Matcher} matcher 181 | * 182 | * @return {T[]} 183 | */ 184 | export function without(arr, matcher) { 185 | 186 | if (isUndefined(arr)) { 187 | return []; 188 | } 189 | 190 | ensureArray(arr); 191 | 192 | const matchFn = toMatcher(matcher); 193 | 194 | return arr.filter(function(el, idx) { 195 | return !matchFn(el, idx); 196 | }); 197 | 198 | } 199 | 200 | 201 | /** 202 | * Reduce collection, returning a single result. 203 | * 204 | * @template T 205 | * @template V 206 | * 207 | * @param {Collection} collection 208 | * @param {(result: V, entry: T, index: any) => V} iterator 209 | * @param {V} result 210 | * 211 | * @return {V} result returned from last iterator 212 | */ 213 | export function reduce(collection, iterator, result) { 214 | 215 | forEach(collection, function(value, idx) { 216 | result = iterator(result, value, idx); 217 | }); 218 | 219 | return result; 220 | } 221 | 222 | 223 | /** 224 | * Return true if every element in the collection 225 | * matches the criteria. 226 | * 227 | * @param {Object|Array} collection 228 | * @param {Function} matcher 229 | * 230 | * @return {Boolean} 231 | */ 232 | export function every(collection, matcher) { 233 | 234 | return !!reduce(collection, function(matches, val, key) { 235 | return matches && matcher(val, key); 236 | }, true); 237 | } 238 | 239 | 240 | /** 241 | * Return true if some elements in the collection 242 | * match the criteria. 243 | * 244 | * @param {Object|Array} collection 245 | * @param {Function} matcher 246 | * 247 | * @return {Boolean} 248 | */ 249 | export function some(collection, matcher) { 250 | 251 | return !!find(collection, matcher); 252 | } 253 | 254 | 255 | /** 256 | * Transform a collection into another collection 257 | * by piping each member through the given fn. 258 | * 259 | * @param {Object|Array} collection 260 | * @param {Function} fn 261 | * 262 | * @return {Array} transformed collection 263 | */ 264 | export function map(collection, fn) { 265 | 266 | let result = []; 267 | 268 | forEach(collection, function(val, key) { 269 | result.push(fn(val, key)); 270 | }); 271 | 272 | return result; 273 | } 274 | 275 | 276 | /** 277 | * Get the collections keys. 278 | * 279 | * @param {Object|Array} collection 280 | * 281 | * @return {Array} 282 | */ 283 | export function keys(collection) { 284 | return collection && Object.keys(collection) || []; 285 | } 286 | 287 | 288 | /** 289 | * Shorthand for `keys(o).length`. 290 | * 291 | * @param {Object|Array} collection 292 | * 293 | * @return {Number} 294 | */ 295 | export function size(collection) { 296 | return keys(collection).length; 297 | } 298 | 299 | 300 | /** 301 | * Get the values in the collection. 302 | * 303 | * @param {Object|Array} collection 304 | * 305 | * @return {Array} 306 | */ 307 | export function values(collection) { 308 | return map(collection, (val) => val); 309 | } 310 | 311 | 312 | /** 313 | * Group collection members by attribute. 314 | * 315 | * @param {Object|Array} collection 316 | * @param {Extractor} extractor 317 | * 318 | * @return {Object} map with { attrValue => [ a, b, c ] } 319 | */ 320 | export function groupBy(collection, extractor, grouped = {}) { 321 | 322 | extractor = toExtractor(extractor); 323 | 324 | forEach(collection, function(val) { 325 | let discriminator = extractor(val) || '_'; 326 | 327 | let group = grouped[discriminator]; 328 | 329 | if (!group) { 330 | group = grouped[discriminator] = []; 331 | } 332 | 333 | group.push(val); 334 | }); 335 | 336 | return grouped; 337 | } 338 | 339 | 340 | export function uniqueBy(extractor, ...collections) { 341 | 342 | extractor = toExtractor(extractor); 343 | 344 | let grouped = {}; 345 | 346 | forEach(collections, (c) => groupBy(c, extractor, grouped)); 347 | 348 | let result = map(grouped, function(val, key) { 349 | return val[0]; 350 | }); 351 | 352 | return result; 353 | } 354 | 355 | 356 | export const unionBy = uniqueBy; 357 | 358 | 359 | 360 | /** 361 | * Sort collection by criteria. 362 | * 363 | * @template T 364 | * 365 | * @param {Collection} collection 366 | * @param {Extractor} extractor 367 | * 368 | * @return {Array} 369 | */ 370 | export function sortBy(collection, extractor) { 371 | 372 | extractor = toExtractor(extractor); 373 | 374 | let sorted = []; 375 | 376 | forEach(collection, function(value, key) { 377 | let disc = extractor(value, key); 378 | 379 | let entry = { 380 | d: disc, 381 | v: value 382 | }; 383 | 384 | for (var idx = 0; idx < sorted.length; idx++) { 385 | let { d } = sorted[idx]; 386 | 387 | if (disc < d) { 388 | sorted.splice(idx, 0, entry); 389 | return; 390 | } 391 | } 392 | 393 | // not inserted, append (!) 394 | sorted.push(entry); 395 | }); 396 | 397 | return map(sorted, (e) => e.v); 398 | } 399 | 400 | 401 | /** 402 | * Create an object pattern matcher. 403 | * 404 | * @example 405 | * 406 | * ```javascript 407 | * const matcher = matchPattern({ id: 1 }); 408 | * 409 | * let element = find(elements, matcher); 410 | * ``` 411 | * 412 | * @template T 413 | * 414 | * @param {T} pattern 415 | * 416 | * @return { (el: any) => boolean } matcherFn 417 | */ 418 | export function matchPattern(pattern) { 419 | 420 | return function(el) { 421 | 422 | return every(pattern, function(val, key) { 423 | return el[key] === val; 424 | }); 425 | 426 | }; 427 | } 428 | 429 | 430 | /** 431 | * @param {string | ((e: any) => any) } extractor 432 | * 433 | * @return { (e: any) => any } 434 | */ 435 | function toExtractor(extractor) { 436 | 437 | /** 438 | * @satisfies { (e: any) => any } 439 | */ 440 | return isFunction(extractor) ? extractor : (e) => { 441 | 442 | // @ts-ignore: just works 443 | return e[extractor]; 444 | }; 445 | } 446 | 447 | 448 | /** 449 | * @template T 450 | * @param {Matcher} matcher 451 | * 452 | * @return {MatchFn} 453 | */ 454 | function toMatcher(matcher) { 455 | return isFunction(matcher) ? matcher : (e) => { 456 | return e === matcher; 457 | }; 458 | } 459 | 460 | 461 | function identity(arg) { 462 | return arg; 463 | } 464 | 465 | function toNum(arg) { 466 | return Number(arg); 467 | } 468 | -------------------------------------------------------------------------------- /lib/fn.d.ts: -------------------------------------------------------------------------------- 1 | export type DebouncedFunction = { 2 | (...args: any[]): any; 3 | flush: () => void; 4 | cancel: () => void; 5 | }; 6 | 7 | /** 8 | * Debounce fn, calling it only once if 9 | * the given time elapsed between calls. 10 | * 11 | * @param fn 12 | * @param timeout 13 | * 14 | * @return debounced function 15 | */ 16 | export function debounce(fn: Function, timeout: number): DebouncedFunction; 17 | 18 | /** 19 | * Throttle fn, calling at most once 20 | * in the given interval. 21 | * 22 | * @param fn 23 | * @param interval 24 | * 25 | * @return throttled function 26 | */ 27 | export function throttle(fn: Function, interval: number): (...args: any[]) => void; 28 | 29 | /** 30 | * Bind function against target . 31 | * 32 | * @param fn 33 | * @param target 34 | * 35 | * @return bound function 36 | */ 37 | export function bind(fn: T, target: object): T; -------------------------------------------------------------------------------- /lib/fn.js: -------------------------------------------------------------------------------- 1 | /* global setTimeout clearTimeout */ 2 | 3 | /** 4 | * @typedef { { 5 | * (...args: any[]): any; 6 | * flush: () => void; 7 | * cancel: () => void; 8 | * } } DebouncedFunction 9 | */ 10 | 11 | /** 12 | * Debounce fn, calling it only once if the given time 13 | * elapsed between calls. 14 | * 15 | * Lodash-style the function exposes methods to `#clear` 16 | * and `#flush` to control internal behavior. 17 | * 18 | * @param {Function} fn 19 | * @param {Number} timeout 20 | * 21 | * @return {DebouncedFunction} debounced function 22 | */ 23 | export function debounce(fn, timeout) { 24 | 25 | let timer; 26 | 27 | let lastArgs; 28 | let lastThis; 29 | 30 | let lastNow; 31 | 32 | function fire(force) { 33 | 34 | let now = Date.now(); 35 | 36 | let scheduledDiff = force ? 0 : (lastNow + timeout) - now; 37 | 38 | if (scheduledDiff > 0) { 39 | return schedule(scheduledDiff); 40 | } 41 | 42 | fn.apply(lastThis, lastArgs); 43 | 44 | clear(); 45 | } 46 | 47 | function schedule(timeout) { 48 | timer = setTimeout(fire, timeout); 49 | } 50 | 51 | function clear() { 52 | if (timer) { 53 | clearTimeout(timer); 54 | } 55 | 56 | timer = lastNow = lastArgs = lastThis = undefined; 57 | } 58 | 59 | function flush() { 60 | if (timer) { 61 | fire(true); 62 | } 63 | 64 | clear(); 65 | } 66 | 67 | /** 68 | * @type { DebouncedFunction } 69 | */ 70 | function callback(...args) { 71 | lastNow = Date.now(); 72 | 73 | lastArgs = args; 74 | lastThis = this; 75 | 76 | // ensure an execution is scheduled 77 | if (!timer) { 78 | schedule(timeout); 79 | } 80 | } 81 | 82 | callback.flush = flush; 83 | callback.cancel = clear; 84 | 85 | return callback; 86 | } 87 | 88 | /** 89 | * Throttle fn, calling at most once 90 | * in the given interval. 91 | * 92 | * @param {Function} fn 93 | * @param {Number} interval 94 | * 95 | * @return {Function} throttled function 96 | */ 97 | export function throttle(fn, interval) { 98 | let throttling = false; 99 | 100 | return function(...args) { 101 | 102 | if (throttling) { 103 | return; 104 | } 105 | 106 | fn(...args); 107 | throttling = true; 108 | 109 | setTimeout(() => { 110 | throttling = false; 111 | }, interval); 112 | }; 113 | } 114 | 115 | /** 116 | * Bind function against target . 117 | * 118 | * @param {Function} fn 119 | * @param {Object} target 120 | * 121 | * @return {Function} bound function 122 | */ 123 | export function bind(fn, target) { 124 | return fn.bind(target); 125 | } -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './array.js'; 2 | export * from './collection.js'; 3 | export * from './fn.js'; 4 | export * from './lang.js'; 5 | export * from './object.js'; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export * from './array.js'; 2 | export * from './collection.js'; 3 | export * from './fn.js'; 4 | export * from './lang.js'; 5 | export * from './object.js'; -------------------------------------------------------------------------------- /lib/lang.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Collection 3 | } from './collection.js'; 4 | 5 | export function isUndefined(obj: any): obj is null | undefined; 6 | export function isDefined(obj: any): obj is Exclude; 7 | export function isNil(obj: any): obj is null | undefined; 8 | export function isArray(obj: any): obj is Array; 9 | export function isObject(obj: any): obj is object; 10 | export function isNumber(obj: any): obj is number; 11 | export function isFunction(obj: any): obj is Function; 12 | export function isString(obj: any): obj is string; 13 | 14 | /** 15 | * Ensure collection is an array. 16 | * 17 | * @param obj 18 | */ 19 | export function ensureArray(obj: Collection): void | never; 20 | 21 | /** 22 | * Return true, if target owns a property with the given key. 23 | * 24 | * @param target 25 | * @param key 26 | * 27 | * @return 28 | */ 29 | export function has(target: any, key: string): boolean; 30 | -------------------------------------------------------------------------------- /lib/lang.js: -------------------------------------------------------------------------------- 1 | const nativeToString = Object.prototype.toString; 2 | const nativeHasOwnProperty = Object.prototype.hasOwnProperty; 3 | 4 | export function isUndefined(obj) { 5 | return obj === undefined; 6 | } 7 | 8 | export function isDefined(obj) { 9 | return obj !== undefined; 10 | } 11 | 12 | export function isNil(obj) { 13 | return obj == null; 14 | } 15 | 16 | export function isArray(obj) { 17 | return nativeToString.call(obj) === '[object Array]'; 18 | } 19 | 20 | export function isObject(obj) { 21 | return nativeToString.call(obj) === '[object Object]'; 22 | } 23 | 24 | export function isNumber(obj) { 25 | return nativeToString.call(obj) === '[object Number]'; 26 | } 27 | 28 | /** 29 | * @param {any} obj 30 | * 31 | * @return {boolean} 32 | */ 33 | export function isFunction(obj) { 34 | const tag = nativeToString.call(obj); 35 | 36 | return ( 37 | tag === '[object Function]' || 38 | tag === '[object AsyncFunction]' || 39 | tag === '[object GeneratorFunction]' || 40 | tag === '[object AsyncGeneratorFunction]' || 41 | tag === '[object Proxy]' 42 | ); 43 | } 44 | 45 | export function isString(obj) { 46 | return nativeToString.call(obj) === '[object String]'; 47 | } 48 | 49 | 50 | /** 51 | * Ensure collection is an array. 52 | * 53 | * @param {Object} obj 54 | */ 55 | export function ensureArray(obj) { 56 | 57 | if (isArray(obj)) { 58 | return; 59 | } 60 | 61 | throw new Error('must supply array'); 62 | } 63 | 64 | /** 65 | * Return true, if target owns a property with the given key. 66 | * 67 | * @param {Object} target 68 | * @param {String} key 69 | * 70 | * @return {Boolean} 71 | */ 72 | export function has(target, key) { 73 | return !isNil(target) && nativeHasOwnProperty.call(target, key); 74 | } -------------------------------------------------------------------------------- /lib/object.d.ts: -------------------------------------------------------------------------------- 1 | type PropertyName = string | number | symbol; 2 | 3 | /** 4 | * Copy the values of all of the enumerable own properties from one or more source objects to a 5 | * target object. Returns the target object. 6 | * 7 | * @param target The target object to copy to. 8 | * @param source The source object from which to copy properties. 9 | */ 10 | export function assign(target: T, source: U): T & U; 11 | 12 | /** 13 | * Copy the values of all of the enumerable own properties from one or more source objects to a 14 | * target object. Returns the target object. 15 | * 16 | * @param target The target object to copy to. 17 | * @param source1 The first source object from which to copy properties. 18 | * @param source2 The second source object from which to copy properties. 19 | */ 20 | export function assign(target: T, source1: U, source2: V): T & U & V; 21 | 22 | /** 23 | * Copy the values of all of the enumerable own properties from one or more source objects to a 24 | * target object. Returns the target object. 25 | * 26 | * @param target The target object to copy to. 27 | * @param source1 The first source object from which to copy properties. 28 | * @param source2 The second source object from which to copy properties. 29 | * @param source3 The third source object from which to copy properties. 30 | */ 31 | export function assign(target: T, source1: U, source2: V, source3: W): T & U & V & W; 32 | 33 | /** 34 | * Copy the values of all of the enumerable own properties from one or more source objects to a 35 | * target object. Returns the target object. 36 | * 37 | * @param target The target object to copy to. 38 | * @param sources One or more source objects from which to copy properties 39 | */ 40 | export function assign(target: T, ...sources: any[]): T; 41 | 42 | /** 43 | * Gets a nested property of a given object, with an optional default value. 44 | * 45 | * @param target The target of the get operation. 46 | * @param path The path to the nested value. 47 | * @param defaultValue The result to return if the property does not exist. 48 | * 49 | * @return any 50 | */ 51 | export function get(target: any, path: (string|number)[], defaultValue?: any): any; 52 | 53 | /** 54 | * Sets a nested property of a given object to the specified value. 55 | * 56 | * This mutates the object and returns it. 57 | * 58 | * @param target The target of the set operation. 59 | * @param path The path to the nested value. 60 | * @param value The value to set. 61 | * 62 | * @return the element 63 | */ 64 | export function set(target: T, path: PropertyName[], value: any): T; 65 | 66 | /** 67 | * Pick properties from the given target. 68 | * 69 | * @param target 70 | * @param properties 71 | * 72 | * @return 73 | */ 74 | export function pick(target: T, properties: Array): Pick; 75 | 76 | /** 77 | * Pick properties from the given target. 78 | * 79 | * @param target 80 | * @param properties 81 | * 82 | * @return 83 | */ 84 | export function pick(target: T, properties: V): Partial; 85 | 86 | /** 87 | * Pick all target properties, excluding the given ones. 88 | * 89 | * @param target 90 | * @param properties 91 | * 92 | * @return target 93 | */ 94 | export function omit(target: T, properties: V): Omit; 95 | 96 | /** 97 | * Pick all target properties, excluding the given ones. 98 | * 99 | * @param target 100 | * @param properties 101 | * 102 | * @return target 103 | */ 104 | export function omit(target: T, properties: V): Pick>; 105 | 106 | /** 107 | * Copy the values of all of the enumerable own properties from one or more source objects to a 108 | * target object. Returns the target object. 109 | * @param target The target object to copy to. 110 | * @param sources One or more source objects from which to copy properties 111 | */ 112 | export function merge(target: object, ...sources: any[]): any; -------------------------------------------------------------------------------- /lib/object.js: -------------------------------------------------------------------------------- 1 | import { forEach } from './collection.js'; 2 | 3 | import { 4 | isObject, 5 | isUndefined, 6 | isDefined, 7 | isNil 8 | } from './lang.js'; 9 | 10 | 11 | /** 12 | * Convenience wrapper for `Object.assign`. 13 | * 14 | * @param {Object} target 15 | * @param {...Object} others 16 | * 17 | * @return {Object} the target 18 | */ 19 | export function assign(target, ...others) { 20 | return Object.assign(target, ...others); 21 | } 22 | 23 | /** 24 | * Sets a nested property of a given object to the specified value. 25 | * 26 | * This mutates the object and returns it. 27 | * 28 | * @template T 29 | * 30 | * @param {T} target The target of the set operation. 31 | * @param {(string|number)[]} path The path to the nested value. 32 | * @param {any} value The value to set. 33 | * 34 | * @return {T} 35 | */ 36 | export function set(target, path, value) { 37 | 38 | let currentTarget = target; 39 | 40 | forEach(path, function(key, idx) { 41 | 42 | if (typeof key !== 'number' && typeof key !== 'string') { 43 | throw new Error('illegal key type: ' + typeof key + '. Key should be of type number or string.'); 44 | } 45 | 46 | if (key === 'constructor') { 47 | throw new Error('illegal key: constructor'); 48 | } 49 | 50 | if (key === '__proto__') { 51 | throw new Error('illegal key: __proto__'); 52 | } 53 | 54 | let nextKey = path[idx + 1]; 55 | let nextTarget = currentTarget[key]; 56 | 57 | if (isDefined(nextKey) && isNil(nextTarget)) { 58 | nextTarget = currentTarget[key] = isNaN(+nextKey) ? {} : []; 59 | } 60 | 61 | if (isUndefined(nextKey)) { 62 | if (isUndefined(value)) { 63 | delete currentTarget[key]; 64 | } else { 65 | currentTarget[key] = value; 66 | } 67 | } else { 68 | currentTarget = nextTarget; 69 | } 70 | }); 71 | 72 | return target; 73 | } 74 | 75 | 76 | /** 77 | * Gets a nested property of a given object. 78 | * 79 | * @param {Object} target The target of the get operation. 80 | * @param {(string|number)[]} path The path to the nested value. 81 | * @param {any} [defaultValue] The value to return if no value exists. 82 | * 83 | * @return {any} 84 | */ 85 | export function get(target, path, defaultValue) { 86 | 87 | let currentTarget = target; 88 | 89 | forEach(path, function(key) { 90 | 91 | // accessing nil property yields 92 | if (isNil(currentTarget)) { 93 | currentTarget = undefined; 94 | 95 | return false; 96 | } 97 | 98 | currentTarget = currentTarget[key]; 99 | }); 100 | 101 | return isUndefined(currentTarget) ? defaultValue : currentTarget; 102 | } 103 | 104 | /** 105 | * Pick properties from the given target. 106 | * 107 | * @template T 108 | * @template {any[]} V 109 | * 110 | * @param {T} target 111 | * @param {V} properties 112 | * 113 | * @return Pick 114 | */ 115 | export function pick(target, properties) { 116 | 117 | let result = {}; 118 | 119 | let obj = Object(target); 120 | 121 | forEach(properties, function(prop) { 122 | 123 | if (prop in obj) { 124 | result[prop] = target[prop]; 125 | } 126 | }); 127 | 128 | return result; 129 | } 130 | 131 | /** 132 | * Pick all target properties, excluding the given ones. 133 | * 134 | * @template T 135 | * @template {any[]} V 136 | * 137 | * @param {T} target 138 | * @param {V} properties 139 | * 140 | * @return {Omit} target 141 | */ 142 | export function omit(target, properties) { 143 | 144 | let result = {}; 145 | 146 | let obj = Object(target); 147 | 148 | forEach(obj, function(prop, key) { 149 | 150 | if (properties.indexOf(key) === -1) { 151 | result[key] = prop; 152 | } 153 | }); 154 | 155 | return result; 156 | } 157 | 158 | /** 159 | * Recursively merge `...sources` into given target. 160 | * 161 | * Does support merging objects; does not support merging arrays. 162 | * 163 | * @param {Object} target 164 | * @param {...Object} sources 165 | * 166 | * @return {Object} the target 167 | */ 168 | export function merge(target, ...sources) { 169 | 170 | if (!sources.length) { 171 | return target; 172 | } 173 | 174 | forEach(sources, function(source) { 175 | 176 | // skip non-obj sources, i.e. null 177 | if (!source || !isObject(source)) { 178 | return; 179 | } 180 | 181 | forEach(source, function(sourceVal, key) { 182 | 183 | if (key === '__proto__') { 184 | return; 185 | } 186 | 187 | let targetVal = target[key]; 188 | 189 | if (isObject(sourceVal)) { 190 | 191 | if (!isObject(targetVal)) { 192 | 193 | // override target[key] with object 194 | targetVal = {}; 195 | } 196 | 197 | target[key] = merge(targetVal, sourceVal); 198 | } else { 199 | target[key] = sourceVal; 200 | } 201 | 202 | }); 203 | }); 204 | 205 | return target; 206 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "min-dash", 3 | "version": "4.2.3", 4 | "description": "Minimum utility toolbelt", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.esm.js", 12 | "require": "./dist/index.cjs", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "all": "run-s lint test distro test:types", 22 | "bundle": "rollup -c --bundleConfigAsCjs", 23 | "copy": "cpx 'lib/*.d.ts' dist", 24 | "distro": "run-s copy bundle test:integration test:bundle", 25 | "dev": "npm test -- --watch", 26 | "lint": "eslint .", 27 | "prepublishOnly": "run-s distro", 28 | "test": "mocha -r source-map-support/register --full-trace test/*.spec.js", 29 | "test:bundle": "rollup -c test/bundling/rollup.config.js", 30 | "test:integration": "mocha --full-trace test/integration/*.spec.{cjs,js}", 31 | "test:types": "run-s test:types:*", 32 | "test:types:cjs": "tsc --noEmit", 33 | "test:types:esm": "tsc --noEmit --module node16" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/bpmn-io/min-dash.git" 38 | }, 39 | "keywords": [ 40 | "lodash", 41 | "utility", 42 | "tool", 43 | "belt" 44 | ], 45 | "author": "bpmn.io ", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/bpmn-io/min-dash/issues" 49 | }, 50 | "sideEffects": false, 51 | "homepage": "https://github.com/bpmn-io/min-dash", 52 | "devDependencies": { 53 | "@rollup/plugin-node-resolve": "^15.3.1", 54 | "@rollup/plugin-terser": "^0.4.4", 55 | "@types/mocha": "^10.0.10", 56 | "@types/node": "^20.11.5", 57 | "@types/sinon": "^17.0.3", 58 | "@types/sinon-chai": "^3.2.12", 59 | "chai": "^4.5.0", 60 | "cpx": "^1.5.0", 61 | "eslint": "^8.56.0", 62 | "eslint-plugin-bpmn-io": "^1.0.0", 63 | "mocha": "^10.8.2", 64 | "npm-run-all": "^4.1.1", 65 | "rollup": "^4.34.7", 66 | "sinon": "^17.0.1", 67 | "sinon-chai": "^3.7.0", 68 | "source-map-support": "^0.5.19", 69 | "ts-expect": "^1.3.0", 70 | "typescript": "^5.3.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | 3 | import pkg from './package.json'; 4 | 5 | function pgl(plugins = []) { 6 | return [ 7 | ...plugins 8 | ]; 9 | } 10 | 11 | const umdDist = 'dist/min-dash.js'; 12 | 13 | export default [ 14 | 15 | // browser-friendly UMD build 16 | { 17 | input: 'lib/index.js', 18 | output: { 19 | name: 'MinDash', 20 | file: umdDist, 21 | format: 'umd' 22 | }, 23 | plugins: pgl() 24 | }, 25 | { 26 | input: 'lib/index.js', 27 | output: { 28 | name: 'MinDash', 29 | file: umdDist.replace(/\.js$/, '.min.js'), 30 | format: 'umd' 31 | }, 32 | plugins: pgl([ 33 | terser() 34 | ]) 35 | }, 36 | { 37 | input: 'lib/index.js', 38 | output: [ 39 | { file: pkg.main, format: 'cjs' }, 40 | { file: pkg.module, format: 'es' } 41 | ], 42 | plugins: pgl() 43 | } 44 | ]; -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:bpmn-io/mocha" 3 | } -------------------------------------------------------------------------------- /test/array.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { 4 | flatten 5 | } from '../lib/array.js'; 6 | 7 | 8 | describe('array', function() { 9 | 10 | describe('flatten', function() { 11 | 12 | it('should handle null values', function() { 13 | 14 | // then 15 | expect(flatten(null)).to.eql([]); 16 | 17 | // @ts-ignore-error "missing arg" 18 | expect(flatten()).to.eql([]); 19 | }); 20 | 21 | 22 | it('should flatten, one level deep', function() { 23 | 24 | // given 25 | let arr = [ 26 | [ 'A', 1 ], 27 | [ 'B' ], 28 | [ 'C', [ 1, 2, 3 ] ], 29 | [ 'D' ] 30 | ]; 31 | 32 | // then 33 | expect(flatten(arr)).to.eql([ 34 | 'A', 1, 'B', 'C', [ 1, 2, 3 ], 'D' 35 | ]); 36 | }); 37 | 38 | }); 39 | 40 | }); -------------------------------------------------------------------------------- /test/bundling/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | has 3 | } from '../..'; 4 | 5 | export function foo(a, b) { 6 | return has(a, b); 7 | } -------------------------------------------------------------------------------- /test/bundling/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | 3 | export default [ 4 | { 5 | input: 'test/bundling/index.js', 6 | output: [ 7 | { file: 'test/bundling/bundled.js', format: 'es' } 8 | ], 9 | plugins: [ 10 | resolve() 11 | ] 12 | } 13 | ]; -------------------------------------------------------------------------------- /test/collection.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect 3 | } from 'chai'; 4 | 5 | import { 6 | find, 7 | findIndex, 8 | filter, 9 | forEach, 10 | without, 11 | reduce, 12 | every, 13 | some, 14 | map, 15 | values, 16 | keys, 17 | groupBy, 18 | uniqueBy, 19 | unionBy, 20 | size, 21 | sortBy, 22 | matchPattern 23 | } from '../lib/collection.js'; 24 | 25 | 26 | describe('collection', function() { 27 | 28 | describe('find', function() { 29 | 30 | it('should work on Array', function() { 31 | 32 | // given 33 | let arr = [ 'A', 'B', 'C' ]; 34 | 35 | // when 36 | let result = find(arr, (el) => el === 'B'); 37 | let resultByIndex = find(arr, (el, idx) => idx === 2); 38 | 39 | // then 40 | expect(result).to.eql('B'); 41 | expect(resultByIndex).to.eql('C'); 42 | }); 43 | 44 | 45 | it('should work on Object', function() { 46 | 47 | // given 48 | let obj = { 49 | foo: 'FOO', 50 | bar: 'BAR' 51 | }; 52 | 53 | // when 54 | let result = find(obj, (el) => el === 'BAR'); 55 | let resultByIndex = find(obj, (el, key) => key === 'foo'); 56 | 57 | // then 58 | expect(result).to.eql('BAR'); 59 | expect(resultByIndex).to.eql('FOO'); 60 | }); 61 | 62 | 63 | it('should be null-safe', function() { 64 | 65 | // when 66 | let result = find(null, (el) => el === 'BAR'); 67 | 68 | // then 69 | expect(result).not.to.exist; 70 | }); 71 | 72 | 73 | it('should strict equality check arg', function() { 74 | 75 | // given 76 | let arr = [ 0, '', null ]; 77 | 78 | // when 79 | let result = find(arr, 0); 80 | 81 | // then 82 | expect(result).to.equal(0); 83 | }); 84 | 85 | }); 86 | 87 | 88 | describe('findIndex', function() { 89 | 90 | it('should work on Array', function() { 91 | 92 | // given 93 | let arr = [ 'A', 'B', 'C' ]; 94 | 95 | // when 96 | let result = findIndex(arr, (el) => el === 'B'); 97 | let resultByIndex = findIndex(arr, (el, idx) => idx === 2); 98 | 99 | // then 100 | expect(result).to.equal(1); 101 | expect(resultByIndex).to.equal(2); 102 | }); 103 | 104 | 105 | it('should work on Object', function() { 106 | 107 | // given 108 | let obj = { 109 | foo: 'FOO', 110 | bar: 'BAR' 111 | }; 112 | 113 | // when 114 | let result = findIndex(obj, (el) => el === 'BAR'); 115 | let resultByIndex = findIndex(obj, (el, key) => key === 'foo'); 116 | 117 | // then 118 | expect(result).to.eql('bar'); 119 | expect(resultByIndex).to.eql('foo'); 120 | }); 121 | 122 | 123 | it('should be null-safe', function() { 124 | 125 | // when 126 | let result = findIndex(null, (el) => el === 'BAR'); 127 | 128 | // then 129 | expect(result).to.be.undefined; 130 | }); 131 | 132 | 133 | it('should strict equality check arg', function() { 134 | 135 | // given 136 | let obj = { 137 | a: 0, 138 | b: '', 139 | c: null 140 | }; 141 | 142 | // when 143 | let result = findIndex(obj, null); 144 | 145 | // then 146 | expect(result).to.equal('c'); 147 | }); 148 | 149 | }); 150 | 151 | 152 | describe('filter', function() { 153 | 154 | it('should work on Array', function() { 155 | 156 | // given 157 | let arr = [ 50, 200, 500 ]; 158 | 159 | // when 160 | let result = filter(arr, (el) => el > 100); 161 | let resultByIndex = filter(arr, (el, idx) => idx < 2); 162 | 163 | // then 164 | expect(result).to.eql([ 200, 500 ]); 165 | expect(resultByIndex).to.eql([ 50, 200 ]); 166 | }); 167 | 168 | 169 | it('should work on Object', function() { 170 | 171 | // given 172 | let obj = { 173 | a: 1, 174 | b: 2, 175 | c: 3 176 | }; 177 | 178 | // when 179 | let result = filter(obj, (el) => el > 1); 180 | let resultByIndex = filter(obj, (el, key) => key !== 'b'); 181 | 182 | // then 183 | expect(result).to.eql([ 2, 3 ]); 184 | expect(resultByIndex).to.eql([ 1, 3 ]); 185 | }); 186 | 187 | 188 | it('should be null-safe', function() { 189 | 190 | // when 191 | let result = filter(null, (a) => a); 192 | 193 | // then 194 | expect(result).to.eql([]); 195 | }); 196 | 197 | }); 198 | 199 | 200 | describe('forEach', function() { 201 | 202 | it('should work on Array', function() { 203 | 204 | // given 205 | let arr = [ {}, {}, {} ]; 206 | 207 | let called = 0; 208 | 209 | // when 210 | forEach(arr, function(el, idx) { 211 | 212 | called++; 213 | 214 | // then 215 | expect(arr[idx]).to.equal(el); 216 | }); 217 | 218 | expect(called).to.eql(3); 219 | }); 220 | 221 | 222 | it('should work on Object', function() { 223 | 224 | // given 225 | let obj = { 226 | a: {}, 227 | b: {}, 228 | c: {} 229 | }; 230 | 231 | let called = 0; 232 | 233 | // when 234 | forEach(obj, function(el, key) { 235 | 236 | called++; 237 | 238 | // then 239 | expect(obj[key]).to.equal(el); 240 | }); 241 | 242 | expect(called).to.eql(3); 243 | }); 244 | 245 | 246 | it('should break on returning ', function() { 247 | 248 | // given 249 | let arr = [ 1, 2, 3 ]; 250 | 251 | let called = 0; 252 | 253 | // when 254 | forEach(arr, function(el, idx) { 255 | 256 | called++; 257 | 258 | if (el === 2) { 259 | return false; 260 | } 261 | }); 262 | 263 | expect(called).to.eql(2); 264 | }); 265 | 266 | 267 | it('should be null-safe', function() { 268 | 269 | expect(function() { 270 | forEach(null, function() { }); 271 | }).not.to.throw; 272 | 273 | }); 274 | 275 | 276 | it('should return the result that stopped the iteration', function() { 277 | 278 | // given 279 | let arr = [ 1, 2, 3 ]; 280 | 281 | let result; 282 | 283 | // when 284 | result = forEach(arr, function(el) { 285 | 286 | if (el === 2) { 287 | return false; 288 | } 289 | }); 290 | 291 | expect(result).to.eql(2); 292 | 293 | }); 294 | 295 | }); 296 | 297 | 298 | describe('without', function() { 299 | 300 | it('should work on Array', function() { 301 | 302 | // given 303 | let obj = { }; 304 | let arr = [ 1, obj, false ]; 305 | 306 | // when 307 | let filtered = without(arr, obj); 308 | let filteredByMatcher = without(arr, (e) => e); 309 | let filteredByIndex = without(arr, (e, idx) => idx === 2); 310 | 311 | // then 312 | expect(filtered).to.eql([ 1, false ]); 313 | expect(filteredByMatcher).to.eql([ false ]); 314 | expect(filteredByIndex).to.eql([ 1, obj ]); 315 | }); 316 | 317 | 318 | it('should not work on Object', function() { 319 | 320 | expect(function() { 321 | 322 | // @ts-ignore: error case 323 | without({}, 1); 324 | }).to.throw; 325 | 326 | }); 327 | 328 | }); 329 | 330 | 331 | describe('reduce', function() { 332 | 333 | it('should work on Array', function() { 334 | 335 | // given 336 | let arr = [ 4, 4, 4 ]; 337 | 338 | // when 339 | let result = reduce(arr, (a, val) => a + val, 0); 340 | 341 | // then 342 | expect(result).to.eql(12); 343 | }); 344 | 345 | 346 | it('should work on Object', function() { 347 | 348 | // given 349 | let obj = { 350 | a: 1, 351 | b: 2, 352 | c: 3 353 | }; 354 | 355 | // when 356 | let result = reduce(obj, (a, val) => a + val, 0); 357 | 358 | // then 359 | expect(result).to.eql(6); 360 | }); 361 | 362 | 363 | it('should be null-safe', function() { 364 | 365 | expect(function() { 366 | let result = reduce(null, (a, val) => a + val, 0); 367 | 368 | expect(result).to.equal(0); 369 | }).not.to.throw; 370 | 371 | }); 372 | 373 | }); 374 | 375 | 376 | describe('every', function() { 377 | 378 | it('should work on Array', function() { 379 | 380 | // given 381 | let arr = [ 4, 4, 4 ]; 382 | 383 | // when 384 | let result = every(arr, (val) => val === 4); 385 | let resultByIndex = every(arr, (val, idx) => idx < 2); 386 | 387 | // then 388 | expect(result).to.be.true; 389 | expect(resultByIndex).to.be.false; 390 | }); 391 | 392 | 393 | it('should work on Object', function() { 394 | 395 | // given 396 | let obj = { 397 | a: 4, 398 | b: 4, 399 | c: 4 400 | }; 401 | 402 | // when 403 | let result = every(obj, (val) => val === 4); 404 | let resultByIndex = every(obj, (val, key) => key !== 'c'); 405 | 406 | // then 407 | expect(result).to.be.true; 408 | expect(resultByIndex).to.be.false; 409 | }); 410 | 411 | 412 | it('should be null-safe', function() { 413 | 414 | expect(every(null, () => false)).to.be.true; 415 | 416 | }); 417 | 418 | 419 | it('should always return boolean', function() { 420 | 421 | // given 422 | let collection = [ 1, true, 'word' ]; 423 | 424 | // when 425 | let result = every(collection, val => val); 426 | 427 | // then 428 | expect(result).to.be.true; 429 | }); 430 | 431 | }); 432 | 433 | 434 | describe('some', function() { 435 | 436 | it('should work on Array', function() { 437 | 438 | // given 439 | let arr = [ 1, 2, 3 ]; 440 | 441 | // when 442 | let resultTrue = some(arr, (val) => val === 3); 443 | let resultFalse = some(arr, (val) => val === false); 444 | 445 | let resultByIndex = some(arr, (val, idx) => idx === 4); 446 | 447 | // then 448 | expect(resultTrue).to.be.true; 449 | expect(resultFalse).to.be.false; 450 | 451 | expect(resultByIndex).to.be.false; 452 | }); 453 | 454 | 455 | it('should work on Object', function() { 456 | 457 | // given 458 | let obj = { 459 | a: 1, 460 | b: 2, 461 | c: 3 462 | }; 463 | 464 | // when 465 | let resultTrue = some(obj, (val) => val === 3); 466 | let resultFalse = some(obj, (val) => val === false); 467 | 468 | let resultByIndex = some(obj, (val, key) => key === 'blub'); 469 | 470 | // then 471 | expect(resultTrue).to.be.true; 472 | expect(resultFalse).to.be.false; 473 | 474 | expect(resultByIndex).to.be.false; 475 | }); 476 | 477 | 478 | it('should be null-safe', function() { 479 | 480 | expect(some(null, () => false)).to.be.false; 481 | 482 | }); 483 | 484 | }); 485 | 486 | 487 | describe('map', function() { 488 | 489 | it('should work on Array', function() { 490 | 491 | // given 492 | let arr = [ 1, 2, 3 ]; 493 | 494 | // when 495 | let result = map(arr, (val) => val + 3); 496 | 497 | // then 498 | expect(result).to.eql([ 4, 5, 6 ]); 499 | }); 500 | 501 | 502 | it('should work on Object', function() { 503 | 504 | // given 505 | let obj = { 506 | a: 1, 507 | b: 2, 508 | c: 3 509 | }; 510 | 511 | // when 512 | let result = map(obj, (val) => val + 3); 513 | 514 | // then 515 | expect(result).to.eql([ 4, 5, 6 ]); 516 | 517 | }); 518 | 519 | 520 | it('should be null-safe', function() { 521 | 522 | expect(map(undefined, () => false)).to.eql([]); 523 | 524 | }); 525 | 526 | }); 527 | 528 | 529 | describe('values', function() { 530 | 531 | it('should work on Array', function() { 532 | 533 | expect(values([ 1, 2, 3 ])).to.eql([ 1, 2, 3 ]); 534 | 535 | }); 536 | 537 | 538 | it('should work on Object', function() { 539 | 540 | expect(values({ a: 'A', b: 'B' })).to.eql([ 'A', 'B' ]); 541 | 542 | }); 543 | 544 | 545 | it('should be null-safe', function() { 546 | 547 | expect(values(undefined)).to.eql([]); 548 | 549 | }); 550 | 551 | }); 552 | 553 | 554 | describe('keys', function() { 555 | 556 | it('should work on Array', function() { 557 | 558 | expect(keys([ 1, 2, 3 ])).to.eql([ '0', '1', '2' ]); 559 | 560 | }); 561 | 562 | 563 | it('should work on Object', function() { 564 | 565 | expect(keys({ a: 'A', b: 'B' })).to.eql([ 'a', 'b' ]); 566 | 567 | }); 568 | 569 | 570 | it('should be null-safe', function() { 571 | 572 | expect(keys(undefined)).to.eql([]); 573 | 574 | }); 575 | 576 | }); 577 | 578 | 579 | describe('groupBy', function() { 580 | 581 | it('should work on Array', function() { 582 | 583 | // given 584 | let arr = [ 585 | { a: '1' }, 586 | { a: '2', b: '1' }, 587 | { a: '2', b: '2' }, 588 | { a: '3' } 589 | ]; 590 | 591 | // when 592 | let groupedByAttr = groupBy(arr, 'a'); 593 | let groupedByFn = groupBy(arr, (el) => el.b); 594 | 595 | // then 596 | expect(groupedByAttr).to.eql({ 597 | '1': [ { a: '1' } ], 598 | '2': [ { a: '2', b: '1' }, { a: '2', b: '2' } ], 599 | '3': [ { a: '3' } ] 600 | }); 601 | 602 | expect(groupedByFn).to.eql({ 603 | '_': [ { a: '1' }, { a: '3' } ], 604 | '1': [ { a: '2', b: '1' } ], 605 | '2': [ { a: '2', b: '2' } ] 606 | }); 607 | 608 | }); 609 | 610 | 611 | it('should work on Object'); 612 | 613 | 614 | it('should use supplied group', function() { 615 | 616 | // given 617 | let group = { '1': [ 2 ] }; 618 | 619 | let arr = [ 620 | { a: '1' } 621 | ]; 622 | 623 | // when 624 | let groupedByAttr = groupBy(arr, 'a', group); 625 | 626 | // then 627 | expect(groupedByAttr).to.eql({ 628 | '1': [ 2, { a: '1' } ] 629 | }); 630 | 631 | }); 632 | 633 | }); 634 | 635 | 636 | describe('uniqueBy', function() { 637 | 638 | it('should process by attribute', function() { 639 | 640 | // given 641 | let arr = [ 642 | { a: 1 }, 643 | { a: 2 } 644 | ]; 645 | 646 | let arr2 = [ 647 | { a: 1 }, 648 | { a: 3 } 649 | ]; 650 | 651 | let arr3 = [ 652 | { a: 2 } 653 | ]; 654 | 655 | // when 656 | let unique = uniqueBy('a', arr, arr2, arr3); 657 | 658 | // then 659 | expect(unique[0]).to.equal(arr[0]); 660 | expect(unique[1]).to.equal(arr[1]); 661 | expect(unique[2]).to.equal(arr2[1]); 662 | 663 | expect(unique).to.have.length(3); 664 | 665 | }); 666 | 667 | 668 | it('should process by discriminator fn'); 669 | 670 | }); 671 | 672 | 673 | describe('unionBy', function() { 674 | 675 | it('should === uniqueBy', function() { 676 | expect(uniqueBy).to.equal(unionBy); 677 | }); 678 | 679 | }); 680 | 681 | 682 | describe('sortBy', function() { 683 | 684 | 685 | it('should process by attribute', function() { 686 | 687 | // given 688 | let arr = [ 689 | { a: 1 }, 690 | { a: 2 }, 691 | { a: 1 }, 692 | { a: 3 }, 693 | { a: 2 } 694 | ]; 695 | 696 | // when 697 | let sorted = sortBy(arr, 'a'); 698 | 699 | // then 700 | expect(sorted[0]).to.equal(arr[0]); 701 | expect(sorted[1]).to.equal(arr[2]); 702 | expect(sorted[2]).to.equal(arr[1]); 703 | expect(sorted[3]).to.equal(arr[4]); 704 | expect(sorted[4]).to.equal(arr[3]); 705 | 706 | expect(sorted).to.have.length(5); 707 | }); 708 | 709 | 710 | it('should process by discriminator fn', function() { 711 | 712 | // given 713 | let arr = [ 714 | { a: 1 }, 715 | { a: 2 }, 716 | { a: 1 }, 717 | { a: 3 }, 718 | { a: 2 } 719 | ]; 720 | 721 | // when 722 | let sorted = sortBy(arr, (e) => e.a * -1); 723 | 724 | // then 725 | expect(sorted[0]).to.equal(arr[3]); 726 | expect(sorted[1]).to.equal(arr[1]); 727 | expect(sorted[2]).to.equal(arr[4]); 728 | expect(sorted[3]).to.equal(arr[0]); 729 | expect(sorted[4]).to.equal(arr[2]); 730 | 731 | expect(sorted).to.have.length(5); 732 | }); 733 | 734 | }); 735 | 736 | 737 | describe('matchPattern', function() { 738 | 739 | it('should strictly equal { key: value }', function() { 740 | 741 | // when 742 | let matcher = matchPattern({ a: 1 }); 743 | 744 | // then 745 | expect(matcher({ a: 1, b: 10 })).to.be.true; 746 | expect(matcher({ a: 3, b: 10 })).to.be.false; 747 | expect(matcher({ a: true, b: 10 })).to.be.false; 748 | }); 749 | 750 | }); 751 | 752 | 753 | describe('size', function() { 754 | 755 | it('should return # of keys for Array', function() { 756 | 757 | // given 758 | let arr = [ 1, 2, 3 ]; 759 | 760 | // then 761 | expect(size(arr)).to.eql(3); 762 | }); 763 | 764 | 765 | it('should return # of keys for Object', function() { 766 | 767 | // given 768 | let obj = { 769 | a: 1, 770 | b: true, 771 | c: undefined 772 | }; 773 | 774 | // then 775 | expect(size(obj)).to.eql(3); 776 | }); 777 | 778 | }); 779 | 780 | }); -------------------------------------------------------------------------------- /test/fn.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | use as chaiUse, 3 | expect 4 | } from 'chai'; 5 | 6 | import sinon from 'sinon'; 7 | import sinonChai from 'sinon-chai'; 8 | 9 | chaiUse(sinonChai); 10 | 11 | import { 12 | bind, 13 | debounce, 14 | throttle 15 | } from '../lib/fn.js'; 16 | 17 | 18 | describe('fn', function() { 19 | 20 | describe('bind', function() { 21 | 22 | it('should bind fn', function() { 23 | 24 | // given 25 | let fn = function() { 26 | 27 | // @ts-ignore-error "this" 28 | return this.foo; 29 | }; 30 | 31 | let target = { foo: 'FOO' }; 32 | 33 | // when 34 | let boundFn = bind(fn, target); 35 | 36 | let result = boundFn(); 37 | 38 | // then 39 | expect(result).to.eql('FOO'); 40 | }); 41 | 42 | }); 43 | 44 | 45 | describe('debounce', function() { 46 | 47 | let clock, clearTimeout; 48 | 49 | beforeEach(function() { 50 | clock = sinon.useFakeTimers(); 51 | }); 52 | 53 | afterEach(function() { 54 | clock.restore(); 55 | 56 | if (clearTimeout) { 57 | clearTimeout.restore(); 58 | } 59 | }); 60 | 61 | 62 | it('should debounce fn', function() { 63 | 64 | let callback = sinon.spy(); 65 | let debounced = debounce(callback, 100); 66 | 67 | // when 68 | debounced(); 69 | 70 | // then 71 | expect(callback).not.to.have.been.called; 72 | 73 | // ticked... 74 | clock.tick(99); 75 | 76 | // then 77 | expect(callback).not.to.have.been.called; 78 | 79 | // when 80 | debounced(); 81 | 82 | // then 83 | expect(callback).not.to.have.been.called; 84 | 85 | // debounce timer elapsed 86 | clock.tick(101); 87 | 88 | // then 89 | expect(callback).to.have.been.calledOnce; 90 | }); 91 | 92 | 93 | it('should pass last args', function() { 94 | 95 | let callback = sinon.spy(); 96 | let debounced = debounce(callback, 100); 97 | 98 | // when 99 | debounced(1); 100 | debounced('BAR', 3); 101 | 102 | // ticked... 103 | clock.tick(101); 104 | 105 | // then 106 | expect(callback).to.have.been.calledOnceWith('BAR', 3); 107 | }); 108 | 109 | 110 | it('should use last this', function() { 111 | 112 | let self = {}; 113 | 114 | let callback = sinon.spy(function() { 115 | 116 | // @ts-ignore-error "this" 117 | expect(this).to.equal(self); 118 | }); 119 | 120 | let debounced = debounce(callback, 100); 121 | 122 | // when 123 | debounced.apply({}); 124 | debounced.apply(self, [ 'BAR', 3 ]); 125 | 126 | // ticked... 127 | clock.tick(101); 128 | 129 | // then 130 | expect(callback).to.have.been.calledOnce; 131 | }); 132 | 133 | 134 | it('should not repetitively call #clearTimeout', function() { 135 | 136 | let callback = sinon.spy(); 137 | let debounced = debounce(callback, 100); 138 | 139 | clearTimeout = sinon.spy(global, 'clearTimeout'); 140 | 141 | // when 142 | debounced(); 143 | debounced(); 144 | 145 | // ticked... 146 | clock.tick(99); 147 | 148 | debounced(); 149 | 150 | // debounce timer elapsed 151 | clock.tick(101); 152 | 153 | // then 154 | expect(callback).to.have.been.calledOnce; 155 | expect(clearTimeout).to.have.been.calledOnce; 156 | }); 157 | 158 | 159 | it('should #cancel', function() { 160 | 161 | var callback = sinon.spy(); 162 | var debounced = debounce(callback, 100); 163 | 164 | // when 165 | debounced(); 166 | debounced.cancel(); 167 | 168 | // debounce timer elapsed 169 | clock.tick(101); 170 | 171 | // then 172 | expect(callback).not.to.have.been.called; 173 | }); 174 | 175 | 176 | it('should #flush', function() { 177 | 178 | var callback = sinon.spy(); 179 | var debounced = debounce(callback, 100); 180 | 181 | // when 182 | debounced(); 183 | debounced.flush(); 184 | 185 | // then 186 | expect(callback).to.have.been.calledOnce; 187 | 188 | // but when 189 | // debounce timer elapsed 190 | clock.tick(101); 191 | 192 | // then 193 | expect(callback).to.have.been.calledOnce; 194 | }); 195 | 196 | }); 197 | 198 | 199 | describe('throttle', function() { 200 | 201 | let clock; 202 | 203 | beforeEach(function() { 204 | clock = sinon.useFakeTimers(); 205 | }); 206 | 207 | afterEach(function() { 208 | clock.restore(); 209 | }); 210 | 211 | 212 | it('should throttle fn', function() { 213 | 214 | let callback = sinon.spy(); 215 | let throttled = throttle(callback, 100); 216 | 217 | // when 218 | throttled(); 219 | 220 | // then 221 | expect(callback).to.have.been.calledOnce; 222 | 223 | // ticked... 224 | clock.tick(99); 225 | 226 | throttled(); 227 | 228 | // then 229 | expect(callback).to.have.been.calledOnce; 230 | 231 | // throttle interval elapsed 232 | clock.tick(101); 233 | 234 | // when 235 | throttled(); 236 | 237 | // then 238 | expect(callback).to.have.been.calledTwice; 239 | }); 240 | 241 | }); 242 | 243 | }); -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'ts-expect'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { 6 | flatten 7 | } from '../lib/index.js'; 8 | 9 | 10 | describe('min-dash', function() { 11 | 12 | describe('should work', function() { 13 | 14 | it('flatten', function() { 15 | 16 | // then 17 | expectType(flatten([ [ 'A', 'B', 'C' ], 'B' ])); 18 | expectType(flatten([ 'A', 'B', 'C', 'B' ])); 19 | expectType(flatten([ [ 'A' ], [ 'B' ], [ 'C', 'B'] ])); 20 | 21 | expectType<(string|number|number[])[]>(flatten([ 22 | [ 'A', 1 ], 23 | [ 'B' ], 24 | [ 'C', [ 1, 2, 3 ] ], 25 | [ 'D' ] 26 | ])); 27 | 28 | expectType(flatten([ null ])); 29 | expectType(flatten(null)); 30 | 31 | // when 32 | expect(flatten([ [ 'A', 'B', 'C' ], 'B' ])).to.eql([ 'A', 'B', 'C' ]); 33 | }); 34 | 35 | }); 36 | 37 | }); -------------------------------------------------------------------------------- /test/integration/bundle.spec.cjs: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | 4 | describe('integration', function() { 5 | 6 | describe('bundle', function() { 7 | 8 | // when 9 | const md = require('../..'); 10 | 11 | it('should expose array utils', function() { 12 | 13 | // then 14 | expect(md.flatten).to.exist; 15 | 16 | }); 17 | 18 | 19 | it('should expose collection utils', function() { 20 | 21 | // then 22 | expect(md.find).to.exist; 23 | 24 | }); 25 | 26 | 27 | it('should expose fn utils', function() { 28 | 29 | // then 30 | expect(md.bind).to.exist; 31 | 32 | }); 33 | 34 | 35 | it('should expose lang utils', function() { 36 | 37 | // then 38 | expect(md.isArray).to.exist; 39 | 40 | }); 41 | 42 | 43 | it('should expose object utils', function() { 44 | 45 | // then 46 | expect(md.pick).to.exist; 47 | 48 | }); 49 | 50 | }); 51 | 52 | }); -------------------------------------------------------------------------------- /test/integration/bundle.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect 3 | } from 'chai'; 4 | 5 | import { 6 | flatten, 7 | find, 8 | bind, 9 | isArray, 10 | pick 11 | } from '../../dist/index.esm.js'; 12 | 13 | 14 | describe('integration', function() { 15 | 16 | describe('bundle', function() { 17 | 18 | it('should expose array utils', function() { 19 | 20 | // then 21 | expect(flatten).to.exist; 22 | 23 | }); 24 | 25 | 26 | it('should expose collection utils', function() { 27 | 28 | // then 29 | expect(find).to.exist; 30 | 31 | }); 32 | 33 | 34 | it('should expose fn utils', function() { 35 | 36 | // then 37 | expect(bind).to.exist; 38 | 39 | }); 40 | 41 | 42 | it('should expose lang utils', function() { 43 | 44 | // then 45 | expect(isArray).to.exist; 46 | 47 | }); 48 | 49 | 50 | it('should expose object utils', function() { 51 | 52 | // then 53 | expect(pick).to.exist; 54 | 55 | }); 56 | 57 | }); 58 | 59 | }); -------------------------------------------------------------------------------- /test/lang.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect 3 | } from 'chai'; 4 | 5 | import { 6 | has, 7 | isDefined, 8 | isFunction, 9 | isUndefined, 10 | isNil 11 | } from '../lib/lang.js'; 12 | 13 | 14 | describe('lang', function() { 15 | 16 | describe('has', function() { 17 | 18 | it('should work for {}', function() { 19 | 20 | // given 21 | let obj = { 22 | a: 1, 23 | e: undefined 24 | }; 25 | 26 | Object.defineProperty(obj, 'b', { value: 1 }); 27 | 28 | // then 29 | expect(has(obj, 'a')).to.be.true; 30 | expect(has(obj, 'b')).to.be.true; 31 | expect(has(obj, 'e')).to.be.true; 32 | 33 | expect(has(obj, 'c')).to.be.false; 34 | }); 35 | 36 | 37 | it('should work for []', function() { 38 | 39 | // given 40 | let arr = [ 1, 2, 3 ]; 41 | 42 | // then 43 | expect(has(arr, '1')).to.be.true; 44 | expect(has(arr, '5')).to.be.false; 45 | }); 46 | 47 | 48 | it('should handle invalid input', function() { 49 | 50 | expect(has(null, '1')).to.be.false; 51 | 52 | expect(has(undefined, '1')).to.be.false; 53 | 54 | expect(has(0, '1')).to.be.false; 55 | 56 | expect(has('', 'length')).to.be.true; 57 | }); 58 | 59 | }); 60 | 61 | 62 | describe('isDefined', function() { 63 | 64 | it('should work', function() { 65 | 66 | // then 67 | expect(isDefined(1)).to.be.true; 68 | expect(isDefined(0)).to.be.true; 69 | expect(isDefined('')).to.be.true; 70 | expect(isDefined({})).to.be.true; 71 | 72 | expect(isDefined(null)).to.be.true; 73 | 74 | // @ts-ignore-error "missing arg" 75 | expect(isDefined()).to.be.false; 76 | expect(isDefined(undefined)).to.be.false; 77 | expect(isDefined(void 0)).to.be.false; 78 | }); 79 | 80 | }); 81 | 82 | 83 | describe('isUndefined', function() { 84 | 85 | it('should work', function() { 86 | 87 | // then 88 | expect(isUndefined(1)).to.be.false; 89 | expect(isUndefined(0)).to.be.false; 90 | expect(isUndefined('')).to.be.false; 91 | expect(isUndefined({})).to.be.false; 92 | 93 | expect(isUndefined(null)).to.be.false; 94 | 95 | // @ts-ignore-error "missing arg" 96 | expect(isUndefined()).to.be.true; 97 | expect(isUndefined(undefined)).to.be.true; 98 | expect(isUndefined(void 0)).to.be.true; 99 | }); 100 | 101 | }); 102 | 103 | 104 | describe('isNil', function() { 105 | 106 | it('should work', function() { 107 | 108 | // then 109 | expect(isNil(1)).to.be.false; 110 | expect(isNil(0)).to.be.false; 111 | expect(isNil('')).to.be.false; 112 | expect(isNil({})).to.be.false; 113 | 114 | expect(isNil(null)).to.be.true; 115 | 116 | // @ts-ignore-error "missing arg" 117 | expect(isNil()).to.be.true; 118 | expect(isNil(undefined)).to.be.true; 119 | expect(isNil(void 0)).to.be.true; 120 | }); 121 | 122 | }); 123 | 124 | 125 | describe('isFunction', function() { 126 | 127 | it('should work', function() { 128 | 129 | // then 130 | expect(isFunction(function() {})).to.be.true; 131 | expect(isFunction(async function() {})).to.be.true; 132 | 133 | expect(isFunction(() => {})).to.be.true; 134 | expect(isFunction(async () => {})).to.be.true; 135 | 136 | expect(isFunction(function* generator() {})).to.be.true; 137 | expect(isFunction(async function* asyncGenerator() {})).to.be.true; 138 | 139 | expect(isFunction({})).to.be.false; 140 | expect(isFunction(undefined)).to.be.false; 141 | }); 142 | 143 | }); 144 | 145 | }); -------------------------------------------------------------------------------- /test/object.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect 3 | } from 'chai'; 4 | 5 | import { 6 | pick, 7 | assign, 8 | merge, 9 | omit, 10 | set, 11 | get 12 | } from '../lib/object.js'; 13 | 14 | 15 | describe('object', function() { 16 | 17 | describe('pick', function() { 18 | 19 | it('should take selected attributes', function() { 20 | 21 | // given 22 | let obj = { 23 | a: 1, 24 | b: false, 25 | c: null, 26 | e: undefined 27 | }; 28 | 29 | // when 30 | let picked = pick(obj, [ 'a', 'c', 'd', 'e' ]); 31 | 32 | // then 33 | expect(picked).to.eql({ 34 | a: 1, 35 | c: null, 36 | e: undefined 37 | }); 38 | 39 | // and when 40 | let otherPicked = pick(obj, [ 'a', 'b' ]); 41 | 42 | // then 43 | expect(otherPicked.a).to.eql(1); 44 | }); 45 | 46 | 47 | it('should handle computed and non-enumerable properties', function() { 48 | 49 | // given 50 | let obj = {}; 51 | 52 | Object.defineProperty(obj, 'a', { value: 1 }); 53 | Object.defineProperty(obj, 'b', { get: () => false }); 54 | Object.defineProperty(obj, 'c', { get: () => null }); 55 | Object.defineProperty(obj, 'e', { value: undefined }); 56 | 57 | // when 58 | let picked = pick(obj, [ 'a', 'c', 'd', 'e' ]); 59 | 60 | // then 61 | expect(picked).to.eql({ 62 | a: 1, 63 | c: null, 64 | e: undefined 65 | }); 66 | 67 | }); 68 | 69 | 70 | it('should pick inherited properties', function() { 71 | 72 | // given 73 | let proto = { a: 1 }; 74 | let obj = Object.create(proto); 75 | 76 | 77 | // when 78 | let picked = pick(obj, [ 'a' ]); 79 | 80 | // then 81 | expect(picked).to.eql({ 82 | a: 1 83 | }); 84 | 85 | }); 86 | 87 | }); 88 | 89 | 90 | describe('omit', function() { 91 | 92 | it('should omit selected attributes', function() { 93 | 94 | // given 95 | let obj = { 96 | a: 1, 97 | b: false, 98 | c: null, 99 | e: undefined 100 | }; 101 | 102 | // when 103 | let omitted = omit(obj, [ 'a', 'd', 'e' ]); 104 | 105 | // then 106 | expect(omitted).to.eql({ 107 | b: false, 108 | c: null 109 | }); 110 | 111 | }); 112 | 113 | 114 | it('should ignore non-enumerable properties', function() { 115 | 116 | // given 117 | let obj = {}; 118 | 119 | Object.defineProperty(obj, 'b', { enumerable: true, get: () => false }); 120 | Object.defineProperty(obj, 'c', { get: () => null }); 121 | 122 | // when 123 | let omited = omit(obj, [ 'a', 'd', 'e' ]); 124 | 125 | // then 126 | expect(omited).to.eql({ 127 | b: false 128 | }); 129 | 130 | }); 131 | 132 | }); 133 | 134 | 135 | describe('assign', function() { 136 | 137 | it('should merge objects', function() { 138 | 139 | // given 140 | let obj1 = { 141 | a: 1, 142 | b: false, 143 | c: null 144 | }; 145 | 146 | let obj2 = { 147 | a: false, 148 | d: undefined 149 | }; 150 | 151 | // when 152 | let result = assign({}, obj1, obj2, null); 153 | 154 | // then 155 | expect(result).to.eql({ 156 | a: false, 157 | b: false, 158 | c: null, 159 | d: undefined 160 | }); 161 | 162 | }); 163 | 164 | 165 | it('should handle null objects', function() { 166 | 167 | // when 168 | const result = assign({ bar: 'Bar' }, null, undefined, false, 0, { foo: 'Foo' }); 169 | 170 | // then 171 | expect(result).to.exist; 172 | }); 173 | 174 | 175 | it('should not override prototype', function() { 176 | 177 | function Bar() {} 178 | function Foo() {} 179 | 180 | // given 181 | let obj1 = new Foo(); 182 | 183 | let obj2 = new Bar(); 184 | 185 | // when 186 | let result = assign(obj1, obj2); 187 | 188 | // then 189 | expect(result).to.eql(obj1); 190 | 191 | expect(result.__proto__).to.eql(obj1.__proto__); 192 | }); 193 | 194 | 195 | it('should not allow prototype pollution', function() { 196 | 197 | // given 198 | let target = { merge: { me: 'nested' } }; 199 | let source = JSON.parse('{ "__proto__": { "alert": 1 } }'); 200 | 201 | // when 202 | assign(target, source); 203 | 204 | // then 205 | expect({}.alert).to.be.undefined; 206 | }); 207 | 208 | }); 209 | 210 | 211 | describe('merge', function() { 212 | 213 | it('should merge recursively', function() { 214 | 215 | // given 216 | let obj = { 217 | a: { 218 | a: 'A', 219 | c: { 220 | d: [ 0, 1, 2 ] 221 | } 222 | }, 223 | b: false 224 | }; 225 | 226 | let other = { 227 | a: { 228 | c: { 229 | e: 'E', 230 | 231 | // overrides obj.a.c.d 232 | d: [ 5, 6, 7 ] 233 | } 234 | }, 235 | 236 | // overridden by other2 237 | b: 'foo' 238 | }; 239 | 240 | let other2 = { 241 | a: { 242 | a: 'A2' 243 | }, 244 | b: { 245 | c: undefined 246 | } 247 | }; 248 | 249 | // when 250 | let result = merge(obj, other, null, other2); 251 | 252 | // then 253 | expect(result).to.equal(obj); 254 | 255 | expect(result).to.eql({ 256 | a: { 257 | a: 'A2', 258 | c: { 259 | e: 'E', 260 | d: [ 5, 6, 7 ] 261 | } 262 | }, 263 | b: { 264 | c: undefined 265 | } 266 | }); 267 | 268 | }); 269 | 270 | 271 | it('should not override prototype', function() { 272 | 273 | function Bar() {} 274 | function Foo() {} 275 | 276 | // given 277 | let obj1 = new Foo(); 278 | 279 | let obj2 = new Bar(); 280 | 281 | // when 282 | let result = merge(obj1, obj2); 283 | 284 | // then 285 | expect(result).to.eql(obj1); 286 | 287 | expect(result.__proto__).to.eql(obj1.__proto__); 288 | }); 289 | 290 | 291 | it('should not allow prototype pollution', function() { 292 | 293 | // given 294 | let target = { merge: { me: 'nested' } }; 295 | let source = JSON.parse('{ "__proto__": { "alert": 1 } }'); 296 | 297 | // when 298 | merge(target, source); 299 | 300 | // then 301 | expect({}.alert).to.be.undefined; 302 | }); 303 | 304 | }); 305 | 306 | 307 | describe('set', function() { 308 | 309 | it('should return modified object', function() { 310 | 311 | // given 312 | let x = {}; 313 | 314 | // when 315 | let modified = set(x, [ 'a' ], true); 316 | 317 | // then 318 | expect(modified).to.equal(x); 319 | }); 320 | 321 | 322 | it('should set property value', function() { 323 | expect(set({}, [ 'a' ], true)).to.eql({ 324 | a: true 325 | }); 326 | 327 | expect(set({}, [ '' ], 'A')).to.eql({ 328 | '': 'A' 329 | }); 330 | 331 | expect(set({}, [ 0 ], 'A')).to.eql({ 332 | '0': 'A' 333 | }); 334 | }); 335 | 336 | 337 | it('should set array value', function() { 338 | 339 | expect(set([ 0, 1, 2 ], [ 1 ], 'A')).to.eql([ 0, 'A', 2 ]); 340 | 341 | expect(set([ 0, 1, 2 ], [ 1 ], 0)).to.eql([ 0, 0, 2 ]); 342 | 343 | expect(set({ 344 | a: [ 0, 0 ] 345 | }, [ 'a', 1 ], 1)).to.eql({ 346 | a: [ 0, 1 ] 347 | }); 348 | 349 | expect( 350 | set({ 351 | a: [ 352 | { b: 'FOO' } 353 | ] 354 | }, [ 'a', 0, 'b' ], 'BAR') 355 | ).to.eql({ 356 | a: [ 357 | { b: 'BAR' } 358 | ] 359 | }); 360 | }); 361 | 362 | 363 | it('should set array with string keys', function() { 364 | 365 | expect(set([ 0, 1, 2 ], [ '1' ], 'A')).to.eql([ 0, 'A', 2 ]); 366 | 367 | expect(set({ 368 | a: [ 0, 0 ] 369 | }, [ 'a', '1' ], 1)).to.eql({ 370 | a: [ 0, 1 ] 371 | }); 372 | }); 373 | 374 | 375 | it('should delete value', function() { 376 | expect(set({ 377 | a: false 378 | }, [ 'a' ], undefined)).to.eql({}); 379 | 380 | expect(set({ 381 | '': false 382 | }, [ '' ], undefined)).to.eql({}); 383 | }); 384 | 385 | 386 | it('should set nested value', function() { 387 | 388 | expect(set({ 389 | a: { 390 | b: { } 391 | } 392 | }, [ 'a', 'b' ], false)).to.eql({ 393 | a: { 394 | b: false 395 | } 396 | }); 397 | 398 | expect(set({ 399 | a: { 400 | b: { } 401 | } 402 | }, [ 'a', 'b', 'c' ], 'C')).to.eql({ 403 | a: { 404 | b: { 405 | c: 'C' 406 | } 407 | } 408 | }); 409 | }); 410 | 411 | 412 | it('should scaffold object hierarchy', function() { 413 | 414 | expect(set({}, [ 'a', 'b', 'c' ], 'C')).to.eql({ 415 | a: { 416 | b: { 417 | c: 'C' 418 | } 419 | } 420 | }); 421 | 422 | expect( 423 | set({ a: null }, [ 'a', 'b', 'c' ], 'C') 424 | ).to.eql({ 425 | a: { 426 | b: { 427 | c: 'C' 428 | } 429 | } 430 | }); 431 | }); 432 | 433 | 434 | it('should scaffold array hierarchy', function() { 435 | 436 | expect(set({}, [ 'a', 1, 2 ], 'C')).to.eql({ 437 | a: [ 438 | undefined, 439 | [ undefined, undefined, 'C' ] 440 | ] 441 | }); 442 | 443 | expect(set({}, [ 'a', '1', '2' ], 'C')).to.eql({ 444 | a: [ 445 | undefined, 446 | [ undefined, undefined, 'C' ] 447 | ] 448 | }); 449 | }); 450 | 451 | 452 | it('should not allow prototype polution', function() { 453 | expect(function() { 454 | set({}, [ '__proto__' ], { foo: 'bar' }); 455 | }).to.throw(/illegal key/); 456 | }); 457 | 458 | 459 | it('should not allow prototype polution via constructor', function() { 460 | expect(function() { 461 | set({}, [ 'constructor', 'prototype', 'polluted' ], 'success'); 462 | }).to.throw(/illegal key/); 463 | }); 464 | 465 | 466 | it('should not allow array as key', function() { 467 | expect(function() { 468 | 469 | // @ts-ignore: illegal call 470 | set({}, [ [ '__proto__' ], 'polluted' ], 'success'); 471 | }).to.throw(/illegal key type/); 472 | }); 473 | 474 | 475 | it('should not allow object as key', function() { 476 | expect(function() { 477 | 478 | // @ts-ignore: illegal call 479 | set({}, [ {}, 'polluted' ], 'success'); 480 | }).to.throw(/illegal key type/); 481 | }); 482 | 483 | }); 484 | 485 | 486 | describe('get', function() { 487 | 488 | it('should return object property', function() { 489 | expect(get({}, [ 'a' ])).to.equal(undefined); 490 | expect(get({}, [ 'a' ], 'FOO')).to.equal('FOO'); 491 | 492 | expect(get({ a: 0 }, [ 'a' ])).to.equal(0); 493 | expect(get({ a: 0 }, [ 'a' ], 1)).to.equal(0); 494 | 495 | expect(get({ a: { b: 0 } }, [ 'a', 'b' ])).to.equal(0); 496 | expect(get({ a: { } }, [ 'a', 'b' ], 1)).to.equal(1); 497 | 498 | expect(get(null, [ 'a' ])).to.equal(undefined); 499 | expect(get(null, [ 'a' ], 1)).to.equal(1); 500 | 501 | expect(get({ a: null }, [ 'a' ])).to.equal(null); 502 | expect(get({ a: null }, [ 'a', 'b' ])).to.equal(undefined); 503 | expect(get({ a: null }, [ 'a', 'b' ], 1)).to.equal(1); 504 | }); 505 | 506 | 507 | it('should return array property', function() { 508 | expect(get([], [ 0 ])).to.equal(undefined); 509 | expect(get([], [ '0' ])).to.equal(undefined); 510 | expect(get([], [ 0 ], 'FOO')).to.equal('FOO'); 511 | 512 | expect(get([ [ 0, 1, 2 ] ], [ 0, 1 ])).to.equal(1); 513 | expect(get([ [ 0, 1, 2 ] ], [ 0, 3 ])).to.equal(undefined); 514 | expect(get([ [ 0, 1, 2 ] ], [ 0, 3 ], 'FOO')).to.equal('FOO'); 515 | 516 | expect(get(null, [ 0 ])).to.equal(undefined); 517 | expect(get([ null ], [ 0 ])).to.equal(null); 518 | expect(get([ null ], [ 0, 3 ])).to.equal(undefined); 519 | expect(get([ null ], [ 0, 3 ], 1)).to.equal(1); 520 | }); 521 | 522 | }); 523 | 524 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "test/*.js", 4 | "test/*.ts" 5 | ], 6 | "compilerOptions": { 7 | "strict": true, 8 | "checkJs": true, 9 | "noImplicitAny": false, 10 | "esModuleInterop": true, 11 | "lib": [ 12 | "ES2019" 13 | ] 14 | } 15 | } --------------------------------------------------------------------------------