最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Finding total sum of JS array and excluding all NULL values - Stack Overflow

programmeradmin3浏览0评论

Assuming that I have a js object array with my monthly expense data like this

var data=[
    {
    "date": "2016-09-01",
    "bills": "34"            
    },{
    "date": "2016-09-02",
    "bills": "34" 
    },{
    "date": "2016-09-03",
    "bills": null
    },{
    "date": "2016-09-04",
    "bills": "34"
    },{
    "date": "2016-09-05",
    "bills": null
    },{
    "date": "2016-09-06",
    "bills": "34"            
    },{
    "date": "2016-09-07",
    "bills": "34" 
    },{
    "date": "2016-09-08",
    "bills": null
    },{
    "date": "2016-09-09",
    "bills": "34"
    },{
    "date": "2016-09-10",
    "bills": null
    }
   ];

  var totalAmount = function(response){        
    for(var i=0; i<response.length; i++ ){
      if (response.bills!==null){
      var totalSum = "";
      totalSum += response.bills;
      }
    }
    return totalSum;
  };
  totalAmount(data);

the dotted lines denote the data for other days, and as you can see some of the days has bill amount denoted as integers and somedays it has value of null. I could use zero, but assuming i got null values in my array and i want to find total sum of expenses of a month, how do i write the function? Check this jsfiddle

Assuming that I have a js object array with my monthly expense data like this

var data=[
    {
    "date": "2016-09-01",
    "bills": "34"            
    },{
    "date": "2016-09-02",
    "bills": "34" 
    },{
    "date": "2016-09-03",
    "bills": null
    },{
    "date": "2016-09-04",
    "bills": "34"
    },{
    "date": "2016-09-05",
    "bills": null
    },{
    "date": "2016-09-06",
    "bills": "34"            
    },{
    "date": "2016-09-07",
    "bills": "34" 
    },{
    "date": "2016-09-08",
    "bills": null
    },{
    "date": "2016-09-09",
    "bills": "34"
    },{
    "date": "2016-09-10",
    "bills": null
    }
   ];

  var totalAmount = function(response){        
    for(var i=0; i<response.length; i++ ){
      if (response.bills!==null){
      var totalSum = "";
      totalSum += response.bills;
      }
    }
    return totalSum;
  };
  totalAmount(data);

the dotted lines denote the data for other days, and as you can see some of the days has bill amount denoted as integers and somedays it has value of null. I could use zero, but assuming i got null values in my array and i want to find total sum of expenses of a month, how do i write the function? Check this jsfiddle

Share Improve this question edited Sep 29, 2016 at 4:40 anoop chandran asked Sep 29, 2016 at 4:33 anoop chandrananoop chandran 1,5105 gold badges25 silver badges44 bronze badges 3
  • Please edit your question to show the JS directly in the question rather than relying on a JSFiddle link. Also, the data structure in your fiddle is different. – nnnnnn Commented Sep 29, 2016 at 4:37
  • 2 Use reduce. data.reduce((prev, curr) => prev + (Number(curr.bills) || 0), 0) – Tushar Commented Sep 29, 2016 at 4:43
  • 1 What did you find when you walked through the code with a debugger? – user663031 Commented Sep 29, 2016 at 4:56
Add a ment  | 

4 Answers 4

Reset to default 4

You've written code which loops over one thing and checks another thing and adds yet another thing, which gives you lots of opportunities for errors of various kinds, as you found! A more modern style of programming not only allows you to write your programs more pactly, but also reduces the chances for bugs and helps you (and others) convince yourself that your program is correct just by reading through it.

We'll approach this top-down. The most basic thing you want to do is to create the sum of something. So we'll start off with the simplest possible program:

sum(bills)

Of course, now we have to implement sum, and figure out where to get bills from!

Getting the sum

Let's get sum out of the way right away. We just loop over some array and add up the numbers:

function sum(array) {
  var total = 0;
  for (var i = 0; i < array.length; i++) total += array[i];
  return total;
}

Now we have a nice, clean, general-purpose, obviously correct function to add up a list of numbers, which we can use with confidence any time we want, even in new projects. It knows nothing about bills. In program design terms, it's "decoupled".

We might also use a new feature in JavaScript, the for...of loop, to write it a bit more tersely. for...of gives you the elements of an array directly, without you having to worry about indexes.

function sum(array) {
  var total = 0;
  for (var num of array) total += num;
  return total;
}

Notice that we are not cluttering up this sum function with checks for null or conversions of strings to numbers. We'll assume that whoever calls it has already taken responsibility for providing a clean list of numbers. This notion is called "separation of concerns". We separate the concern of "getting a sum" from the concern of "filtering/converting the list of numbers".

There are other more fancy ways to write this, such as using the useful reduce function, but we'll skip that for now.

Getting the bills

Next, of course, we have to get the bills value to pass to sum. This is just an array of numbers, like [34, 29, 72]. We'll extract the bills from the input data using a function getBills, so we can write our top-level program as

