jQuery Calculation Plug-in: Making calculating easy...

Categories: JavaScript, jQuery

I actually wrote this plug-in months ago, but only made passing mention about in on the jQuery mailing list. Since the topic of dynamic calculation came up again today on the mailing list, I thought I'd go ahead and officially announce the plug-in here.

My jQuery Calculation Plug-in was designed as a generic mathematical library to make it easy to do things like sum or average values that are displayed on the page. For example, to get the sum of all of the elements with the class of "price" you'd use:

alert( $(".price").sum() );

Since I did not want to restrict calculation to only form elements, the plug-in has helper method called parseNumber() which is called internally which parses a numeric value from an element. It can parse both form elements and normal HTML elements (like <div> or <span> elements.) In order to allow parsing of HTML elements that might contain additional formatting in the element, I'm using a regular expression of /\d+(,\d{3})*(\.\d{1,})?/g to parse the contents of the element to find the first thing that looks like a number. And don't worry, you can configure the regular expression if it doesn't meet your needs.

This plug-in actually gives you a lot of power. For example, let's say we have a form that looks like:

calc.plugin.cart

Wouldn't it be great if you could update all the calculations on the screen without writing a ton of code? Well, this is where the Calculation Plug-in really shines. The HTML for the table above is:

<table width="500">
<col style="width: 50px;" />
<col />
<col style="width: 60px;" />
<col style="width: 110px;" />
<tr>
    <th>
        Qty
    </th>
    <th align="left">
        Product
    </th>
    <th>
        Price
    </th>
    <th>
        Total
    </th>
</tr>
<tr>
    <td align="center">
        <input type="text" name="qty_item_1" id="qty_item_1" value="1" size="2" />
    </td>
    <td>
        <a href="http://www.packtpub.com/jQuery/book">Learning jQuery</a>
    </td>
    <td align="center" id="price_item_1">
        $39.99
    </td>
    <td align="center" id="total_item_1">
        $39.99
    </td>
</tr>
<tr>
    <td align="center">
        <input type="text" name="qty_item_2" id="qty_item_2" value="1" size="2" />
    </td>
    <td>
        <a href="http://jquery.com/">jQuery Donation</a>
    </td>
    <td align="center" id="price_item_2">
        $14.99
    </td>
    <td align="center" id="total_item_2">
        $14.99
    </td>
</tr>
<tr>
    <td colspan="3" align="right">
        <strong>Grand Total:</strong>
    </td>
    <td align="center" id="grandTotal">
    </td>
</tr>
</table>

In order to hook our form up so that all of the totals are automatically calculated on the fly, we just need to bind a function to the quantity fields to be triggered on each key press:

// bind the recalc function to the quantity fields
$("input[@name^=qty_item_]").bind("keyup", recalc);

Our recalc() function looks like this:

function recalc(){
    // run the calc() method on each of the "total" fields
    $("[@id^=total_item]").calc(
        // the equation to use for the calculation
        "qty * price",
        // we now define the values for the variables defined in the equation above
        {
            // instead of using a static value, we use a jQuery object which grabs all the quantities
            qty: $("input[@name^=qty_item_]"),
            // now we define the jQuery object which reads in the "price" from the table cell
            price: $("[@id^=price_item_]")
        },
        // this function is execute after the calculation is completed, which allows us to
        // add formatting to our value
        function (s){
            // return the number as a dollar amount
            return "$" + s.toFixed(2);
        },
        // once all calculations are completed, we execute the code below
        function ($this){
            // now we get the sum() of all the values we just calculated
            var sum = $this.sum();

            // now that we have the grand total, we must update the screen
            $("#grandTotal").text(
                // round the results to 2 digits
                "$" + sum.toFixed(2)
            );
        }
    );
}

Well this code may look a little overwhelming at first, it's actually very easy to implement. You can see this example code in action on the Calculation Plug-in's home page.

The one big "gotcha" with using jQuery selectors as variables for the equations is that your selectors must return arrays of equal size. The calc() engine needs matching arrays, so that it can correct apply the calculations to each position in the array. For example, the above code actually translates to:

qty[0] * price[0] = total[0]
qty[1] * price[1] = total[1]

I think for the most part this should be a big deal, but I guess if you had a really strange layout trying to get your selectors to match up array items correctly could be programmatic. Unfortunately, I'm not sure there's a solution to that problem that doesn't involve a lot of complexity.

Run example code

Related Blog Entries

Comments

Rui Gomes's Gravatar hi,
this plug-in doesn´t work on IE 7.


