├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── benchmark.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── LICENSE_BANNER ├── README.md ├── benchmark └── joins.ts ├── debug ├── index.ejs └── index.ts ├── index.lodash.d.ts ├── index.lodash.ts ├── index.ts ├── karma.conf.ts ├── lib ├── cartesianProduct.ts ├── hash │ ├── hashFullOuterJoin.ts │ ├── hashInnerJoin.ts │ ├── hashLeftAntiJoin.ts │ ├── hashLeftOuterJoin.ts │ ├── hashLeftSemiJoin.ts │ ├── hashRightAntiJoin.ts │ ├── hashRightOuterJoin.ts │ ├── hashRightSemiJoin.ts │ ├── index.ts │ └── util │ │ ├── index.ts │ │ └── toStringAccessor.ts ├── index.spec.ts ├── index.ts ├── nestedLoop │ ├── index.ts │ ├── nestedLoopFullOuterJoin.ts │ ├── nestedLoopInnerJoin.ts │ ├── nestedLoopLeftAntiJoin.ts │ ├── nestedLoopLeftOuterJoin.ts │ ├── nestedLoopLeftSemiJoin.ts │ ├── nestedLoopRightAntiJoin.ts │ ├── nestedLoopRightOuterJoin.ts │ └── nestedLoopRightSemiJoin.ts ├── sortedMerge │ ├── index.ts │ ├── sortedMergeFullOuterJoin.ts │ ├── sortedMergeInnerJoin.ts │ ├── sortedMergeLeftAntiJoin.ts │ ├── sortedMergeLeftOuterJoin.ts │ ├── sortedMergeLeftSemiJoin.ts │ ├── sortedMergeRightAntiJoin.ts │ ├── sortedMergeRightOuterJoin.ts │ ├── sortedMergeRightSemiJoin.ts │ └── util │ │ ├── index.ts │ │ ├── isUndefined.ts │ │ ├── mergeLists.ts │ │ └── yieldRightSubList.ts ├── typings.ts └── util │ ├── basicAccessor.ts │ ├── basicMerger.ts │ ├── index.ts │ └── joinWrapper.ts ├── package-lock.json ├── package.json ├── test.ts ├── tsconfig.json └── webpack.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | 'import', 7 | 'jasmine' 8 | ], 9 | env: { 10 | 'es6': true, 11 | 'node': true, 12 | 'browser': true, 13 | 'jasmine': true 14 | }, 15 | extends: [ 16 | 'eslint:recommended', 17 | 'plugin:@typescript-eslint/eslint-recommended', 18 | 'plugin:@typescript-eslint/recommended', 19 | 'plugin:import/errors', 20 | 'plugin:import/warnings', 21 | 'plugin:import/typescript', 22 | 'plugin:jasmine/recommended' 23 | ], 24 | rules: { 25 | indent: [2, 4], 26 | "max-len": [2, 140], 27 | "jasmine/new-line-between-declarations": [0], 28 | "jasmine/new-line-before-expect": [0], 29 | "jasmine/no-spec-dupes": [0] 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: benchmark 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node-version: [20.x] 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Use Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - run: npm ci 17 | - run: npm run benchmark 18 | env: 19 | CI: true 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [16.x, 17.x, 18.x, 19.x, 20.x] 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm ci 20 | - run: npm run lint 21 | - run: npm run build 22 | - run: npm test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.iml 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE_BANNER: -------------------------------------------------------------------------------- 1 | /*! 2 | * <%= pkg.name %> - v<%= pkg.version %> - <%= date %> 3 | * <%= pkg.repository.url %> 4 | * Copyright 2014-<%= date.getFullYear() %> <%= pkg.author.name %> <<%= pkg.author.email %>> 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Node.js CI](https://github.com/mtraynham/lodash-joins/workflows/test/badge.svg) 2 | 3 | # lodash-joins 4 | 5 | ## Contents 6 | 7 | - [About](#about) 8 | - [Description](#description) 9 | - [Implementations](#implementations) 10 | - [Supported Join Types](#supported-join-types) 11 | - [Usage](#usage) 12 | - [Available Functions](#available-functions) 13 | - [Example](#example) 14 | - [Testing](#testing) 15 | - [Latest Benchmarks](#latest-benchmarks) 16 | - [Full Outer Joins](#full-outer-joins) 17 | - [Inner Joins](#inner-joins) 18 | - [Left Anti Joins](#left-anti-joins) 19 | - [Left Outer Joins](#left-outer-joins) 20 | - [Left Semi Joins](#left-semi-joins) 21 | - [Future](#future) 22 | 23 | ## About 24 | A library providing join algorithms for JavaScript Arrays. LoDash is the only dependency and this library appends itself as an extension to that library. 25 | 26 | Lodash already supports some standard SQL-like features: 27 | 28 | * [_.pluck](http://lodash.com/docs#pluck) (ES6 could use destructuring assignments) 29 | * [_.sortBy](http://lodash.com/docs#sortBy) 30 | * [_.groupBy](http://lodash.com/docs#groupBy) 31 | * [_.filter](http://lodash.com/docs#filter) 32 | 33 | Limit and offset can be accomplished using [```Array.slice```](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice). 34 | 35 | ### Description 36 | These functions only work on arrays of objects (comparable to database rows). Key comparisons are coercive, so Dates should work as well. 37 | 38 | For merge-type joins, a merger function may be provided to customize the output array. By default, all joined rows are generated from 39 | LoDash's [```assign```](http://lodash.com/docs#assign) function: 40 | 41 | _.assign({}, leftRow, rightRow); 42 | 43 | A merger function takes both the left and right record and provides a new output record. The left or right record may be 44 | `null` in cases like outer joins which do not have matching row keys. 45 | 46 | Order of the output is indeterminate. 47 | 48 | ## Implementations 49 | * [Nested Loop](http://en.wikipedia.org/wiki/Nested_loop_join) 50 | * [Hash](http://en.wikipedia.org/wiki/Hash_join) - Uses LoDash _.groupBy to hash, thus key comparisons are String based 51 | * [Sorted Merge](http://en.wikipedia.org/wiki/Sort-merge_join) - Uses natural sort comparisons (`<`, `>`), and should expect keys of the same type ([#43](https://github.com/mtraynham/lodash-joins/issues/43)) 52 | 53 | ### Supported Join Types 54 | * [Full Outer-Joins](http://en.wikipedia.org/wiki/Join_(SQL)#Full_outer_join) 55 | * [Inner-Joins](http://en.wikipedia.org/wiki/Join_(SQL)#Inner_join) 56 | * [Left Anti-Joins](http://en.wikipedia.org/wiki/Relational_algebra#Antijoin_.28.E2.96.B7.29) 57 | * [Left Outer-Joins](http://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join) 58 | * [Left Semi-Joins](http://en.wikipedia.org/wiki/Relational_algebra#Semijoin_.28.E2.8B.89.29.28.E2.8B.8A.29) 59 | * [Right Anti-Joins](http://en.wikipedia.org/wiki/Relational_algebra#Antijoin_.28.E2.96.B7.29) 60 | * [Right Outer-Joins](http://en.wikipedia.org/wiki/Join_(SQL)#Right_outer_join) 61 | * [Right Semi-Joins](http://en.wikipedia.org/wiki/Relational_algebra#Semijoin_.28.E2.8B.89.29.28.E2.8B.8A.29) 62 | 63 | ### Usage 64 | Each join function accepts two arrays and two accessor functions for each array that will act as the pluck function for key comparison. 65 | 66 | _.joinFunction(leftArray, leftKeyAccessor, rightArray, rightKeyAccessor, merger); 67 | 68 | ### Available Functions 69 | * _.hashFullOuterJoin 70 | * _.hashInnerJoin 71 | * _.hashLeftOuterJoin 72 | * _.hashLeftSemiJoin 73 | * _.hashRightOuterJoin 74 | * _.hashRightSemiJoin 75 | * _.nestedLoopFullOuterJoin 76 | * _.nestedLoopInnerJoin 77 | * _.nestedLoopLeftOuterJoin 78 | * _.nestedLoopLeftSemiJoin 79 | * _.nestedLoopRightOuterJoin 80 | * _.nestedLoopRightSemiJoin 81 | * _.sortedMergeFullOuterJoin 82 | * _.sortedMergeInnerJoin 83 | * _.sortedMergeLeftOuterJoin 84 | * _.sortedMergeLeftSemiJoin 85 | * _.sortedMergeRightOuterJoin 86 | * _.sortedMergeRightSemiJoin 87 | 88 | #### Example: 89 | > var _ = require('lodash-joins'); 90 | > var left = [ 91 | ... {id: 'c', left: 0}, 92 | ... {id: 'c', left: 1}, 93 | ... {id: 'e', left: 2}, 94 | ... ], 95 | ... right = [ 96 | ... {id: 'a', right: 0}, 97 | ... {id: 'b', right: 1}, 98 | ... {id: 'c', right: 2}, 99 | ... {id: 'c', right: 3}, 100 | ... {id: 'd', right: 4}, 101 | ... {id: 'f', right: 5}, 102 | ... {id: 'g', right: 6} 103 | ... ], 104 | ... accessor = function (obj) { 105 | ... return obj['id']; 106 | ... }; 107 | > 108 | > var a = _.hashInnerJoin(left, accessor, right, accessor); 109 | undefined 110 | > a 111 | [ { id: 'c', left: 0, right: 2 }, 112 | { id: 'c', left: 1, right: 2 }, 113 | { id: 'c', left: 0, right: 3 }, 114 | { id: 'c', left: 1, right: 3 } ] 115 | > 116 | > var b = _.nestedLoopLeftOuterJoin(left, accessor, right, accessor); 117 | undefined 118 | > b 119 | [ { id: 'c', left: 0, right: 2 }, 120 | { id: 'c', left: 0, right: 3 }, 121 | { id: 'c', left: 1, right: 2 }, 122 | { id: 'c', left: 1, right: 3 }, 123 | { id: 'e', left: 2 } ] 124 | > 125 | > var c = _.sortedMergeFullOuterJoin(left, accessor, right, accessor); 126 | undefined 127 | > c 128 | [ { id: 'a', right: 0 }, 129 | { id: 'b', right: 1 }, 130 | { id: 'c', left: 0, right: 2 }, 131 | { id: 'c', left: 0, right: 3 }, 132 | { id: 'c', left: 1, right: 2 }, 133 | { id: 'c', left: 1, right: 3 }, 134 | { id: 'd', right: 4 }, 135 | { id: 'e', left: 2 }, 136 | { id: 'f', right: 5 }, 137 | { id: 'g', right: 6 } ] 138 | 139 | ## Testing 140 | Tested using [Jasmine](https://jasmine.github.io/) and [Karma](https://karma-runner.github.io/latest/index.html). See the ```/spec``` directory. There is also a browser test invoked through 141 | Webpack and Mocha. 142 | 143 | ### Latest Benchmarks 144 | Typically for the Inner & Outer joins, with larger arrays stick with the Sorted Merge, then Hash, then Nested. With the 145 | Anti & Semi joins, Nested can outperform when there is a small cardinality of keys, but Hash may be more efficient 146 | since only one of the Arrays needs to be hashed. 147 | 148 | Each suite performs three joins on randomly generated arrays with string keys. The sizes 'Large', 'Medium' and 'Small' 149 | correlate to the size of the join arrays being used, i.e. 1000, 100, 10 respectively. 150 | 151 | #### Full Outer Joins 152 | Running suite Full Outer Joins Large... 153 | - Hash Join x 5.45 ops/sec ±2.85% (18 runs sampled) 154 | - Sorted Merge Join x 21.05 ops/sec ±3.92% (40 runs sampled) 155 | - Nested Loop Join x 4.54 ops/sec ±4.48% (16 runs sampled) 156 | - Fastest is Sorted Merge Join 157 | 158 | Running suite Full Outer Joins Medium... 159 | - Hash Join x 3,841 ops/sec ±1.15% (92 runs sampled) 160 | - Sorted Merge Join x 3,192 ops/sec ±0.61% (96 runs sampled) 161 | - Nested Loop Join x 2,295 ops/sec ±1.82% (90 runs sampled) 162 | - Fastest is Hash Join 163 | 164 | Running suite Full Outer Joins Small... 165 | - Hash Join x 130,893 ops/sec ±1.41% (96 runs sampled) 166 | - Sorted Merge Join x 98,704 ops/sec ±1.66% (92 runs sampled) 167 | - Nested Loop Join x 136,792 ops/sec ±0.65% (95 runs sampled) 168 | - Fastest is Nested Loop Join 169 | 170 | #### Inner Joins 171 | Running suite Inner Joins Large... 172 | - Hash Join x 4.63 ops/sec ±9.42% (16 runs sampled) 173 | - Sorted Merge Join x 15.29 ops/sec ±6.01% (43 runs sampled) 174 | - Nested Loop Join x 4.41 ops/sec ±5.68% (16 runs sampled) 175 | - Fastest is Sorted Merge Join 176 | 177 | Running suite Inner Joins Medium... 178 | - Hash Join x 3,970 ops/sec ±0.30% (97 runs sampled) 179 | - Sorted Merge Join x 3,086 ops/sec ±0.78% (96 runs sampled) 180 | - Nested Loop Join x 2,452 ops/sec ±1.26% (95 runs sampled) 181 | - Fastest is Hash Join 182 | 183 | Running suite Inner Joins Small... 184 | - Hash Join x 255,618 ops/sec ±0.45% (94 runs sampled) 185 | - Sorted Merge Join x 142,653 ops/sec ±0.54% (92 runs sampled) 186 | - Nested Loop Join x 232,739 ops/sec ±0.59% (97 runs sampled) 187 | - Fastest is Hash Join 188 | 189 | #### Left Anti Joins 190 | Running suite Left Anti Joins Large... 191 | - Hash Join x 15,104 ops/sec ±1.18% (97 runs sampled) 192 | - Sorted Merge Join x 2,755 ops/sec ±0.27% (99 runs sampled) 193 | - Nested Loop Join x 18,295 ops/sec ±1.84% (89 runs sampled) 194 | - Fastest is Nested Loop Join 195 | 196 | Running suite Left Anti Joins Medium... 197 | - Hash Join x 147,805 ops/sec ±1.86% (88 runs sampled) 198 | - Sorted Merge Join x 33,078 ops/sec ±0.87% (96 runs sampled) 199 | - Nested Loop Join x 250,000 ops/sec ±0.83% (94 runs sampled) 200 | - Fastest is Nested Loop Join 201 | 202 | Running suite Left Anti Joins Small... 203 | - Hash Join x 1,421,230 ops/sec ±1.02% (96 runs sampled) 204 | - Sorted Merge Join x 468,908 ops/sec ±0.45% (99 runs sampled) 205 | - Nested Loop Join x 2,111,985 ops/sec ±1.77% (92 runs sampled) 206 | - Fastest is Nested Loop Join 207 | 208 | ### Left Outer Joins 209 | Running suite Left Outer Joins Large... 210 | - Hash Join x 4.92 ops/sec ±4.17% (17 runs sampled) 211 | - Sorted Merge Join x 17.43 ops/sec ±4.73% (49 runs sampled) 212 | - Nested Loop Join x 4.26 ops/sec ±5.10% (15 runs sampled) 213 | - Fastest is Sorted Merge Join 214 | 215 | Running suite Left Outer Joins Medium... 216 | - Hash Join x 3,854 ops/sec ±2.16% (88 runs sampled) 217 | - Sorted Merge Join x 3,108 ops/sec ±0.58% (97 runs sampled) 218 | - Nested Loop Join x 2,339 ops/sec ±0.98% (96 runs sampled) 219 | - Fastest is Hash Join 220 | 221 | Running suite Left Outer Joins Small... 222 | - Hash Join x 200,502 ops/sec ±1.04% (96 runs sampled) 223 | - Sorted Merge Join x 119,723 ops/sec ±0.44% (95 runs sampled) 224 | - Nested Loop Join x 165,741 ops/sec ±0.34% (93 runs sampled) 225 | - Fastest is Hash Join 226 | 227 | #### Left Semi Joins 228 | Running suite Left Semi Joins Large... 229 | - Hash Join x 13,379 ops/sec ±1.92% (89 runs sampled) 230 | - Sorted Merge Join x 2,414 ops/sec ±1.10% (94 runs sampled) 231 | - Nested Loop Join x 22,084 ops/sec ±1.26% (93 runs sampled) 232 | - Fastest is Nested Loop Join 233 | 234 | Running suite Left Semi Joins Medium... 235 | - Hash Join x 147,064 ops/sec ±0.47% (95 runs sampled) 236 | - Sorted Merge Join x 31,176 ops/sec ±0.60% (95 runs sampled) 237 | - Nested Loop Join x 99,841 ops/sec ±2.38% (90 runs sampled) 238 | - Fastest is Hash Join 239 | 240 | Running suite Left Semi Joins Small... 241 | - Hash Join x 1,301,283 ops/sec ±1.51% (94 runs sampled) 242 | - Sorted Merge Join x 409,401 ops/sec ±0.26% (96 runs sampled) 243 | - Nested Loop Join x 1,749,187 ops/sec ±0.92% (95 runs sampled) 244 | - Fastest is Nested Loop Join 245 | 246 | 247 | ## Future 248 | * Support for smart picking join implementations 249 | * Rework Hash join object comparisons 250 | * More tests! 251 | -------------------------------------------------------------------------------- /benchmark/joins.ts: -------------------------------------------------------------------------------- 1 | import {Event, Suite} from 'benchmark'; 2 | import Chance from 'chance'; 3 | import assign from 'lodash/assign'; 4 | 5 | import { 6 | Join, 7 | hashLeftSemiJoin, 8 | hashLeftOuterJoin, 9 | hashLeftAntiJoin, 10 | hashInnerJoin, 11 | hashFullOuterJoin, 12 | nestedLoopLeftSemiJoin, 13 | nestedLoopLeftOuterJoin, 14 | nestedLoopLeftAntiJoin, 15 | nestedLoopInnerJoin, 16 | nestedLoopFullOuterJoin, 17 | sortedMergeLeftSemiJoin, 18 | sortedMergeLeftOuterJoin, 19 | sortedMergeLeftAntiJoin, 20 | sortedMergeInnerJoin, 21 | sortedMergeFullOuterJoin 22 | } from '../lib'; 23 | 24 | interface Row { 25 | id: string; 26 | } 27 | 28 | type BenchJoin = Join; 29 | 30 | interface ChanceExtended extends Chance.Chance { 31 | row(): Row; 32 | } 33 | 34 | /** 35 | * Generate a join bench test. 36 | */ 37 | export default function joinBench ( 38 | name: string, 39 | size: number, 40 | hashJoin: BenchJoin, 41 | sortedMergeJoin: BenchJoin, 42 | nestedLoopJoin: BenchJoin 43 | ): Suite { 44 | const chance: ChanceExtended = new Chance() as ChanceExtended; 45 | chance.mixin({row: (): Row => ({id: chance.character({pool: 'aeiouy'})})}); 46 | const left: Row[] = chance.n(chance.row, size), 47 | right: Row[] = chance.n(chance.row, size), 48 | accessor = (obj: Row): string => obj.id, 49 | merger = (a: Row, b: Row): Row => assign({}, a, b); 50 | return (new Suite(name)) 51 | .add('Hash Join', (): Row[] => hashJoin(left, accessor, right, accessor, merger)) 52 | .add('Sorted Merge Join', (): Row[] => sortedMergeJoin(left, accessor, right, accessor, merger)) 53 | .add('Nested Loop Join', (): Row[] => nestedLoopJoin(left, accessor, right, accessor, merger)) 54 | .on('start', function () { 55 | console.log(`Running suite ${name}...`); 56 | }) 57 | .on('cycle', function(this: Suite, event: Event) { 58 | console.log(String(event.target)); 59 | }) 60 | .on('complete', function(this: Suite) { 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 62 | console.log('Fastest is ' + this.filter('fastest').map((d: any) => d.name)); 63 | console.log('\n'); 64 | }); 65 | } 66 | 67 | const joinTests: {name: string; joins: [BenchJoin, BenchJoin, BenchJoin]}[] = [ 68 | { 69 | name: 'Full Outer Joins', 70 | joins: [hashFullOuterJoin, sortedMergeFullOuterJoin, nestedLoopFullOuterJoin] 71 | }, 72 | { 73 | name: 'Inner Joins', 74 | joins: [hashInnerJoin, sortedMergeInnerJoin, nestedLoopInnerJoin] 75 | }, 76 | { 77 | name: 'Left Anti Joins', 78 | joins: [hashLeftAntiJoin, sortedMergeLeftAntiJoin, nestedLoopLeftAntiJoin] 79 | }, 80 | { 81 | name: 'Left Outer Joins', 82 | joins: [hashLeftOuterJoin, sortedMergeLeftOuterJoin, nestedLoopLeftOuterJoin] 83 | }, 84 | { 85 | name: 'Left Semi Joins', 86 | joins: [hashLeftSemiJoin, sortedMergeLeftSemiJoin, nestedLoopLeftSemiJoin] 87 | } 88 | ]; 89 | 90 | const sizeTests: {name: string; size: number}[] = [ 91 | {name: 'Large', size: 1000}, 92 | {name: 'Medium', size: 100}, 93 | {name: 'Small', size: 10} 94 | ]; 95 | 96 | for (const joinTest of joinTests) { 97 | for (const sizeTest of sizeTests) { 98 | joinBench( 99 | `${joinTest.name} ${sizeTest.name}`, 100 | sizeTest.size, 101 | ...joinTest.joins 102 | ).run(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /debug/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /debug/index.ts: -------------------------------------------------------------------------------- 1 | import Chance from 'chance'; 2 | import assign from 'lodash/assign'; 3 | import { 4 | hashLeftOuterJoin, 5 | sortedMergeFullOuterJoin, 6 | nestedLoopLeftOuterJoin 7 | } from '../index'; 8 | 9 | interface Row { 10 | date: Date; 11 | vowels: string; 12 | county: string; 13 | age: number; 14 | bool: boolean; 15 | float: number; 16 | integer: number; 17 | positiveInteger: number; 18 | string: string; 19 | } 20 | 21 | interface ChanceExtended extends Chance.Chance { 22 | row(): Row; 23 | } 24 | 25 | // Test Data 26 | const chance: ChanceExtended = new Chance() as ChanceExtended; 27 | chance.mixin({row: () => ({ 28 | date: chance.date({year: 2016}), 29 | vowels: chance.character({pool: 'aeiouy'}), 30 | county: chance.country(), 31 | age: chance.age({type: 'adult'}), 32 | bool: chance.bool(), 33 | float: chance.floating({min: -25, max: 25}), 34 | integer: chance.integer({min: -25, max: 25}), 35 | positiveInteger: chance.integer({min: 0, max: 25}), 36 | string: chance.string({length: 4}) 37 | })}); 38 | 39 | const left: Row[] = chance.n(chance.row, 100); 40 | const right: Row[] = chance.n(chance.row, 100); 41 | const accessor = (d: Row): number => d.age; 42 | const merger = (a: Row, b: Row): Row => assign({}, a, b); 43 | 44 | let results: Row[] = hashLeftOuterJoin(left, accessor, right, accessor, merger); 45 | console.log(results); 46 | results = sortedMergeFullOuterJoin(left, accessor, right, accessor, merger); 47 | console.log(results); 48 | results = nestedLoopLeftOuterJoin(left, accessor, right, accessor, merger); 49 | console.log(results); 50 | -------------------------------------------------------------------------------- /index.lodash.d.ts: -------------------------------------------------------------------------------- 1 | import { LoDashStatic } from 'lodash'; 2 | 3 | export = _; 4 | export as namespace _; 5 | 6 | declare var _: _.LoDashJoinsStatic; 7 | 8 | declare namespace _ { 9 | export interface LoDashJoinsStatic extends LoDashStatic { 10 | cartesianProduct: ICartesianProduct; 11 | 12 | hashFullOuterJoin: IOuterJoin; 13 | hashInnerJoin: IInnerJoin; 14 | hashLeftAntiJoin: INonMergeLeftJoin; 15 | hashLeftOuterJoin: IMergeLeftJoin; 16 | hashLeftSemiJoin: INonMergeLeftJoin; 17 | hashRightAntiJoin: INonMergeRightJoin; 18 | hashRightOuterJoin: IMergeRightJoin; 19 | hashRightSemiJoin: INonMergeRightJoin; 20 | 21 | nestedLoopFullOuterJoin: IOuterJoin; 22 | nestedLoopInnerJoin: IInnerJoin; 23 | nestedLoopLeftAntiJoin: INonMergeLeftJoin; 24 | nestedLoopLeftOuterJoin: IMergeLeftJoin; 25 | nestedLoopLeftSemiJoin: INonMergeLeftJoin; 26 | nestedLoopRightAntiJoin: INonMergeRightJoin; 27 | nestedLoopRightOuterJoin: IMergeRightJoin; 28 | nestedLoopRightSemiJoin: INonMergeRightJoin; 29 | 30 | sortedMergeFullOuterJoin: IOuterJoin; 31 | sortedMergeInnerJoin: IInnerJoin; 32 | sortedMergeLeftAntiJoin: INonMergeLeftJoin; 33 | sortedMergeLeftOuterJoin: IMergeLeftJoin; 34 | sortedMergeLeftSemiJoin: INonMergeLeftJoin; 35 | sortedMergeRightAntiJoin: INonMergeRightJoin; 36 | sortedMergeRightOuterJoin: IMergeRightJoin; 37 | sortedMergeRightSemiJoin: INonMergeRightJoin; 38 | } 39 | 40 | /** 41 | * An accessor is a function that returns a coercive value from an item. Coercive 42 | * items include: 43 | * - Primitives (boolean, number, string) 44 | * - Dates 45 | * - Objects that implement .valueOf() 46 | * - Arrays of the previous 3 47 | * 48 | * Key comparisons within the library are performed using the following trick: 49 | * 50 | * const equals = a <= b & a >= b; 51 | * 52 | * This is done because == and === are either not coercive or type converting: 53 | * @example 54 | * var x = ['a', 'b']; 55 | * var y = ['a', 'c']; 56 | * var z = ['a', 'b']; 57 | * x == z // false 58 | * x === z // false 59 | * x <= z && x >= z // true (converts to String) 60 | * x <= y && x >= y //false 61 | * 62 | * x = new Date(); 63 | * y = new Date(x.getTime()); 64 | * x == y // false 65 | * x === y // false 66 | * x <= y && x >= y // true (converts to Integer) 67 | */ 68 | export interface IAccessor extends Function { 69 | (a: TObject): TValueOf 70 | } 71 | 72 | export interface IMerger extends Function { 73 | (left: TLeft, right: TRight): TMergeResult; 74 | } 75 | 76 | export interface ICartesianProduct extends Function { 77 | ( 78 | ...arrays: any[][] 79 | ): any[][] 80 | } 81 | 82 | export interface SelfJoin extends Function { 83 | ( 84 | left: TLeft[], 85 | leftAccessor: IAccessor 86 | ): TLeft[]; 87 | } 88 | 89 | export interface IOuterJoin extends SelfJoin { 90 | ( 91 | left: TLeft[], 92 | accessor: IAccessor, 93 | right: TRight[] 94 | ): (TLeft | TRight | TLeft & TRight)[]; 95 | 96 | ( 97 | left: TLeft[], 98 | leftAccessor: IAccessor, 99 | right: TRight[], 100 | rightAccessor: IAccessor 101 | ): (TLeft | TRight | TLeft & TRight)[]; 102 | 103 | ( 104 | left: TLeft[], 105 | leftAccessor: IAccessor, 106 | right: TRight[], 107 | rightAccessor: IAccessor, 108 | merger: IMerger 109 | ): TMergeResult[]; 110 | } 111 | 112 | export interface IInnerJoin extends SelfJoin { 113 | ( 114 | left: TLeft[], 115 | accessor: IAccessor, 116 | right: TRight[] 117 | ): (TLeft & TRight)[]; 118 | 119 | ( 120 | left: TLeft[], 121 | leftAccessor: IAccessor, 122 | right: TRight[], 123 | rightAccessor: IAccessor 124 | ): (TLeft & TRight)[]; 125 | 126 | ( 127 | left: TLeft[], 128 | leftAccessor: IAccessor, 129 | right: TRight[], 130 | rightAccessor: IAccessor, 131 | merger: IMerger 132 | ): TMergeResult[]; 133 | } 134 | 135 | export interface IMergeLeftJoin extends SelfJoin { 136 | ( 137 | left: TLeft[], 138 | accessor: IAccessor, 139 | right: TRight[] 140 | ): (TLeft | TLeft & TRight)[]; 141 | 142 | ( 143 | left: TLeft[], 144 | leftAccessor: IAccessor, 145 | right: TRight[], 146 | rightAccessor: IAccessor 147 | ): (TLeft | TLeft & TRight)[]; 148 | 149 | ( 150 | left: TLeft[], 151 | leftAccessor: IAccessor, 152 | right: TRight[], 153 | rightAccessor: IAccessor, 154 | merger: IMerger 155 | ): TMergeResult[]; 156 | } 157 | 158 | export interface IMergeRightJoin extends SelfJoin { 159 | ( 160 | left: TLeft[], 161 | accessor: IAccessor, 162 | right: TRight[] 163 | ): (TRight | TLeft & TRight)[]; 164 | 165 | ( 166 | left: TLeft[], 167 | leftAccessor: IAccessor, 168 | right: TRight[], 169 | rightAccessor: IAccessor 170 | ): (TRight | TLeft & TRight)[]; 171 | 172 | ( 173 | left: TLeft[], 174 | leftAccessor: IAccessor, 175 | right: TRight[], 176 | rightAccessor: IAccessor, 177 | merger: IMerger 178 | ): TMergeResult[]; 179 | } 180 | 181 | export interface INonMergeLeftJoin extends SelfJoin { 182 | ( 183 | left: TLeft[], 184 | accessor: IAccessor, 185 | right: TRight[] 186 | ): TLeft[]; 187 | 188 | ( 189 | left: TLeft[], 190 | leftAccessor: IAccessor, 191 | right: TRight[], 192 | rightAccessor: IAccessor 193 | ): TLeft[]; 194 | } 195 | 196 | export interface INonMergeRightJoin extends SelfJoin { 197 | ( 198 | left: TLeft[], 199 | accessor: IAccessor, 200 | right: TRight[] 201 | ): TRight[]; 202 | 203 | ( 204 | left: TLeft[], 205 | leftAccessor: IAccessor, 206 | right: TRight[], 207 | rightAccessor: IAccessor 208 | ): TRight[]; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /index.lodash.ts: -------------------------------------------------------------------------------- 1 | import {runInContext} from 'lodash'; 2 | import * as lib from './lib'; 3 | import {joinWrapper} from './lib/util'; 4 | 5 | const _ = runInContext(); 6 | _.mixin({ 7 | cartesianProduct: lib.cartesianProduct, 8 | hashFullOuterJoin: joinWrapper(lib.hashFullOuterJoin), 9 | hashInnerJoin: joinWrapper(lib.hashInnerJoin), 10 | hashLeftOuterJoin: joinWrapper(lib.hashLeftOuterJoin), 11 | hashLeftSemiJoin: joinWrapper(lib.hashLeftSemiJoin), 12 | hashLeftAntiJoin: joinWrapper(lib.hashLeftAntiJoin), 13 | hashRightOuterJoin: joinWrapper(lib.hashRightOuterJoin), 14 | hashRightSemiJoin: joinWrapper(lib.hashRightSemiJoin), 15 | hashRightAntiJoin: joinWrapper(lib.hashRightAntiJoin), 16 | sortedMergeFullOuterJoin: joinWrapper(lib.sortedMergeFullOuterJoin), 17 | sortedMergeInnerJoin: joinWrapper(lib.sortedMergeInnerJoin), 18 | sortedMergeLeftOuterJoin: joinWrapper(lib.sortedMergeLeftOuterJoin), 19 | sortedMergeLeftSemiJoin: joinWrapper(lib.sortedMergeLeftSemiJoin), 20 | sortedMergeLeftAntiJoin: joinWrapper(lib.sortedMergeLeftAntiJoin), 21 | sortedMergeRightOuterJoin: joinWrapper(lib.sortedMergeRightOuterJoin), 22 | sortedMergeRightSemiJoin: joinWrapper(lib.sortedMergeRightSemiJoin), 23 | sortedMergeRightAntiJoin: joinWrapper(lib.sortedMergeRightAntiJoin), 24 | nestedLoopFullOuterJoin: joinWrapper(lib.nestedLoopFullOuterJoin), 25 | nestedLoopInnerJoin: joinWrapper(lib.nestedLoopInnerJoin), 26 | nestedLoopLeftOuterJoin: joinWrapper(lib.nestedLoopLeftOuterJoin), 27 | nestedLoopLeftSemiJoin: joinWrapper(lib.nestedLoopLeftSemiJoin), 28 | nestedLoopLeftAntiJoin: joinWrapper(lib.nestedLoopLeftAntiJoin), 29 | nestedLoopRightOuterJoin: joinWrapper(lib.nestedLoopRightOuterJoin), 30 | nestedLoopRightSemiJoin: joinWrapper(lib.nestedLoopRightSemiJoin), 31 | nestedLoopRightAntiJoin: joinWrapper(lib.nestedLoopRightAntiJoin) 32 | }); 33 | export default _; 34 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | import {Config} from 'karma'; 2 | import find from 'lodash/find'; 3 | import {join} from 'path'; 4 | import {Configuration} from 'webpack'; 5 | 6 | import webpackConfig from './webpack.config'; 7 | 8 | export default function(config: Config): void { 9 | config.set({ 10 | browsers: ['ChromeHeadless'], 11 | client: {clearContext: false}, 12 | coverageIstanbulReporter: { 13 | dir: join(__dirname, './coverage'), 14 | reports: ['html', 'lcovonly', 'text-summary'], 15 | fixWebpackSourcePaths: true 16 | }, 17 | files: ['./node_modules/lodash/lodash.js', 'test.ts'], 18 | frameworks: ['jasmine'], 19 | preprocessors: {'test.ts': ['webpack', 'sourcemap']}, 20 | reporters: ['spec', 'coverage-istanbul'], 21 | restartOnFileChange: true, 22 | singleRun: true, 23 | webpack: find(webpackConfig, ({name}: Configuration) => name === 'karma'), 24 | webpackMiddleware: {noInfo: true} 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /lib/cartesianProduct.ts: -------------------------------------------------------------------------------- 1 | import flatten from 'lodash/flatten'; 2 | import map from 'lodash/map'; 3 | import reduce from 'lodash/reduce'; 4 | 5 | /** 6 | * Produce the cartesian product of multiple arrays. 7 | * @param {Array>} [arrays=[]] 8 | * @returns {Array} 9 | */ 10 | /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/consistent-type-assertions */ 11 | export default function cartesianProduct(...arrays: any[][]): any[][] { 12 | return arrays.length ? 13 | reduce(arrays, (a, b) => 14 | flatten(map(a, x => 15 | map(b, y => 16 | x.concat([y])))), 17 | [[]]) : 18 | []; 19 | } 20 | /* eslint-enable @typescript-eslint/no-explicit-any,@typescript-eslint/consistent-type-assertions */ 21 | -------------------------------------------------------------------------------- /lib/hash/hashFullOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import flatten from 'lodash/flatten'; 3 | import groupBy from 'lodash/groupBy'; 4 | import has from 'lodash/has'; 5 | import map from 'lodash/map'; 6 | import reduceRight from 'lodash/reduceRight'; 7 | import values from 'lodash/values'; 8 | 9 | import {Accessor, Merger} from '../typings'; 10 | import {toStringAccessor} from './util'; 11 | 12 | /** 13 | * Hash full outer join 14 | */ 15 | export default function hashFullOuterJoin( 16 | a: LeftRow[], 17 | aAccessor: Accessor, 18 | b: RightRow[], 19 | bAccessor: Accessor, 20 | merger: Merger 21 | ): MergeResult[] { 22 | if (a.length < 1 || b.length < 1) { 23 | return [ 24 | ...a.map((a: LeftRow) => merger(a, undefined)), 25 | ...b.map((b: RightRow) => merger(undefined, b)) 26 | ]; 27 | } 28 | const leftAccessor: Accessor = toStringAccessor(aAccessor), 29 | rightAccessor: Accessor = toStringAccessor(bAccessor), 30 | seen: {[key: string]: boolean} = {}; 31 | let index: {[key: string]: (LeftRow | RightRow)[]}, 32 | result: MergeResult[], 33 | key: string; 34 | if (a.length < b.length) { 35 | index = groupBy(a, leftAccessor); 36 | result = reduceRight(b, (previous: MergeResult[], bDatum: RightRow) => { 37 | seen[key = rightAccessor(bDatum)] = true; 38 | if (has(index, key)) { 39 | return map(index[key], (aDatum: LeftRow) => merger(aDatum, bDatum)).concat(previous); 40 | } 41 | previous.unshift(merger(undefined, bDatum)); 42 | return previous; 43 | }, []); 44 | return result.concat( 45 | map( 46 | flatten(values(filter( 47 | index, 48 | (val: (LeftRow | RightRow)[], key: string) => 49 | !has(seen, key)))), 50 | (aDatum: LeftRow) => 51 | merger(aDatum, undefined))); 52 | } 53 | index = groupBy(b, rightAccessor); 54 | result = reduceRight(a, (previous: MergeResult[], aDatum: LeftRow) => { 55 | seen[key = leftAccessor(aDatum)] = true; 56 | if (has(index, key)) { 57 | return map(index[key], (bDatum: RightRow) => merger(aDatum, bDatum)).concat(previous); 58 | } 59 | previous.unshift(merger(aDatum, undefined)); 60 | return previous; 61 | }, []); 62 | return result.concat( 63 | map( 64 | flatten(values(filter( 65 | index, 66 | (val: (LeftRow | RightRow)[], key: string) => 67 | !has(seen, key)))), 68 | (bDatum: RightRow) => 69 | merger(undefined, bDatum))); 70 | } 71 | -------------------------------------------------------------------------------- /lib/hash/hashInnerJoin.ts: -------------------------------------------------------------------------------- 1 | import groupBy from 'lodash/groupBy'; 2 | import has from 'lodash/has'; 3 | import map from 'lodash/map'; 4 | import reduceRight from 'lodash/reduceRight'; 5 | 6 | import {Accessor, Merger} from '../typings'; 7 | import {toStringAccessor} from './util'; 8 | 9 | /** 10 | * Hash inner join 11 | */ 12 | export default function hashInnerJoin( 13 | a: LeftRow[], 14 | aAccessor: Accessor, 15 | b: RightRow[], 16 | bAccessor: Accessor, 17 | merger: Merger 18 | ): MergeResult[] { 19 | if (a.length < 1 || b.length < 1) { 20 | return []; 21 | } 22 | const leftAccessor: Accessor = toStringAccessor(aAccessor), 23 | rightAccessor: Accessor = toStringAccessor(bAccessor); 24 | let index: {[key: string]: (LeftRow | RightRow)[]}, 25 | key: string; 26 | if (a.length < b.length) { 27 | index = groupBy(a, leftAccessor); 28 | return reduceRight(b, (previous: MergeResult[], bDatum: RightRow) => { 29 | key = rightAccessor(bDatum); 30 | if (has(index, key)) { 31 | return map(index[key], (aDatum: LeftRow) => merger(aDatum, bDatum)).concat(previous); 32 | } 33 | return previous; 34 | }, []); 35 | } 36 | index = groupBy(b, rightAccessor); 37 | return reduceRight(a, (previous: MergeResult[], aDatum: LeftRow) => { 38 | key = leftAccessor(aDatum); 39 | if (has(index, key)) { 40 | return map(index[key], (bDatum: RightRow) => merger(aDatum, bDatum)).concat(previous); 41 | } 42 | return previous; 43 | }, []); 44 | } 45 | -------------------------------------------------------------------------------- /lib/hash/hashLeftAntiJoin.ts: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import has from 'lodash/has'; 3 | import keyBy from 'lodash/keyBy'; 4 | 5 | import {Accessor} from '../typings'; 6 | import {toStringAccessor} from './util'; 7 | 8 | /** 9 | * Hash left anti join 10 | */ 11 | export default function hashLeftAntiJoin( 12 | a: LeftRow[], 13 | aAccessor: Accessor, 14 | b: RightRow[], 15 | bAccessor: Accessor 16 | ): LeftRow[] { 17 | if (a.length < 1 || b.length < 1) { 18 | return a; 19 | } 20 | const index: {[key: string]: RightRow} = keyBy(b, toStringAccessor(bAccessor)), 21 | leftAccessor: Accessor = toStringAccessor(aAccessor); 22 | return filter(a, (aDatum: LeftRow) => !has(index, leftAccessor(aDatum))); 23 | } 24 | -------------------------------------------------------------------------------- /lib/hash/hashLeftOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import flatten from 'lodash/flatten'; 3 | import groupBy from 'lodash/groupBy'; 4 | import has from 'lodash/has'; 5 | import map from 'lodash/map'; 6 | import reduceRight from 'lodash/reduceRight'; 7 | import values from 'lodash/values'; 8 | 9 | import {Accessor, Merger} from '../typings'; 10 | import {toStringAccessor} from './util'; 11 | 12 | /** 13 | * Hash left outer join 14 | */ 15 | export default function hashLeftOuterJoin( 16 | a: LeftRow[], 17 | aAccessor: Accessor, 18 | b: RightRow[], 19 | bAccessor: Accessor, 20 | merger: Merger 21 | ): MergeResult[] { 22 | if (a.length < 1 || b.length < 1) { 23 | return map(a, (a: LeftRow) => merger(a, undefined)); 24 | } 25 | const leftAccessor: Accessor = toStringAccessor(aAccessor), 26 | rightAccessor: Accessor = toStringAccessor(bAccessor); 27 | let index: {[key: string]: (LeftRow | RightRow)[]}, 28 | key: string; 29 | if (a.length < b.length) { 30 | const seen: {[key: string]: boolean} = {}; 31 | index = groupBy(a, leftAccessor); 32 | return reduceRight(b, (previous: MergeResult[], bDatum: RightRow) => { 33 | seen[key = rightAccessor(bDatum)] = true; 34 | if (has(index, key)) { 35 | return map(index[key], (aDatum: LeftRow) => merger(aDatum, bDatum)).concat(previous); 36 | } 37 | return previous; 38 | }, []).concat( 39 | map( 40 | flatten(values(filter( 41 | index, 42 | (val: (LeftRow | RightRow)[], key: string) => 43 | !has(seen, key)))), 44 | (aDatum: LeftRow) => merger(aDatum, undefined))); 45 | } 46 | index = groupBy(b, rightAccessor); 47 | return reduceRight(a, (previous: MergeResult[], aDatum: LeftRow) => { 48 | key = leftAccessor(aDatum); 49 | if (has(index, key)) { 50 | return map(index[key], (bDatum: RightRow) => merger(aDatum, bDatum)).concat(previous); 51 | } 52 | previous.unshift(merger(aDatum, undefined)); 53 | return previous; 54 | }, []); 55 | } 56 | -------------------------------------------------------------------------------- /lib/hash/hashLeftSemiJoin.ts: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import has from 'lodash/has'; 3 | import keyBy from 'lodash/keyBy'; 4 | 5 | import {Accessor} from '../typings'; 6 | import {toStringAccessor} from './util'; 7 | 8 | /** 9 | * Hash left semi join 10 | */ 11 | export default function hashLeftSemiJoin( 12 | a: LeftRow[], 13 | aAccessor: Accessor, 14 | b: RightRow[], 15 | bAccessor: Accessor 16 | ): LeftRow[] { 17 | if (a.length < 1 || b.length < 1) { 18 | return []; 19 | } 20 | const index: {[key: string]: RightRow} = keyBy(b, toStringAccessor(bAccessor)), 21 | leftAccessor: Accessor = toStringAccessor(aAccessor); 22 | return filter(a, (aDatum: LeftRow) => has(index, leftAccessor(aDatum))); 23 | } 24 | -------------------------------------------------------------------------------- /lib/hash/hashRightAntiJoin.ts: -------------------------------------------------------------------------------- 1 | import hashLeftAntiJoin from './hashLeftAntiJoin'; 2 | 3 | import {Accessor} from '../typings'; 4 | 5 | /** 6 | * Hash right anti join 7 | */ 8 | export default function hashRightAntiJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor 13 | ): RightRow[] { 14 | return hashLeftAntiJoin(b, bAccessor, a, aAccessor); 15 | } 16 | -------------------------------------------------------------------------------- /lib/hash/hashRightOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import hashLeftOuterJoin from './hashLeftOuterJoin'; 2 | 3 | import {Accessor, Merger} from '../typings'; 4 | 5 | /** 6 | * Hash right outer join 7 | */ 8 | export default function hashRightOuterJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor, 13 | merger: Merger 14 | ): MergeResult[] { 15 | return hashLeftOuterJoin(b, bAccessor, a, aAccessor, merger); 16 | } 17 | -------------------------------------------------------------------------------- /lib/hash/hashRightSemiJoin.ts: -------------------------------------------------------------------------------- 1 | import hashLeftSemiJoin from './hashLeftSemiJoin'; 2 | 3 | import {Accessor} from '../typings'; 4 | 5 | /** 6 | * Hash right semi join 7 | */ 8 | export default function hashRightSemiJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor 13 | ): RightRow[] { 14 | return hashLeftSemiJoin(b, bAccessor, a, aAccessor); 15 | } 16 | -------------------------------------------------------------------------------- /lib/hash/index.ts: -------------------------------------------------------------------------------- 1 | import hashFullOuterJoin from './hashFullOuterJoin'; 2 | import hashInnerJoin from './hashInnerJoin'; 3 | import hashLeftAntiJoin from './hashLeftAntiJoin'; 4 | import hashLeftOuterJoin from './hashLeftOuterJoin'; 5 | import hashLeftSemiJoin from './hashLeftSemiJoin'; 6 | import hashRightAntiJoin from './hashRightAntiJoin'; 7 | import hashRightOuterJoin from './hashRightOuterJoin'; 8 | import hashRightSemiJoin from './hashRightSemiJoin'; 9 | 10 | export { 11 | hashFullOuterJoin, 12 | hashInnerJoin, 13 | hashLeftAntiJoin, 14 | hashLeftOuterJoin, 15 | hashLeftSemiJoin, 16 | hashRightAntiJoin, 17 | hashRightOuterJoin, 18 | hashRightSemiJoin 19 | }; 20 | -------------------------------------------------------------------------------- /lib/hash/util/index.ts: -------------------------------------------------------------------------------- 1 | import toStringAccessor from './toStringAccessor'; 2 | 3 | export { 4 | toStringAccessor 5 | }; 6 | -------------------------------------------------------------------------------- /lib/hash/util/toStringAccessor.ts: -------------------------------------------------------------------------------- 1 | import toString from 'lodash/toString'; 2 | 3 | import {Accessor} from '../../typings'; 4 | 5 | export default function toStringAccessor( 6 | accessor: Accessor 7 | ): Accessor { 8 | return (row: Row): string => toString(accessor(row)); 9 | } 10 | -------------------------------------------------------------------------------- /lib/index.spec.ts: -------------------------------------------------------------------------------- 1 | import assign from 'lodash/assign'; 2 | import * as joins from './index'; 3 | import {Join, Accessor, Merger, NonMergeJoin} from './typings'; 4 | 5 | interface Row { 6 | id: string; 7 | left?: number; 8 | right?: number; 9 | } 10 | 11 | function testJoins( 12 | groupName: string, 13 | fullOuterJoinFn: Join, 14 | innerJoinFn: Join, 15 | leftAntiJoinFn: NonMergeJoin, 16 | leftOuterJoinFn: Join, 17 | leftSemiJoinFn: NonMergeJoin, 18 | rightAntiJoinFn: NonMergeJoin, 19 | rightOuterJoinFn: Join, 20 | rightSemiJoinFn: NonMergeJoin 21 | ) { 22 | describe(groupName, () => { 23 | const left: Row[] = [ 24 | {id: 'c', left: 0}, 25 | {id: 'c', left: 1}, 26 | {id: 'e', left: 2} 27 | ]; 28 | const right: Row[] = [ 29 | {id: 'a', right: 0}, 30 | {id: 'b', right: 1}, 31 | {id: 'c', right: 2}, 32 | {id: 'c', right: 3}, 33 | {id: 'd', right: 4}, 34 | {id: 'f', right: 5}, 35 | {id: 'g', right: 6} 36 | ]; 37 | const accessor: Accessor = (obj: Row): string => obj.id; 38 | const merger: Merger = (l: Row, r: Row): Row => assign({}, l, r); 39 | describe(`#${fullOuterJoinFn.name}()`, () => { 40 | const expectedA = [ 41 | {id: 'a', right: 0}, 42 | {id: 'b', right: 1}, 43 | {id: 'c', left: 0, right: 2}, 44 | {id: 'c', left: 0, right: 3}, 45 | {id: 'c', left: 1, right: 2}, 46 | {id: 'c', left: 1, right: 3}, 47 | {id: 'd', right: 4}, 48 | {id: 'e', left: 2}, 49 | {id: 'f', right: 5}, 50 | {id: 'g', right: 6} 51 | ], 52 | expectedB = [ 53 | {id: 'a', right: 0}, 54 | {id: 'b', right: 1}, 55 | {id: 'c', right: 2, left: 0}, 56 | {id: 'c', right: 2, left: 1}, 57 | {id: 'c', right: 3, left: 0}, 58 | {id: 'c', right: 3, left: 1}, 59 | {id: 'd', right: 4}, 60 | {id: 'e', left: 2}, 61 | {id: 'f', right: 5}, 62 | {id: 'g', right: 6} 63 | ], 64 | resultA = fullOuterJoinFn(left, accessor, right, accessor, merger), 65 | resultB = fullOuterJoinFn(right, accessor, left, accessor, merger), 66 | resultC = fullOuterJoinFn([], accessor, [], accessor, merger), 67 | resultD = fullOuterJoinFn(left, accessor, [], accessor, merger), 68 | resultE = fullOuterJoinFn([], accessor, right, accessor, merger), 69 | resultF = fullOuterJoinFn([left[0]], accessor, [right[0]], accessor, merger); 70 | it('should return 10 rows if parent is left', () => 71 | expect(resultA.length).toBe(10)); 72 | it('should match the expected output if parent is left', () => 73 | expect(resultA).toEqual(jasmine.arrayWithExactContents(expectedA))); 74 | it('should return 8 rows if parent is right', () => 75 | expect(resultB.length).toBe(10)); 76 | it('should match the expected output if parent is right', () => 77 | expect(resultB).toEqual(jasmine.arrayWithExactContents(expectedB))); 78 | it('should return empty results for empty input', () => 79 | expect(resultC.length).toBe(0)); 80 | it('should return just the left side if empty right side', () => 81 | expect(resultD.length).toBe(left.length)); 82 | it('should return just the right side if empty left side', () => 83 | expect(resultE.length).toBe(right.length)); 84 | it('should yield just 2 results', () => 85 | expect(resultF.length).toBe(2)); 86 | }); 87 | describe(`#${innerJoinFn.name}()`, () => { 88 | const expectedA = [ 89 | {id: 'c', left: 0, right: 2}, 90 | {id: 'c', left: 0, right: 3}, 91 | {id: 'c', left: 1, right: 2}, 92 | {id: 'c', left: 1, right: 3} 93 | ], 94 | expectedB = [ 95 | {id: 'c', right: 2, left: 0}, 96 | {id: 'c', right: 2, left: 1}, 97 | {id: 'c', right: 3, left: 0}, 98 | {id: 'c', right: 3, left: 1} 99 | ], 100 | resultA = innerJoinFn(left, accessor, right, accessor, merger), 101 | resultB = innerJoinFn(right, accessor, left, accessor, merger), 102 | resultC = innerJoinFn([], accessor, right, accessor, merger); 103 | it('should return 5 rows if parent is left', () => 104 | expect(resultA.length).toBe(4)); 105 | it('should match the expected output if parent is left', () => 106 | expect(resultA).toEqual(jasmine.arrayWithExactContents(expectedA))); 107 | it('should return 5 rows if parent is right', () => 108 | expect(resultB.length).toBe(4)); 109 | it('should match the expected output if parent is right', () => 110 | expect(resultB).toEqual(jasmine.arrayWithExactContents(expectedB))); 111 | it('should return empty results for empty input', () => 112 | expect(resultC.length).toBe(0)); 113 | }); 114 | describe(`#${leftAntiJoinFn.name}()`, () => { 115 | const expected = [ 116 | {id: 'e', left: 2} 117 | ], 118 | result = leftAntiJoinFn(left, accessor, right, accessor), 119 | resultB = leftAntiJoinFn([], accessor, right, accessor); 120 | it('should return 1 rows', () => 121 | expect(result.length).toBe(1)); 122 | it('should match the expected output', () => 123 | expect(result).toEqual(expected)); 124 | it('should return empty results for empty input', () => 125 | expect(resultB.length).toBe(0)); 126 | }); 127 | describe(`#${leftOuterJoinFn.name}()`, () => { 128 | const expected = [ 129 | {id: 'c', left: 0, right: 2}, 130 | {id: 'c', left: 0, right: 3}, 131 | {id: 'c', left: 1, right: 2}, 132 | {id: 'c', left: 1, right: 3}, 133 | {id: 'e', left: 2} 134 | ], 135 | result = leftOuterJoinFn(left, accessor, right, accessor, merger), 136 | resultB = leftOuterJoinFn([], accessor, right, accessor, merger), 137 | resultC = leftOuterJoinFn(left, accessor, [], accessor, merger); 138 | it('should return 5 rows', () => 139 | expect(result.length).toBe(5)); 140 | it('should match the expected output', () => 141 | expect(result).toEqual(jasmine.arrayWithExactContents(expected))); 142 | it('should return empty results for empty input', () => 143 | expect(resultB.length).toBe(0)); 144 | it('should return just the left side if empty right side', () => 145 | expect(resultC.length).toBe(left.length)); 146 | }); 147 | describe(`#${leftSemiJoinFn.name}()`, () => { 148 | const expected = [ 149 | {id: 'c', left: 0}, 150 | {id: 'c', left: 1} 151 | ], 152 | result = leftSemiJoinFn(left, accessor, right, accessor), 153 | resultB = leftSemiJoinFn([], accessor, right, accessor); 154 | it('should return 2 rows', () => 155 | expect(result.length).toBe(2)); 156 | it('should match the expected output', () => 157 | expect(result).toEqual(expected)); 158 | it('should return empty results for empty input', () => 159 | expect(resultB.length).toBe(0)); 160 | }); 161 | describe(`#${rightAntiJoinFn.name}()`, () => { 162 | const expected = [ 163 | {id: 'a', right: 0}, 164 | {id: 'b', right: 1}, 165 | {id: 'd', right: 4}, 166 | {id: 'f', right: 5}, 167 | {id: 'g', right: 6} 168 | ], 169 | result = rightAntiJoinFn(left, accessor, right, accessor), 170 | resultB = rightAntiJoinFn(left, accessor, [], accessor); 171 | it('should return 5 rows', () => 172 | expect(result.length).toBe(5)); 173 | it('should match the expected output', () => 174 | expect(result).toEqual(expected)); 175 | it('should match the left anti join with right as the parent', () => 176 | expect(leftAntiJoinFn(right, accessor, left, accessor)).toEqual(result)); 177 | it('should return empty results for empty input', () => 178 | expect(resultB.length).toBe(0)); 179 | }); 180 | describe(`#${rightOuterJoinFn.name}()`, () => { 181 | const expected = [ 182 | {id: 'a', right: 0}, 183 | {id: 'b', right: 1}, 184 | {id: 'c', right: 2, left: 0}, 185 | {id: 'c', right: 2, left: 1}, 186 | {id: 'c', right: 3, left: 0}, 187 | {id: 'c', right: 3, left: 1}, 188 | {id: 'd', right: 4}, 189 | {id: 'f', right: 5}, 190 | {id: 'g', right: 6} 191 | ], 192 | result = rightOuterJoinFn(left, accessor, right, accessor, merger), 193 | resultB = rightOuterJoinFn(left, accessor, [], accessor, merger); 194 | it('should return 9 rows', () => 195 | expect(result.length).toBe(9)); 196 | it('should match the expected output', () => 197 | expect(result).toEqual(jasmine.arrayWithExactContents(expected))); 198 | it('should match the left outer join with right as the parent', () => 199 | expect(leftOuterJoinFn(right, accessor, left, accessor, merger)).toEqual(result)); 200 | it('should return empty results for empty input', () => 201 | expect(resultB.length).toBe(0)); 202 | }); 203 | describe(`#${rightSemiJoinFn.name}()`, () => { 204 | const expected = [ 205 | {id: 'c', right: 2}, 206 | {id: 'c', right: 3} 207 | ], 208 | result = rightSemiJoinFn(left, accessor, right, accessor), 209 | resultB = rightSemiJoinFn(left, accessor, [], accessor); 210 | it('should return 2 rows', () => 211 | expect(result.length).toBe(2)); 212 | it('should match the expected output', () => 213 | expect(result).toEqual(expected)); 214 | it('should match the left semi join with right as the parent', () => 215 | expect(leftSemiJoinFn(right, accessor, left, accessor)).toEqual(result)); 216 | it('should return empty results for empty input', () => 217 | expect(resultB.length).toBe(0)); 218 | }); 219 | }); 220 | } 221 | 222 | testJoins( 223 | 'Hash Joins', 224 | joins.hashFullOuterJoin, 225 | joins.hashInnerJoin, 226 | joins.hashLeftAntiJoin, 227 | joins.hashLeftOuterJoin, 228 | joins.hashLeftSemiJoin, 229 | joins.hashRightAntiJoin, 230 | joins.hashRightOuterJoin, 231 | joins.hashRightSemiJoin, 232 | ); 233 | testJoins( 234 | 'Nested Loop Joins', 235 | joins.nestedLoopFullOuterJoin, 236 | joins.nestedLoopInnerJoin, 237 | joins.nestedLoopLeftAntiJoin, 238 | joins.nestedLoopLeftOuterJoin, 239 | joins.nestedLoopLeftSemiJoin, 240 | joins.nestedLoopRightAntiJoin, 241 | joins.nestedLoopRightOuterJoin, 242 | joins.nestedLoopRightSemiJoin, 243 | ); 244 | testJoins( 245 | 'Sorted Merge Joins', 246 | joins.sortedMergeFullOuterJoin, 247 | joins.sortedMergeInnerJoin, 248 | joins.sortedMergeLeftAntiJoin, 249 | joins.sortedMergeLeftOuterJoin, 250 | joins.sortedMergeLeftSemiJoin, 251 | joins.sortedMergeRightAntiJoin, 252 | joins.sortedMergeRightOuterJoin, 253 | joins.sortedMergeRightSemiJoin 254 | ); 255 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import cartesianProduct from './cartesianProduct'; 2 | export {cartesianProduct}; 3 | export * from './hash'; 4 | export * from './nestedLoop'; 5 | export * from './sortedMerge'; 6 | export * from './typings'; 7 | -------------------------------------------------------------------------------- /lib/nestedLoop/index.ts: -------------------------------------------------------------------------------- 1 | import nestedLoopFullOuterJoin from './nestedLoopFullOuterJoin'; 2 | import nestedLoopInnerJoin from './nestedLoopInnerJoin'; 3 | import nestedLoopLeftAntiJoin from './nestedLoopLeftAntiJoin'; 4 | import nestedLoopLeftOuterJoin from './nestedLoopLeftOuterJoin'; 5 | import nestedLoopLeftSemiJoin from './nestedLoopLeftSemiJoin'; 6 | import nestedLoopRightAntiJoin from './nestedLoopRightAntiJoin'; 7 | import nestedLoopRightOuterJoin from './nestedLoopRightOuterJoin'; 8 | import nestedLoopRightSemiJoin from './nestedLoopRightSemiJoin'; 9 | 10 | export { 11 | nestedLoopFullOuterJoin, 12 | nestedLoopInnerJoin, 13 | nestedLoopLeftAntiJoin, 14 | nestedLoopLeftOuterJoin, 15 | nestedLoopLeftSemiJoin, 16 | nestedLoopRightAntiJoin, 17 | nestedLoopRightOuterJoin, 18 | nestedLoopRightSemiJoin 19 | }; 20 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopFullOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import has from 'lodash/has'; 3 | import map from 'lodash/map'; 4 | import reduceRight from 'lodash/reduceRight'; 5 | 6 | import {Accessor, Merger} from '../typings'; 7 | 8 | /** 9 | * Nested loop left semi join 10 | */ 11 | export default function nestedLoopFullOuterJoin( 12 | a: LeftRow[], 13 | aAccessor: Accessor, 14 | b: RightRow[], 15 | bAccessor: Accessor, 16 | merger: Merger 17 | ): MergeResult[] { 18 | if (a.length < 1 || b.length < 1) { 19 | return [ 20 | ...a.map((a: LeftRow) => merger(a, undefined)), 21 | ...b.map((b: RightRow) => merger(undefined, b)) 22 | ]; 23 | } 24 | const seen: {[index: number]: boolean} = {}; 25 | let key: Key, 26 | otherKey: Key, 27 | tmpLength: number, 28 | output: MergeResult[]; 29 | if (a.length < b.length) { 30 | return reduceRight(a, (previous: MergeResult[], aDatum: LeftRow) => { 31 | key = aAccessor(aDatum); 32 | tmpLength = previous.length; 33 | output = reduceRight(b, (oPrevious: MergeResult[], bDatum: RightRow, bIndex: number) => { 34 | otherKey = bAccessor(bDatum); 35 | if (key <= otherKey && key >= otherKey) { 36 | seen[bIndex] = true; 37 | oPrevious.unshift(merger(aDatum, bDatum)); 38 | } 39 | return oPrevious; 40 | }, []).concat(previous); 41 | if (tmpLength === output.length) { 42 | output.unshift(merger(aDatum, undefined)); 43 | } 44 | return output; 45 | }, []).concat( 46 | map( 47 | filter(b, (bDatum: RightRow, bIndex: number) => !has(seen, bIndex)), 48 | (bDatum: RightRow) => merger(undefined, bDatum))); 49 | } 50 | return reduceRight(b, (previous: MergeResult[], bDatum: RightRow) => { 51 | key = bAccessor(bDatum); 52 | tmpLength = previous.length; 53 | output = reduceRight(a, (oPrevious: MergeResult[], aDatum: LeftRow, aIndex: number) => { 54 | otherKey = aAccessor(aDatum); 55 | if (key <= otherKey && key >= otherKey) { 56 | seen[aIndex] = true; 57 | oPrevious.unshift(merger(aDatum, bDatum)); 58 | } 59 | return oPrevious; 60 | }, []).concat(previous); 61 | if (tmpLength === output.length) { 62 | output.unshift(merger(undefined, bDatum)); 63 | } 64 | return output; 65 | }, []).concat( 66 | map( 67 | filter(a, (aDatum: LeftRow, aIndex: number) => !has(seen, aIndex)), 68 | (aDatum: LeftRow) => merger(aDatum, undefined))); 69 | } 70 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopInnerJoin.ts: -------------------------------------------------------------------------------- 1 | import reduceRight from 'lodash/reduceRight'; 2 | 3 | import {Accessor, Merger} from '../typings'; 4 | 5 | /** 6 | * Nested loop inner join 7 | */ 8 | export default function nestedLoopInnerJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor, 13 | merger: Merger 14 | ): MergeResult[] { 15 | if (a.length < 1 || b.length < 1) { 16 | return []; 17 | } 18 | let key: Key, 19 | otherKey: Key; 20 | if (a.length < b.length) { 21 | return reduceRight(a, (previous: MergeResult[], aDatum: LeftRow) => { 22 | key = aAccessor(aDatum); 23 | return reduceRight(b, (oPrevious: MergeResult[], bDatum: RightRow) => { 24 | otherKey = bAccessor(bDatum); 25 | if (key <= otherKey && key >= otherKey) { 26 | oPrevious.unshift(merger(aDatum, bDatum)); 27 | } 28 | return oPrevious; 29 | }, []).concat(previous); 30 | }, []); 31 | } 32 | return reduceRight(b, (previous: MergeResult[], bDatum: RightRow) => { 33 | key = bAccessor(bDatum); 34 | return reduceRight(a, (oPrevious: MergeResult[], aDatum: LeftRow) => { 35 | otherKey = aAccessor(aDatum); 36 | if (key <= otherKey && key >= otherKey) { 37 | oPrevious.unshift(merger(aDatum, bDatum)); 38 | } 39 | return oPrevious; 40 | }, []).concat(previous); 41 | }, []); 42 | } 43 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopLeftAntiJoin.ts: -------------------------------------------------------------------------------- 1 | import every from 'lodash/every'; 2 | import filter from 'lodash/filter'; 3 | 4 | import {Accessor} from '../typings'; 5 | 6 | /** 7 | * Nested loop left anti join 8 | */ 9 | export default function nestedLoopLeftAntiJoin( 10 | a: LeftRow[], 11 | aAccessor: Accessor, 12 | b: RightRow[], 13 | bAccessor: Accessor 14 | ): LeftRow[] { 15 | if (a.length < 1 || b.length < 1) { 16 | return a; 17 | } 18 | let key: Key, 19 | otherKey: Key; 20 | return filter(a, (aDatum: LeftRow) => { 21 | key = aAccessor(aDatum); 22 | return every(b, (bDatum: RightRow) => { 23 | otherKey = bAccessor(bDatum); 24 | return !(key <= otherKey && key >= otherKey); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopLeftOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import has from 'lodash/has'; 3 | import map from 'lodash/map'; 4 | import reduceRight from 'lodash/reduceRight'; 5 | 6 | import {Accessor, Merger} from '../typings'; 7 | 8 | /** 9 | * Nested loop left outer join 10 | */ 11 | export default function nestedLoopLeftOuterJoin( 12 | a: LeftRow[], 13 | aAccessor: Accessor, 14 | b: RightRow[], 15 | bAccessor: Accessor, 16 | merger: Merger 17 | ): MergeResult[] { 18 | if (a.length < 1 || b.length < 1) { 19 | return map(a, (a: LeftRow) => merger(a, undefined)); 20 | } 21 | let key: Key, 22 | otherKey: Key, 23 | tmpLength: number, 24 | output: MergeResult[]; 25 | if (a.length < b.length) { 26 | return reduceRight(a, (previous: MergeResult[], aDatum: LeftRow) => { 27 | key = aAccessor(aDatum); 28 | tmpLength = previous.length; 29 | output = reduceRight(b, (oPrevious: MergeResult[], bDatum: RightRow) => { 30 | otherKey = bAccessor(bDatum); 31 | if (key <= otherKey && key >= otherKey) { 32 | oPrevious.unshift(merger(aDatum, bDatum)); 33 | } 34 | return oPrevious; 35 | }, []).concat(previous); 36 | if (tmpLength === output.length) { 37 | output.unshift(merger(aDatum, undefined)); 38 | } 39 | return output; 40 | }, []); 41 | } 42 | const seen: {[index: number]: boolean} = {}; 43 | return reduceRight(b, (previous: MergeResult[], bDatum: RightRow) => { 44 | key = bAccessor(bDatum); 45 | return reduceRight(a, (oPrevious: MergeResult[], aDatum: LeftRow, aIndex: number) => { 46 | otherKey = aAccessor(aDatum); 47 | if (key <= otherKey && key >= otherKey) { 48 | seen[aIndex] = true; 49 | oPrevious.unshift(merger(aDatum, bDatum)); 50 | } 51 | return oPrevious; 52 | }, []).concat(previous); 53 | }, []).concat( 54 | map( 55 | filter(a, (aDatum: LeftRow, aIndex: number) => !has(seen, aIndex)), 56 | (aDatum: LeftRow) => merger(aDatum, undefined))); 57 | } 58 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopLeftSemiJoin.ts: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import some from 'lodash/some'; 3 | 4 | import {Accessor} from '../typings'; 5 | 6 | /** 7 | * Nested loop left semi join 8 | */ 9 | export default function nestedLoopLeftSemiJoin( 10 | a: LeftRow[], 11 | aAccessor: Accessor, 12 | b: RightRow[], 13 | bAccessor: Accessor 14 | ): LeftRow[] { 15 | if (a.length < 1 || b.length < 1) { 16 | return []; 17 | } 18 | let key: Key, 19 | otherKey: Key; 20 | return filter(a, (aDatum: LeftRow) => { 21 | key = aAccessor(aDatum); 22 | return some(b, (bDatum: RightRow) => { 23 | otherKey = bAccessor(bDatum); 24 | return key <= otherKey && key >= otherKey; 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopRightAntiJoin.ts: -------------------------------------------------------------------------------- 1 | import nestedLoopLeftAntiJoin from './nestedLoopLeftAntiJoin'; 2 | 3 | import {Accessor} from '../typings'; 4 | 5 | /** 6 | * Nested loop right outer join 7 | */ 8 | export default function nestedLoopRightAntiJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor 13 | ): RightRow[] { 14 | return nestedLoopLeftAntiJoin(b, bAccessor, a, aAccessor); 15 | } 16 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopRightOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import nestedLoopLeftOuterJoin from './nestedLoopLeftOuterJoin'; 2 | 3 | import {Accessor, Merger} from '../typings'; 4 | 5 | /** 6 | * Nested loop right outer join 7 | */ 8 | export default function nestedLoopRightOuterJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor, 13 | merger: Merger 14 | ): MergeResult[] { 15 | return nestedLoopLeftOuterJoin(b, bAccessor, a, aAccessor, merger); 16 | } 17 | -------------------------------------------------------------------------------- /lib/nestedLoop/nestedLoopRightSemiJoin.ts: -------------------------------------------------------------------------------- 1 | import nestedLoopLeftSemiJoin from './nestedLoopLeftSemiJoin'; 2 | 3 | import {Accessor} from '../typings'; 4 | 5 | /** 6 | * Nested loop right semi join 7 | */ 8 | export default function nestedLoopRightSemiJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor 13 | ): RightRow[] { 14 | return nestedLoopLeftSemiJoin(b, bAccessor, a, aAccessor); 15 | } 16 | -------------------------------------------------------------------------------- /lib/sortedMerge/index.ts: -------------------------------------------------------------------------------- 1 | import sortedMergeFullOuterJoin from './sortedMergeFullOuterJoin'; 2 | import sortedMergeInnerJoin from './sortedMergeInnerJoin'; 3 | import sortedMergeLeftAntiJoin from './sortedMergeLeftAntiJoin'; 4 | import sortedMergeLeftOuterJoin from './sortedMergeLeftOuterJoin'; 5 | import sortedMergeLeftSemiJoin from './sortedMergeLeftSemiJoin'; 6 | import sortedMergeRightAntiJoin from './sortedMergeRightAntiJoin'; 7 | import sortedMergeRightOuterJoin from './sortedMergeRightOuterJoin'; 8 | import sortedMergeRightSemiJoin from './sortedMergeRightSemiJoin'; 9 | 10 | export { 11 | sortedMergeFullOuterJoin, 12 | sortedMergeInnerJoin, 13 | sortedMergeLeftAntiJoin, 14 | sortedMergeLeftOuterJoin, 15 | sortedMergeLeftSemiJoin, 16 | sortedMergeRightAntiJoin, 17 | sortedMergeRightOuterJoin, 18 | sortedMergeRightSemiJoin 19 | }; 20 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeFullOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import map from 'lodash/map'; 2 | import sortBy from 'lodash/sortBy'; 3 | 4 | import {Accessor, Merger} from '../typings'; 5 | import {mergeLists, yieldRightSubList, Sublist} from './util'; 6 | 7 | /** 8 | * Sorted merge left outer join. Returns a new array. 9 | */ 10 | export default function sortedMergeFullOuterJoin( 11 | a: LeftRow[], 12 | aAccessor: Accessor, 13 | b: RightRow[], 14 | bAccessor: Accessor, 15 | merger: Merger 16 | ): MergeResult[] { 17 | if (a.length < 1 || b.length < 1) { 18 | return [ 19 | ...a.map((a: LeftRow) => merger(a, undefined)), 20 | ...b.map((b: RightRow) => merger(undefined, b)) 21 | ]; 22 | } 23 | const aSorted: LeftRow[] = sortBy(a, aAccessor), 24 | bSorted: RightRow[] = sortBy(b, bAccessor), 25 | aGenerator: Generator> = yieldRightSubList(aSorted, aAccessor), 26 | bGenerator: Generator> = yieldRightSubList(bSorted, bAccessor); 27 | let rows: MergeResult[] = [], 28 | aDatums: Sublist = aGenerator.next().value, 29 | bDatums: Sublist = bGenerator.next().value; 30 | while (aDatums && bDatums) { 31 | if (aDatums.key > bDatums.key) { 32 | rows = map(aDatums.rows, (aDatum: LeftRow) => merger(aDatum, undefined)).concat(rows); 33 | aDatums = aGenerator.next().value; 34 | } else if (aDatums.key < bDatums.key) { 35 | rows = map(bDatums.rows, (bDatum: RightRow) => merger(undefined, bDatum)).concat(rows); 36 | bDatums = bGenerator.next().value; 37 | } else { 38 | rows = mergeLists(aDatums.rows, bDatums.rows, merger).concat(rows); 39 | aDatums = aGenerator.next().value; 40 | bDatums = bGenerator.next().value; 41 | } 42 | } 43 | while (bDatums) { 44 | rows = map(bDatums.rows, (bDatum: RightRow) => merger(undefined, bDatum)).concat(rows); 45 | bDatums = bGenerator.next().value; 46 | } 47 | while (aDatums) { 48 | rows = map(aDatums.rows, (aDatum: LeftRow) => merger(aDatum, undefined)).concat(rows); 49 | aDatums = aGenerator.next().value; 50 | } 51 | return rows; 52 | } 53 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeInnerJoin.ts: -------------------------------------------------------------------------------- 1 | import sortBy from 'lodash/sortBy'; 2 | 3 | import {Accessor, Merger} from '../typings'; 4 | import {mergeLists, yieldRightSubList, Sublist} from './util'; 5 | 6 | /** 7 | * Sorted merge inner join. Returns a new array. 8 | */ 9 | export default function sortedMergeInnerJoin( 10 | a: LeftRow[], 11 | aAccessor: Accessor, 12 | b: RightRow[], 13 | bAccessor: Accessor, 14 | merger: Merger 15 | ): MergeResult[] { 16 | if (a.length < 1 || b.length < 1) { 17 | return []; 18 | } 19 | const aSorted: LeftRow[] = sortBy(a, aAccessor), 20 | bSorted: RightRow[] = sortBy(b, bAccessor), 21 | aGenerator: Generator> = yieldRightSubList(aSorted, aAccessor), 22 | bGenerator: Generator> = yieldRightSubList(bSorted, bAccessor); 23 | let rows: MergeResult[] = [], 24 | aDatums: Sublist = aGenerator.next().value, 25 | bDatums: Sublist = bGenerator.next().value; 26 | while (aDatums && bDatums) { 27 | if (aDatums.key > bDatums.key) { 28 | aDatums = aGenerator.next().value; 29 | } else if (aDatums.key < bDatums.key) { 30 | bDatums = bGenerator.next().value; 31 | } else { 32 | rows = mergeLists(aDatums.rows, bDatums.rows, merger).concat(rows); 33 | aDatums = aGenerator.next().value; 34 | bDatums = bGenerator.next().value; 35 | } 36 | } 37 | return rows; 38 | } 39 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeLeftAntiJoin.ts: -------------------------------------------------------------------------------- 1 | import sortBy from 'lodash/sortBy'; 2 | 3 | import {Accessor} from '../typings'; 4 | import {isUndefined} from './util'; 5 | 6 | /** 7 | * Sorted merge left semi join. Returns a new array. 8 | */ 9 | export default function sortedMergeLeftAntiJoin( 10 | a: LeftRow[], 11 | aAccessor: Accessor, 12 | b: RightRow[], 13 | bAccessor: Accessor 14 | ): LeftRow[] { 15 | if (a.length < 1 || b.length < 1) { 16 | return a; 17 | } 18 | const aSorted: LeftRow[] = sortBy(a, aAccessor), 19 | bSorted: RightRow[] = sortBy(b, bAccessor), 20 | rows: LeftRow[] = []; 21 | let aDatum: LeftRow = aSorted.pop(), 22 | bDatum: RightRow = bSorted.pop(), 23 | aKey: Key = aAccessor(aDatum), 24 | bKey: Key = bAccessor(bDatum); 25 | while (aDatum && bDatum) { 26 | if (aKey > bKey) { 27 | rows.unshift(aDatum); 28 | aKey = isUndefined(aDatum = aSorted.pop(), aAccessor); 29 | } else if (aKey < bKey) { 30 | bKey = isUndefined(bDatum = bSorted.pop(), bAccessor); 31 | } else { 32 | aKey = isUndefined(aDatum = aSorted.pop(), aAccessor); 33 | } 34 | } 35 | if (aDatum) { 36 | rows.unshift(aDatum); 37 | } 38 | return aSorted.concat(rows); 39 | } 40 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeLeftOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import map from 'lodash/map'; 2 | import sortBy from 'lodash/sortBy'; 3 | 4 | import {Accessor, Merger} from '../typings'; 5 | import {mergeLists, yieldRightSubList, Sublist} from './util'; 6 | 7 | /** 8 | * Sorted merge left outer join. Returns a new array. 9 | */ 10 | export default function sortedMergeLeftOuterJoin( 11 | a: LeftRow[], 12 | aAccessor: Accessor, 13 | b: RightRow[], 14 | bAccessor: Accessor, 15 | merger: Merger 16 | ): MergeResult[] { 17 | if (a.length < 1 || b.length < 1) { 18 | return map(a, (a: LeftRow) => merger(a, undefined)); 19 | } 20 | const aSorted: LeftRow[] = sortBy(a, aAccessor), 21 | bSorted: RightRow[] = sortBy(b, bAccessor), 22 | aGenerator: Generator> = yieldRightSubList(aSorted, aAccessor), 23 | bGenerator: Generator> = yieldRightSubList(bSorted, bAccessor); 24 | let rows: MergeResult[] = [], 25 | aDatums: Sublist = aGenerator.next().value, 26 | bDatums: Sublist = bGenerator.next().value; 27 | while (aDatums && bDatums) { 28 | if (aDatums.key > bDatums.key) { 29 | rows = map(aDatums.rows, aDatum => merger(aDatum, undefined)).concat(rows); 30 | aDatums = aGenerator.next().value; 31 | } else if (aDatums.key < bDatums.key) { 32 | bDatums = bGenerator.next().value; 33 | } else { 34 | rows = mergeLists(aDatums.rows, bDatums.rows, merger).concat(rows); 35 | aDatums = aGenerator.next().value; 36 | bDatums = bGenerator.next().value; 37 | } 38 | } 39 | while (aDatums) { 40 | rows = map(aDatums.rows, aDatum => merger(aDatum, undefined)).concat(rows); 41 | aDatums = aGenerator.next().value; 42 | } 43 | return rows; 44 | } 45 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeLeftSemiJoin.ts: -------------------------------------------------------------------------------- 1 | import sortBy from 'lodash/sortBy'; 2 | 3 | import {Accessor} from '../typings'; 4 | import {isUndefined} from './util'; 5 | 6 | /** 7 | * Sorted merge left semi join. Returns a new array. 8 | */ 9 | export default function sortedMergeLeftSemiJoin( 10 | a: LeftRow[], 11 | aAccessor: Accessor, 12 | b: RightRow[], 13 | bAccessor: Accessor 14 | ): LeftRow[] { 15 | if (a.length < 1 || b.length < 1) { 16 | return []; 17 | } 18 | const aSorted: LeftRow[] = sortBy(a, aAccessor), 19 | bSorted: RightRow[] = sortBy(b, bAccessor), 20 | rows: LeftRow[] = []; 21 | let aDatum: LeftRow = aSorted.pop(), 22 | bDatum: RightRow = bSorted.pop(), 23 | aKey: Key = aAccessor(aDatum), 24 | bKey: Key = bAccessor(bDatum); 25 | while (aDatum && bDatum) { 26 | if (aKey > bKey) { 27 | aKey = isUndefined(aDatum = aSorted.pop(), aAccessor); 28 | } else if (aKey < bKey) { 29 | bKey = isUndefined(bDatum = bSorted.pop(), bAccessor); 30 | } else { 31 | rows.unshift(aDatum); 32 | aKey = isUndefined(aDatum = aSorted.pop(), aAccessor); 33 | } 34 | } 35 | return rows; 36 | } 37 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeRightAntiJoin.ts: -------------------------------------------------------------------------------- 1 | import sortedMergeLeftAntiJoin from './sortedMergeLeftAntiJoin'; 2 | 3 | import {Accessor} from '../typings'; 4 | 5 | /** 6 | * Sorted merge right semi join. Returns the b-array reference. 7 | */ 8 | export default function sortedMergeRightAntiJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor 13 | ): RightRow[] { 14 | return sortedMergeLeftAntiJoin(b, bAccessor, a, aAccessor); 15 | } 16 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeRightOuterJoin.ts: -------------------------------------------------------------------------------- 1 | import sortedMergeLeftOuterJoin from './sortedMergeLeftOuterJoin'; 2 | 3 | import {Accessor, Merger} from '../typings'; 4 | 5 | /** 6 | * Sorted merge right outer join. Returns the b-array reference. 7 | */ 8 | export default function sortedMergeRightOuterJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor, 13 | merger: Merger 14 | ): MergeResult[] { 15 | return sortedMergeLeftOuterJoin(b, bAccessor, a, aAccessor, merger); 16 | } 17 | -------------------------------------------------------------------------------- /lib/sortedMerge/sortedMergeRightSemiJoin.ts: -------------------------------------------------------------------------------- 1 | import sortedMergeLeftSemiJoin from './sortedMergeLeftSemiJoin'; 2 | 3 | import {Accessor} from '../typings'; 4 | 5 | /** 6 | * Sorted merge right semi join. Returns the b-array reference. 7 | */ 8 | export default function sortedMergeRightSemiJoin( 9 | a: LeftRow[], 10 | aAccessor: Accessor, 11 | b: RightRow[], 12 | bAccessor: Accessor 13 | ): RightRow[] { 14 | return sortedMergeLeftSemiJoin(b, bAccessor, a, aAccessor); 15 | } 16 | -------------------------------------------------------------------------------- /lib/sortedMerge/util/index.ts: -------------------------------------------------------------------------------- 1 | import isUndefined from './isUndefined'; 2 | import mergeLists from './mergeLists'; 3 | import yieldRightSubList, {Sublist} from './yieldRightSubList'; 4 | 5 | export { 6 | isUndefined, 7 | mergeLists, 8 | yieldRightSubList, 9 | Sublist 10 | } 11 | -------------------------------------------------------------------------------- /lib/sortedMerge/util/isUndefined.ts: -------------------------------------------------------------------------------- 1 | import isUndefined from 'lodash/isUndefined'; 2 | 3 | /** 4 | * Given an object, execute a function if that object is defined. 5 | * @param {*} obj 6 | * @param {Function} fn 7 | * @returns {*} 8 | */ 9 | export default function undef(obj: T | undefined, fn: (obj: T) => R): R | undefined { 10 | return isUndefined(obj) ? obj : fn(obj); 11 | } 12 | -------------------------------------------------------------------------------- /lib/sortedMerge/util/mergeLists.ts: -------------------------------------------------------------------------------- 1 | import reduceRight from 'lodash/reduceRight'; 2 | 3 | import {Merger} from '../../typings'; 4 | 5 | /** 6 | * Merge two lists into one 7 | */ 8 | export default function mergeLists( 9 | aDatumsR: LeftRow[], 10 | bDatumsR: RightRow[], 11 | merger: Merger 12 | ): MergeResult[] { 13 | return reduceRight(aDatumsR, (previous: MergeResult[], datum: LeftRow) => 14 | reduceRight(bDatumsR, (prev: MergeResult[], cDatum: RightRow) => { 15 | prev.unshift(merger(datum, cDatum)); 16 | return prev; 17 | }, []).concat(previous), []); 18 | } 19 | -------------------------------------------------------------------------------- /lib/sortedMerge/util/yieldRightSubList.ts: -------------------------------------------------------------------------------- 1 | import {Accessor} from '../../typings'; 2 | 3 | export interface Sublist { 4 | rows: Row[]; 5 | key: Key; 6 | } 7 | 8 | /** 9 | * From a sorted list, yield a subList where the accessor values are the same. 10 | */ 11 | export default function* yieldRightSubList( 12 | sortedList: Row[], 13 | accessor: Accessor 14 | ): Generator> { 15 | if (sortedList.length === 1) { 16 | yield {rows: sortedList, key: accessor(sortedList[sortedList.length - 1])}; 17 | } else { 18 | let i: number = sortedList.length, 19 | rows: Row[] = [sortedList[--i]], 20 | key: Key = accessor(rows[0]), 21 | tmpKey: Key; 22 | // for each subsequent value, we'll yield when there is a 23 | // new tmpVal that is not equal the current val 24 | while (i--) { 25 | tmpKey = accessor(sortedList[i]); 26 | if (key <= tmpKey && key >= tmpKey) { 27 | rows.unshift(sortedList[i]); 28 | } else { 29 | yield {rows, key}; 30 | rows = [sortedList[i]]; 31 | key = tmpKey; 32 | } 33 | } 34 | yield {rows, key}; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/typings.ts: -------------------------------------------------------------------------------- 1 | export interface Accessor extends Function { 2 | (a: Row): Key; 3 | } 4 | 5 | export interface Merger extends Function { 6 | (left: LeftRow, right: RightRow): MergeResult; 7 | } 8 | 9 | export interface Join extends Function { 10 | ( 11 | left: LeftRow[], 12 | leftAccessor: Accessor, 13 | right: RightRow[], 14 | rightAccessor: Accessor, 15 | merger: Merger 16 | ): MergeResult[]; 17 | } 18 | 19 | export interface NonMergeJoin extends Function { 20 | ( 21 | left: LeftRow[], 22 | leftAccessor: Accessor, 23 | right: RightRow[], 24 | rightAccessor: Accessor 25 | ): LeftRow[] | RightRow[]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/util/basicAccessor.ts: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/isArray'; 2 | import isString from 'lodash/isString'; 3 | import property from 'lodash/property'; 4 | 5 | import {Accessor} from '../typings'; 6 | 7 | /** 8 | * Create an accessor from a string, string[] or a different accessor. 9 | * If it's a string or an array, use _.property. 10 | */ 11 | export default function basicAccessor( 12 | obj: Accessor | string | string[] 13 | ): Accessor { 14 | return isString(obj) || isArray(obj) ? 15 | property(obj) : 16 | obj; 17 | } 18 | -------------------------------------------------------------------------------- /lib/util/basicMerger.ts: -------------------------------------------------------------------------------- 1 | import assign from 'lodash/assign'; 2 | 3 | /** 4 | * The default merger just creates a combined object using _.assign. 5 | */ 6 | export default function basicMerger( 7 | left: LeftRow, 8 | right: RightRow, 9 | ): LeftRow & RightRow { 10 | return assign({}, left, right); 11 | } 12 | -------------------------------------------------------------------------------- /lib/util/index.ts: -------------------------------------------------------------------------------- 1 | import basicAccessor from './basicAccessor'; 2 | import basicMerger from './basicMerger'; 3 | import joinWrapper from './joinWrapper'; 4 | 5 | export { 6 | basicAccessor, 7 | basicMerger, 8 | joinWrapper 9 | }; 10 | -------------------------------------------------------------------------------- /lib/util/joinWrapper.ts: -------------------------------------------------------------------------------- 1 | import {Accessor, Join, Merger, NonMergeJoin} from '../typings'; 2 | import basicAccessor from './basicAccessor'; 3 | import basicMerger from './basicMerger'; 4 | 5 | /** 6 | * Wrap a join function to process inputs in a more succinct manner. 7 | */ 8 | /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/consistent-type-assertions */ 9 | function joinWrapper( 10 | joinFn: NonMergeJoin 11 | ): NonMergeJoin; 12 | function joinWrapper( 13 | joinFn: NonMergeJoin 14 | ): NonMergeJoin; 15 | function joinWrapper( 16 | joinFn: Join 17 | ): Join; 18 | function joinWrapper( 19 | joinFn: Join 20 | ): Join; 21 | function joinWrapper( 22 | joinFn: Join 23 | ): Join { 24 | return ( 25 | a: LeftRow[], 26 | aAccessor: Accessor, 27 | b: RightRow[] = a, 28 | bAccessor: Accessor = aAccessor, 29 | merger: Merger = basicMerger 30 | ): MergeResult[] => { 31 | if (!a) { 32 | throw new Error('Missing required left array'); 33 | } else if (!aAccessor) { 34 | throw new Error('Missing required left accessor'); 35 | } 36 | return joinFn( 37 | a, 38 | basicAccessor(aAccessor), 39 | b, 40 | basicAccessor(bAccessor), 41 | merger 42 | ); 43 | }; 44 | } 45 | /* eslint-enable @typescript-eslint/no-explicit-any,@typescript-eslint/consistent-type-assertions */ 46 | 47 | export default joinWrapper; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lodash-joins", 3 | "description": "SQL-like joins for JS", 4 | "version": "3.2.0", 5 | "author": { 6 | "name": "Matt Traynham", 7 | "email": "skitch920@gmail.com" 8 | }, 9 | "keywords": [ 10 | "joins" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/mtraynham/lodash-joins/issues" 14 | }, 15 | "license": "Apache-2.0", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/mtraynham/lodash-joins.git" 19 | }, 20 | "files": [ 21 | "LICENSE", 22 | "README.md", 23 | "dist/", 24 | "lib/", 25 | "index.ts", 26 | "index.lodash.ts", 27 | "index.lodash.d.ts" 28 | ], 29 | "main": "dist/lodash-joins.js", 30 | "types": "index.lodash.d.ts", 31 | "scripts": { 32 | "all": "npm run lint && npm run test && npm run build && npm run benchmark", 33 | "build:joins": "webpack --config-name dist-joins", 34 | "build:lodash-joins": "webpack --config-name dist-lodash-joins", 35 | "build": "npm run build:joins && npm run build:lodash-joins", 36 | "lint": "eslint benchmark/**/*.ts debug/**/*.ts lib/**/*.ts karma.conf.ts webpack.config.ts", 37 | "test:browser": "karma start", 38 | "test:browser:debug": "karma start --single-run=false --browsers Chrome --reporters kjhtml", 39 | "test:node": "jasmine --require=node_modules/ts-node/register/index.js lib/**/*.spec.ts", 40 | "test": "npm run test:node", 41 | "benchmark": "node --require=ts-node/register benchmark/joins.ts", 42 | "debug": "webpack-dev-server --open --config-name debug", 43 | "prepublishOnly": "npm run lint && npm run test && npm run build" 44 | }, 45 | "dependencies": { 46 | "lodash": "^4" 47 | }, 48 | "devDependencies": { 49 | "@jsdevtools/coverage-istanbul-loader": "~3.0", 50 | "@types/benchmark": "~2.1", 51 | "@types/chance": "~1.1", 52 | "@types/html-webpack-plugin": "~3.2", 53 | "@types/jasmine": "~5.1", 54 | "@types/karma": "~6.3", 55 | "@types/karma-coverage-istanbul-reporter": "~2.1", 56 | "@types/karma-webpack": "~2.0", 57 | "@types/lodash": "^4", 58 | "@types/webpack-env": "~1.18", 59 | "@typescript-eslint/eslint-plugin": "~6.20", 60 | "@typescript-eslint/parser": "~6.20", 61 | "benchmark": "~2.1", 62 | "chance": "~1.1", 63 | "eslint": "~8.56", 64 | "eslint-plugin-import": "~2.29", 65 | "eslint-plugin-jasmine": "~4.1", 66 | "html-webpack-plugin": "~5.6", 67 | "import": "~0.0", 68 | "jasmine": "~5.1", 69 | "jasmine-spec-reporter": "~7.0", 70 | "karma": "~6.4", 71 | "karma-chrome-launcher": "~3.2", 72 | "karma-coverage-istanbul-reporter": "~3.0", 73 | "karma-firefox-launcher": "~2.1", 74 | "karma-jasmine": "~5.1", 75 | "karma-jasmine-html-reporter": "~2.1", 76 | "karma-sourcemap-loader": "~0.4", 77 | "karma-spec-reporter": "~0.0", 78 | "karma-webpack": "~5.0", 79 | "ts-loader": "~9.5", 80 | "ts-node": "~10.9", 81 | "typescript": "~5.3", 82 | "webpack": "~5.90", 83 | "webpack-cli": "~5.1", 84 | "webpack-dev-server": "~4.15" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | // This is kind of hack. If we specify webpock source files in the karma config, 2 | // they'll all get loaded as separate modules which is bad for performance 3 | // and it also tries to add angular to the page multiple times. 4 | const testsContext = require.context('./lib/', true, /\.spec\.(js|ts)$/); 5 | testsContext.keys().forEach(testsContext); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "./", 6 | "declaration": false, 7 | "downlevelIteration": true, 8 | "esModuleInterop": true, 9 | "importHelpers": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "outDir": "./dist/out-tsc", 13 | "sourceMap": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import template from 'lodash/template'; 2 | import {readFileSync} from 'fs'; 3 | import {join, resolve, sep} from 'path'; 4 | import {BannerPlugin, Configuration} from 'webpack'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | 7 | const pkg: {name: string} = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')); 8 | const banner: string = template(readFileSync(join(__dirname, 'LICENSE_BANNER'), 'utf8'))({ 9 | pkg, 10 | date: new Date() 11 | }); 12 | 13 | const baseConfiguration: Partial = { 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.ts$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | } 21 | ] 22 | }, 23 | resolve: { 24 | extensions: ['.ts', '.js'], 25 | }, 26 | plugins: [ 27 | new BannerPlugin({banner, raw: true}) 28 | ], 29 | externals: [ 30 | // handle splitting modern lodash paths: 31 | // import merge from 'lodash/merge'; -> _.merge 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | ({request}, callback: (error: any, result: any) => void): any => { 34 | if (/^lodash/.test(request)) { 35 | const paths = request.split(sep); 36 | return callback(null, { 37 | root: ['_'].concat(paths.length > 1 ? [paths[paths.length - 1]] : []), 38 | commonjs: request, 39 | commonjs2: request, 40 | amd: request, 41 | toJSON: () => request // Fixes the source map output (sort of) 42 | }); 43 | } 44 | return callback(undefined, undefined); 45 | } 46 | ], 47 | output: { 48 | filename: '[name].js', 49 | libraryTarget: 'umd', 50 | devtoolModuleFilenameTemplate: `webpack:///${pkg.name}/[resource-path]`, 51 | globalObject: 'this' 52 | } 53 | }; 54 | 55 | export default [ 56 | { 57 | ...baseConfiguration, 58 | name: 'dist-joins', 59 | mode: 'production', 60 | devtool: 'source-map', 61 | entry: { 62 | joins: resolve(__dirname, './index.ts') 63 | } 64 | }, 65 | { 66 | ...baseConfiguration, 67 | name: 'dist-lodash-joins', 68 | mode: 'production', 69 | devtool: 'source-map', 70 | entry: { 71 | [pkg.name]: resolve(__dirname, './index.lodash.ts') 72 | }, 73 | output: { 74 | ...baseConfiguration.output, 75 | library: { 76 | name: '_', // Re-export as lodash mixin (_) 77 | type: 'umd', 78 | export: 'default', 79 | } 80 | } 81 | }, 82 | { 83 | ...baseConfiguration, 84 | name: 'karma', 85 | mode: 'development', 86 | devtool: 'inline-source-map', 87 | module: { 88 | ...baseConfiguration.module, 89 | rules: [ 90 | ...baseConfiguration.module.rules, 91 | { 92 | test: /\.(js|ts)$/, 93 | exclude: /(node_modules|\.spec\.(js|ts)$)/, 94 | loader: '@jsdevtools/coverage-istanbul-loader', 95 | enforce: 'post', 96 | options: {esModules: true} 97 | } 98 | ] 99 | }, 100 | output: { 101 | ...baseConfiguration.output, 102 | globalObject: 'self' // https://github.com/ryanclark/karma-webpack/issues/497 103 | } 104 | }, 105 | { 106 | ...baseConfiguration, 107 | name: 'debug', 108 | mode: 'development', 109 | devtool: 'inline-source-map', 110 | entry: { 111 | [pkg.name]: resolve(__dirname, './debug/index.ts') 112 | }, 113 | plugins: [ 114 | ...baseConfiguration.plugins, 115 | new HtmlWebpackPlugin({ 116 | title: 'Debug', 117 | template: resolve(__dirname, './debug/index.ejs') 118 | }) 119 | ] 120 | } 121 | ] as Configuration[]; 122 | --------------------------------------------------------------------------------