Split array into two different arrays using functional JavaScript
Asked Answered
R

7

14

I was wondering what would be the best way to split an array into two different arrays using JavaScript, but to keep it in the realms of functional programming.

Let's say that the two arrays should be created depending on some logic. For instance splitting one array should only contain strings with less than four characters and the other the rest.

const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];

I have thought about different methods:

Filter:

const lessThanFour = arr.filter((animal) => {
    return animal.length < 4;
});
const fourAndMore = arr.filter((animal) => {
    return animal.length >= 4;
});

The problem with this for me is that you have to go through your data twice, but it is very readable. Would there be a massive impact doing this twice if you have a rather large array?

Reduce:

const threeFourArr = arr.reduce((animArr, animal) => {
  if (animal.length < 4) {
    return [[...animArr[0], animal], animArr[1]];
  } else {
    return  [animArr[0], [...animArr[1], animal]];
  }
}, [[], []]);

Where the array's 0 index contains the array of less than four and the 1 index contains the array of more than three.

I don't like this too much, because it seems that the data structure is going to give a bit of problems, seeing that it is an array of arrays. I've thought about building an object with the reduce, but I can't imagine that it would be better than the array within an array solution.

I've managed to look at similar questions online as well as Stack Overflow, but many of these break the idea of immutability by using push() or they have very unreadable implementations, which in my opinion breaks the expressiveness of functional programming.

Are there any other ways of doing this? (functional of course)

Ruwenzori answered 9/8, 2016 at 21:20 Comment(9)
What is the problem with the array of arrays? I would (and have) construct it as an object, but you seem to feel there's an actual problem / risk, so what is it?Irriguous
You can shorten your map code : const lessThanFour = arr.filter(animal => animal.length < 4); const fourAndMore = arr.filter(animal => animal.length >= 4);Crooks
@Amit, I'm worried about efficiency. Both ways: filter you need to go through data twice. reduce you create either an array with arrays or object with arrays, which has to be recreated on every step of the fold.Ruwenzori
If you want to maintain immutability, you're right, but do you consider the act of constructing an object one step at a time as mutating it? I think that's borderline...Irriguous
animArr[0].concat(animal) in a loop is going to introduce a O^2 complexity because it'll have to copy a longer and longer array on each iteration... Modifying the object inline (animalArr[0].push(animal); return animalArr) is going to be much more performant on large arraysCalaboose
Is requirement to not use .push(), and not create an array of arrays?Fourdimensional
I don't think there could be a better way than returning an array of arrays. How else is a split function return more than one results?Advertising
@worker11811 Is requirement to not use .push(), and to not create an array of arrays? Not certain what expected result is? Or, specific approach which must be used to return expected result?Fourdimensional
I refactored a bit to use a spread operator instead of declaring a new array as showed by @naomikRuwenzori
C
16

collateBy

I just shared a similar answer here

I like this solution better because it abstracts away the collation but allows you to control how items are collated using a higher-order function.

Notice how we don't say anything about animal.length or < 4 or animals[0].push inside collateBy. This procedure has no knowledge of the kind of data you might be collating.

// generic collation procedure
const collateBy = f => g => xs => {
  return xs.reduce((m,x) => {
    let v = f(x)
    return m.set(v, g(m.get(v), x))
  }, new Map())
}

// custom collator
const collateByStrLen4 =
  // collate by length > 4 using array concatenation for like elements
  // note i'm using `[]` as the "seed" value for the empty collation
  collateBy (x=> x.length > 4) ((a=[],b)=> [...a,b])

// sample data
const arr = ['horse','elephant','dog','crocodile','cat']

// get collation
let collation = collateByStrLen4 (arr)

// output specific collation keys
console.log('greater than 4', collation.get(true))
console.log('not greater than 4', collation.get(false))

// output entire collation
console.log('all entries', Array.from(collation.entries()))

Check out that other answer I posted to see other usage varieties. It's a pretty handy procedure.