sum(getBills(data))

Now all we have to do is to write getBills! The most obvious way to do this is to write another for loop:

function getBills(data) {
  var bills = [];
  for (var i = 0; i < data.length; i++) bills.push(data[i].bills);
  return bills;
}

Or again, reminding ourselves how to use for...of:

function getBills(data) {
  var bills = [];
  for (var obj of data) bills.push(obj.bills);
  return bills;
}

That's all fine, but actually JavaScript provides a handy, general way to do this--create a new array from an old array by applying some rule to each element of the old one. That's called map, and we use it by calling it on the old array:

newarray = oldarray.map(rule)

where rule is a function which accepts each element from the old array, and returns some value to put into the new array. In this case, the element from the old array is the little object of the form {"date": "2016-09-02", "bills": "34" }, and the rule we want to apply is to extract the value of the bills property from that object. That's real simple:

function extractBills(object) { return object.bills; }

So now we can write getBills simply as

function getBills(data) {
  return data.map(extractBills);
}

What's going on here? We're passing a function as a parameter to another function? Can you even do that? Yes, in JavaScript functions are "first-class values", which can be passed around just as easily as you would pass around the number 1. You can pass a function into another function as we are doing here, and then that function can "invoke" the function we passed to it. In this case, we are passing a function to map, and map is invoking that function on each element. Such functions are often called "callbacks", because map is "calling back" to our function for each element.

Fixing up the list of bills: getting rid of nulls

We're almost there! The only remaining problem is to get rid of the null values for the bills property in some of the little objects. Instead of mixing this check into our other code, we'll handle this separately. Let's write a little function called nonNull which takes an old array and returns a new array with all the null values removed. Again, the most obvious way to write this is as a loop:

function nonNull(array) {
  var result = [];
  for (var i = 0; i < array.length; i++) {
    var value = array[i];
    if (value !== null) result.push(value);
  }
  return result;
}

Or again, using for...of:

function nonNull(array) {
  var result = [];
  for (var value of array) if (value !== null) result.push(value);
  return result;
}

That's fine, but it turns out that there is yet another handy function that already does this process, called "filtering", creating a new array from an old array, including only certain elements. This function is called filter, obviously enough. To tell it which elements to include in the new array, we give it a rule, again, but this time the rule says whether to include an element or exclude it. This rule is also written as a function, which is passed an element, and returns true or false, meaning to include or not include.

Our rule is simply: is the value null? So we can write this real easily as

function notNull(val) { return val !== null; }

And now we can filter out the null values in some array by saying

newarray = oldarray.filter(notNull);

We can easily test this in stand-alone fashion by typing [1, null, 2].filter(notNull) into the console.

We could also write this as

newarray = oldarray.filter(Boolean)

which applies the built-in Boolean function to each element, which will return false (and thus exclude the element) only if the element is "falsy", meaning undefined, null, 0, or empty string. In this case, that would work too. Try this out by typing Boolean(null) into the console.

Fixing up the list of bills some more: converting to numbers

However, there's one more problem: the values of the bills property are strings. But sum is designed to work on numbers. Again, we don't want to gum up our nice clean sum function with special checks and conversions. We therefore want to do a separate map to convert all the values in an array to numbers. Again, we can use map:

arrayOfNumbers = arrayOfStrings.map(Number)

This calls the Number built-in function (try it in your console by typing Number("34")) on each element of arrayOfStrings, and gives us back an array containing each element converted to a number.

We now have all the pieces we need to plete our little program, which is just

sum(getBills(data).filter(notNull).map(Number))

You can read this from the "inside". In other words, we first get the bills from the data; this will be a value such as ["34", ..., null]. Then we filter this array to remove the nulls. Then we map this array to convert its values from strings into numbers. Finally we call sum on the whole thing. Voilà, we're done.

We can almost read this now as a regular English sentence, which says

getBills from data, filter to keep only notNulls, map them to Numbers, and take the sum of the whole thing.

I find it useful to adopt a kind of "literate programming" style, using ments which this styles makes it easy to write:

sum(                   // Find the sum of
  getBills(data)       // the bills from the data
   .filter(notNull)    // with nulls removed
   .map(Number)        // and strings converted to numbers
)

Complete program

Our plete program is now

var data=[
  {"date": "2016-09-01", "bills": "34"},
  {"date": "2016-09-02", "bills": "34"},
  {"date": "2016-09-08", "bills": null}
];

function sum(array) {
  var total = 0;
  for (var num of array) total += num;
  return total;
}

function extractBills(object) { return object.bills; }
function getBills(data)       { return data.map(extractBills); }
function notNull(val)         { return val !== null; }

function addUpBills(data)     { return sum(getBills(data).filter(notNull).map(Number)); }

console.log(addUpBills(data));

