3

I'm using the following function to add specific numbers into an array that I later want to be assigned to a variable. For this I'm using two for loops, but I feel like there is a more succinct way to do it. I tried merging the two loops in one without getting an error, but the output is not the same.

Working Example:

function fill () {
    var array = [];
    for (var index = 0; index < arguments.length; index++) {
        for (var number = arguments[index][0]; number <= arguments[index][1]; number++)
            array.push(number);
    }
    return array;
};

/* Use */
var keys = fill([1, 10], [32, 34]);

/* Output */
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 32, 33, 34]

Merged Example:

function fill () {
    var array = [];
    for (var index = 0, number = arguments[index][0];
      index < arguments.length && number <= arguments[index][1];
      index++ && number++) {
        array.push(number);
    }
    return array;
};

/* Use */
var keys = fill([1, 10], [32, 34]);

/* Output */
[1, 1]

Is it possible to actually merge the two loops into one? If not, is there a way to write the foregoing function in less code?

Angel Politis
  • 10,955
  • 14
  • 48
  • 66
  • 3
    I think the first example is about as short as you can get it. – Cerbrus Aug 23 '16 at 13:30
  • 1
    When you say "less code", what is your aim? Generally for small snippets like this, the more readable but slightly longer version is preferable. – DBS Aug 23 '16 at 13:31
  • 1
    Nested loops are fine. I believe you are using them exactly as intended. – Glen Despaux Jr Aug 23 '16 at 13:32
  • I'm rewriting some old functions from the day I was first learning JavaScript and I'm just aiming to rewrite the above function with the least possible code @DBS – Angel Politis Aug 23 '16 at 13:36
  • After ~2 years I managed to [merge the loops](https://stackoverflow.com/questions/39102572/can-two-loops-be-merged-into-one/51795676#51795676) instead of using an alternate route. – Angel Politis Aug 11 '18 at 01:26

6 Answers6

1

Your code in the first example is fine. There is no real "clean" way to remove the nested loops.

You could iterate over them with forEach, but then you'd still have nested loops, even if one of them is disguised as a function call:

function fill () {
    var array = [];
    Array.prototype.slice.apply(arguments) // Make an array out of arguments.
        .forEach(function(arg){
            for (var number = arg[0]; number <= arg[1]; number++){
                array.push(number);
            }
        });
    return array;
};

console.log(fill([1, 10], [32, 34]));

And you'd have to use Array.prototype.slice.apply to convert arguments to an actual array. (which is ugly)

So, basically, nested loops aren't necessarily "evil". Your first example is as good as it gets.

Cerbrus
  • 70,800
  • 18
  • 132
  • 147
1

JavaScript is a functional language. For the sake of modern coding purposes a functional approach is best for the coder's benefit.

var fillArray = (...args) => args.reduce((res,arg) => res.concat(Array(...Array(arg[1]-arg[0]+1)).map((e,i) => i + arg[0])),[]),
       filled = fillArray([1, 10], [32, 34]);
console.log(filled);

OK what happens here.. It's very simple. We do the job by fillArray function. fillArray function takes indefinite number of arguments. So we collect them all in an array called args by utilizing the ES6 rest operator ....

var fillArray = (...args)

Now that we have our source arrays in the args array we will apply a reduce operation to this array with an initial value of an empty array (res). What we will do is.. as per each source (arg) array we will create a new array and then we will concatenate this to the res array. Ok we receive [1,10] as source which means we need an array of length arg[1]-arg[0]+1 right. So comes

Array(...Array(arg[1]-arg[0]+1))

we could also do like Array(arg[1]-arg[0]+1).fill() same thing. We now have an array filled with "undefinite" in the needed length. Then comes map. This is really very simple as we apply to this undefinites array like

.map((e,i) => i + arg[0]))

which means each item will be the current index + offset which is the arg[0] Then we concatenate this array to our results array and pass to the next source array. So you see it is very straight forward and maintainable.

