62

I have two arrays: Question and UserProfile

  • The userProfiles: [] array contain { id, name } objects
  • The questions: [] array contains { id, text, createdBy } objects

The createdBy integer in questions is always one of the id values in userProfiles.

Is there a way I could "join" the arrays in much the same way as I would join up two SQL tables if I was using a database.

What I need as an end result is an array that contains

{ id, text, name }

Corresponding SQL would be:

SELECT u.id, q.text, u.name 
FROM userProfiles u 
JOIN questions q ON q.createdBy=u.id
Ludovic Aubert
  • 9,534
  • 4
  • 16
  • 28
Alan2
  • 23,493
  • 79
  • 256
  • 450

17 Answers17

62

I think what you want is an inner join, which is simple enough to implement in JavaScript:

const innerJoin = (xs, ys, sel) =>
    xs.reduce((zs, x) =>
    ys.reduce((zs, y) =>        // cartesian product - all combinations
    zs.concat(sel(x, y) || []), // filter out the rows and columns you want
    zs), []);

For the purpose of demonstration we'll use the following data set (thank you @AshokDamani):

const userProfiles = [
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"},
];

const questions = [
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3},
];

This is how you would use it:

const result = innerJoin(userProfiles, questions,
    ({id: uid, name}, {id, text, createdBy}) =>
        createdBy === uid && {id, text, name});

In SQL terms this would be similar to:

SELECT questions.id, questions.text, userProfiles.name
FROM userProfiles INNER JOIN questions
ON questions.createdBy = userProfiles.id;

Putting it all together:

const innerJoin = (xs, ys, sel) =>
    xs.reduce((zs, x) =>
    ys.reduce((zs, y) =>        // cartesian product - all combinations
    zs.concat(sel(x, y) || []), // filter out the rows and columns you want
    zs), []);

const userProfiles = [
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"},
];

const questions = [
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3},
];

const result = innerJoin(userProfiles, questions,
    ({id: uid, name}, {id, text, createdBy}) =>
        createdBy === uid && {id, text, name});

console.log("Open your browser console to see the output.");

console.table(result);

Edit: However this is not the best solution. Since the above solution loops through the Cartesian product it takes O(m × n) time to run. With a little bit of modification we can make it run in O(m + n) time - @pebbl found it first:

const equijoin = (xs, ys, primary, foreign, sel) => {
    const ix = xs.reduce((ix, row) => // loop through m items
        ix.set(row[primary], row),    // populate index for primary table
    new Map);                         // create an index for primary table

    return ys.map(row =>              // loop through n items
        sel(ix.get(row[foreign]),     // get corresponding row from primary
        row));                        // select only the columns you need
};

Now you could use it as follows:

const result = equijoin(userProfiles, questions, "id", "createdBy",
    ({name}, {id, text}) => ({id, text, name}));

Putting it all together:

const equijoin = (xs, ys, primary, foreign, sel) => {
    const ix = xs.reduce((ix, row) => ix.set(row[primary], row), new Map);
    return ys.map(row => sel(ix.get(row[foreign]), row));
};

const userProfiles = [
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"},
];

const questions = [
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3},
];

const result = equijoin(userProfiles, questions, "id", "createdBy",
    ({name}, {id, text}) => ({id, text, name}));

console.log("Open your browser console to see the output.");

