Julio Marins
Julio Marins

Reputation: 10649

How to early break reduce() method?

How can I break the iteration of reduce() method?

for:

for (var i = Things.length - 1; i >= 0; i--) {
  if(Things[i] <= 0){
    break;
  }
};

reduce()

Things.reduce(function(memo, current){
  if(current <= 0){
    //break ???
    //return; <-- this will return undefined to memo, which is not what I want
  }
}, 0)

Upvotes: 169

Views: 140150

Answers (20)

Dam Fa
Dam Fa

Reputation: 448

I don't see the point of using a reduce if you have to “break” and stop before the end, because in that case a simple “for…in” will do the job.

I take the example of Tobiah Rex's popular answer (here), and I can do the same thing with almost the same Algorithmic Complexity.

const chunks = ['apple', '-pen', '-pineapple', '-pen'];
const result = () => {
    let result = '';
    for (const i in chunks) {
        result += chunks[i];
        if (i == 2) break;
    }
    return result;
};

Upvotes: 0

trincot
trincot

Reputation: 351031

Since ECMAScript 2025 we have iterator helpers, which are useful to avoid creating intermediate arrays, and to avoid visiting all array elements that follow the one that meets the break-condition. The slice and splice solutions offered by others suffer from both problems.

For your example, you could first find the index where the break condition is met (using findIndex), and then use iterator helpers to only take the items that precede that index, and chain reduce on that:

const things = [1, 2, 5, 0, 9, 2, 3];
// Get the index where the break-condition is met:
const i = (things.findIndex(current => current <= 0) + 1 || Infinity) - 1;
// Reduce only the elements that precede that index:
const result = things.values().take(i)
                              .reduce((acc, curr) => acc + curr, 0);

console.log(result); // Sum of all values up to the first zero = 8

Note that:

  • values() returns an iterator. It is lazy; it only yields the array values that are consumed by the method chain that follows it.
  • reduce() is here not an array method, but an iterator helper.
  • This approach cannot be used if the break-condition depends on the accumulated value during the reduction process.

Upvotes: 0

Tobiah Rex
Tobiah Rex

Reputation: 2357

You CAN break on any iteration of a .reduce() invocation by mutating the 4th argument of the reduce function: "array". No need for a custom reduce function. See Docs for full list of .reduce() parameters.

Array.prototype.reduce((acc, curr, i, array))

The 4th argument is the array being iterated over.

const chunks = ['apple', '-pen', '-pineapple', '-pen'];

const result = chunks
  .reduce((acc, chunk, i, arr) => {
    if(i === 2) arr.splice(1);  // eject early
    return acc += chunk;
  }, '');

console.log('result: ', result);  // result:  apple-pen-pineapple

WHY?:

The one and only reason I can think of to use this instead of the many other solutions presented is if you want to maintain a functional programming methodology to your algorithm, and you want the most declarative approach possible to accomplish that. If your entire goal is to literally REDUCE an array to an alternate non-falsey primitive (string, number, boolean, Symbol) then I would argue this IS in fact, the best approach.

WHY NOT?

There's a whole list of arguments to make for NOT mutating function parameters as it's a bad practice.


UPDATE

Some of the commentators make a good point that the original array is being mutated in order to break early inside the .reduce() logic.

Therefore, I've modified the answer slightly by adding a .slice(0) before calling a follow-on .reduce() step, yielding a copy of the original array. NOTE: Similar ops that accomplish the same task are slice() (less explicit), and spread operator [...array] (slightly less performant). Bear in mind, all of these add an additional constant factor of linear time to the overall runtime ... + O(n).

The copy, serves to preserve the original array from the eventual mutation that causes ejection from iteration.

const chunks = ['apple', '-pen', '-pineapple', '-pen'];

const result = chunks
    .slice(0)                         // create copy of "array" for iterating
    .reduce((acc, chunk, i, arr) => {
       if (i === 2) arr.splice(1);    // eject early by mutating iterated copy
       return (acc += chunk);
    }, '');