Redu
  • 25,060
  • 6
  • 56
  • 76
  • 3
    Shorter? Yea. Maintainable? Not so much, I'd argue. – Cerbrus Aug 23 '16 at 13:51
  • @Cerbrus you are not wrong.. But in fact the above snippet is very very easy to read and understand provided one leaves aside imperative coding and gets used to the functional approaches. It might take a little time but once you get used to this you will never go back to old style imperative C like coding. :) – Redu Aug 23 '16 at 13:57
  • 1
    The snippet is a long line of brackets and dots. Without a proper syntax highlighter, it's hard to keep track of what function call ends where. – Cerbrus Aug 23 '16 at 13:59
  • 2
    *JavaScript is a functional language.* [citation needed]. Sure, it has what some people consider functional aspects, but considering there's no formal definition of "functional language"... See also http://stackoverflow.com/q/3962604/215552 – Heretic Monkey Aug 23 '16 at 14:02
  • Javascript has functional components, but it is missing what makes most functional languages really practical: immutable, persistent data structures. Without that, functional approaches (which is what I have done in my answer as well) are rarely as clean. @Redu - the code is understandable, but programming declaratively still needs to have the code indented properly and not on one like just because you can... – Josh Aug 23 '16 at 14:04
  • 1
    You can get rid of `reduce` call `var fillArray = (...args) => [].concat(...args.map(([start, end]) => Array(...Array(end - start + 1)).map((_, i) => i + start)))` – Yury Tarabanko Aug 23 '16 at 14:10
  • @Angel Politis Thanks.. I have to say it's a very simple code. Please have a look at my additional explanation just annexed to my answer. – Redu Aug 23 '16 at 14:10
  • @Redu: You're missing the point. I'm not saying the code / process is complicated. I'm saying that it's not as ___readable___. – Cerbrus Aug 23 '16 at 14:12
  • @Yury Tarabanko beautiful and makes more sense. Good thinking. – Redu Aug 23 '16 at 14:13
  • @Cerbrus Its all about getting experienced with. I am sure you will read faster after some time you start dealing with this. There had been times that i wouldn't be able to appreciate the better idea in Yury Tarabankos comment. Now i saw it and in less than 5 secs i got his rightful point. It would be better and logical to use map instead of reduce. I am just using a side effect of reduce to achieve my goal and this in fact disturbed me. But yes.. the code is very maintainable... among him and me not because we are super coders but because we've got used to read it by experience i suppose. – Redu Aug 23 '16 at 14:23
  • Hey @Redu ^_^ I think this answered could be improved by breaking the steps up into reusable parts. – Mulan Sep 18 '16 at 07:00
1

You might not be able to escape the two loops, but that shouldn't necessarily be a goal either. The loop themselves aren't really harmful – it's only if you're iterating over the same data multiple times that you might want to reconsider your code

Consider this entirely different approach

const range = (x , y) =>
  x > y
    ? []
    : [ x, ...range (x + 1, y) ]

const concat = (xs, ys) =>
  xs .concat (ys);

const flatMap = (f, xs) =>
  xs .reduce ((acc, x) => concat (acc, f (x)), [])

const apply = f => xs =>
  f (...xs)

const fill = (...ranges) =>
  flatMap (apply (range), ranges);

console.log
  (fill ( [1,10]
        , [32,34]
        , [40,49]
        , [100,100]
        )
  )

So yes, @Redu is on the right track with "JavaScript is a functional language", but I think his/her answer falls short of delivering a well-composed functional answer.

The answer above shows how functions with individualized concerns can be easy to read, easy to write, and easy to combine to achieve complex computations.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • I have to agree that at first sight this code is seems more readable and might stand as a good example for introduction to functional JS yet eventually people won't bother all this and simply chain them up once they gain some experience. Yet to me the best answer to this question is Yury Tarabanko's as shown in his comment to my answer. + for functional approach. – Redu Sep 18 '16 at 07:22
0

In ES6, you could use the rest operator and build a new array, based on the items.

function fill(...p) {
    return p.reduce((r, a) => r.concat(Array.apply(null, { length: a[1] - a[0] + 1 }).map(() => a[0]++)), []);
};