console.table(result);
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • 3
    You've already got a +1 from me on this answer, it should be voted higher though.. very readable and using a callback is a nice touch. – Pebbl Jul 06 '13 at 10:15
  • 2
    Very nice pattern for marrying object properties from different APIs +1 – David Apr 18 '14 at 16:11
  • I think this answer would be vastly improved if the initial cartesian solution was simply deleted – ErichBSchulz Mar 11 '15 at 10:43
  • 1
    This answer is amazing. Could you please make an outerjoin too? – Cos Mar 18 '15 at 21:49
  • Looks great! One change I am considering is to check if select(x,y) returns undefined before pushing to the c array within your equijoin function. This will allow you to perform what are essentially where clauses by checking for conditions within the callback's return statement. – ferr Jun 01 '16 at 16:55
  • This is great! How could we add an option to get a distinct list if we wanted? – user2531854 Mar 07 '18 at 15:00
  • code snippets **breaks** if data changes like modify id key of userprofiles from 3 to non matching say 7 https://jsfiddle.net/bilbobaggins/84zfn94c/ – Rizwan Patel Jun 02 '18 at 16:32
  • @RizwanPatel That's because your data is inconsistent. The `createdBy` property in the `questions` relation is a [foreign key](https://en.wikipedia.org/wiki/Foreign_key), which means that it must have a corresponding [primary key](https://en.wikipedia.org/wiki/Primary_key) (i.e. `id` property) in the `userProfiles` relation. However, the questions with id `5` and `6` are created by the user with id `3`, and that user doesn't exist in your `userProfiles` relation. There's nothing wrong with my code. – Aadit M Shah Jun 03 '18 at 15:36
  • @AaditMShah it is called "left join scenario" not inconsistent data ! – Rizwan Patel Jun 19 '18 at 08:10
  • @RizwanPatel A left (outer) join is not the same as an equi-join or an inner join. Hence, you're using the wrong function. If you intended to use an equi-join then your data is inconsistent or incomplete. – Aadit M Shah Jun 21 '18 at 04:11
  • @AaditMShah again data is *not* inconsistent or incomplete your function works like inner joins period ! – Rizwan Patel Jun 22 '18 at 07:03
  • @AaditMShah please check alasql for further reference ! – Rizwan Patel Jun 22 '18 at 07:04
  • @RizwanPatel Considering that my function is called `innerJoin` it *is* supposed to work like an inner join. Furthermore, an [equi-join](https://en.wikipedia.org/wiki/Join_(SQL)#Equi-join) is a specific type of inner join. Hence, I don't know what you're complaining about. My function does what is supposed to do. It doesn't have any bugs. – Aadit M Shah Jun 22 '18 at 08:51
  • @RizwanPatel Also, equi-join is **not** supposed to behave like a left join. If you want to do a left join then use a left join. Don't use an equi-join. You can't eat your cake and have it too. You can't use an equi-join and then complain when it doesn't work because your data is inconsistent (**and yes, your data *is* inconsistent because questions 5 and 6 are created by user 3, and user 3 DOES NOT EXIST**). – Aadit M Shah Jun 22 '18 at 08:55
  • so called "obsure js lib" is pure insecure individual perspective for aformentioned lib since it is not even a transpiler and requires academic level rdbms knowledge. cheers ! hence basic spoon feeding continues .... https://jsfiddle.net/bilbobaggins/zdb270ne/ – Rizwan Patel Jun 23 '18 at 11:05
17

This seems to be an important general-purpose question, and although there are many answers, some have borderline behavior like modifying the existing data, solving a completely different problem than the issue at hand, using up to 93,057 bytes of JavaScript (not to mention producing the wrong result), producing overly complex additional nesting of data structures, requiring a lot of code on each invocation, and most seriously, not being a self-contained solution to the important more general-purpose problem at the heart of this question.

So for better or for worse, I wrote a shim that extends the JavaScript Array object with a method .joinWith intended to be used in order to join this array of objects with that array of objects, by a common indexing field. It is possible to select a list of fields desired in output (good for merging arrays of objects with many fields when only a few are wanted) or to omit a list of fields in output (good for merging arrays of objects when most fields are desired but a few are not).

The shim code isn't made to look pretty, so it will be at the end, with the example of how to use it for the OP's particular kind of data coming first:

/* this line will produce the array of objects as desired by the OP */
joined_objects_array = userProfiles.joinWith(questions, 'id', ['createdBy'], 'omit');

/* edit: I just want to make 100% sure that this solution works for you, i.e.,
 *       does exactly what you need. I haven't seen your actual data, so it's
 *       possible that your IDs are are not in common, (i.e., your createdBy
 *       is in common like you said, but not the IDs, and if so you could
 *       morph your data first like this:
 * questions.map(function(x) { x.id = x.createdBy; });
 *       before joining the arrays of objects together.
 *
 */

The following code is for demonstration:

var array1 = [{ id: 3124, name: 'Mr. Smith' },
              { id: 710, name: 'Mrs. Jones' }];
var array2 = [{ id: 3124, text: 'wow', createdBy: 'Mr. Jones' },
              { id: 710, text: 'amazing' }];

var results_all = array1.joinWith(array2, 'id');

// [{id:3124, name:"Mr. Smith", text:"wow", createdBy:"Mr. Jones"},
// {id:710, name:"Mrs. Jones", text:"amazing"}]*

var results_selected = array1.joinWith(array2, 'id', ['id', 'text', 'name']);

// [{id:3124, name:"Mr. Smith", text:"wow"},
// {id:710, name:"Mrs. Jones", text:"amazing"}]*

/* or equivalently, */
var results_omitted = array1.joinWith(array2, 'id', ['createdBy'], 1);

// [{id:3124, name:"Mr. Smith", text:"wow"},
// {id:710, name:"Mrs. Jones", text:"amazing"}]*

There are some other nice things this solution does (one of them is preserving the ability to access the resulting data by its indexing key, despite returning an array).

Enjoy!

/* Array.joinWith - shim by Joseph Myers 7/6/2013 */


if (!Array.prototype.joinWith) {
    +function () {
        Array.prototype.joinWith = function(that, by, select, omit) {
            var together = [], length = 0;
            if (select) select.map(function(x){select[x] = 1;});
            function fields(it) {
                var f = {}, k;
                for (k in it) {
                    if (!select) { f[k] = 1; continue; }
                    if (omit ? !select[k] : select[k]) f[k] = 1;
                }
                return f;
            }
            function add(it) {
                var pkey = '.'+it[by], pobj = {};
                if (!together[pkey]) together[pkey] = pobj,
                    together[length++] = pobj;
                pobj = together[pkey];
                for (var k in fields(it))
                    pobj[k] = it[k];
            }
            this.map(add);
            that.map(add);
            return together;
        }
    }();
}

Documentation:

        /* this and that both refer to an array of objects, each containing
           object[by] as one of their fields */
        /*
         N.B. It is the responsibility of the user of this method
         to ensure that the contents of the [by] fields are
         consistent with each other between the two arrays!
        */
        /* select is an array of field names to be included in the resulting
           objects--all other fields will be excluded, or, if the Boolean value
           of omit evaluates to true, then select is an array of field names to
           be excluded from the resulting objects--all others will be included.
        */
Joseph Myers
  • 6,434
  • 27
  • 36
  • @NagaJolokia Thank you. My feeling about shims is that they are not supposed to look pretty, but to simply work well. That's why I put it at the end. – Joseph Myers Jul 06 '13 at 16:40
  • 3
    Well, you can minify and compress your code, but not if you want people to read it! :-) – 1983 Jul 06 '13 at 16:48
  • 1
    Does this solutions run in O(m + n) like the final version of @aadit does? – AXMIM Mar 09 '16 at 21:55
  • @AXMIM Let me explain how to figure it out. First, check any inner loops, which are in this case represented by the `fields` function. Does this involve `m` or `n`? No. So this component of the algorithm serves as a proportionality constant depending on the fixed number of keys per object. (In fact, it could be factored out of the loops completely if every object had the same fields.) Then look at the outer loop, which is implicit in the `map` method. There are two of these loops, not nested, but run separately, for a total time complexity of `O(m+n)`. – Joseph Myers Mar 10 '16 at 05:32