We can almost tell, just by looking at this program, that it is going to work and do what we want it to. It's pretty much impossible for this program to have the kind of bugs you had in your original code, such as initializing some variable inside the loop instead of outside.

But is this all necessary?

In this simple case, perhaps not. You could just as easily write the following:

var total = 0;
for (var obj of data) total += +obj.bills;

taking advantage of the unary + for converting string to number, which also treats null as 0. But the functional concepts will still stand you in good stead as you move forward in your programming endeavors working on more plex problems.

Summary

This is a simple example of what can be called "functional programming", because we are organizing our logic, such as extracting this, or converting that, or filtering some other thing, into functions such as extractBills etc., and then using functions such as map and filter to apply those functions, step-by-step, to our data to arrive at our output.

With functional programming, we often find ourselves working "top-down". In other words, instead of starting off by writing a whole bunch of code, we write a single, clean, top-level solution to our problem. Often we can test that right away by providing some dummy data. Then, we proceed to "fill in the blanks", writing the lower-level pieces that the top-level logic needs. We can also test the lower-level pieces easily, one-by-one; for instance, we can test sum by just typing sum([1, 2, 3]) right into the console, without worrying about bills or anything else.

Another key point is keeping different things separate. In this case, we have looping, and we have checking for null, and we have converting a string to a number, and we have adding things up. Instead of jumbling all these things up into one tangled program, we handle them separately, often by writing them as stand-alone mini-functions, which we then "pose", with the help of functions such as map and filter, to create the solution to our problem. Handling things separately also has the benefit that the little pieces we create can easily be reused, or tested, or changed without breaking other things.

There are several problems in your existing code:

  1. response.bills should be response[i].bills (in two places)
  2. The totalSum variable should be declared before the loop and initialised to 0, not to an empty string.
  3. The values in your data are strings, so you need to convert them to numbers before trying to add them up.

Here's an updated version that fixes those issues:

var totalAmount = function(response){
  var totalSum = 0;      
  for(var i=0; i<response.length; i++ ){
    if (response[i].bills!==null){
      totalSum += Number(response[i].bills);
    }
  }
  return totalSum;
};

But given that Number(null) returns 0, you don't need the if:

var totalAmount = function(response){
  var totalSum = 0;      
  for(var i=0; i<response.length; i++ ){
    totalSum += Number(response[i].bills);
  }
  return totalSum;
};

Or use .reduce():

var totalAmount = function(response){
  return response.reduce(function(p, c) { return p + Number(c.bills); }, 0);      
};

Working demo:

var data=[
    {
    "date": "2016-09-01",
    "bills": "34"            
    },{
    "date": "2016-09-02",
    "bills": "34" 
    },{
    "date": "2016-09-03",
    "bills": null
    },{
    "date": "2016-09-04",
    "bills": "34"
    },{
    "date": "2016-09-05",
    "bills": null
    },{
    "date": "2016-09-06",
    "bills": "34"            
    },{
    "date": "2016-09-07",
    "bills": "34" 
    },{
    "date": "2016-09-08",
    "bills": null
    },{
    "date": "2016-09-09",
    "bills": "34"
    },{
    "date": "2016-09-10",
    "bills": null
    }
   ];

var totalAmountLoopIf = function(response){
  var totalSum = 0;      
  for(var i=0; i<response.length; i++ ){
    if (response[i].bills!==null){
      totalSum += Number(response[i].bills);
    }
  }
  return totalSum;
};

var totalAmountLoop = function(response){
  var totalSum = 0;      
  for(var i=0; i<response.length; i++ ){
    totalSum += Number(response[i].bills);
  }
  return totalSum;
};

var totalAmountReduce = function(response){
  return response.reduce(function(p, c) { return p + Number(c.bills); }, 0);      
};

console.log(totalAmountLoopIf(data));
console.log(totalAmountLoop(data));
console.log(totalAmountReduce(data));

modify your function as below:

var totalAmount = function(response){
        var totalSum = 0;
  for(var i=0; i<response.length; i++ ){
    if (response[i].bills!==null){

      totalSum += parseFloat(response[i].bills);
    }
  }
  return totalSum;
};

totalSum was recreated everytime loop started and you were losing your count. Declare it before the loop starts. Also bills is a string. It needs to be converted to a number before adding it to sum. Also response variable is an array. You were accessing values inside it like an object.

Try this if you want to calculate it for certain month and year considering your data can contain other months as well:

var totalAmount = function(response,forYear,forMonth){
   var totalSum = 0;

   for(var i=0; i<response.length; i++ ){
       if (response[i].bills!==null){
           var d = new Date(response[i].date);
           if(forYear== d.getFullYear() && forMonth==d.getMonth())
               totalSum += parseInt(response[i].bills);
       }
    }
   return totalSum;
};
alert(totalAmount(data,2016,8)); //note Jan=0,Feb=1
发布评论

评论列表(0)

  1. 暂无评论