console.log("result: ", result, "\noriginal Arr: ", chunks);
// x:  apple-pen-pineapple
// original Arr:  ['apple', '-pen', '-pineapple', '-pen']

Upvotes: 161

Tanmay Tupe
Tanmay Tupe

Reputation: 51

I find below way as the only elegant way to do it. Idea is to create a small function which simply returns passed thing. I call this function dumbReducer. and a wrapper function which decides whether to call dumbReducer or function which does some processing.

const dumbReducer = (acc, _value) => acc;
const reducer = (acc, value) => acc + value; // or whatever your processing is
const reducerWrapper = (acc, value) => {
  if(acc < 10) { return reducer(acc, value) } // Your condition
  return dumbReducer(acc, value)
};
things = [1,2,3,4,5,6];
things.reduce((acc, current_thing) => {
  return reducerWrapper(acc, current_thing);
}, 0) // Returns 10 i.e 1 + 2 + 3 + 4 without processing (or dumbly processing) 5,6

Works nice unless you have very high number of things to skip. In that case we are sort of wasting lot of cycles. I use this approach when things is actually a list of functions with matching signature to be applied on a starting value and there is a in between break away condition.

Upvotes: 0

Inspiraller
Inspiraller

Reputation: 3806

You could to write your own reduce method. Invoking it like this, so it follows same logic and you control your own escape / break solution. It retains functional style and allows breaking.

const reduce = (arr, fn, accum) => {
  const len = arr.length;
  let result = null;
  for(let i = 0; i < len; i=i+1) {
    result = fn(accum, arr[i], i)
    if (accum.break === true) {
      break;
    }
  }
  return result
}

const arr = ['a', 'b', 'c', 'shouldnotgethere']
const myResult = reduce(arr, (accum, cur, ind) => {
  accum.result = accum.result + cur;
  if(ind === 2) {
    accum.break = true
  }
  return accum
}, {result:'', break: false}).result

console.log({myResult})

Or create your own reduce recursion method:

const rcReduce = (arr, accum = '', ind = 0) => {
  const cur = arr.shift();
  accum += cur;
  const isBreak = ind > 1
  return arr.length && !isBreak ? rcReduce(arr, accum, ind + 1) : accum
}

const myResult = rcReduce(['a', 'b', 'c', 'shouldngethere'])
console.log({myResult})

Upvotes: 0

Bryant
Bryant

Reputation: 73

You can use try...catch to exit the loop.

try {
  Things.reduce(function(memo, current){
    if(current <= 0){
      throw 'exit loop'
      //break ???
      //return; <-- this will return undefined to memo, which is not what I want
    }
  }, 0)
} catch {
  // handle logic
}

Upvotes: 1

Stefan Jelner
Stefan Jelner

Reputation: 111

The problem is, that inside of the accumulator it is not possible to just stop the whole process. So by design something in the outer scope must be manipulated, which always leads to a necessary mutation.

As many others already mentioned throw with try...catch is not really an approach which can be called "solution". It is more a hack with many unwanted side effects.

The only way to do this WITHOUT ANY MUTATIONS is by using a second compare function, which decides whether to continue or stop. To still avoid a for-loop, it has to be solved with a recursion.

The code:

function reduceCompare(arr, cb, cmp, init) {
    return (function _(acc, i) {
        return i < arr.length && cmp(acc, arr[i], i, arr) === true ? _(cb(acc, arr[i], i, arr), i + 1) : acc;
    })(typeof init !== 'undefined' ? init : arr[0], 0);
}

This can be used like:

var arr = ['a', 'b', 'c', 'd'];

function join(acc, curr) {
    return acc + curr;
}

console.log(
    reduceCompare(
        arr,
        join,
        function(acc) { return acc.length < 1; },
        ''
    )
); // logs 'a'