7

i just about always use underscore.js because it has such good support for arrays and "map reduce" which this problem can be solved with.

here is a fiddle with a solution for your question ( it assumes there is only one question per user as your original post suggests)

http://jsfiddle.net/x5Z7f/

(open the browser console to see the output)

    var userProfiles = [{ id:'1', name:'john' }, { id:'2', name:'mary' }];

var questions =[ { id:'1', text:'question john', createdBy:'1' }, { id:'2', text:'question mary', createdBy:'2' }];

var rows = _.map(userProfiles, function(user){ 
    var question = _.find(questions, function(q){ return q.createdBy == user.id });
    user.text = question? question.text:'';
    return user; 
})

_.each(rows, function(row){ console.log(row) });

the above answer assumes you are using id == createdBy as the joining column.

Anton
  • 7,709
  • 5
  • 31
  • 33
  • I prefer this approach because underscore includes so many other useful functions. Rather to include this than keep reinventing the wheel. – Douglas Timms Mar 09 '17 at 17:06
7

If it were me, I'd approach this in the following manner:

The set-up:

var userProfiles = [], questions = [];

userProfiles.push( {id:1, name:'test'} );
userProfiles.push( {id:2, name:'abc'} );
userProfiles.push( {id:3, name:'def'} );
userProfiles.push( {id:4, name:'ghi'} );

