| Web Developer Resources | |
JavaScript Time Card Calculator
Update:
There is an updated version of the online timecard calculator here.
It has some new features, includes bug fixes, and provides a variety of integrated sanity checks.
If you have a moment, I'd love your feedback.
Introduction
Having been converted from a salaried to an hourly employee, I found myself wanting to make midweek evaluations
to determine if I was ahead or behind in my accumulation of hours for the week. Having been ruined by forms and calculators,
I find it difficult to do the calculations in my head particularly when it gets down to exact minutes.
So, my solution was to write a JavaScript to do that for me. Since I figure I'm not alone in this problem, I thought
I'd share this with anyone interested. Let me know if you run into any problems or if you have suggestions for
improving this... I can't make any promises, but I'd love your input.
Let's get started
What I basically want to do is walk through the process of calculating a single day, then possibly
expand that code to cover multiple days (a week, or two week period for example).
Breaking down the overall problem of calculating a timesheet, I came up with this series of steps
that will need to happen.
- Get user input for start and stop times and a lunch adjustment.
- Convert the start and stop hours into 24 hour format.
- Convert the minutes from 60 to 100ths.
- Subtract the converted start time from the converted stop time.
- Subtract the lunch adjustment.
- Display the total hours in 100ths.
Again, I'm going to start by calculating a single day's time card related hours in order to simplify this.
The functional example -- Try Me!
<form name="ivForm"> <div id="error_chunk" style="color:red;"></div> <table cellpadding="4" cellspacing="0"> <tbody><tr style="background:#ddd;" class="header_row"> <td>Row</td> <td>Starting Time (HH:MM)</td> <td>Ending Time (HH:MM)</td> <td>Lunch/Breaks(HH:MM)</td> <td>Calculated Total</td> </tr> <script type="text/javascript"><!-- for(var i=1; i<6; i++){ var html = '' +' <tr style="background:#eee;">' +' <td><u>'+i+'</u></td>' +' <td><input name="start_hr'+i+'" value="00" class="t1" onchange="if(!this.init) this.init=1; return calc(this)" accesskey="'+i+'"> :' +' <input name="start_min'+i+'" value="00" class="t1" onchange="return calc(this)"> ' +' <select name="start_time'+i+'" onchange="return calc(this)">' +' <option>AM<option>PM' +' </select>' +' </td>' +' <td><input name="end_hr'+i+'" value="00" class="t1" onchange="if(!this.init){ this.init=1; this.form.end_time'+i+'.options.selectedIndex=1; }; return calc(this)"> :' +' <input name="end_min'+i+'" value="00" class="t1" onchange="return calc(this)"> ' +' <select name="end_time'+i+'" onchange="return calc(this)">' +' <option>AM<option>PM' +' </select>' +' </td>' +' <td><input name="break_hr'+i+'" value="00" class="t1" onchange="return calc(this)"> :' +' <input name="break_min'+i+'" value="00" class="t1" onchange="return calc(this)"> ' +' </td>' +' <td><div id="stot'+i+'"></div></td>' +' </tr>' document.write(html) } //--></script> <tr style="background:#eee;"> <td><u>1</u></td> <td><input name="start_hr1" value="00" class="t1" onchange="if(!this.init) this.init=1; return calc(this)" accesskey="1"> : <input name="start_min1" value="00" class="t1" onchange="return calc(this)"> <select name="start_time1" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="end_hr1" value="00" class="t1" onchange="if(!this.init){ this.init=1; this.form.end_time1.options.selectedIndex=1; }; return calc(this)"> : <input name="end_min1" value="00" class="t1" onchange="return calc(this)"> <select name="end_time1" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="break_hr1" value="00" class="t1" onchange="return calc(this)"> : <input name="break_min1" value="00" class="t1" onchange="return calc(this)"> </td> <td><div id="stot1">0.00</div></td> </tr> <tr style="background:#eee;"> <td><u>2</u></td> <td><input name="start_hr2" value="00" class="t1" onchange="if(!this.init) this.init=1; return calc(this)" accesskey="2"> : <input name="start_min2" value="00" class="t1" onchange="return calc(this)"> <select name="start_time2" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="end_hr2" value="00" class="t1" onchange="if(!this.init){ this.init=1; this.form.end_time2.options.selectedIndex=1; }; return calc(this)"> : <input name="end_min2" value="00" class="t1" onchange="return calc(this)"> <select name="end_time2" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="break_hr2" value="00" class="t1" onchange="return calc(this)"> : <input name="break_min2" value="00" class="t1" onchange="return calc(this)"> </td> <td><div id="stot2">0.00</div></td> </tr> <tr style="background:#eee;"> <td><u>3</u></td> <td><input name="start_hr3" value="00" class="t1" onchange="if(!this.init) this.init=1; return calc(this)" accesskey="3"> : <input name="start_min3" value="00" class="t1" onchange="return calc(this)"> <select name="start_time3" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="end_hr3" value="00" class="t1" onchange="if(!this.init){ this.init=1; this.form.end_time3.options.selectedIndex=1; }; return calc(this)"> : <input name="end_min3" value="00" class="t1" onchange="return calc(this)"> <select name="end_time3" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="break_hr3" value="00" class="t1" onchange="return calc(this)"> : <input name="break_min3" value="00" class="t1" onchange="return calc(this)"> </td> <td><div id="stot3">0.00</div></td> </tr> <tr style="background:#eee;"> <td><u>4</u></td> <td><input name="start_hr4" value="00" class="t1" onchange="if(!this.init) this.init=1; return calc(this)" accesskey="4"> : <input name="start_min4" value="00" class="t1" onchange="return calc(this)"> <select name="start_time4" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="end_hr4" value="00" class="t1" onchange="if(!this.init){ this.init=1; this.form.end_time4.options.selectedIndex=1; }; return calc(this)"> : <input name="end_min4" value="00" class="t1" onchange="return calc(this)"> <select name="end_time4" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="break_hr4" value="00" class="t1" onchange="return calc(this)"> : <input name="break_min4" value="00" class="t1" onchange="return calc(this)"> </td> <td><div id="stot4">0.00</div></td> </tr> <tr style="background:#eee;"> <td><u>5</u></td> <td><input name="start_hr5" value="00" class="t1" onchange="if(!this.init) this.init=1; return calc(this)" accesskey="5"> : <input name="start_min5" value="00" class="t1" onchange="return calc(this)"> <select name="start_time5" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="end_hr5" value="00" class="t1" onchange="if(!this.init){ this.init=1; this.form.end_time5.options.selectedIndex=1; }; return calc(this)"> : <input name="end_min5" value="00" class="t1" onchange="return calc(this)"> <select name="end_time5" onchange="return calc(this)"> <option>AM</option><option>PM </option></select> </td> <td><input name="break_hr5" value="00" class="t1" onchange="return calc(this)"> : <input name="break_min5" value="00" class="t1" onchange="return calc(this)"> </td> <td><div id="stot5">0.00</div></td> </tr> <tr> <td colspan="5" align="right" id="stots"><b>Total Hrs = 0</b></td> </tr> </tbody></table> <br><input type="button" name="mcalc" value="Calculate" onclick=" /* walk through the rows, use known input to generate cases for calc to operate on */ f = this.form; for(var i=0; i<f.elements.length; i++){ if(f.elements[i].name.match(/start_hr/)){ calc(f.elements[i]); } } cal_days_sum() ">
<script type="text/javascript"><!-- err = function(msg,obj){ /* global error handler */ errobj = document.getElementById('error_chunk') errobj.innerHTML = msg if(obj){ window.errObj = obj; setTimeout('if(window.errObj.focus){window.errObj.focus();}if(window.errObj.select){window.errObj.select();} window.errObj=0;',250) } return false; }
validate = function(obj){ tmp = parseInt(obj.value) //if(tmp.length<1) obj.value='00'; return
if(obj.name.match(/(start|end)_min/)){ if(tmp<0 || tmp>59){ return err('Minutes must be between 00 and 59', obj); } } else if(obj.name.match(/(start|end)_hr/) && obj.init){ if(tmp<1 || tmp>12){ return err('Hour must be between 01 and 12', obj); } } }
cal_days_sum = function(){ /* sum the hours column(s) and give a total of the hours shown -- mainly for multiple days */ if(!this.arr){ this.arr = new Array() var tmparr = document.getElementsByTagName('div') for(var i=0; i<tmparr.length; i++){ if(tmparr[i].id.match(/^stot/)) this.arr.push(tmparr[i]) } } var sum = 0 for(var i=0; i<this.arr.length; i++){ sum += parseFloat(this.arr[i].innerHTML) } obj = document.getElementById('stots') obj.innerHTML = '<b>Total Hrs = '+sum+'</b>' }
calc = function(obj){ /* get the sibling objects */ objN = obj.name.match(/\d+$/); //get the object number so we can find it's siblings f = obj.form start_hr = eval('f.start_hr'+objN) start_min = eval('f.start_min'+objN) start_time = eval('f.start_time'+objN) end_hr = eval('f.end_hr'+objN) end_min = eval('f.end_min'+objN) end_time = eval('f.end_time'+objN) break_hr = eval('f.break_hr'+objN) break_min = eval('f.break_min'+objN)
/* validate hour and minute limits */ err(''); /* clear error */ t = start_hr; validate(t) t = start_min; validate(t) t = end_hr; validate(t) t = end_min; validate(t) t = break_hr; validate(t) t = break_min; validate(t)
/* convert to 24 hour */ start_hr.val = parseInt(start_hr.value); //force it to be a number if(start_time.options[start_time.selectedIndex].text.match(/PM/,'i')) start_hr.val += 12; end_hr.val = parseInt(end_hr.value); if(end_hr.val==12) end_hr.val += 0; else if(end_time.options[end_time.selectedIndex].text.match(/PM/,'i')) end_hr.val += 12; break_hr.val = parseInt(break_hr.value); tot_hr = (end_hr.val - start_hr.val - break_hr.val)
/* convert minutes from 60 to 100 */ start_min.val = parseFloat(start_min.value) / 60 end_min.val = parseFloat(end_min.value) / 60 break_min.val = parseFloat(break_min.value) / 60 tot_min = (end_min.val - start_min.val - break_min.val)
/* accommodate graveyard */ if(start_hr.val > end_hr.val){ end_hr.val += 24 tot_hr = (end_hr.val - start_hr.val) }
t = parseFloat(parseFloat(tot_hr) + parseFloat(tot_min)) if(t<0) t = 'NA' else{ // need a decimal point + two digits to it's right if(!t.toString().match(/\./)) t+= '.00' else if(!t.toString().match(/.+\.[0-9][0-9]/)){ t+='00' } t = t.toString().match(/.+\.[0-9][0-9]/) } //alert(start_hr.val +'--'+ end_hr.val+'--tot_hr='+tot_hr+'--tot_min='+tot_min+'--t='+t) obj = document.getElementById('stot'+objN); if(errobj && errobj.innerHTML) t = 'NA' obj.innerHTML = t
cal_days_sum() }
/* start by forcing a calc */ document.ivForm.mcalc.click() //--></script> </form>
Source Code for Example
Notes from JavaScript Time Card Calculator Project
Dirty Object Relationships
The JavaScript in this example leaves a bit to be desired.
Because there is a relationship between all of the form elements on a given row we have to create
script that is aware of the relationship, but it's not as intuitive as perhaps it could be.
My example builds those relationships each time a change requiring a refactoring of those relationships
occurs. It does so by having the "changed" object (form element) call a function passing itself as
an argument. The function assumes the caller ends with a number which it obtains using a regular expression.
It further assumes it can use that number combined with an expected set of names to attach to and evaluate
all of the related form elements for that row. Some part of me cringes to have to use that instead
of some more graceful mechanism, but a more efficient mechanism didn't materialize, so there it is.
Testing to Validate or Invalidate your logic
It's important to define test cases to validate (or invalidate) your approach to solving the problem.
Here are the cases I used:
- 8AM - 5PM (best case scenario)
- 8:30AM - 5:30PM (test minutes)
- 10AM - 11AM (doesn't cross AM/PM boundary)
- 11AM - 10AM (error case? or does this imply an 23 hour stint... unsure.)
- 12PM - 12AM (12 hour boundary case A)
- 12AM - 12PM (12 hour boundary case B)
- 11AM - 11PM (sanity check)
- 11PM - 11AM (sanity check)
- 9PM - 6AM (graveyard outlyer)
Don't Pull Your Hair Out
Programming is like puzzle building in a lot of ways. It's often tedious and aggravating.
I went through numerous iterations trying to get the calculations generated by all the related items on
a row to match all of the test cases I outlined. My strategy over time has become to hibernate the computer
(sorry, I use Windows... at least until true Linux migration is feasible -- including all the apps)
and walk away to let my brain relax. It's very easy to program yourself into a corner. While it's happening,
your mind latches onto what it believes is a solution, and then you start trying to force the other pieces
into play around that solution... whether they really fit or not.
If you allow this process to continue your mind cramps up like a clenched muscle and you begin to inject
code into the process which you will kick yourself for doing later. So, when you first recognize that you
are banging your head on the wall, put down the computer and walk away. You'll discover that when your
mind is really tuned to programming, it just attracts and organizes the answers like a magnet.
Wish List
Here are some of the things I'd like to add (unless I've already added them and just forgot to pull the item...er, oops.)
- Use ajax and a cgi to persist the user's data instead of making them type it each time. Incremental changes
might be faster and easier than starting from scratch each time.
- Use cookies to persist the data before bothering with ajax and a cgi... less mechanism needed!
Keywords: javascript time card calculator timecard esqsoft timesheet time sheet calculator
|