console.log(
    reduceCompare(
        arr,
        join,
        function(acc, curr) { return curr !== 'c'; },
        ''
    )
); // logs 'ab'

console.log(
    reduceCompare(
        arr,
        join,
        function(acc, curr, i) { return i < 3; },
        ''
    )
); // logs 'abc'

I made an npm library out of this, also containing a TypeScript and ES6 version. Feel free to use it:

https://www.npmjs.com/package/array-reduce-compare

or on GitHub:

https://github.com/StefanJelner/array-reduce-compare

Upvotes: 0

RobG
RobG

Reputation: 147483

You can use functions like some and every as long as you don't care about the return value. every breaks when the callback returns false, some when it returns true:

things.every(function(v, i, o) {
  // do stuff 
  if (timeToBreak) {
    return false;
  } else {
    return true;
  }
}, thisArg);

Edit

A couple of comments that "this doesn't do what reduce does", which is true, but it can. Here's an example of using every in a similar manner to reduce that returns as soon as the break condition is reached.

// Soruce data
let data = [0,1,2,3,4,5,6,7,8];

// Multiple values up to 5 by 6, 
// create a new array and stop processing once 
// 5 is reached

let result = [];

data.every(a => a < 5? result.push(a*6) : false);

console.log(result);

This works because the return value from push is the length of the result array after the new element has been pushed, which will always be 1 or greater (hence true), otherwise it returns false and the loop stops.

Upvotes: 21

fedeghe
fedeghe

Reputation: 1318

I solved it like follows, for example in the some method where short circuiting can save a lot:

const someShort = (list, fn) => {
  let t;
  try {
    return list.reduce((acc, el) => {
      t = fn(el);
      console.log('found ?', el, t)
      if (t) {
        throw ''
      }
      return t
    }, false)
  } catch (e) {
    return t
  }
}

const someEven = someShort([1, 2, 3, 1, 5], el => el % 2 === 0)

console.log(someEven)

UPDATE

Away more generic answer could be something like the following

const escReduce = (arr, fn, init, exitFn) => {
    try {
      return arr.reduce((...args) => {
          if (exitFn && exitFn(...args)) {
              throw args[0]
          }
          return fn(...args)
        }, init)
    } catch(e){ return e }
}

escReduce(
  Array.from({length: 100}, (_, i) => i+1),
  (acc, e, i) => acc * e,
    1,
    acc => acc > 1E9 
); // 6227020800

give we pass an optional exitFn which decides to break or not

Upvotes: -1

luzaranza
luzaranza

Reputation: 59

So, to terminate even earlier the idiom to use would be arr.splice(0). Which prompts the question, why can't one just use arr = [] in this case? I tried it and the reduce ignored the assignment, continuing on unchanged. The reduce idiom appears to respond to forms such as splice but not forms such as the assignment operator??? - completely unintuitive - and has to be rote-learnt as precepts within the functional programming credo ...

const array = ['9', '91', '95', '96', '99'];
const x = array
.reduce((acc, curr, i, arr) => {
    if(i === 2) arr.splice(1);  // eject early
    return acc += curr;
  }, '');
console.log('x: ', x);  // x:  99195

Upvotes: 0

Rewind
Rewind

Reputation: 2814

Providing you do not need to return an array, perhaps you could use some()?

Use some instead which auto-breaks when you want. Send it a this accumulator. Your test and accumulate function cannot be an arrow function as their this is set when the arrow function is created.

const array = ['a', 'b', 'c', 'd', 'e'];
var accum = {accum: ''};
function testerAndAccumulator(curr, i, arr){
    this.tot += arr[i];
    return curr==='c';
};
accum.tot = "";
array.some(testerAndAccumulator, accum);

var result = accum.tot;

In my opinion this is the better solution to the accepted answer provided you do not need to return an array (eg in a chain of array operators), as you do not alter the original array and you do not need to make a copy of it which could be bad for large arrays.

Upvotes: 0

Erik Waters
Erik Waters