bifilter

This is another solution that captures both out outputs of a filter function, instead of throwing away filtered values like Array.prototype.filter does.

This is basically what your reduce implementation does but it is abstracted into a generic, parameterized procedure. It does not use Array.prototype.push but in the body of a closure, localized mutation is generally accepted as OK.

const bifilter = (f,xs) => {
  return xs.reduce(([T,F], x, i, arr)=> {
    if (f(x, i, arr) === false)
      return [T, [...F,x]]
    else
      return [[...T,x] ,F]
  }, [[],[]])
}

const arr = ['horse','elephant','dog','crocodile','cat']

let [truthy,falsy] = bifilter(x=> x.length > 4, arr)
console.log('greater than 4', truthy)
console.log('not greater than 4', falsy)

Though it might be a little more straightforward, it's not nearly as powerful as collateBy. Either way, pick whichever one you like, adapt it to meet your needs if necessary, and have fun !


If this is your own app, go nuts and add it to Array.prototype

// attach to Array.prototype if this is your own app
// do NOT do this if this is part of a lib that others will inherit
Array.prototype.bifilter = function(f) {
  return bifilter(f,this)
}
Checkerwork answered 10/8, 2016 at 3:32 Comment(2)
I love the collateBy method. Edited it a bit and took it for my own future use :) Thanks. For the collateBy example: using (a = [], b) => { a.push(b); return a; } as the aggregator function instead of (a=[],b)=> [...a,b], even though it is a bit more verbose, would immensely improve the performance here. This is a problem with the example and not the performance of the method itself.Advertising
Ozan correct, a.push(b) is totally acceptable here because the mutation is localized to the collation procedure and no state is leaked. I used immutable operations here because the OP was specifically looking for how to do it without mutation.Checkerwork
C
8

The function you are trying to build is usually known as partition and can be found under that name in many libraries, such as underscore.js. (As far as I know its not a builtin method)

var threeFourArr = _.partition(animals, function(x){ return x.length < 4 });

I don't like this too much, because it seems that the data structure is going to give a bit of problems, seeing that it is an array of arrays

Well, that is the only way to have a function in Javascript that returns two different values. It looks a bit better if you can use destructuring assignment (an ES6 feature):

var [smalls, bigs] = _.partition(animals, function(x){ return x.length < 4 });

Look at it as returning a pair of arrays instead of returning an array of arrays. "Array of arrays" suggests that you may have a variable number of arrays.

I've managed to look at similar questions online as well as Stack Overflow, but many of these break the idea of immutability by using push() or they have very unreadable implementations, which in my opinion breaks the expressiveness of functional programming.

Mutability is not a problem if you localize it inside a single function. From the outside its just as immutable as before and sometimes using some mutability will be more idiomatic than trying to do everything in a purely functional manner. If I had to code a partition function from scratch I would write something along these lines:

function partition(xs, pred){
   var trues = [];
   var falses = [];
   xs.forEach(function(x){
       if(pred(x)){
           trues.push(x);
       }else{
           falses.push(x);
       }
   });
   return [trues, falses];
}
Calliecalligraphy answered 9/8, 2016 at 21:52 Comment(2)
I was just writing out something similar too. Just hide your push logic inside a function and use that from then on.Advertising
...and sometimes, using some mutability will be more idiomatic...Irriguous
A
6

A shorter .reduce() version would be:

const split = arr.reduce((animArr, animal) => {
  animArr[animal.length < 4 ? 0 : 1].push(animal);
  return animArr
}, [ [], [] ]);

Which might be combined with destructuring:

const [ lessThanFour,  fourAndMore ] = arr.reduce(...)
Apportionment answered 9/8, 2016 at 22:19 Comment(0)
H
2

If you are not opposed to using underscore there is a neat little function called groupBy that does exactly what you are looking for:

const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];

var results = _.groupBy(arr, function(cur) {
    return cur.length > 4;
});

const greaterThanFour = results.true;
const lessThanFour = results.false;