var keys = fill([1, 10], [32, 34]);
console.log(keys);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • ohai there ^_^ I think this answer would benefit from a little functional decomposition. +1 for correct/creative solution anyhow – Mulan Sep 18 '16 at 07:04
0

Similar to another answer, but a little more complete:

const arrays = [[1,10],[32,34],[9,12]]
const range = (a,b) => a >= b ? [] :
    [...Array(b-a).keys()].map(i => i+a)
const result = arrays.reduce( (a,c) =>
     a.concat( range(c[0], c[1]+1) ), [] )

// =>   [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 32, 33, 34, 9, 10, 11, 12 ]

If you prefer a more traditional range function, then:

const arrays = [[1,10],[32,34],[9,12]]

function range(a,b) {
    var arr = []
    for (let i = a; i < b; i++)
        arr.push(i)
    return arr
}

const result = arrays.reduce( function(a,c) {
    return a.concat( range(c[0], c[1]+1) )
}, [] )
Josh
  • 4,726
  • 2
  • 20
  • 32
  • I think I'd move the `+1` to the `range` function. Or maybe change range so you can do `range(c)`. Any way, this is more readable than the one-liner :D – Cerbrus Aug 23 '16 at 14:06
  • @Cerbrus the archetypal `range` function is inclusive of the lower bound, and exclusive of the higher bound, not inclusive of both. This is a general purpose range function. see [clojure](https://clojuredocs.org/clojure.core/range), [python](https://docs.python.org/2/library/functions.html#range) ... I would also add a 1-arity version of range for my own purposes, but for this solution, only a 2-arity version is needed. – Josh Aug 23 '16 at 14:23
-1

After almost 2 years and some great answers that were posted to this thread proposing interesting alternatives, I found a way to merge the two loops into one, but it ain't pretty!

Code:

function fill () {
    var
      ar = [],
      imax = arguments.length,
      
      /* The functions that calculate the bounds of j. */
      jmin = i => arguments[i][0],
      jmax = i => arguments[i][1] + 1;
    
    for (
      let i = 0, j = jmin(i);
      i < imax && j < jmax(i);
      
      /* If j reaches max increment i and if i is less than max set j to min. */
      ar.push(j++), (j == jmax(i)) && (i++, (i < imax) && (j = jmin(i)))
    );
    return ar;
};

/* Use */
var keys = fill([1, 10], [32, 34], [76, 79]);
console.log.apply(console, keys);
Angel Politis
  • 10,955
  • 14
  • 48
  • 66
  • Sure, you're free to change the accepted answer if you think of a different way to answer your question, but did you have to remove your upvote as well? If you look back at this code in 2 years, will you be able to tell, at a glance, what it does? This doesn't seem very readable at all. – Cerbrus Aug 12 '18 at 20:21
  • Sorry, that wasn't me. I hadn't upvoted any of the answers; probably because none answered the question directly, I guess. I'm sorry I had to un-accept your answer, but since I found a solution that answers the question directly, even if kinda late, I thought it would be a good idea to add it for future readers. Regarding the readability of the code, I agree, but I believe the comment above the "stinky part" explains it adequately. Also, since there's only one loop now, this answer will be significantly faster than all given answers for a high number of iterations, so that's a plus. – Angel Politis Aug 12 '18 at 21:18
  • Angel, I think you're over-estimating the speed gain... It won't be very significant. – Cerbrus Aug 13 '18 at 05:20
  • For 38,723,599 iterations, my initial code with the nested loops took ~15s after 7 tries to complete, while this answer needs only ~6s for the same number of tries @Cerbrus. It's not *that* much, especially for less iterations and with only a few arguments, but it's surely an improvement. Your answer fares well too needing ~7.5s, so I'll give you an upvote for the one you lost. – Angel Politis Aug 13 '18 at 18:54
  • Seems that way, but still the requirement of the question to find a way to merge the loops into one. – Angel Politis Aug 16 '18 at 01:26