questions.push( {id:1, text:'monkey', createdBy:1} );
questions.push( {id:2, text:'Monkey', createdBy:1} );
questions.push( {id:3, text:'big',    createdBy:2} );
questions.push( {id:4, text:'string', createdBy:2} );
questions.push( {id:5, text:'monKey', createdBy:3} );

First, would be to create a look-up object, where the linking id is used as a key

var createObjectLookup = function( arr, key ){
  var i, l, obj, ret = {};
  for ( i=0, l=arr.length; i<l; i++ ) {
    obj = arr[i];
    ret[obj[key]] = obj;
  }
  return ret;
};

var up = createObjectLookup(userProfiles, 'id');

Now that you have this, it should be easy to step through the questions, and find your user object to merge:

var i, l, question, user, result = [];
for ( i=0, l=questions.length; i<l; i++ ) {
  if ( (question = questions[i]) && (user = up[question.createdBy]) ) {
    result.push({
      id: question.id,
      text: question.text,
      name: user.name
    });
  }
}

You should now have everything you need in result

console.log(result);
Pebbl
  • 34,937
  • 6
  • 62
  • 64
  • 1
    Where is `obj` defined in your `createObjectLookup` function? What does it do? As it currently stands your code won't execute. – Aadit M Shah Jul 06 '13 at 07:57
  • @AaditMShah - very good point, i thought I could remove the obj var creation and make the code smaller, but missed the fact I was still referencing it... have put it back. Thanks. – Pebbl Jul 06 '13 at 08:13
  • 1
    You might want to add `obj` to your list of variable declarations unless you want it to be a global variable. – Aadit M Shah Jul 06 '13 at 08:37
  • @AaditMShah -- doh! I should really learn not to tinker with tested code once I've posted it here. Thanks again :) – Pebbl Jul 06 '13 at 10:12
5

A simple way to do the SQL joins in JavaScript:

let userProfiles = [ { id: 3, name: "Paquito"}, { id: 2, name: "Jaime" } ];
let questions = [ { id: 22, text: "My question", createdBy: 3 }, { id: 44, text: "Other question", createdBy: 5 } ];

let left_join = questions
.map ( q => ({ ...userProfiles.find( u => q.createdBy === u.id ), ...q }) );

document.write("<p>Left join: <br>", JSON.stringify(left_join));

let right_join = userProfiles
.map ( u => ({ ...questions.find( q => q.createdBy === u.id ), ...u }) );;

document.write("</p><p>Right join: <br>", JSON.stringify(right_join));

let inner_join = questions
.filter( q => userProfiles.find( u => q.createdBy === u.id ) )
.map ( q => ({ ...userProfiles.find( u => q.createdBy === u.id ), ...q }) );

document.write("</p><p>Inner join: <br>", JSON.stringify(inner_join));
SnakeDrak
  • 3,406
  • 4
  • 28
  • 41
4

This is my attempt to make a somehow generic solution. I'm using Array.map and the Array.index methods here:

var arr1 = [
    {id: 1, text:"hello", oid:2},
    {id: 2, text:"juhu", oid:3},
    {id: 3, text:"wohoo", oid:4},
    {id: 4, text:"yeehaw", oid:1}
];
var arr2 = [
    {id: 1, name:"yoda"},
    {id: 2, name:"herbert"},
    {id: 3, name:"john"},
    {id: 4, name:"walter"},
    {id: 5, name:"clint"}
];

function merge(arr1, arr2, prop1, prop2) {
    return arr1.map(function(item){
        var p = item[prop1];
        el = arr2.filter(function(item) {
            return item[prop2] === p;
        });
        if (el.length === 0) {
            return null;
        }
        var res = {};
        for (var i in item) {
            if (i !== prop1) {
                res[i] = item[i];
            }
        }
        for (var i in el[0]) {
            if (i !== prop2) {
                res[i] = el[0][i];
            }
        }
        return res;
    }).filter(function(el){
        return el !== null;
    });
}