regards,
Rui
Dan G. Switzer, II's Gravatar @Rui:

I just checked this in IE7 and it's working fine for me. Is there something specific that's not working for you?
Rui Gomes's Gravatar hi, thks for the reply.

if tested with your demo link and nothing (calculations) it´s working, but it works fine on FF.

regards,
Dan G. Switzer, II's Gravatar @Rui:

I tested the code on my example page under IE7 and everything worked as expected. Perhaps you have something filtering/blocking content that's running under IE7.

What errors are you seeing? I have tested the code under two different IE7 installations and the page works on both machines. It also works under IE6 for me.
Rui Gomes's Gravatar ok.

...and what might be filtering/blocking ?
i don´t have any firewall installed and i´ve stopped my anti-virus also.
Rui Gomes's Gravatar if you want i´ll send you a print screen of it.
Rui Gomes's Gravatar i´ll test on another IE7, ok ?

regards,
Dan G. Switzer, II's Gravatar @Rui:

One culprit might be my check to see if Firefox is installed:

bIsFirebugReady = (!!window.console && !!window.console.log);

In normal circumstances, IE7 should not return true for that line--but perhaps you have something installed (like a debugger) which is causing the bIsFirebugReady check to be true.

I'll certainly look into any errors you post for me.
Rui Gomes's Gravatar ok, thks again for the reply.

regards,
Dan G. Switzer, II's Gravatar @Rui:

Whatever info you can give me, I'll definitely try to fix the problem. It's just hard to address when you can't replicate and without more information on the error(s) you're getting, there's not a whole lot I can do. :)
Fontzter's Gravatar Hi Dan,

Thanks for the great plug-in. One question/feature request: It does not handle negative numbers. I think I was able to overcome this by changing the regex to allow an minus sign. Do you think this will suffice?

reNumbers: /-?\d+(,\d{3})*(\.\d{1,})?/g

I suspect that is why you have the regex as an option. My point in writing was merely to ask if my regex would work okay and allow negative numbers; and, if so, to suggest that you may want to make this the default. For example, on your demo page, if you enter a negative number, it actually sums or averages the absolute of it, which is a little misleading.

Thanks again for the great plug-in!

Dave
Dan G. Switzer, II's Gravatar @Dave:

That's definitely one of the reasons I saw the need to make the regex configurable. Plus, European number formatting is different from the US.

Adding support for negative numbers was definitely an oversight on my part. The regex you have should work fine.

As soon as I get a chance to do some more testing, I'll add the check for negative numbers to the next revision.

I think the work I did last week to the sum() and avg() methods make it much easier to use. I'm always open to other suggestions as well.
Dan G. Switzer, II's Gravatar @Dave:

I updated the code this weekend and added support for negative numbers. Please test the new build and let me know how it works. I also added some callback methods for handling when the parseNumber() method can't find a numeric value in the element.
Venkat's Gravatar Hi Dan,

Thanks for the nice plug-in.
How to calculate the time can u pl give an idea

Thanks
Venkat
:)
balboa's Gravatar how do i do calculation like this: (qty * price) + fee. is there another event other than keyup? coz i use select box to trigger the calculation
Dan G. Switzer, II's Gravatar @balboa:

