├── .github └── workflows │ ├── release.yml │ └── test-on-push.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── chaining │ ├── chain.ts │ ├── cmp.ts │ ├── else-chain.ts │ └── then-chain.ts ├── comparators │ ├── asc.comparator.ts │ ├── desc.comparator.ts │ └── index.ts ├── functional │ └── queue.mutation.ts ├── index.ts ├── interfaces.ts └── mutations │ ├── condition.mutation.ts │ ├── index.ts │ ├── map.mutation.ts │ └── reverse.mutation.ts ├── test ├── chaining │ └── chain.spec.ts ├── comparators │ └── asc.comparator.spec.ts ├── functional │ └── queue.mutation.spec.ts ├── mutations │ ├── condition.mutation.spec.ts │ ├── map.mutation.spec.ts │ └── reverse.mutation.spec.ts └── temp.spec.ts ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | test: 8 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | - macos-latest 15 | - windows-latest 16 | node: [ '8', '10', '12', '14' ] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v2.1.4 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: npm install 23 | - run: npm test 24 | 25 | release: 26 | name: Create Release 27 | runs-on: ubuntu-latest 28 | needs: [test] 29 | steps: 30 | - uses: actions/create-release@v1 31 | name: Create Release 32 | id: create_release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | tag_name: ${{ github.ref }} 37 | release_name: Release ${{ github.ref }} 38 | body: To be defined 39 | draft: false 40 | prerelease: false 41 | 42 | publish-npm: 43 | name: Publish on NPM 44 | runs-on: ubuntu-latest 45 | needs: [release] 46 | steps: 47 | - uses: actions/checkout@v2 48 | - uses: actions/setup-node@v2.1.4 49 | with: 50 | node-version: '14' 51 | registry-url: 'https://registry.npmjs.org' 52 | - run: npm install 53 | - run: npm publish 54 | name: Publish to NPM 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | -------------------------------------------------------------------------------- /.github/workflows/test-on-push.yml: -------------------------------------------------------------------------------- 1 | name: Test on push 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | jobs: 7 | test: 8 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | - macos-latest 15 | - windows-latest 16 | node: [ '8', '10', '12', '14' ] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v2.1.4 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: npm install 23 | - run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | # https://github.com/Microsoft/TypeScript/blob/master/.gitignore 4 | node_modules/ 5 | .node_modules/ 6 | built/* 7 | tests/cases/rwc/* 8 | tests/cases/test262/* 9 | tests/cases/perf/* 10 | !tests/cases/webharness/compilerToString.js 11 | test-args.txt 12 | ~*.docx 13 | \#*\# 14 | .\#* 15 | tests/baselines/local/* 16 | tests/baselines/local.old/* 17 | tests/services/baselines/local/* 18 | tests/baselines/prototyping/local/* 19 | tests/baselines/rwc/* 20 | tests/baselines/test262/* 21 | tests/baselines/reference/projectOutput/* 22 | tests/baselines/local/projectOutput/* 23 | tests/baselines/reference/testresults.tap 24 | tests/services/baselines/prototyping/local/* 25 | tests/services/browser/typescriptServices.js 26 | src/harness/*.js 27 | src/compiler/diagnosticInformationMap.generated.ts 28 | src/compiler/diagnosticMessages.generated.json 29 | src/parser/diagnosticInformationMap.generated.ts 30 | src/parser/diagnosticMessages.generated.json 31 | rwc-report.html 32 | *.swp 33 | build.json 34 | *.actual 35 | tests/webTestServer.js 36 | tests/webTestServer.js.map 37 | tests/webhost/*.d.ts 38 | tests/webhost/webtsc.js 39 | tests/cases/**/*.js 40 | tests/cases/**/*.js.map 41 | *.config 42 | scripts/debug.bat 43 | scripts/run.bat 44 | scripts/word2md.js 45 | scripts/buildProtocol.js 46 | scripts/ior.js 47 | scripts/authors.js 48 | scripts/configurePrerelease.js 49 | scripts/open-user-pr.js 50 | scripts/processDiagnosticMessages.d.ts 51 | scripts/processDiagnosticMessages.js 52 | scripts/produceLKG.js 53 | scripts/importDefinitelyTypedTests/importDefinitelyTypedTests.js 54 | scripts/generateLocalizedDiagnosticMessages.js 55 | scripts/*.js.map 56 | scripts/typings/ 57 | coverage/ 58 | internal/ 59 | **/.DS_Store 60 | .settings 61 | **/.vs 62 | **/.vscode 63 | !**/.vscode/tasks.json 64 | !tests/cases/projects/projectOption/**/node_modules 65 | !tests/cases/projects/NodeModulesSearch/**/* 66 | !tests/baselines/reference/project/nodeModules*/**/* 67 | .idea 68 | yarn.lock 69 | yarn-error.log 70 | .parallelperf.* 71 | tests/cases/user/*/package-lock.json 72 | tests/cases/user/*/node_modules/ 73 | tests/cases/user/*/**/*.js 74 | tests/cases/user/*/**/*.js.map 75 | tests/cases/user/*/**/*.d.ts 76 | !tests/cases/user/zone.js/ 77 | !tests/cases/user/bignumber.js/ 78 | !tests/cases/user/discord.js/ 79 | tests/baselines/reference/dt 80 | .failed-tests 81 | TEST-results.xml 82 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | 5 | scripts: 6 | - npm install codecov -g 7 | after_success: 8 | - jest --coverage 9 | - codecov 10 | 11 | cache: 12 | directories: 13 | - node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### Unreleased 8 | 9 | - Update readme. Fix npm publish. Add travis CI [`#3`](https://github.com/lightness/type-comparator/pull/3) 10 | - v0.1.2 [`#2`](https://github.com/lightness/type-comparator/pull/2) 11 | - Totally refactor [`c3f890b`](https://github.com/lightness/type-comparator/commit/c3f890b5e7b4abffd55942e62d1e9cc27a6e512e) 12 | - Initial commit [`855e129`](https://github.com/lightness/type-comparator/commit/855e12996f2b89e0966896ab06c5eae0c4f86943) 13 | - Rename `transform` to `map` [`f259ae5`](https://github.com/lightness/type-comparator/commit/f259ae57b0a86f1ff5e758faab6a774ee1599bde) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Uladzimir Aleshka 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Logo 3 | 4 | 5 | NPM Version 6 | 7 | 8 | Package License 9 | 10 | 11 | NPM Downloads 12 | 13 | 14 | Travis 15 | 16 | 17 | Travis 18 | 19 | 20 | # Type Comparator 21 | 22 | Useful comparator functions written on Typescript (But you can use it on your JS project) 23 | 24 | Image 25 | 26 | ## Table of Contents 27 | 28 | - [Type Comparator](#type-comparator) 29 | - [Table of Contents](#table-of-contents) 30 | - [Installation](#installation) 31 | - [Usage](#usage) 32 | - [Base comparators: `asc` and `desc`](#base-comparators-asc-and-desc) 33 | - [Functional way](#functional-way) 34 | - [Function `reverse(comparator)`](#function-reversecomparator) 35 | - [Function `map(mapper, comparator)`](#function-mapmapper-comparator) 36 | - [Function `condition(conditionFn, comparatorA, comparatorB)`](#function-conditionconditionfn-comparatora-comparatorb) 37 | - [Function `queue(comparators)`](#function-queuecomparators) 38 | - [Chaining way](#chaining-way) 39 | - [Basic usage](#basic-usage) 40 | - [Chain `.reverse()`](#chain-reverse) 41 | - [Chain `.map(mapper)`](#chain-mapmapper) 42 | - [Chain `.if(conditionFn)`](#chain-ifconditionfn) 43 | - [Chain `.then(comparator)`](#chain-thencomparator) 44 | - [Chain `.elif(conditionFn)`](#chain-elifconditionfn) 45 | - [Chain `.else(comparator)`](#chain-elsecomparator) 46 | - [Chain `.use(comparators)`](#chain-usecomparators) 47 | - [Something more complex?... Ok!](#something-more-complex-ok) 48 | - [Q&A](#qa) 49 | - [Support](#support) 50 | - [Contributing](#contributing) 51 | 52 | ## Installation 53 | 54 | ```sh 55 | npm i type-comparator 56 | ``` 57 | 58 | ## Usage 59 | 60 | ### Base comparators: `asc` and `desc` 61 | 62 | `asc` is simple comparator contains just base comparison logic. 63 | 64 | It works well with numbers... 65 | 66 | ```ts 67 | const array = [17, 4, -17, 42, -3, 0]; 68 | 69 | // [-17, -3, 0, 4, 17, 42] 70 | array.slice().sort(asc); 71 | // [42, 17, 4, 0, -3, -17] 72 | array.slice().sort(desc); 73 | ``` 74 | 75 | And with strings... 76 | 77 | ```ts 78 | const array = ['aaa', 'bax', 'a', 'x', 'ax', 'ab', 'ba', 'bx']; 79 | 80 | // ["a", "aaa", "ab", "ax", "ba", "bax", "bx", "x"] 81 | array.slice().sort(asc); 82 | // ["x", "bx", "bax", "ba", "ax", "ab", "aaa", "a"] 83 | array.slice().sort(desc); 84 | ``` 85 | 86 | Even with dates... 87 | 88 | ```ts 89 | const array = [new Date(2018, 0, 1), new Date(2017, 0, 1), new Date(2019, 0, 1)]; 90 | 91 | // [Date(2017, 0, 1), Date(2018, 0, 1), Date(2019, 0, 1)] 92 | array.slice().sort(asc); 93 | // [Date(2019, 0, 1), Date(2018, 0, 1), Date(2017, 0, 1)] 94 | array.slice().sort(desc); 95 | ``` 96 | 97 | Actually it works well with everything comparable by `>` and `<`. 98 | 99 | **NOTE** Every values which are neither `>` nor `<` are equal. 100 | 101 | As you can see below, the initial order remains. If you want to sort by value of some item property, take a look on `map` function. 102 | 103 | ```ts 104 | var array1 = [{a: 1}, {a: 5}]; 105 | var array2 = [{a: 5}, {a: 1}]; 106 | 107 | array1.slice().sort(asc); // [{a: 1}, {a: 5}] 108 | array2.slice().sort(asc); // [{a: 5}, {a: 1}] 109 | array1.slice().sort(desc); // [{a: 1}, {a: 5}] 110 | array2.slice().sort(desc); // [{a: 5}, {a: 1}] 111 | ``` 112 | 113 | ***** 114 | 115 | ### Functional way 116 | 117 | #### Function `reverse(comparator)` 118 | Just swap comparator args. 119 | ```ts 120 | import { asc, cmp, reverse } from 'type-comparator'; 121 | 122 | const functionalCmp = reverse(asc); 123 | const array = [17, 4, -17, 42, -3, 0]; 124 | 125 | // [ 42, 17, 4, 0, -3, -17 ] 126 | array.slice().sort(functionalCmp); 127 | ``` 128 | 129 | #### Function `map(mapper, comparator)` 130 | Maps each args with `mapper` and apply `comparator`. 131 | 132 | ```ts 133 | import { asc, cmp, map } from 'type-comparator'; 134 | 135 | const mapper = x => x.a; 136 | const comparator = map(mapper, asc); 137 | const array = [{ a: 15 }, { a: 5 }]; 138 | 139 | // [ { a: 5 }, { a: 15 } ] 140 | array.slice().sort(comparator); 141 | ``` 142 | 143 | #### Function `condition(conditionFn, comparatorA, comparatorB)` 144 | Has following logic: 145 | - Applies `comparatorA`, if both args satisfy `conditionFn`. 146 | - Applies `comparatorB`, if both args do not satisfy `conditionFn`. 147 | - Returns positive value, if only first arg satisfies `conditionFn`. 148 | - Returns negative value, if only second arg satisfies `conditionFn`. 149 | 150 | ```ts 151 | import { asc, cmp, condition } from 'type-comparator'; 152 | 153 | const conditionFn = x => x % 2 === 0; 154 | const comparator = condition(conditionFn, asc, desc); 155 | const array = [17, 4, -17, 42, -3, 0]; 156 | 157 | // [ 17, -3, -17, 0, 4, 42 ] 158 | array.slice().sort(comparator); 159 | ``` 160 | 161 | #### Function `queue(comparators)` 162 | Applies first comparator from `comparators`. 163 | - If comparator returns non-zero value, returns it as result. 164 | - If comparator returns `0`, apply next comparator from `comparators`. 165 | - If there is no more comparator in `comparators` list, returns `0`. 166 | 167 | ```ts 168 | import { asc, cmp, desc, map, queue } from 'type-comparator'; 169 | 170 | const comparator = queue([ 171 | map(x => x.name, asc), 172 | map(x => x.age, desc), 173 | ]); 174 | const array = [ 175 | { name: 'Alex', age: 21 }, 176 | { name: 'Jane', age: 19 }, 177 | { name: 'Alex', age: 26 }, 178 | ]; 179 | 180 | // [ 181 | // { name: 'Alex', age: 26 }, 182 | // { name: 'Alex', age: 21 }, 183 | // { name: 'Jane', age: 19 } 184 | // ] 185 | array.slice().sort(comparator); 186 | ``` 187 | 188 | ****** 189 | 190 | ### Chaining way 191 | 192 | #### Basic usage 193 | `cmp()` - just starts chaining. 194 | `.use(comparator)` - applies comparator and terminates chaining. 195 | 196 | **Note:** `use()` chain can work with any comparator function (not only produced by `type-comparator`) 197 | 198 | ```ts 199 | import { asc, cmp } from 'type-comparator'; 200 | 201 | // same as just `asc` function 202 | const comparator1 = cmp().use(asc); 203 | 204 | // works like `asc` but just for numbers 205 | const comparator2 = cmp().use((a, b) => a - b); 206 | 207 | // not a lot of sense, but it's possible 208 | const comparator3 = cmp().use(comparator1); 209 | ``` 210 | 211 | #### Chain `.reverse()` 212 | 213 | ```ts 214 | import { asc, cmp, reverse } from 'type-comparator'; 215 | 216 | const comparator = cmp().reverse().use(asc); 217 | const array = [17, 4, -17, 42, -3, 0]; 218 | 219 | // [ 42, 17, 4, 0, -3, -17 ] 220 | array.slice().sort(comparator); 221 | ``` 222 | #### Chain `.map(mapper)` 223 | 224 | ```ts 225 | import { asc, cmp, map } from 'type-comparator'; 226 | 227 | const mapper = x => x.a; 228 | const comparator = cmp().map(mapper).use(asc); 229 | const array = [{ a: 15 }, { a: 5 }]; 230 | 231 | // [ { a: 5 }, { a: 15 } ] 232 | array.slice().sort(comparator); 233 | ``` 234 | 235 | #### Chain `.if(conditionFn)` 236 | Checks `conditionFn` for each arg. 237 | - Applies next `.then` chain, if both args satisfy `conditionFn`. 238 | - Applies next `.else`/`.elif` chain, if both args do not satisfy `conditionFn`. 239 | - Returns positive value, if only first arg satisfies `conditionFn`. 240 | - Returns negative value, if only second arg satisfies `conditionFn`. 241 | 242 | **Note:** After `.if()` chain, only `.then` chain is available. 243 | ```ts 244 | import { asc, cmp } from 'type-comparator'; 245 | 246 | const conditionFn = x => x % 4 === 0; 247 | const conditionFn2 = x => x % 2 === 0; 248 | const chainingCmp = cmp() 249 | .if(conditionFn).then(asc) 250 | .elif(conditionFn2).then(asc) 251 | .else(asc); 252 | const array = [17, 4, -17, 42, -3, 0]; 253 | 254 | // [ -17, -3, 17, 42, 0, 4 ] 255 | array.slice().sort(chainingCmp); 256 | ``` 257 | 258 | #### Chain `.then(comparator)` 259 | Applies `comparator`, if condition from previous `.if()`chain satisfies for both args. 260 | 261 | **Note:** After `.then()` chain, only `.elif()` or `.else()` chains are available. 262 | **Note:** `.then()` chain is available only after `.if()` or `.elif()` chains. 263 | 264 | #### Chain `.elif(conditionFn)` 265 | Works same `.if()` chain. 266 | 267 | **Note:** After `.elif()` chain, only `.then()` chain is available. 268 | **Note:** `.elif()` chain is available only after `.then()` chain. 269 | 270 | #### Chain `.else(comparator)` 271 | Applies `comparator`, if both args do not satisfy comparators from previous `.if()`/`.elif` chains. 272 | 273 | **Note:** `.else()` chain is available only after `.then()` chain. 274 | **Note:** `.else()` chain finishes chaining and returns result comparator function. 275 | 276 | #### Chain `.use(comparators)` 277 | Works same as `queue(comparators)` and terminates chaining. 278 | 279 | ```ts 280 | import { asc, cmp, desc } from 'type-comparator'; 281 | 282 | const comparator = cmp().use([ 283 | cmp().map(x => x.name).use(asc), 284 | cmp().map(x => x.age).use(desc), 285 | ]); 286 | const array = [ 287 | { name: 'Alex', age: 21 }, 288 | { name: 'Jane', age: 19 }, 289 | { name: 'Alex', age: 26 }, 290 | ]; 291 | 292 | // [ 293 | // { name: 'Alex', age: 26 }, 294 | // { name: 'Alex', age: 21 }, 295 | // { name: 'Jane', age: 19 } 296 | // ] 297 | array.slice().sort(comparator); 298 | ``` 299 | 300 | ********************************* 301 | 302 | ### Something more complex?... Ok! 303 | ```ts 304 | import { asc, cmp, desc } from 'type-comparator'; 305 | 306 | const comparator = cmp() 307 | .map(x => x.a) 308 | .use([ 309 | cmp() 310 | .map(x => x.b) 311 | .use(desc), 312 | cmp() 313 | .if(x => (x.b + x.c) % 2 === 0) 314 | .map(x => x.c) 315 | .use(asc), 316 | ]); 317 | 318 | const array = [ 319 | { a: { b: 1, c: 7 } }, 320 | { a: { b: 1, c: 6 } }, 321 | { a: { b: 1, c: 5 } }, 322 | { a: { b: 1, c: 4 } }, 323 | { a: { b: 1, c: 3 } }, 324 | { a: { b: 3, c: 2 } }, 325 | ]; 326 | 327 | // [ 328 | // { a: { b: 3, c: 2 } }, 329 | // { a: { b: 1, c: 4 } }, 330 | // { a: { b: 1, c: 6 } }, 331 | // { a: { b: 1, c: 3 } }, 332 | // { a: { b: 1, c: 5 } }, 333 | // { a: { b: 1, c: 7 } }, 334 | // ] 335 | array.slice().sort(comparator); 336 | ``` 337 | 338 | **** 339 | ### Q&A 340 | 341 | **Q:** Should `reverse(cmp)` be equals to reversed array with `cmp` ? 342 | 343 | **A:** In general, it should not. 344 | `Array.prototype.reverse` just reverse all elements order, regardless its values. 345 | When comparator suppose both values are equal, it returns `0`. And these elements save original order. 346 | 347 | ```ts 348 | const array = [1, 2, 4]; 349 | const comparator = cmp().map(x => x % 2 === 0).use(asc); 350 | 351 | // [2, 4, 1] 352 | array.slice().sort(reverse(comparator)); 353 | 354 | // [4, 2, 1] 355 | array.slice().sort(comparator).reverse(); 356 | ``` 357 | 358 | 359 | ## Support 360 | 361 | Please [open an issue](https://github.com/lightness/type-comparator/issues/new) for support. 362 | 363 | ## Contributing 364 | 365 | Please contribute using [Github Flow](https://guides.github.com/introduction/flow/). Create a branch, add commits, and [open a pull request](https://github.com/lightness/type-comparator/compare/). 366 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testRegex: "test/.*\.spec\.ts$", 5 | testMatch: null, 6 | globals: { 7 | 'ts-jest': { 8 | tsConfig: 'tsconfig.spec.json' 9 | } 10 | } 11 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-comparator", 3 | "version": "0.2.9", 4 | "description": "Useful comparator functions written on Typescript", 5 | "main": "build/index.js", 6 | "typings": "build/index.d.ts", 7 | "scripts": { 8 | "start": "node ./build/main.js", 9 | "test": "jest", 10 | "test:coverage": "jest --coverage", 11 | "build": "tsc", 12 | "clean": "rimraf build", 13 | "prepublish": "npm run clean && npm run build", 14 | "tslint": "tslint src/**/*.ts", 15 | "tslint:fix": "tslint --fix src/**/*.ts" 16 | }, 17 | "author": { 18 | "name": "Uladzimir Aleshka", 19 | "email": "uladzimir.aleshka@gmail.com" 20 | }, 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/jest": "^24.0.4", 24 | "jest": ">=22 <24", 25 | "rimraf": "^2.6.3", 26 | "ts-jest": "^23.10.5", 27 | "ts-node": "^8.0.2", 28 | "tslint": "^5.12.1", 29 | "typescript": "^3.3.3" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/lightness/type-comparator.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/lightness/type-comparator/issues" 37 | }, 38 | "keywords": [ 39 | "comparator", 40 | "comparators", 41 | "type", 42 | "types", 43 | "typescript" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /src/chaining/chain.ts: -------------------------------------------------------------------------------- 1 | import {Chainable, MutationDescriptor, Mapper, MutationType, Comparator, Condition} from "../interfaces"; 2 | import {condition, reverse, map} from "../mutations"; 3 | import {ThenChain} from "./then-chain"; 4 | import {asc, desc} from "../comparators"; 5 | 6 | export class Chain implements Chainable { 7 | 8 | private mutations: MutationDescriptor[]; 9 | 10 | public constructor() { 11 | this.mutations = []; 12 | } 13 | 14 | public reverse() { 15 | this.mutations.push({type: MutationType.REVERSE}); 16 | 17 | return this; 18 | } 19 | 20 | public map(mapper: Mapper) { 21 | this.mutations.push({type: MutationType.MAP, mapper: mapper}); 22 | 23 | return this; 24 | } 25 | 26 | public if(condition: Condition) { 27 | this.mutations.push({type: MutationType.IF, condition}); 28 | 29 | return new ThenChain(this); 30 | } 31 | 32 | public use(comparatorOrComparators: Comparator | Comparator[]) { 33 | if (Array.isArray(comparatorOrComparators)) { 34 | return (a, b) => { 35 | for (const comparator of comparatorOrComparators) { 36 | const result = this.use(comparator)(a, b); 37 | 38 | if (result !== 0) { 39 | return result; 40 | } 41 | } 42 | 43 | return 0; 44 | }; 45 | } 46 | 47 | return Chain.mutate(comparatorOrComparators, this.mutations); 48 | } 49 | 50 | public asc() { 51 | return this.use(asc); 52 | } 53 | 54 | public desc() { 55 | return this.use(desc); 56 | } 57 | 58 | private static mutate(fn: Comparator, descriptors: MutationDescriptor[]): Comparator { 59 | let currentFn = fn; 60 | 61 | for (const descriptor of descriptors.slice().reverse()) { 62 | currentFn = ((prevFn) => { 63 | switch (descriptor.type) { 64 | case MutationType.REVERSE: 65 | return reverse(prevFn); 66 | case MutationType.MAP: 67 | return map(descriptor.mapper, prevFn); 68 | case MutationType.IF: 69 | return condition(descriptor.condition, descriptor.comparator, prevFn); 70 | } 71 | })(currentFn); 72 | } 73 | 74 | return currentFn; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/chaining/cmp.ts: -------------------------------------------------------------------------------- 1 | import { Chainable } from "../interfaces"; 2 | import { Chain } from "./chain"; 3 | 4 | export function cmp(): Chainable { 5 | return new Chain(); 6 | } 7 | -------------------------------------------------------------------------------- /src/chaining/else-chain.ts: -------------------------------------------------------------------------------- 1 | import {Chain} from "./chain"; 2 | import {Comparator, MutationType} from "../interfaces"; 3 | import {ThenChain} from "./then-chain"; 4 | 5 | export class ElseChain { 6 | 7 | public constructor( 8 | private originalChain: Chain 9 | ) { 10 | } 11 | 12 | public else(comparator): Comparator { 13 | return this.originalChain.use(comparator); 14 | } 15 | 16 | public elif(condition): ThenChain { 17 | const mutations = this.originalChain['mutations']; 18 | mutations.push({ type: MutationType.IF, condition }); 19 | 20 | return new ThenChain(this.originalChain); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/chaining/then-chain.ts: -------------------------------------------------------------------------------- 1 | import {Chain} from "./chain"; 2 | import {IfMutationDescriptor} from "../interfaces"; 3 | import {ElseChain} from "./else-chain"; 4 | 5 | export class ThenChain { 6 | 7 | public constructor( 8 | private originalChain: Chain 9 | ) { 10 | } 11 | 12 | public then(comparator): ElseChain { 13 | const mutations = this.originalChain['mutations']; 14 | const ifMutataionDescriptor: IfMutationDescriptor = mutations[mutations.length - 1] as IfMutationDescriptor; 15 | 16 | ifMutataionDescriptor.comparator = comparator; 17 | 18 | return new ElseChain(this.originalChain); 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/comparators/asc.comparator.ts: -------------------------------------------------------------------------------- 1 | import { Comparator } from "../interfaces"; 2 | 3 | export const asc: Comparator = (a, b) => { 4 | if (a > b) { 5 | return 1; 6 | } else if (a < b) { 7 | return -1; 8 | } else { 9 | return 0; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/comparators/desc.comparator.ts: -------------------------------------------------------------------------------- 1 | import { Comparator } from "../interfaces"; 2 | import { reverse } from "../mutations"; 3 | import { asc } from "./asc.comparator"; 4 | 5 | export const desc: Comparator = reverse(asc); 6 | -------------------------------------------------------------------------------- /src/comparators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asc.comparator'; 2 | export * from './desc.comparator'; 3 | -------------------------------------------------------------------------------- /src/functional/queue.mutation.ts: -------------------------------------------------------------------------------- 1 | import { Comparator } from "../interfaces"; 2 | 3 | export function queue(comparators: Comparator[]): Comparator { 4 | return (a, b) => { 5 | for (const comparator of comparators) { 6 | const result = comparator(a, b); 7 | 8 | if (result !== 0) { 9 | return result; 10 | } 11 | } 12 | 13 | return 0; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './comparators'; 2 | export * from './mutations'; 3 | export * from './chaining/cmp'; 4 | export * from './functional/queue.mutation'; 5 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Comparator { 3 | (a: any, b: any): number; 4 | } 5 | 6 | export type Mutation = (c: Comparator) => Comparator; 7 | 8 | export enum MutationType { 9 | MAP = 'map', 10 | REVERSE = 'reverse', 11 | IF = 'if' 12 | } 13 | 14 | export type MutationDescriptor = ReverseMutationDescriptor | MapMutationDescriptor | IfMutationDescriptor; 15 | 16 | export interface MutationDescriptorBase { 17 | type: MutationType; 18 | } 19 | 20 | export interface ReverseMutationDescriptor extends MutationDescriptorBase { 21 | type: MutationType.REVERSE; 22 | } 23 | 24 | export interface MapMutationDescriptor extends MutationDescriptorBase { 25 | type: MutationType.MAP; 26 | mapper: Mapper; 27 | } 28 | 29 | export interface IfMutationDescriptor extends MutationDescriptorBase { 30 | type: MutationType.IF; 31 | condition: Condition; 32 | comparator?: Comparator; 33 | } 34 | 35 | export type Mapper = (item: any) => any; 36 | export type Condition = (item: any) => boolean; 37 | 38 | export interface ThenChainable { 39 | then(comparator: Comparator): ElseChainable; 40 | } 41 | 42 | export interface ElseChainable { 43 | else(comparator: Comparator): Comparator; 44 | elif(condition: Condition): ThenChainable; 45 | } 46 | 47 | export interface Chainable { 48 | reverse(): this; 49 | map(mapper: Mapper): this; 50 | if(condition: Condition): ThenChainable; 51 | use(comparatorOrComparators: Comparator | Comparator[]): Comparator; 52 | asc(): Comparator; 53 | desc(): Comparator; 54 | } 55 | -------------------------------------------------------------------------------- /src/mutations/condition.mutation.ts: -------------------------------------------------------------------------------- 1 | import {Comparator} from "../interfaces"; 2 | 3 | export function condition(cond: (item: any) => boolean, thenCmp: Comparator, elseCmp: Comparator): Comparator { 4 | return (a, b) => { 5 | const condA = cond(a); 6 | const condB = cond(b); 7 | 8 | if (condA) { 9 | if (condB) { 10 | return thenCmp(a, b); 11 | } else { 12 | return 1; 13 | } 14 | } else { 15 | if (condB) { 16 | return -1; 17 | } else { 18 | return elseCmp(a, b); 19 | } 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/mutations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './condition.mutation'; 2 | export * from './reverse.mutation'; 3 | export * from './map.mutation'; 4 | -------------------------------------------------------------------------------- /src/mutations/map.mutation.ts: -------------------------------------------------------------------------------- 1 | import { Comparator } from "../interfaces"; 2 | 3 | export function map(t: (item: any) => any, f: Comparator): Comparator { 4 | return (a, b) => { 5 | return f(t(a), t(b)); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/mutations/reverse.mutation.ts: -------------------------------------------------------------------------------- 1 | import { Comparator } from "../interfaces"; 2 | 3 | export function reverse(f: Comparator): Comparator { 4 | return (a, b) => { 5 | return f(b, a); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /test/chaining/chain.spec.ts: -------------------------------------------------------------------------------- 1 | import {cmp} from "../../src/chaining/cmp"; 2 | import {asc} from "../../src/comparators"; 3 | 4 | describe(`chain`, () => { 5 | 6 | describe(`use`, () => { 7 | 8 | test(`should save original sorting if 'use' method called with []`, () => { 9 | const array = [1, -2, 5, 0]; 10 | const comparator = cmp().use([]); 11 | const actual = array.slice().sort(comparator); 12 | const expected = [1, -2, 5, 0]; 13 | 14 | expect(actual).toEqual(expected); 15 | }); 16 | 17 | }); 18 | 19 | describe(`asc`, () => { 20 | 21 | test(`should apply "asc" comparator`, () => { 22 | const array = [0, 3, 2, 1, 4]; 23 | const comparator = cmp().asc(); 24 | 25 | const actual = array.sort(comparator); 26 | const expected = [0, 1, 2, 3, 4]; 27 | 28 | expect(actual).toEqual(expected); 29 | }); 30 | 31 | }); 32 | 33 | describe(`desc`, () => { 34 | 35 | test(`should apply "desc" comparator`, () => { 36 | const array = [0, 3, 2, 1, 4]; 37 | const comparator = cmp().desc(); 38 | 39 | const actual = array.sort(comparator); 40 | const expected = [4, 3, 2, 1, 0]; 41 | 42 | expect(actual).toEqual(expected); 43 | }); 44 | 45 | }); 46 | 47 | describe(`reverse`, () => { 48 | 49 | test(`should change elements order for comparator calls`, () => { 50 | const ascMock = jest.fn(asc); 51 | const comparator = cmp().reverse().use(ascMock); 52 | 53 | const args: [number, number] = [1, 2]; 54 | comparator(...args); 55 | 56 | expect(ascMock).toBeCalledTimes(1); 57 | expect(ascMock).toBeCalledWith(...args.reverse()); 58 | }); 59 | 60 | }); 61 | 62 | describe(`if / else`, () => { 63 | 64 | const condition = (x) => x % 2 === 0; 65 | const returnValue1 = 111; 66 | const returnValue2 = 222; 67 | 68 | test(`should call comparator from "then" chain if both args satisfy condition`, () => { 69 | const conditionMock = jest.fn(condition).mockReturnValue(true); 70 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 71 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 72 | 73 | const comparator = cmp() 74 | .if(conditionMock).then(cmpMock1) 75 | .else(cmpMock2); 76 | 77 | const result = comparator(null, null); 78 | 79 | expect(conditionMock).toBeCalledTimes(2); 80 | expect(cmpMock1).toBeCalledTimes(1); 81 | expect(cmpMock2).not.toBeCalled(); 82 | expect(result).toBe(returnValue1); 83 | }); 84 | 85 | test(`should call comparator from "else" chain if both args do not satisfy condition`, () => { 86 | const conditionMock = jest.fn(condition).mockReturnValue(false); 87 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 88 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 89 | 90 | const comparator = cmp() 91 | .if(conditionMock).then(cmpMock1) 92 | .else(cmpMock2); 93 | 94 | const result = comparator(null, null); 95 | 96 | expect(conditionMock).toBeCalledTimes(2); 97 | expect(cmpMock1).not.toBeCalled(); 98 | expect(cmpMock2).toBeCalledTimes(1); 99 | expect(result).toBe(returnValue2); 100 | }); 101 | 102 | test(`should return positive value, if only first arg satisfy condition`, () => { 103 | const conditionMock = jest.fn(condition).mockReturnValueOnce(true).mockReturnValueOnce(false); 104 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 105 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 106 | 107 | const comparator = cmp() 108 | .if(conditionMock).then(cmpMock1) 109 | .else(cmpMock2); 110 | 111 | const result = comparator(null, null); 112 | 113 | expect(conditionMock).toBeCalledTimes(2); 114 | expect(cmpMock1).not.toBeCalled(); 115 | expect(cmpMock2).not.toBeCalled(); 116 | expect(result).toBeGreaterThan(0); 117 | }); 118 | 119 | test(`should return negative value, if only second arg satisfy condition`, () => { 120 | const conditionMock = jest.fn(condition).mockReturnValueOnce(false).mockReturnValueOnce(true); 121 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 122 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 123 | 124 | const comparator = cmp() 125 | .if(conditionMock).then(cmpMock1) 126 | .else(cmpMock2); 127 | 128 | const result = comparator(null, null); 129 | 130 | expect(conditionMock).toBeCalledTimes(2); 131 | expect(cmpMock1).not.toBeCalled(); 132 | expect(cmpMock2).not.toBeCalled(); 133 | expect(result).toBeLessThan(0); 134 | }); 135 | 136 | }); 137 | 138 | describe(`if / elif / else`, () => { 139 | 140 | const condition1 = (x) => x % 2 === 0; 141 | const condition2 = (x) => x % 3 === 0; 142 | const returnValue1 = 111; 143 | const returnValue2 = 222; 144 | const returnValue3 = 333; 145 | 146 | test(`should check "elif" condition, if both args do not satisfy "if" condition`, () => { 147 | const conditionMock1 = jest.fn(condition1).mockReturnValue(false); 148 | const conditionMock2 = jest.fn(condition2).mockReturnValue(false); 149 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 150 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 151 | const cmpMock3 = jest.fn(asc).mockReturnValue(returnValue3); 152 | 153 | const comparator = cmp() 154 | .if(conditionMock1).then(cmpMock1) 155 | .elif(conditionMock2).then(cmpMock2) 156 | .else(cmpMock3); 157 | 158 | const result = comparator(null, null); 159 | 160 | expect(conditionMock1).toBeCalledTimes(2); 161 | expect(conditionMock2).toBeCalledTimes(2); 162 | expect(cmpMock1).not.toBeCalled(); 163 | expect(cmpMock2).not.toBeCalled(); 164 | expect(cmpMock3).toBeCalledTimes(1); 165 | expect(result).toBe(returnValue3); 166 | }); 167 | 168 | test(`should not check "elif" condition, if both args satisfy "if" condition`, () => { 169 | const conditionMock1 = jest.fn(condition1).mockReturnValue(true); 170 | const conditionMock2 = jest.fn(condition2).mockReturnValue(false); 171 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 172 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 173 | const cmpMock3 = jest.fn(asc).mockReturnValue(returnValue3); 174 | 175 | const comparator = cmp() 176 | .if(conditionMock1).then(cmpMock1) 177 | .elif(conditionMock2).then(cmpMock2) 178 | .else(cmpMock3); 179 | 180 | const result = comparator(null, null); 181 | 182 | expect(conditionMock1).toBeCalledTimes(2); 183 | expect(conditionMock2).not.toBeCalled(); 184 | expect(cmpMock1).toBeCalledTimes(1); 185 | expect(cmpMock2).not.toBeCalled(); 186 | expect(cmpMock3).not.toBeCalled(); 187 | expect(result).toBe(returnValue1); 188 | }); 189 | 190 | test(`should not check "elif" condition, if only first arg satisfy "if" condition`, () => { 191 | const conditionMock1 = jest.fn(condition1).mockReturnValueOnce(true).mockReturnValue(false); 192 | const conditionMock2 = jest.fn(condition2).mockReturnValue(false); 193 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 194 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 195 | const cmpMock3 = jest.fn(asc).mockReturnValue(returnValue3); 196 | 197 | const comparator = cmp() 198 | .if(conditionMock1).then(cmpMock1) 199 | .elif(conditionMock2).then(cmpMock2) 200 | .else(cmpMock3); 201 | 202 | const result = comparator(null, null); 203 | 204 | expect(conditionMock1).toBeCalledTimes(2); 205 | expect(conditionMock2).not.toBeCalled(); 206 | expect(cmpMock1).not.toBeCalled(); 207 | expect(cmpMock2).not.toBeCalled(); 208 | expect(cmpMock3).not.toBeCalled(); 209 | expect(result).toBeGreaterThan(0); 210 | }); 211 | 212 | test(`should not check "elif" condition, if only second arg satisfy "if" condition`, () => { 213 | const conditionMock1 = jest.fn(condition1).mockReturnValueOnce(false).mockReturnValue(true); 214 | const conditionMock2 = jest.fn(condition2).mockReturnValue(false); 215 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 216 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 217 | const cmpMock3 = jest.fn(asc).mockReturnValue(returnValue3); 218 | 219 | const comparator = cmp() 220 | .if(conditionMock1).then(cmpMock1) 221 | .elif(conditionMock2).then(cmpMock2) 222 | .else(cmpMock3); 223 | 224 | const result = comparator(null, null); 225 | 226 | expect(conditionMock1).toBeCalledTimes(2); 227 | expect(conditionMock2).not.toBeCalled(); 228 | expect(cmpMock1).not.toBeCalled(); 229 | expect(cmpMock2).not.toBeCalled(); 230 | expect(cmpMock3).not.toBeCalled(); 231 | expect(result).toBeLessThan(0); 232 | }); 233 | 234 | test(`should apply comparator from next "then", if both args satisfy "elif" condition`, () => { 235 | const conditionMock1 = jest.fn(condition1).mockReturnValue(false); 236 | const conditionMock2 = jest.fn(condition2).mockReturnValue(true); 237 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 238 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 239 | const cmpMock3 = jest.fn(asc).mockReturnValue(returnValue3); 240 | 241 | const comparator = cmp() 242 | .if(conditionMock1).then(cmpMock1) 243 | .elif(conditionMock2).then(cmpMock2) 244 | .else(cmpMock3); 245 | 246 | const result = comparator(null, null); 247 | 248 | expect(conditionMock1).toBeCalledTimes(2); 249 | expect(conditionMock2).toBeCalledTimes(2); 250 | expect(cmpMock1).not.toBeCalled(); 251 | expect(cmpMock2).toBeCalledTimes(1); 252 | expect(cmpMock3).not.toBeCalled(); 253 | expect(result).toBe(returnValue2); 254 | }); 255 | 256 | test(`should apply comparator from next "elif", if both args do not satisfy current "elif" condition`, () => { 257 | const conditionMock1 = jest.fn(condition1).mockReturnValue(false); 258 | const conditionMock2 = jest.fn(condition2).mockReturnValue(false); 259 | const conditionMock3 = jest.fn(condition1).mockReturnValue(true); 260 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 261 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 262 | const cmpMock3 = jest.fn(asc).mockReturnValue(returnValue3); 263 | const cmpMock4 = jest.fn(asc).mockReturnValue(returnValue1); 264 | 265 | const comparator = cmp() 266 | .if(conditionMock1).then(cmpMock1) 267 | .elif(conditionMock2).then(cmpMock2) 268 | .elif(conditionMock3).then(cmpMock3) 269 | .else(cmpMock4); 270 | 271 | const result = comparator(null, null); 272 | 273 | expect(conditionMock1).toBeCalledTimes(2); 274 | expect(conditionMock2).toBeCalledTimes(2); 275 | expect(conditionMock3).toBeCalledTimes(2); 276 | expect(cmpMock1).not.toBeCalled(); 277 | expect(cmpMock2).not.toBeCalled(); 278 | expect(cmpMock3).toBeCalledTimes(1); 279 | expect(cmpMock4).not.toBeCalled(); 280 | expect(result).toBe(returnValue3); 281 | }); 282 | 283 | test(`should apply comparator from next "else", if both args do not satisfy "elif" condition and there is no more "elif" chains`, () => { 284 | const conditionMock1 = jest.fn(condition1).mockReturnValue(false); 285 | const conditionMock2 = jest.fn(condition2).mockReturnValue(false); 286 | const cmpMock1 = jest.fn(asc).mockReturnValue(returnValue1); 287 | const cmpMock2 = jest.fn(asc).mockReturnValue(returnValue2); 288 | const cmpMock3 = jest.fn(asc).mockReturnValue(returnValue3); 289 | 290 | const comparator = cmp() 291 | .if(conditionMock1).then(cmpMock1) 292 | .elif(conditionMock2).then(cmpMock2) 293 | .else(cmpMock3); 294 | 295 | const result = comparator(null, null); 296 | 297 | expect(conditionMock1).toBeCalledTimes(2); 298 | expect(conditionMock2).toBeCalledTimes(2); 299 | expect(cmpMock1).not.toBeCalled(); 300 | expect(cmpMock2).not.toBeCalled(); 301 | expect(cmpMock3).toBeCalledTimes(1); 302 | expect(result).toBe(returnValue3); 303 | }); 304 | 305 | }); 306 | 307 | }); 308 | -------------------------------------------------------------------------------- /test/comparators/asc.comparator.spec.ts: -------------------------------------------------------------------------------- 1 | import {asc} from "../../src/comparators/asc.comparator"; 2 | 3 | describe(`asc should compare numbers`, () => { 4 | 5 | test(`should return 1, if first arg is greater`, () => { 6 | const actual = asc(5, 1); 7 | const expected = 1; 8 | 9 | expect(actual).toBe(expected); 10 | }); 11 | 12 | test(`should return -1, if second arg is greater`, () => { 13 | const actual = asc(1, 5); 14 | const expected = -1; 15 | 16 | expect(actual).toBe(expected); 17 | }); 18 | 19 | test(`should return 0, if args is equal`, () => { 20 | const actual = asc(5, 5); 21 | const expected = 0; 22 | 23 | expect(actual).toBe(expected); 24 | }); 25 | 26 | }); 27 | 28 | describe(`asc should compare strings`, () => { 29 | 30 | test(`should return 1, if first arg is greater`, () => { 31 | const actual = asc('x', 'a'); 32 | const expected = 1; 33 | 34 | expect(actual).toBe(expected); 35 | }); 36 | 37 | test(`should return -1, if second arg is greater`, () => { 38 | const actual = asc('a', 'x'); 39 | const expected = -1; 40 | 41 | expect(actual).toBe(expected); 42 | }); 43 | 44 | test(`should return 0, if args is equal`, () => { 45 | const actual = asc('x', 'x'); 46 | const expected = 0; 47 | 48 | expect(actual).toBe(expected); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /test/functional/queue.mutation.spec.ts: -------------------------------------------------------------------------------- 1 | import {queue, asc} from "../../src"; 2 | 3 | describe(`mutation:queue`, () => { 4 | 5 | const originalComparator1 = (a, b) => asc(a % 2, b % 2); 6 | const originalComparator2 = (a, b) => asc(a, b); 7 | 8 | test(`should apply only first comparator, if it returns non-zero value`, () => { 9 | const originalComparator1Mock = jest.fn(originalComparator1); 10 | const originalComparator2Mock = jest.fn(originalComparator2); 11 | 12 | const comparartor = queue([ 13 | originalComparator1Mock, 14 | originalComparator2Mock 15 | ]); 16 | 17 | const args: [number, number] = [3, 10]; 18 | 19 | const result = comparartor(...args); 20 | 21 | expect(originalComparator1Mock).toBeCalledTimes(1); 22 | expect(originalComparator1Mock).toBeCalledWith(...args); 23 | expect(originalComparator1Mock).toReturnWith(result); 24 | 25 | expect(originalComparator2Mock).toBeCalledTimes(0); 26 | }); 27 | 28 | test(`should apply second comparator, if first one returns zero`, () => { 29 | const originalComparator1Mock = jest.fn(originalComparator1); 30 | const originalComparator2Mock = jest.fn(originalComparator2); 31 | 32 | const comparartor = queue([ 33 | originalComparator1Mock, 34 | originalComparator2Mock 35 | ]); 36 | 37 | const args: [number, number] = [30, 10]; 38 | 39 | const result = comparartor(...args); 40 | 41 | expect(originalComparator1Mock).toBeCalledTimes(1); 42 | expect(originalComparator1Mock).toBeCalledWith(...args); 43 | expect(originalComparator1Mock).toReturnWith(0); 44 | 45 | expect(originalComparator2Mock).toBeCalledTimes(1); 46 | expect(originalComparator2Mock).toBeCalledWith(...args); 47 | expect(originalComparator2Mock).toReturnWith(result); 48 | }); 49 | 50 | test(`should return items in original order, if [] passed`, () => { 51 | const array = [1, -2, 6, 0]; 52 | const comparartor = queue([]); 53 | 54 | const actual = array.slice().sort(comparartor); 55 | const expected = [1, -2, 6, 0]; 56 | 57 | expect(actual).toEqual(expected); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /test/mutations/condition.mutation.spec.ts: -------------------------------------------------------------------------------- 1 | import {condition} from "../../src"; 2 | 3 | describe(`mutation:condition`, () => { 4 | const A = 42; 5 | const B = 101; 6 | 7 | beforeEach(() => { 8 | this.conditionFn = jest.fn(); 9 | this.cmpA = jest.fn(() => A); 10 | this.cmpB = jest.fn(() => B); 11 | }); 12 | 13 | test(`should return positive value, if first arg satisfies condition and second arg not`, () => { 14 | this.conditionFn 15 | .mockReturnValueOnce(true) 16 | .mockReturnValueOnce(false); 17 | 18 | const comparator = condition(this.conditionFn, this.cmpA, this.cmpB); 19 | 20 | const args: [number, number] = [null, null]; 21 | 22 | expect(comparator(...args)).toBeGreaterThan(0); 23 | expect(this.conditionFn).toBeCalledTimes(2); 24 | expect(this.cmpA).not.toBeCalled(); 25 | expect(this.cmpB).not.toBeCalled(); 26 | }); 27 | 28 | test(`should return negative value, if second arg satisfies condition and first arg not`, () => { 29 | this.conditionFn 30 | .mockReturnValueOnce(false) 31 | .mockReturnValueOnce(true); 32 | 33 | const comparator = condition(this.conditionFn, this.cmpA, this.cmpB); 34 | 35 | const args: [number, number] = [null, null]; 36 | 37 | expect(comparator(...args)).toBeLessThan(0); 38 | expect(this.conditionFn).toBeCalledTimes(2); 39 | expect(this.cmpA).not.toBeCalled(); 40 | expect(this.cmpB).not.toBeCalled(); 41 | }); 42 | 43 | test(`should apply first comparator, if both args satisfy condition`, () => { 44 | this.conditionFn 45 | .mockReturnValueOnce(true) 46 | .mockReturnValueOnce(true); 47 | 48 | const comparator = condition(this.conditionFn, this.cmpA, this.cmpB); 49 | 50 | const args: [number, number] = [null, null]; 51 | 52 | expect(comparator(...args)).toBe(A); 53 | expect(this.conditionFn).toBeCalledTimes(2); 54 | expect(this.cmpA).toBeCalledTimes(1); 55 | expect(this.cmpB).not.toBeCalled(); 56 | }); 57 | 58 | test(`should apply first comparator, if both args don't satisfy condition`, () => { 59 | this.conditionFn 60 | .mockReturnValueOnce(false) 61 | .mockReturnValueOnce(false); 62 | 63 | const comparator = condition(this.conditionFn, this.cmpA, this.cmpB); 64 | 65 | const args: [number, number] = [null, null]; 66 | 67 | expect(comparator(...args)).toBe(B); 68 | expect(this.conditionFn).toBeCalledTimes(2); 69 | expect(this.cmpA).not.toBeCalled(); 70 | expect(this.cmpB).toBeCalledTimes(1); 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /test/mutations/map.mutation.spec.ts: -------------------------------------------------------------------------------- 1 | import { map } from "../../src"; 2 | 3 | describe(`mutation:map`, () => { 4 | 5 | const originalComparator = (a, b) => a - b; 6 | const mapper = (x) => x % 2; 7 | 8 | test(`should call original comparator with mapped values`, () => { 9 | const originalComparatorMock = jest.fn(originalComparator); 10 | const mapperMock = jest.fn(mapper); 11 | 12 | const tansformedComparator = map(mapperMock, originalComparatorMock); 13 | 14 | const args: [number, number] = [3, 10]; 15 | 16 | const result = tansformedComparator(...args); 17 | 18 | expect(originalComparatorMock).toBeCalledTimes(1); 19 | expect(originalComparatorMock).toBeCalledWith(...args.map(mapper)); 20 | expect(originalComparatorMock).toReturnWith(result); 21 | 22 | expect(mapperMock).toHaveBeenCalledTimes(2); 23 | expect(mapperMock).toHaveBeenCalledWith(args[0]); 24 | expect(mapperMock).toHaveBeenCalledWith(args[1]); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/mutations/reverse.mutation.spec.ts: -------------------------------------------------------------------------------- 1 | import { reverse } from "../../src"; 2 | 3 | describe(`mutation:reverse`, () => { 4 | 5 | const originalComparator = (a, b) => a - b; 6 | const reversedComparator = reverse(originalComparator); 7 | 8 | test(`should return negative value, when original comparator returns positive one`, () => { 9 | const args: [number, number] = [5, 1]; 10 | 11 | expect(originalComparator(...args)).toBeGreaterThan(0); 12 | expect(reversedComparator(...args)).toBeLessThan(0); 13 | }); 14 | 15 | test(`should return positive value, when original comparator returns negative one`, () => { 16 | const args: [number, number] = [1, 5]; 17 | 18 | expect(originalComparator(...args)).toBeLessThan(0); 19 | expect(reversedComparator(...args)).toBeGreaterThan(0); 20 | }); 21 | 22 | test(`should return zero, when original comparator returns zero`, () => { 23 | const args: [number, number] = [1, 1]; 24 | 25 | expect(originalComparator(...args)).toBe(0); 26 | expect(reversedComparator(...args)).toBe(0); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/temp.spec.ts: -------------------------------------------------------------------------------- 1 | import {map, queue, asc, reverse, cmp, desc} from '../src'; 2 | 3 | describe(`functional`, () => { 4 | 5 | const comparator = map( 6 | x => x.b, 7 | queue([ 8 | map(x => x.a % 10, asc), 9 | map(x => x.a, reverse(asc)) 10 | ]) 11 | ); 12 | 13 | test(`test 1`, () => { 14 | const a = Object.freeze({a: {b: 11}, b: {a: 10}}); 15 | const b = Object.freeze({a: {b: 8}, b: {a: 74}}); 16 | const c = Object.freeze({a: {b: 3}, b: {a: 40}}); 17 | const array = [a, b, c]; 18 | 19 | const actual = array.slice(0).sort(comparator); 20 | const expected = [c, a, b]; 21 | 22 | expect(actual).toEqual(expected); 23 | 24 | }); 25 | 26 | test(`test 2`, () => { 27 | const a = Object.freeze({a: {b: 11}, b: {a: 10}}); 28 | const b = Object.freeze({a: {b: 8}, b: {a: 74}}); 29 | const c = Object.freeze({a: {b: 3}, b: {a: -40}}); 30 | const array = [a, b, c]; 31 | 32 | const actual = array.slice(0).sort(comparator); 33 | const expected = [a, c, b]; 34 | 35 | expect(actual).toEqual(expected); 36 | 37 | }); 38 | }); 39 | 40 | describe(`chaining`, () => { 41 | 42 | const condition = x => x % 10 === 0; 43 | const array = [1, 10, 2, 20]; 44 | 45 | test(`test 1`, () => { 46 | const comparator = cmp().if(condition).then(asc).else(asc); 47 | 48 | expect(comparator(10, 1)).toBe(1); 49 | expect(comparator(1, 10)).toBe(-1); 50 | expect(comparator(20, 10)).toBe(1); 51 | expect(comparator(10, 20)).toBe(-1); 52 | expect(comparator(10, 10)).toBe(0); 53 | 54 | const actual = array.slice(0).sort(comparator); 55 | const expected = [1, 2, 10, 20]; 56 | 57 | expect(actual).toEqual(expected); 58 | }); 59 | 60 | }); 61 | 62 | describe(`chaining with objects`, () => { 63 | 64 | const comparator = cmp() 65 | .map(x => x.a) 66 | .use([ 67 | cmp().map(x => x.b).use(desc), 68 | cmp().if(x => (x.b + x.c) % 2 === 0).then( 69 | cmp().map(x => x.c).use(asc) 70 | ).else( 71 | cmp().map(x => x.c).use(asc) 72 | ) 73 | ]); 74 | 75 | test(`dataset 1`, () => { 76 | const array = [ 77 | { a: { b: 1, c: 7 } }, 78 | { a: { b: 1, c: 6 } }, 79 | { a: { b: 2, c: 5 } }, 80 | { a: { b: 2, c: 4 } }, 81 | { a: { b: 3, c: 3 } }, 82 | { a: { b: 3, c: 2 } }, 83 | ]; 84 | 85 | const expected = [ 86 | { a: { b: 3, c: 2 } }, 87 | { a: { b: 3, c: 3 } }, 88 | { a: { b: 2, c: 5 } }, 89 | { a: { b: 2, c: 4 } }, 90 | { a: { b: 1, c: 6 } }, 91 | { a: { b: 1, c: 7 } }, 92 | ]; 93 | 94 | expect(array.slice().sort(comparator)).toEqual(expected); 95 | }); 96 | 97 | test(`dataset 2`, () => { 98 | const array = [ 99 | { a: { b: 1, c: 7 } }, 100 | { a: { b: 1, c: 6 } }, 101 | { a: { b: 1, c: 5 } }, 102 | { a: { b: 1, c: 4 } }, 103 | { a: { b: 1, c: 3 } }, 104 | { a: { b: 3, c: 2 } }, 105 | ]; 106 | 107 | const expected = [ 108 | { a: { b: 3, c: 2 } }, 109 | { a: { b: 1, c: 4 } }, 110 | { a: { b: 1, c: 6 } }, 111 | { a: { b: 1, c: 3 } }, 112 | { a: { b: 1, c: 5 } }, 113 | { a: { b: 1, c: 7 } }, 114 | ]; 115 | 116 | expect(array.slice().sort(comparator)).toEqual(expected); 117 | }); 118 | 119 | }); 120 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "declaration": true, 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "outDir": "build", 11 | "noImplicitAny": false, 12 | "lib": [ 13 | "es2015", 14 | "dom" 15 | ] 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "test" 23 | ] 24 | } -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "test" 5 | ], 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | true, 5 | "spaces", 6 | 4 7 | ], 8 | "semicolon": true, 9 | "eofline": true 10 | } 11 | } --------------------------------------------------------------------------------