var res = merge(arr1, arr2, "oid", "id");
console.log(res);

So basically you can define two arrays and one property for each array, so that prop1 will be replaced with all the properties of an item in array2 whose prop2 is equal to prop1.

The result in this case would be:

var res = [
    {id: 1, text:"hello", name:"herbert"},
    {id: 2, text:"juhu", name:"john"},
    {id: 3, text:"wohoo", name:"walter"},
    {id: 4, text:"yeehaw", name:"yoda"}
];

Note that if there is more then one match, the first item will be used and if there is no match, the object will be removed from the resulting array.

FIDDLE

basilikum
  • 10,378
  • 5
  • 45
  • 58
3

all u want is the ResultArray calculated below:

    var userProfiles1= new Array(1, "ashok");
    var userProfiles2= new Array(2, "amit");
    var userProfiles3= new Array(3, "rajeev");

    var UArray = new Array(userProfiles1, userProfiles2, userProfiles3);

    var questions1= new Array(1, "text1", 2);
    var questions2= new Array(2, "text2", 2);
    var questions3= new Array(3, "text3", 1);
    var questions4= new Array(4, "text4", 2);
    var questions5= new Array(5, "text5", 3);
    var questions6= new Array(6, "text6", 3);

    var QArray = new Array(questions1, questions2, questions3, questions4, questions5, questions6);

    var ResultArray = new Array();

    for (var i=0; i<UArray.length; i++)
    {
        var uid = UArray[i][0];
        var name = UArray[i][1];

        for(var j=0; j<QArray.length; j++)
        {
            if(uid == QArray[j][2])
            {
                 var qid = QArray[j][0]
                 var text = QArray[j][1];

                 ResultArray.push(qid +"," + text +","+ name)
            }
        }    
    }

for(var i=0; i<ResultArray.length; i++)
    {
        document.write(ResultArray[i] + "<br>")
    }

demo : http://jsfiddle.net/VqmVv/

Ashok Damani
  • 3,896
  • 4
  • 30
  • 48
2

Just wanted to share some generic code:

// Create a cartesian product of the arguments.
// product([1,2],['a','b'],['X']) => [[1,"a","X"],[1,"b","X"],[2,"a","X"],[2,"b","X"]]
// Accepts any number of arguments.
product = function() {
    if(!arguments.length)
        return [[]];
    var p = product.apply(null, [].slice.call(arguments, 1));
    return arguments[0].reduce(function(r, x) {
        return p.reduce(function(r, y) {
            return r.concat([[x].concat(y)]);
        }, r);
    }, []);
}

Your problem:

result = product(userProfiles, questions).filter(function(row) {
    return row[0].id == row[1].createdBy;
}).map(function(row) {
    return {
        userName: row[0].name,
        question: row[1].text
    }
})
georg
  • 211,518
  • 52
  • 313
  • 390
2

You can do this using reduce and map.

First, create a mapping from IDs to names:

var id2name = userProfiles.reduce(function(id2name, profile){
    id2name[profile.id] = profile.name;
    return id2name;
}, {});

Second, create a new array of questions but with the name of the user who created the question in place of their ID:

var qs = questions.map(function(q){
    q.createdByName = id2name[q.createdBy];
    delete q.createdBy;
    return q;
});
1983
  • 5,882
  • 2
  • 27
  • 39
1

I don't know any built-in function allowing to do so.

You can program your own function, something similar to this jsFiddle :

var userProfiles = [{id:1, name:'name1'},{id:2,name:'name2'}];
    var questions = [
        {id:1, text:'text1', createdBy:'foo'},
        {id:1, text:'text2', createdBy:'bar'},
        {id:2, text:'text3', createdBy:'foo'}];

    merged = mergeMyArrays(userProfiles,questions);

    console.log(merged);
    /**
     * This will give you an array like this:
     * [{id:1, name:name1, text:text1}, {...]
     * params : 2 arrays to merge by id
     */
    function mergeMyArrays(u,q){
        var ret = [];
        for(var i = 0, l = u.length; i < l; i++){
            var curU = u[i];
            for(var j = 0, m = q.length; j<m; j++){
                if(q[j].id == curU.id){
                    ret.push({
                        id: curU.id,
                        name: curU.name,
                        text: q[j].text
                    });
                }
            }
        }
        return ret;
    }