Reputation: 327

You cannot break from inside of a reduce method. Depending on what you are trying to accomplish you could alter the final result (which is one reason you may want to do this)

const result = [1, 1, 1].reduce((a, b) => a + b, 0); // returns 3

console.log(result);

const result = [1, 1, 1].reduce((a, b, c, d) => {
  if (c === 1 && b < 3) {
    return a + b + 1;
  } 
  return a + b;
}, 0); // now returns 4

console.log(result);

Keep in mind: you cannot reassign the array parameter directly

const result = [1, 1, 1].reduce( (a, b, c, d) => {
  if (c === 0) {
    d = [1, 1, 2];
  } 
  return a + b;
}, 0); // still returns 3

console.log(result);

However (as pointed out below), you CAN affect the outcome by changing the array's contents:

const result = [1, 1, 1].reduce( (a, b, c, d) => {
  if (c === 0) {
    d[2] = 100;
  } 
  return a + b;
}, 0); // now returns 102

console.log(result);

Upvotes: 0

windmaomao
windmaomao

Reputation: 7680

Reduce functional version with break can be implemented as 'transform', ex. in underscore.

I tried to implement it with a config flag to stop it so that the implementation reduce doesn't have to change the data structure that you are currently using.

const transform = (arr, reduce, init, config = {}) => {
  const result = arr.reduce((acc, item, i, arr) => {
    if (acc.found) return acc

    acc.value = reduce(config, acc.value, item, i, arr)

    if (config.stop) {
      acc.found = true
    }

    return acc
  }, { value: init, found: false })

  return result.value
}

module.exports = transform

Usage1, simple one

const a = [0, 1, 1, 3, 1]

console.log(transform(a, (config, acc, v) => {
  if (v === 3) { config.stop = true }
  if (v === 1) return ++acc
  return acc
}, 0))

Usage2, use config as internal variable

const pixes = Array(size).fill(0)
const pixProcessed = pixes.map((_, pixId) => {
  return transform(pics, (config, _, pic) => {
    if (pic[pixId] !== '2') config.stop = true 
    return pic[pixId]
  }, '0')
})

Usage3, capture config as external variable

const thrusts2 = permute([9, 8, 7, 6, 5]).map(signals => {
  const datas = new Array(5).fill(_data())
  const ps = new Array(5).fill(0)

  let thrust = 0, config
  do {

    config = {}
    thrust = transform(signals, (_config, acc, signal, i) => {
      const res = intcode(
        datas[i], signal,
        { once: true, i: ps[i], prev: acc }
      )

      if (res) {
        [ps[i], acc] = res 
      } else {
        _config.stop = true
      }

      return acc
    }, thrust, config)

  } while (!config.stop)

  return thrust
}, 0)

Upvotes: 0

luxigo
luxigo

Reputation: 113

If you want to chain promises sequentially with reduce using the pattern below:

return [1,2,3,4].reduce(function(promise,n,i,arr){
   return promise.then(function(){
       // this code is executed when the reduce loop is terminated,
       // so truncating arr here or in the call below does not works
       return somethingReturningAPromise(n);
   });
}, Promise.resolve());

But need to break according to something happening inside or outside a promise things become a little bit more complicated because the reduce loop is terminated before the first promise is executed, making truncating the array in the promise callbacks useless, I ended up with this implementation:

function reduce(array, promise, fn, i) {
  i=i||0;
  return promise
  .then(function(){
    return fn(promise,array[i]);
  })
  .then(function(result){
    if (!promise.break && ++i<array.length) {
      return reduce(array,promise,fn,i);
    } else {
      return result;
    }
  })
}

Then you can do something like this:

var promise=Promise.resolve();
reduce([1,2,3,4],promise,function(promise,val){
  return iter(promise, val);
}).catch(console.error);

function iter(promise, val) {
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      if (promise.break) return reject('break');
      console.log(val);
      if (val==3) {promise.break=true;}
      resolve(val);
    }, 4000-1000*val);
  });
}

