Is there some way I can "join" the contents of two javascript arrays much like I would do a join in SQL
Solution 1:
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);
Solution 2:
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.
*/
Solution 3:
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.
Solution 4:
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);