You can use whatever event you want to trigger the re-running of the $.calc() method--my example just uses the keyup. You could use my example pretty much verbatim, but just change the "keyup" event to the event you want to use (for a select box, you'll whant "change".)
Xevo's Gravatar Hey Dan,

I've just started using your script, I like it a lot. But I'm working on a system which also needs to give a discount after a certain quantity number. I've been struggling with it for a while now, but I just can't get it right, could you help me with this?

Thanks!
Dan G. Switzer, II's Gravatar @Xevo:

What's your code look like, is there an example I can see? Also, what problem are you having?
Lukas Stancik's Gravatar The sad thing is that I can not use this beatiful peace of code because whenever the form contains <input name="id"... jQuery throws an error 'z.indexOf is not a function'. I am not able to rename the input field because a lot of my CMS extensions depend on it :(
Dan G. Switzer, II's Gravatar @Lukas:

This isn't a jQuery problem, but an IE problem with form fields which have a name attribute value of "id" or "name".
http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
tom's Gravatar Hi Dan

Great plugin, it's really helped me a lot. Do you have any examples of how to work out the difference between two groups of fields? I know it should be simple but I'm stuck.

e.g. I have a group with the class ".revenue" and one with ".cost" so I need to get the sum() of each and find the difference and put it in a 'total' field.


cheers

t
Dan G. Switzer, II's Gravatar @Tom:

Take a look at the example of the shopping cart and look at the code for the calc() method. It shows off similar functionality.
hal's Gravatar This is great. I now need to be able to submit a form and post the individual item totals and the grand total. It's quite clear I am a real javascript newbie so any help is greatly appreciated.
Thanks, hal
Dan G. Switzer, II's Gravatar @Hal:

You should do the calculations on the server side. Never trust data coming from the client.
Jonne's Gravatar Thanks for this cool plugin. It was very easy to use!
I have one question though. I'd like to format the totals (i want to group the digits).
At the moment I'm using this:
$("input[@name$=_x]").sum("keyup", "#som_month_x");
I tried to convert this to something similar to the calc() example above, so that I'd
have a cbFormat argument.
What would be the best way to realize this? Thanks !!!
Dan G. Switzer, II's Gravatar @Jonne:

You can pass in an "oncalc" option, which is a call back that gets triggered after the calculation is performed. You can use this to format the value.
Jonne's Gravatar Great, and I figured out I should do that like this:
$("input[@name^=input_]").sum({bind:"keyup",oncalc:myOnCalc}, "#som");
Thanks !
jonne's Gravatar Oops, I posted that it worked immediately when I figured out the oncalc callback was called. But now instead of the totals, nothing is displayed. I see that the oncalc method has two arguments, the value and options. But what should the method do? I tried returning nothing, true, false, s, and tried to put s in the totals field. But in all cases, the end result is an empty total field. What am I doing wrong? Thanks.
function formatTotals(s, opt) { return s.toFixed(2); // test }
Stefan's Gravatar @Dan - Could you please provide an example of how to use the oncalc callback? Thanks!
Dan G. Switzer, II's Gravatar @Stefan:

If you look at the max() example in the demo, you could change that to something like:

$("input[@name^=max]").max("keyup", {
   selector: "#numberMax"
   , oncalc: function (value, options){
      // you can use this to format the value
      $(options.selector).val("+" + value);
   }
});

This would add a plus symbol in front of the max value. If you reference "this" from inside the oncalc callback, it'll return a reference to the jQuery object of $("input[@name^=max]").
Stefan's Gravatar @Dan: Thanks a lot! It's working!

One more question: I need to support the German number format for decimals where the "," is used as decimal point. Although I can change the regex for parsing the number format, I need to convert the comma into a dot before I can do any calculations. I can't figure out where to do the "replace()" without changing your plugin code. For the moment I changed your "parseNumber()" function by replacing this line

v = v[0].replace(/[^0-9.\-]/g, "");

with this line

v = v[0].replace(/,/g, ".").replace(/[^0-9.\-]/g, "");

I'm sure there's a better solution without the need to change your code.

Thanks for you help!
Dan G. Switzer, II's Gravatar @Stefan:

Yeah, there's not a good way to handle that right now. What I would suggest is add an onParseNumber callback instead. You could then use that to convert the European format to US for the actual math operations.

It would go something like this...

Add this line above the onParseError in the defaults:

      // a callback function to run once when parsing a number
      , onParseNumber: null

Now change the lines:

               // clense the number one more time to remove extra data (like commas and dollar signs)
               v = v[0].replace(/[^0-9.\-]/g, "");

To:
               // get the number
               v = (jQuery.isFunction(options.onParseNumber)) ? options.onParseNumber.apply($el, [v[0]]) : v[0];
               // cleanse the number one more time to remove extra data (like commas and dollar signs)
               v = v.replace(/[^0-9.\-]/g, "");

What you can do now is define the onParseNumber callback and have it return the value formatted with commas converted to periods.

Add this to your source code (not the jquery.calculation.js code):

$.Calculation.setDefaults({
   onParseNumber: function(value){
      return value.replace(/,/g, ".");
   }
});

I haven't test this, but it should work. It also gives you more flexibility to do other things in the future.
Stefan's Gravatar Dan, it works like a charm! Thank you very much!
Christopher's Gravatar I love this plugin. I'm using it on a clients site and it's working great, but I'm having trouble doing something that a commenter mentioned earlier.

How would I get something like this to work?

qty * price + fee

I'd love to hear what you guys have to say, because I can't get it to work.
Candi's Gravatar I am using your form and it works. But I am running into a problem that I can't solve as I am not that familiar with jquery. I use the grandTotal as a value in a form which will be submitted to a secure server. I've tried everything I can but can't seem to get this work. Is there any easy work around for this?
Dan G. Switzer, II's Gravatar @Candi:

You should never trust values sent from the client to the server--it's too easy to manipulate the values. When calculating values in the browser like this, it should be done simply to improve the UI. You should not be trusting this values, but should always verify data and do calculations on the server-side.
Cenk's Gravatar I love this plugin. I'm using your plugin in an online order form and it's working great. But this is the problem ;

I want to use the grandTotal as a value of an input area. I've tried everything I can but can't work. Any idea :(
Dan G. Switzer, II's Gravatar @Cenk:

There shouldn't be an issues using an input field for value--as the examples show. I'd make sure you're using the most recent version:

http://www.pengoworks.com/workshop/jquery/calculat...

Also, while there shouldn't be any issues with using an input field, I'd advise against it, because there's no general reason to use. You never want to trust a value coming from the client like that--you'd want to do the calculations on the server anyway.
Candi's Gravatar Thanks for responding. I ended up using a different calculating script. It's working great. It's too bad we can't use your plugin for quick calculating forms to be submitted to a merchant since it's so easy to implement. But I appreciate your time and energy!
Dan G. Switzer, II's Gravatar @Candi:

As I said, there should be no problem using an input element to update the value to. However, I would *never* be posting calculated values to a merchant that were generated via JavaScript. This makes your site extremely vulnerable to hacking. I hope you're not in any way trusting these values to be accurate.
Cenk's Gravatar @Dan :

Thx for quick answer. But client side security is not a problem in this project.
This is your(my) script part for grandTotal:
$("[id^=grandTotal]").text(
            // round the results to 2 digits
               sum.toFixed(2) + "TL"
            );

and this the output:
<td (or div) id="grandTotal">'grandTotal value'</td (or /div)>

but I want this output:
<input type="text" id="grandTotal" value="'grandTotal value'" name="whatever">
or
<td (or div) id="grandTotal">'grandTotal value'</td (or /div)>
<input type="hidden" name="whatever" value="'grandTotal value'">
Thanks a lot.
Dan G. Switzer, II's Gravatar @Cenk:

If your trying to update the value of an input element, you want to use val() instead of text()--that's your problem. What I'd recommend doing is something like this instead:

$("input[name^='sum']").sum({
    bind: "keyup"
   , selector: "#grandTotal"
   , oncalc: function (value, settings){
      // you can use this callback to format values
      $(settings.selector).val(value.toFixed(2) + "TL");
   }
});

This would attach an event whenever the user finishes pressing a key in any input field that started with the id of "sum" and update the value in the #grandTotal element automatically. Maybe you don't need the "live" updating function--so this is just a suggestion.

Also, instead of using the selector "[id^=grandTotal]" I'd just use "#grandTotal"--which is much faster and efficient.

The syntax "[id^=grandTotal]" says look through *all* elements for ones that have an id attribute that starts with "grandTotal". That would match <div id="grandTotal1" />, <div id="grandTotal2" />, etc.

On the otherhand, the syntax "#grandTotal" is just a shortcut for document.getElementById("grandTotal")--which is way faster and efficient.
Cenk's Gravatar @Dan:-) Thanks a lot again. It's working brillant :-)))
Benjo's Gravatar this is really a great plug-in. I'm now trying to develop an application but i need some help ...