Or if you want a better "join" (SQL-y) :

var userProfiles = [{id:1, name:'name1'},{id:2,name:'name2'}];
var questions = [
    {id:1, text:'text1', createdBy:'foo'},
    {id:1, text:'text2', createdBy:'bar'},
    {id:2, text:'text3', createdBy:'foo'}];

merged = mergeMyArrays(userProfiles,questions);

console.log(merged);
/**
 * This will give you an array like this:
 * [{id:1, name:name1, questions:[{...}]]
 * params : 2 arrays to merge by id
 */
function mergeMyArrays(u,q){
    var ret = [];
    for(var i = 0, l = u.length; i < l; i++){
        var curU = u[i],
            curId = curU.id,
            tmpObj = {id:curId, name:curU.name, questions:[]};
        for(var j = 0, m = q.length; j<m; j++){
            if(q[j].id == curId){
                tmpObj.questions.push({
                    text: q[j].text,
                    createdBy: q[j].createdBy
                });
            }
        }
        ret.push(tmpObj);
    }
    return ret;
}

Like in this jsFiddle

Michael Geary
  • 28,450
  • 9
  • 65
  • 75
Shikiryu
  • 10,180
  • 8
  • 49
  • 75
1

This is easily done with StrelkiJS

var userProfiles = new StrelkiJS.IndexedArray();
userProfiles.loadArray([
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"}
]);

var questions = new StrelkiJS.IndexedArray();
questions.loadArray([
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3}
]);

var res=questions.query([{
    from_col:  "createdBy", 
    to_table:  userProfiles, 
    to_col:    "id", 
    type:      "outer"
}]);

The result will be:

[
 [
  {"id":1,"text":"text1","createdBy":2},
  {"id":2,"name":"Amit"}
 ],
 [
  {"id":2,"text":"text2","createdBy":2},
  {"id":2,"name":"Amit"}
 ],
 [
  {"id":3,"text":"text3","createdBy":1},
  {"id":1,"name":"Ashok"}
 ],
 [
  {"id":4,"text":"text4","createdBy":2},
  {"id":2,"name":"Amit"}
 ],
 [
  {"id":5,"text":"text5","createdBy":3},
  {"id":3,"name":"Rajeev"}
 ],
 [
  {"id":6,"text":"text6","createdBy":3},
  {"id":3,"name":"Rajeev"}
 ]
]
amaksr
  • 7,555
  • 2
  • 16
  • 17
1

Aadit's second O(m+n) solution looked good to me as concise, functional, and efficient. However, it didn't implement the duplication of records when multiple keys match that I needed for my use case, so I wrote this slight variation:

function equijoin(arrL,arrR,keyL,keyR=keyL){
        const idx = arrL.reduce(
                (idx, objL) => 
                        objL[keyL] === undefined
                        ?idx
                        :idx.set(objL[keyL], [...(idx.get(objL[keyL])||[]), ...[objL]]) 
                ,new Map
            )
        const matches = 
                arrR
                .map(objR =>
                        objR[keyR] === undefined
                        ? []
                        : idx.get(objR[keyR])
                                .map(objL => ({l:objL, r:objR}))
                                .reduce((a,b)=>a.concat(b),[])
                    )
                .reduce((a,b)=>a.concat(b),[])
        return matches
}

(Contrived) Example:

var players =[
    {id:"Alice", team:"Red"},
    {id:"Bob", team:"Red"},
    {id:"Clair", team:"Blue"},
    {id:"Dave"},
    {id:"Elliot"}
];

equijoin(players,players,"team")

[
{l:{id:"Alice",team:"Red"},r:{id:"Alice",team:"Red"}},
{l:{id:"Bob",team:"Red"},r:{id:"Alice",team:"Red"}},
{l:{id:"Alice",team:"Red"},r:{id:"Bob",team:"Red"}},
{l:{id:"Bob",team:"Red"},r:{id:"Bob",team:"Red"}},
{l:{id:"Clair",team:"Blue"},r:{id:"Clair",team:"Blue"}}
]
Fabio Beltramini
  • 2,441
  • 1
  • 16
  • 25
1

