├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | # Type Comparator
21 |
22 | Useful comparator functions written on Typescript (But you can use it on your JS project)
23 |
24 |
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 | }
--------------------------------------------------------------------------------