Is it possible to add some validation before the final result.
To be more precise i need to check if the result will be between 200 and 500. If it is less than i need this app to show the result as 200 and if it is more than 500 to show result as 500. But if it is between 200 and 500 it should display the exact value

for example
20*3 the result should be 200
51*8 the result should be 408
100*6 the result should be 500

i don't have lot of fields just two, the first which is changeable and the other is static

Thank a lot in advance
Dan G. Switzer, II's Gravatar @Benjo:

If you're using the calc() method, just use the complete callback to trigger some validation (which you're have to handle manually.)

$("[id^=total_item]").calc(
   // the equation to use for the calculation
   "qty * price",
   // define the variables used in the equation, these can be a jQuery object
   {
      qty: $("input[name^=qty_item_]"),
      price: $("[id^=price_item_]")
   },
   // define the formatting callback, the results of the calculation are passed to this function
   function (s){
      // return the number as a dollar amount
      return "$" + s.toFixed(2);
   },
   // define the finish callback, this runs after the calculation has been complete
   function ($this){
      // DO VALIDATION HERE
   }
);
Benjo's Gravatar thanks lot. I added my validation at the point you showed me and it works great.
Other question:
Trying something out i tried to make two fields changeable, so no matter what field the user edit (like in this example qty and price) the total should change. Do i have to bind something to both fields like
// bind the recalc function to the quantity fields
$("input[@name^=qty_item_]").bind("keyup", recalc);
and where to put this line of code for my other field???