Upvotes: -1

Paweł
Paweł

Reputation: 4536

As the promises have resolve and reject callback arguments, I created the reduce workaround function with the break callback argument. It takes all the same arguments as native reduce method, except the first one is an array to work on (avoid monkey patching). The third [2] initialValue argument is optional. See the snippet below for the function reducer.

var list = ["w","o","r","l","d"," ","p","i","e","r","o","g","i"];

var result = reducer(list,(total,current,index,arr,stop)=>{
  if(current === " ") stop(); //when called, the loop breaks
  return total + current;
},'hello ');

console.log(result); //hello world

function reducer(arr, callback, initial) {
  var hasInitial = arguments.length >= 3;
  var total = hasInitial ? initial : arr[0];
  var breakNow = false;
  for (var i = hasInitial ? 0 : 1; i < arr.length; i++) {
    var currentValue = arr[i];
    var currentIndex = i;
    var newTotal = callback(total, currentValue, currentIndex, arr, () => breakNow = true);
    if (breakNow) break;
    total = newTotal;
  }
  return total;
}

And here is the reducer as an Array method modified script:

Array.prototype.reducer = function(callback,initial){
  var hasInitial = arguments.length >= 2;
  var total = hasInitial ? initial : this[0];
  var breakNow = false;
  for (var i = hasInitial ? 0 : 1; i < this.length; i++) {
    var currentValue = this[i];
    var currentIndex = i;
    var newTotal = callback(total, currentValue, currentIndex, this, () => breakNow = true);
    if (breakNow) break;
    total = newTotal;
  }
  return total;
};

var list = ["w","o","r","l","d"," ","p","i","e","r","o","g","i"];

var result = list.reducer((total,current,index,arr,stop)=>{
  if(current === " ") stop(); //when called, the loop breaks
  return total + current;
},'hello ');


console.log(result);

Upvotes: 0

Koudela
Koudela

Reputation: 181

You can break every code - and thus every build in iterator - by throwing an exception:

function breakReduceException(value) {
    this.value = value
}

try {
    Things.reduce(function(memo, current) {
        ...
        if (current <= 0) throw new breakReduceException(memo)
        ...
    }, 0)
} catch (e) {
    if (e instanceof breakReduceException) var memo = e.value
    else throw e
}

Upvotes: 1

Aleksei
Aleksei

Reputation: 165

Another simple implementation that I came with solving the same issue:

function reduce(array, reducer, first) {
  let result = first || array.shift()

  while (array.length > 0) {
    result = reducer(result, array.shift())
    if (result && result.reduced) {
      return result.reduced
    }
  }

  return result
}

Upvotes: -1

Johann
Johann

Reputation: 29877

Don't use reduce. Just iterate on the array with normal iterators (for, etc) and break out when your condition is met.

Upvotes: 53

Doug Coburn
Doug Coburn

Reputation: 2575

Array.every can provide a very natural mechanism for breaking out of high order iteration.

const product = function(array) {
    let accumulator = 1;
    array.every( factor => {
        accumulator *= factor;
        return !!factor;
    });
    return accumulator;
}
console.log(product([2,2,2,0,2,2]));
// 0

Upvotes: 6

user663031
user663031

Reputation:

There is no way, of course, to get the built-in version of reduce to exit prematurely.

But you can write your own version of reduce which uses a special token to identify when the loop should be broken.

var EXIT_REDUCE = {};

function reduce(a, f, result) {
  for (let i = 0; i < a.length; i++) {
    let val = f(result, a[i], i, a);
    if (val === EXIT_REDUCE) break;
    result = val;
  }
  return result;
}

Use it like this, to sum an array but exit when you hit 99:

reduce([1, 2, 99, 3], (a, b) => b === 99 ? EXIT_REDUCE : a + b, 0);

> 3

Upvotes: 10

Related Questions