adding to Aadit M Shah's equijoin, there was a request for this to be similar to a left join. But as the method isn't a join, but a direct 1 to 1 equals, the method isn't exactly a left join, more a equijoin with a default if no match. To achieve this however and it make sense reading it, I reversed the method, as it was effectively joining in reverse.

const equijoinWithDefault = (xs, ys, primary, foreign, sel, def) => {
  const iy = ys.reduce((iy, row) => iy.set(row[foreign], row), new Map);
  return xs.map(row => typeof iy.get(row[primary]) !== 'undefined' ? sel(row, iy.get(row[primary])): sel(row, def));
};

example call:

const userProfiles = [
  {id: 1, name: "Ashok"},
  {id: 2, name: "Amit"},
  {id: 3, name: "Rajeev"},
];

const questions2 = [
  {id: 1, text: "text1", createdBy: 2},
  {id: 2, text: "text2", createdBy: 2},
  {id: 3, text: "text3", createdBy: 1},
  {id: 4, text: "text4", createdBy: 2},
  {id: 5, text: "text5", createdBy: 3},
  {id: 6, text: "text6", createdBy: 3},
  {id: 7, text: "text7", createdBy: 4},
];

let result2 = equijoinWithDefault(questions2, userProfiles, "createdBy", "id", ({id, text}, {name}) => ({id, text, name}), {name:null});
Ian
  • 55
  • 4
0
Thats another way

var array1 = [{ id: 1, name: 'Khikmat'},
              { id: 2, name: 'User'}];
var array2 = [{ id: 1, text: 'hi test' },
              { id: 2, text: 'amazing test' }, { id: 2, text: 'test'}];
 
 var result = array1.map((elem) => {
   elem["complaints"] = array2.filter((val) => {
       return val.id === elem.id; 
   });
   return elem;
 });

 
console.log(JSON.stringify(result))
Anonymouse
  • 11
  • 2
0

A TypeScript version of @aadit-m-shah's answer:

export const equiJoin = <T, U, K>(
    xs: T[],
    ys: U[],
    primaryPredicate: (v: T) => unknown,
    foreignPredicate: (v: U) => unknown,
    selectorFn: (value: T|undefined, row: U) => K) =>
{
    const ix = xs.reduce<Map<unknown, T>>(
        (acc, row) => acc.set(primaryPredicate(row), row),
        new Map
    );
    
    return ys.map((row) => selectorFn(ix.get(foreignPredicate(row)), row));
};

How to use:

const userProfiles = [
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"},
];

const questions = [
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3},
];

const joinedArrays = equiJoin(
    userProfiles,
    questions,
    (profile) => profile.id,
    (q) => q.createdBy,
    (p, {id, text}) => ({ id, text, name: p?.name || 'Unknown' })
);
Farzan
  • 745
  • 10
  • 25
  • @RizwanPatel This TS version might also be useful for your use-case of left outer join. Look how I used `name: p?.name || 'Unknown'`. – Farzan Mar 29 '22 at 01:33
0

User innerJoin from Ramda https://ramdajs.com/docs/#innerJoin which returns an array of common elements in two array args, based on a test function you provide.

JFK
  • 11
  • 1
  • 1
    Link-only answers should be updated to include the examples and information from the link, in case links break in the future – Harrison Aug 13 '23 at 13:45
-2

You can use first jQuery.merge() and then jQuery.unique() to achieve this. merge() will add all the items in one array and unique() will remove the duplicates from that array.

http://api.jquery.com/jQuery.merge/

http://api.jquery.com/jQuery.unique/

kaps
  • 468
  • 4
  • 18
  • 1
    I would appreciate if, whoever has voted down the answer, can add some explanation as well in comment. It can help me learn something. – kaps Jul 06 '13 at 06:49
  • 3
    I didn't downvote you but I can see 2 reasons : 1- he never talks about jQuery. Adding a whole library to merge 2 arrays is a bit overkill IMHO. 2- As far as I understand his question, he doesn't want to merge like this, it's bit more subtle. – Shikiryu Jul 06 '13 at 07:04
  • 1
    its not a direct merge, it is like an sql join. – Anton Jul 06 '13 at 07:09
  • Thanks @Shikiryu! I agree with you about "adding the whole library to merge 2 arrays is a bit overkill". – kaps Jul 07 '13 at 16:16