140

Unfortunately, I don't have JQuery or Underscore, just pure javascript (IE9 compatible).

I'm wanting the equivalent of SelectMany() from LINQ functionality.

// SelectMany flattens it to just a list of phone numbers.
IEnumerable<PhoneNumber> phoneNumbers = people.SelectMany(p => p.PhoneNumbers);

Can I do it?

EDIT:

Thanks to answers, I got this working:

var petOwners = 
[
    {
        Name: "Higa, Sidney", Pets: ["Scruffy", "Sam"]
    },
    {
        Name: "Ashkenazi, Ronen", Pets: ["Walker", "Sugar"]
    },
    {
        Name: "Price, Vernette", Pets: ["Scratches", "Diesel"]
    },
];

function property(key){return function(x){return x[key];}}
function flatten(a,b){return a.concat(b);}

var allPets = petOwners.map(property("Pets")).reduce(flatten,[]);

console.log(petOwners[0].Pets[0]);
console.log(allPets.length); // 6

var allPets2 = petOwners.map(function(p){ return p.Pets; }).reduce(function(a, b){ return a.concat(b); },[]); // all in one line

console.log(allPets2.length); // 6
toddmo
  • 20,682
  • 14
  • 97
  • 107
  • 6
    That's not unfortunate at all. Pure JavaScript is amazing. Without context, it's very hard to understand what you're trying to achieve here. – Sterling Archer Nov 12 '15 at 19:42
  • 3
    @SterlingArcher, see how specific the answer turned out to be. There's weren't too many possible answers and the best answer was short and concise. – toddmo Aug 18 '16 at 22:53

11 Answers11

199

for a simple select you can use the reduce function of Array.
Lets say you have an array of arrays of numbers:

var arr = [[1,2],[3, 4]];
arr.reduce(function(a, b){ return a.concat(b); }, []);
=>  [1,2,3,4]

var arr = [{ name: "name1", phoneNumbers : [5551111, 5552222]},{ name: "name2",phoneNumbers : [5553333] }];
arr.map(function(p){ return p.phoneNumbers; })
   .reduce(function(a, b){ return a.concat(b); }, [])
=>  [5551111, 5552222, 5553333]

Edit:
since es6 flatMap has been added to the Array prototype. SelectMany is synonym to flatMap.
The method first maps each element using a mapping function, then flattens the result into a new array. Its simplified signature in TypeScript is:

function flatMap<A, B>(f: (value: A) => B[]): B[]

In order to achieve the task we just need to flatMap each element to phoneNumbers

arr.flatMap(a => a.phoneNumbers);
jsgoupil
  • 3,788
  • 3
  • 38
  • 53