console.log(greaterThanFour); // ["horse", "elephant", "crocodile"]
console.log(lessThanFour); // ["dog", "cat"]
Horror answered 9/8, 2016 at 21:39 Comment(1)
That's fairly simple to construct without underscore, but OP seems to be against the idea (the implementation resembles OP's "array of arrays" solution)Irriguous
B
1

Although, JavaScript engines are quite efficient and can perform 1000s of loops in a few milliseconds, sorting can also be done using a single loop.

const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];

const lessThanFour = [];
const fourAndMore = [];

arr.forEach(i => (i.length < 4 ? lessThanFour : fourAndMore).push(i));

console.log(lessThanFour);
// Array [ "dog", "cat" ]
console.log(fourAndMore);
// Array [ "horse", "elephant", "crocodile" ]
Bombastic answered 24/5, 2023 at 16:35 Comment(0)
G
0

Kudos for the beautiful response of the user Thank you, here an alternative using a recursion,

const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];


const splitBy = predicate => {
  return x = (input, a, b) => {
    if (input.length > 0) {
      const value = input[0];
      const [z, y] = predicate(value) ? [[...a, value], b] : [a, [...b, value]];
      return x(input.slice(1), z, y);
    } else {
      return [a, b];
    }
  }
}

const  splitAt4 = splitBy(x => x.length < 4);
const [lessThan4, fourAndMore ] = splitAt4(arr, [], []);
console.log(lessThan4, fourAndMore);
Goldeye answered 20/1, 2021 at 20:22 Comment(0)
A
-1

I don't think there could be another solution than returning an array of arrays or an object containing arrays. How else is a javascript function return multiple arrays after splitting them?

Write a function containing your push logic for readability.

var myArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var x = split(myArr, v => (v <= 5));
console.log(x);

function split(array, tester) {
  const result = [
    [],
    []
  ];
  array.forEach((v, i, a) => {
    if (tester(v, i, a)) result[0].push(v);
    else result[1].push(v);
  });
  return result;
}
Advertising answered 9/8, 2016 at 21:58 Comment(8)
"You could use a whole bunch of different versions of "pushing to two different arrays inside a loop" but OP specifically stated that's not what he wants." Curious how present approach is different from approach at stackoverflow.com/a/38860736 ?Fourdimensional
It's not really different. Just the extra suggestion of putting it inside a function for readability and reusability. After discussing it, I don't think there is a better way than returning an array of arrays or using push to achieve the wanted result. I've said as much in the answer itself.Advertising
"It's not really different." What is purpose of posting Answer using similar pattern, following comment at #38861143 , if approach is not different ?Fourdimensional
It's purpose should be quite plain to see. I think there is a difference between "answering a question with something OP stated they do not want, telling them you could use ..." and "answering it with something OP did not want BUT stating that even though it is not what was wanted originally, it is the better way to do it". Also, I don't think comment section is an appropriate place for this discussion, so I will be moving on from it. Have a nice day.Advertising
@Fourdimensional the primary difference here is that mutation has been localized to the body of a closure. This sort of mutation is just fine.Checkerwork
@Advertising "I don't think there is a better way than returning an array of arrays or using push to achieve the wanted result." - you can remove more rigid decision-making by using higher-order functions. This means split would not choose your data container for you ([[],[]]), and therefore it would not know how merge the result. I demoed this in my answer.Checkerwork
@naomik, I had originally thought to apply the same concept as that collateBy function, but I had used good old object (didn't really thought of using Map) and decided it was kind of an ugly solution so I had scraped it. It was like this. jsfiddle.net/ozzan/647behka. Your way of using Reduce and Map is admittedly way more cooler :)Advertising
@Advertising yeah I think that's better. The only remaining difference is that solution also decides to use arrays and result[k].push(v) which is only one way to merge things. Anyway, good work ^_^Checkerwork

© 2022 - 2025 — McMap. All rights reserved.