Second question:

What if my table is generated dynamically based on a recordset. How to specify the IDs of those elements? i mean something like a shopping cart... If i have 10 items in my cart i would have 10 rows. Each row would have QTY field, price and total. How to make this plugin recognize how many rows there are and what are there ID's and do the math for each row.

I'm sorry to have so much questions but i really like your plugin and i want to use it as much as possible.
Thanks in advance.
Dan G. Switzer, II's Gravatar @Benjo:

If you want to recalc on each keypress, then you'd have to bind the behavior to any field where the user can press a key. Since jQuery is based on CSS selectors, you can use the comma as an "AND" selector, so something like this would bind to multiple elements:

$("#id1, #i2").bind("keyup", recalc);

As for the second question, you need to understand the CSS selector being used.

The "input[name^=qty_item_]" (drop the @, it's been depreciated) says "Grab all input elements with a name attribute starting with "qty_item_".

This means it would grab every element like:

<input type="text" name="qty_item_1" />
<input type="text" name="qty_item_2" />
<input type="text" name="qty_item_3" />

So, as long as your elements have a nomenclature like above, nothing in your jQuery code need to change.
Benjo's Gravatar Thanks a lot, it works!
Philipp's Gravatar support for European number format.

Hi, I love ur plugin!
I have trouble to get it working with the European number format.

U described the chagnes for Stefan but the it seems it doesn't work with the current version anymore.

can u please give me the changes I have to do in the current version?

THX a lot!!
Dan G. Switzer, II's Gravatar @Philipp:

See this comment for how to use European formatted numbers:

http://blog.pengoworks.com/index.cfm/2008/2/20/Wat...
Mang Dadang's Gravatar I love ur plug in. But, how do I get price an item from db using select box?
Philipp's Gravatar Hi Dan!

Got the same problem like stefan with the german/european comas, the output got every time points. I need 990,50 EUR not 990.5 EUR. How can I do that? I tried allready the hints of the other posts, but got no luck.

Thanks
Philipp
John's Gravatar Thanks for this library. Works great.
But here's a new issue. I have a time-entry system where people can enter their time worked for the day. There are several time entry fields on the form and they enter it in hh:mm format, so if someone worked on project A for an hour and 15 minutes, then they'd enter it as 1:15. So I want a field on the bottom of the column that will add up the fields and show total time. So the calculation must be done base 60 and account for the colons.
Any thoughts?
Dan G. Switzer, II's Gravatar @John:

That's basically outside of the scope of the concept of the calculation plug-in. Time calculations are a completely different beast altogether. You can get into days, months, years, etc.

You're really going to want to create your own solution for your needs here. The calc plug-in is designed to help you solve generic math problems. I may eventually consider date/time functionality, but have no plans to do so currently.
rpcutts's Gravatar Hi Dan,

First off great plugin. 10 points for ease of use.

I have come accross a bug. I noticed it on my site and after checking I get the same result on your demo page.

If you go to the first demo (the sum demo) and in the 4 boxes put:
10
9
5.1
5.21

The answer it gives is: 29.310000000000002

I looked at the source code and am at a loss as to why it happens.


Any ideas?
rpcutts's Gravatar After placing this code in a blank page:

var total = 0;
total += 10;
total += 9;
total += 5.1;
total += 5.21;

alert(total);

I get the same nuts result. Must be a javascript funny....
I think total = total.toFixed(2); will solve my problem.
Dan G. Switzer, II's Gravatar @rpcutts:

This is actually a precision issue. Here's some information that describes the issue:
http://bytes.com/topic/javascript/answers/518574-j...

To see a simple example of the precision issue, copy the text "javascript:alert(0.7 + 0.1)" (without the quotes) into your address bar and press [ENTER].

I'm sure you'll expect the answer to be 0.8, but instead you'll see the value is 0.7999999999999999.

I may look at adding some logic to help make the results more predictable.

However, you can resolve this issue by using the oncalc callback to simply format the number to the precision you want.
rpcutts's Gravatar Yes I've been educated on the fun world of floating point.
Sorry for all the noise.
Dan G. Switzer, II's Gravatar @rpcutts:

Last night I decided to add some precision handling to the code. If you download the new version of the code the sum() and calc() method should automatically round numbers to the correct precision.

It does this by counting the max decimal places being used by the numbers and using that as a reference to figure out the precision.

Add Comment

Leave this field empty


If you subscribe, any new posts to this thread will be sent to your email address.