Sagi
  • 8,972
  • 3
  • 33
  • 41
  • 12
    That last one can also be written as `arr.reduce(function(a, b){ return a.concat(b.phoneNumbers); }, [])` – Timwi Jan 29 '18 at 11:18
  • You shouldn't need to use .map() or .concat(). See my answer below. – WesleyAC Nov 15 '19 at 17:13
  • doesn't this alter the original array? – Ewan Mar 26 '20 at 10:22
  • Less important now, but `flatMap` doesn't meet the OP's request for an IE9 compatible solution -- [browser compat for `flatmap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap#Browser_compatibility). – ruffin Aug 31 '20 at 21:29
  • 2
    reduce() fails on empty array, so you would have to add an initial value. see https://stackoverflow.com/questions/23359173/javascript-reduce-an-empty-array – halllo Dec 09 '20 at 11:07
53

As a simpler option Array.prototype.flatMap() or Array.prototype.flat()

const data = [
 {id: 1, name: 'Data1', details: [{id: 1, name: 'Details1'}, {id: 2, name: 'Details2'}]},
 {id: 2, name: 'Data2', details: [{id: 3, name: 'Details3'}, {id: 4, name: 'Details4'}]},
 {id: 3, name: 'Data3', details: [{id: 5, name: 'Details5'}, {id: 6, name: 'Details6'}]},
]

const result = data.flatMap(a => a.details); // or data.map(a => a.details).flat(1);
console.log(result)
Welcor
  • 2,431
  • 21
  • 32
Necip Sunmaz
  • 1,546
  • 17
  • 22
  • 3
    Simple and concise. By far the best answer. – Ste Brown Jun 12 '19 at 16:15
  • flat() is not available in Edge according to MDN as of 12/4/2019. – pettys Dec 05 '19 at 00:44
  • But the new Edge(based on Chromium) will support it. – Necip Sunmaz Dec 05 '19 at 13:27
  • 4
    It looks like [Array.prototype.flatMap()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) is also a thing, so your example could be simplified to `const result = data.flatMap(a => a.details)` – Kyle Feb 11 '20 at 15:41
12

For those a while later, understanding javascript but still want a simple Typed SelectMany method in Typescript:

function selectMany<TIn, TOut>(input: TIn[], selectListFn: (t: TIn) => TOut[]): TOut[] {
  return input.reduce((out, inx) => {
    out.push(...selectListFn(inx));
    return out;
  }, new Array<TOut>());
}
Joel Harkes
  • 10,975
  • 3
  • 46
  • 65
9

Sagi is correct in using the concat method to flatten an array. But to get something similar to this example, you would also need a map for the select part https://msdn.microsoft.com/library/bb534336(v=vs.100).aspx

/* arr is something like this from the example PetOwner[] petOwners = 
                    { new PetOwner { Name="Higa, Sidney", 
                          Pets = new List<string>{ "Scruffy", "Sam" } },
                      new PetOwner { Name="Ashkenazi, Ronen", 
                          Pets = new List<string>{ "Walker", "Sugar" } },
                      new PetOwner { Name="Price, Vernette", 
                          Pets = new List<string>{ "Scratches", "Diesel" } } }; */

function property(key){return function(x){return x[key];}}
function flatten(a,b){return a.concat(b);}

arr.map(property("pets")).reduce(flatten,[])
Fabio Beltramini
  • 2,441
  • 1
  • 16
  • 25
  • I';m going to make a fiddle; I can use your data as json. Will try to flatten your answer to one line of code. "How to flatten an answer about flattening object hierarchies" lol – toddmo Nov 12 '15 at 19:57
  • Feel free to use your data... I explictly abstracted the map function so that you could easily select any property name without having to write a new function each time. Just replace `arr` with `people` and `"pets"` with `"PhoneNumbers"` – Fabio Beltramini Nov 12 '15 at 20:02
  • Edited my question with flattened version and voted your answer up. Thanks. – toddmo Nov 12 '15 at 20:33
  • 2
    The helper functions do clean things up, but with ES6 you can just do this: `petOwners.map(owner => owner.Pets).reduce((a, b) => a.concat(b), []);`. Or, even simpler, `petOwners.reduce((a, b) => a.concat(b.Pets), []);`. – ErikE Jan 27 '17 at 21:56
3
// you can save this function in a common js file of your project
function selectMany(f){ 
    return function (acc,b) {
        return acc.concat(f(b))
    }
}

var ex1 = [{items:[1,2]},{items:[4,"asda"]}];
var ex2 = [[1,2,3],[4,5]]
var ex3 = []
var ex4 = [{nodes:["1","v"]}]

Let's start

ex1.reduce(selectMany(x=>x.items),[])

=> [1, 2, 4, "asda"]

ex2.reduce(selectMany(x=>x),[])

=> [1, 2, 3, 4, 5]

ex3.reduce(selectMany(x=> "this will not be called" ),[])

=> []

ex4.reduce(selectMany(x=> x.nodes ),[])

=> ["1", "v"]

NOTE: use valid array (non null) as intitial value in the reduce function

Bogdan Manole
  • 374
  • 1
  • 7
3

try this (with es6):

 Array.prototype.SelectMany = function (keyGetter) {
 return this.map(x=>keyGetter(x)).reduce((a, b) => a.concat(b)); 
 }

example array :

 var juices=[
 {key:"apple",data:[1,2,3]},
 {key:"banana",data:[4,5,6]},
 {key:"orange",data:[7,8,9]}
 ]

using :

juices.SelectMany(x=>x.data)
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Cem Tuğut
  • 97
  • 6
3

I would do this (avoiding .concat()):

function SelectMany(array) {
    var flatten = function(arr, e) {
        if (e && e.length)
            return e.reduce(flatten, arr);
        else 
            arr.push(e);
        return arr;
    };

    return array.reduce(flatten, []);
}

var nestedArray = [1,2,[3,4,[5,6,7],8],9,10];
console.log(SelectMany(nestedArray)) //[1,2,3,4,5,6,7,8,9,10]

If you don't want to use .reduce():

function SelectMany(array, arr = []) {
    for (let item of array) {
        if (item && item.length)
            arr = SelectMany(item, arr);
        else
            arr.push(item);
    }
    return arr;
}

If you want to use .forEach():

function SelectMany(array, arr = []) {
    array.forEach(e => {
        if (e && e.length)
            arr = SelectMany(e, arr);
        else
            arr.push(e);
    });

    return arr;
}
WesleyAC
  • 523
  • 6
  • 11
  • 2
    I think it's funny that I asked this 4 years ago, and the stack overflow notification for your answer popped up, and what I am doing at this moment is struggling with js arrays. Nice recursion! – toddmo Nov 16 '19 at 02:59
  • I'm just glad someone saw it! I was surprised that something like I wrote wasn't already listed, given how long the thread has been going and how many attempted answers there are. – WesleyAC Nov 16 '19 at 20:06
  • @toddmo For what it's worth, if you're working on js arrays right now, you might be interested in the solution I added recently here: https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates/58882216#58882216. – WesleyAC Nov 16 '19 at 20:33
2

Here you go, a rewritten version of joel-harkes' answer in TypeScript as an extension, usable on any array. So you can literally use it like somearray.selectMany(c=>c.someprop). Trans-piled, this is javascript.

declare global {
    interface Array<T> {
        selectMany<TIn, TOut>(selectListFn: (t: TIn) => TOut[]): TOut[];
    }
}

Array.prototype.selectMany = function <TIn, TOut>( selectListFn: (t: TIn) => TOut[]): TOut[] {
    return this.reduce((out, inx) => {
        out.push(...selectListFn(inx));
        return out;
    }, new Array<TOut>());
}


export { };
toddmo
  • 20,682
  • 14
  • 97
  • 107
Worthy7
  • 1,455
  • 15
  • 28
1

You can try the manipula package that implements all C# LINQ methods and preserves its syntax:

Manipula.from(petOwners).selectMany(x=>x.Pets).toArray()

https://github.com/litichevskiydv/manipula

https://www.npmjs.com/package/manipula

razon
  • 3,882
  • 2
  • 33
  • 46
1

For later versions of JavaScript you can do this:

  var petOwners = [
    {
      Name: 'Higa, Sidney',
      Pets: ['Scruffy', 'Sam']
    },
    {
      Name: 'Ashkenazi, Ronen',
      Pets: ['Walker', 'Sugar']
    },
    {
      Name: 'Price, Vernette',
      Pets: ['Scratches', 'Diesel']
    }
  ];

  var arrayOfArrays = petOwners.map(po => po.Pets);
  var allPets = [].concat(...arrayOfArrays);

  console.log(allPets); // ["Scruffy","Sam","Walker","Sugar","Scratches","Diesel"]

See example StackBlitz.

Gary McGill
  • 26,400
  • 25
  • 118
  • 202
0

Exception to reduce and concat methods, you can use the native flatMap api.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap

Kamil OZTURK
  • 81
  • 1
  • 4