4

Is there a function that lets me concat several arrays, with delimiters between them (the delimiters are also arrays), similarly to how join works but not restricted to strings?

The function can be standard JS or part of a major library such as lodash (which is why it's referenced in the tags).

Here is an example of usage:

let numbers = [[1], [2], [3]];
let result = _.joinArrays(numbers, [0]);
console.log(result); 
//printed: [1, 0, 2, 0, 3]

This is analogous to:

let strings = ["a", "b", "c"];
let result = strings.join(",");
console.log(result);
//printed: "a,b,c";

However, join can't be used because it turns values into strings, which I don't want to happen.

But it works for any type.

GregRos
  • 8,667
  • 3
  • 37
  • 63
  • 1
    please add some more examples. – Nina Scholz Sep 26 '16 at 07:46
  • 1
    The functional name for what you want is to [intersperse](http://hackage.haskell.org/package/base-4.9.0.0/docs/Data-List.html#v:intersperse) the array with another element. It is currently a [feature-request](https://github.com/lodash/lodash/issues/2339) for Lodash, so go upvote it if you want to see it added to the library! – 4castle Sep 26 '16 at 07:55
  • @4castle Oh cool. Maybe I'll submit a pull request. Thank you!! – GregRos Sep 26 '16 at 08:03
  • Probable duplicate of [this](http://stackoverflow.com/q/37128624/4543207) question. – Redu Sep 26 '16 at 08:21
  • Yeah, fairly similar. Not sure of the full extent of the generator-based answer. I mean holy crap... punching that into babel.js is mindbogglingly over complicated in it's output. – TylerY86 Sep 26 '16 at 08:43
  • I can't post the babel.io link here due to the extremely long url and lack of allowed url shorteners (422 characters); but 10 lines becomes 90 lines. Yikes. – TylerY86 Sep 26 '16 at 08:47
  • @4castle, afaik. he asks for intercalate, not intersperse. intersperse would imo. produce the following output: `[[1], [0], [2], [0], [3]]` – Thomas Sep 26 '16 at 09:35
  • I stumbled across this question looking for a more vanilla intersperse operation. In js that's called "join" https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join – JonnyRaa Jan 31 '20 at 13:56

7 Answers7

9

You could simply use array.reduce to concat the arrays, and push what ever you want to use as your delimiter.

let numbers = [[1], [2], [3]];

let n = numbers.reduce((a, b) => a.concat(0, b))

console.log(n)
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
DavidDomain
  • 14,976
  • 4
  • 42
  • 50
3

Matrix Interspersion

Here's the full monty. Go nuts.

var numbers = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
],
  delimiters = [
  ',', '-', 'x'
];

// matrix interspersion, delimiters into numbers's children
// the rank/order/whatevs of the matrix can be arbitrary and variable
numbers.forEach((x, i) => {
  for (var j = 1, l = x.length; j <= l; j+=2 )
    x.splice(j, 0, delimiters[i]);
})

alert( "Matrix interspersed: " + JSON.stringify(numbers) );

// normal interspersion, a static delimiter into numbers
for (var j = 1, l = numbers.length; j <= l; j+=2 )
    numbers.splice(j, 0, ' AND ');

alert( "Outer array interspersed: " + JSON.stringify(numbers) );

// flattening a 2 rank array into a single array
var flattened = Array.prototype.concat.apply([], numbers);

alert( "Flattened: " + JSON.stringify(flattened) );
TylerY86
  • 3,737
  • 16
  • 29
0
var result = [].concat.apply([], numbers);

console.log(result)
Sachin
  • 2,912
  • 16
  • 25
0

You could use Array#reduce and return an array with the items and glue if necessary.

const join = (array, glue) => array.reduce((a, b) => a.concat(glue, b));

var numbers = [[1], [2], [3]];

console.log(join(numbers, [0]));
console.log(join(numbers, [42, 43]));
console.log(join([[1]], [0]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • 2
    You can remove the ternary statement and the initial value from the reduction: `array.reduce((r, a) => r.concat(glue, a))` – 4castle Sep 26 '16 at 08:07
0

Here is an implementation of a function that does this, with some extra logic, in case someone else wants to do it. I was really asking about whether this exists already.

Requires lodash.

export function intersperse(arrs, delimeter) {
    let joined = [];
    for (var i = 0; i < arrs.length; i++) {
        let arr = arrs[i];
        if (!arr) continue; //handle sparse arrays
        joined.push(...arr);
        if (i === arrs.length - 1) break;
        if (_.isFunction(delimeter)) {
            joined.push(...delimeter());
        } else if (_.isArray(delimeter)) {
            joined.push(...delimeter);
        } else {
            throw new Error("unknown type");
        }
    }
    return joined;
}
GregRos
  • 8,667
  • 3
  • 37
  • 63
0

Many good answers here including 4Castle's comments. As for a change I would like to develop a generic Array.prototype.intersperse() for this job.

In functional JS I could come up with 3 alternatives for this job;

Array.prototype.intersperse_1 = function(s){
  return this.reduce((p,c,i) => (p[2*i]=c,p), new Array(2*this.length-1).fill(s));
};
Array.prototype.intersperse_2 = function(s){
  return this.reduce((p,c,i) => (i ? p.push(s,c) : p.push(c),p),[]);
};
Array.prototype.intersperse_3 = function(s){
  return this.reduce((p,c,i) => i ? p.concat([s],[c]) : p.concat([c]),[]);
};

You should stay away from the 3rd one since .concat() is one of the most expensive operations in functional JS. It will not be able to complete the job for even 100K items.

On the other hand while always being slightly faster in small size arrays, the 1st one turns out to be 2x or even more faster than the 2nd in very large arrays both in FF and Chrome. i.e. 1st intersperses a 10M item array in less than 1000 msec while for the same job the 2nd takes like 2000-2500 msec. Handling this size of course wouldn't be possible with 3rd at all.

So in this particular case it will probably be more expensive compared to the tailored solutions since we have to map the result into primitive values but i guess it's still worth noting the following code. I am sure .concat() adopting tailored solutions will fall behind this when the array length is beyond a certain figure.

Array.prototype.intersperse = function(s){
  return this.reduce((p,c,i) => (p[2*i]=c,p), new Array(2*this.length-1).fill(s));
}

var arr = [[1],[2],[3]],
 result = arr.intersperse([0])
             .map(e => e[0]);
console.log(JSON.stringify(result));
Redu
  • 25,060
  • 6
  • 56
  • 76
  • another idea: `Array.from({length: 2*this.length-1}, (v,i) => i&1? s: this[i>>>1])`; I guess it's the repeated increase of size and therefore re-allocation of memory that makes your 2nd version slow. – Thomas Sep 26 '16 at 10:10
  • @Thomas Interesting approach yet when tested with a 1M+ item arrays it seems to fall behind the 2nd (in Chrome but not in FF) 1st seems to be always doing the best both in FF and Chrome. Check [this](https://repl.it/Dh7N) 10M item performance test up. May be you can modify it. – Redu Sep 26 '16 at 10:29
0

As already mentioned here a few times, the simplest way to do this is

function intercalate(glue, arr){
    return arr.reduce((acc, v) => acc.concat(glue, v));
}

but this is not the best way, since it creates with every iteration a new (intermediate) array, that is then thrown away. This doesn't matter for this short array of values, but if you ever intend to use this on a longer Array, you might notice the impact.

Better would be to create one Array and push the values into that, as they come in.

function intercalate(glue, arr){
    const push = (acc, v) => (Array.isArray(v)? acc.push(...v): acc.push(v), acc);
    return arr.reduce((acc, v, i) => push(i===0? acc: push(acc, glue), v), []);
}

But since this Array is gradually increasing it may still need to allocate a bigger chunk of memory and copy the data. These tasks ar very performant, but still unnecessary (imo); we can do better.

We first create a list containing all Arrays and the delimiter in between, and flatten this by using concat.apply([], list). Therefore we produce one intermediate Array, who's size we can compute ahead of time, and the rest is the problem of Array.concat, and it's underlying implementation.

function intersperse(delimiter, arr){
    if(!arr.length) return [];
    let j = 0, push = (acc, v) => (acc[j++] = v, acc);
    return arr.reduce((acc, v) => push(j===0? acc: push(delimiter, glue), v), Array(2*arr.length-1));
}
//or
function intersperse(delimiter, arr){
    if(!arr.length) return [];
    var out = Array(2*arr.length-1);
        out[0] = arr[0];
    for(var i=1, j=1; j<out.length;){
        out[j++] = delimiter;
        out[j++] = arr[i++];
    }
    return out;
}

//and

function intercalate(glue, arr){
    var emptyArray = [];
    return arr.length? 
        emptyArray.concat.apply(emptyArray, intersperse(glue, arr)): 
        emptyArray;
}

Wich version will be the best/fastest, in the end, is not that easy to tell, since it may depend on the passed values, and wether or not the JIT compiler optimizes the s*** out of it. I made my points, it's up to you to choose wich version/implementation you use.

To the first version: you may prefer to not put this into a lib at all, but write it mostly inline (it's short and simple enough for that), therefore the JIT-compiler may not try to find some common types between the different calls (#monomorphic function/code) and therefore optimize each occurance seperately. On the other hand, this could be premature optimization. It's up to you.

Thomas
  • 11,958
  • 1
  • 14
  • 23