Using JS code libraries, part 2
A working sample with transparent class importing
Let’s step back a simplistic implementation specifically to show how powerful this can make your code reuse. Please start with Using JS code libraries, part 1 if you haven’t read it yet.
Days old service
We’ll make a “Days old” service. The client code supplies the user’s birthday and the service returns how many days old the the user is today, as told by the user’s system clock anyway.
While the goal is somewhat frivolous, the implementation is deceptively complicated. There are sticking points.
- Argument handling and finding the right arguments in the script stack. We dealt with this difficulty in part 1.
- A delta days algorithm is not easy. Dates and times are very difficult
to work with because of leap years and irregular lengths of all
Date()
units. There’s no metric clock. - Anyone who’s reached 3 years-old is over 1,000 days old. Programmatic
math doesn’t give a hang about commas so our algorithm needs a fancy
commify()
routine to turn 10000 into the human friendly 10,000. The built-intoLocaleString
should really handle this for us but its implementation is random from engine to engine.
For each of the three challenges we’ll have a separate library. Date math is something we’d surely like to reuse if we can get it right. Commifying numbers is also something we’ll probably want to use over and over. And we already know how valuable the argument handling can be.
Three libraries
- Code for the Args() classes
- The second version of the argument handling class developed
in Args() plus the new
_findCaller()
we put together in part 1. - NumberExt.js
- An extension of JavaScript’s built-in
Number()
object. We just want to add a commify routine to return a commified string representation of the number. - DateExt.js
- An extension of JavaScript’s built-in
Date()
object. We will add two new methods:julianDays()
anddeltaDays()
.
Three phases of execution
The client still enjoys the simplicity of a single phase but we have to take care of three.
-
Receive the client’s service call. We’ll call this script,
daysOld.js
. -
daysOld.js
writes out <script> tags to bring in JavaScript.-
It writes the HTML to import the libraries–
ArgsObj.js
,DateExt.js
, andNumberExt.js
–into the page’s code space. -
It writes the HTML to import the actual service,
daysOldProper.js
, which will use the libraries.
-
It writes the HTML to import the libraries–
- Execute
daysOldProper.js
, the real service, now that it has access to the library code written into the page bydaysOld.js
.
We have to write all the code involved of course. Best to start at the beginning. The client call will look like most of the others we’ve seen.
Client call to daysOld.js
<script type="text/javascript" src="http://elektrum.org/js/daysOld.js?birthday=1996-03-15"> </script>
The client call has to write out the HTML, namely <script> tags,
that will import the libraries before we try to run the real service
script which is daysOldProper.js
.
For simplicity, we’ll accept the birthday in only one format: YYYY-MM-DD.
Desired output of daysOld.js
<script type="text/javascript" src="http://elektrum.org/js/ArgsObj.js"></script> <script type="text/javascript" src="http://elektrum.org/js/DateExt.js"></script> <script type="text/javascript" src="http://elektrum.org/js/NumberExt.js"></script> <script type="text/javascript" src="http://elektrum.org/js/daysOldProper.js"></script>
Note there is no passing of the arguments from the client call. It’s
not possible to know the query string without having the
ArgsObj()
available. That code only exists after
daysOld.js
is finished executing. We’re still just
building up our script space. The actual service that will send
visible output to the client’s screen is in
daysOldProper.js
.
daysOld.js
document.write('<script type="text/javascript" ' + 'src="http://elektrum.org/js/ArgsObj.js">' + '</script>'); document.write('<script type="text/javascript" ' + 'src="http://elektrum.org/js/DateExt.js">' + '</script>'); document.write('<script type="text/javascript" ' + 'src="http://elektrum.org/js/NumberExt.js">' + '</script>'); document.write('<script type="text/javascript" ' + 'src="http://elektrum.org/js/daysOldProper.js">' + '</script>');
The code for the first library we import, ArgsObj.js
, has
been covered already and can be seen in Args(). We’re
using the second version that bypasses the params
attribute and loads the arguments into the top level of the object;
all param keys become object attributes.
DateExt()
, the extension to the Date()
object, has two prototyped functions, called methods, in it. Once they
are loaded in the namespace, every Date()
object created
will be able to use the methods julianDays()
and
deltaDays()
.
The only difficult part is the algorithm for Julian Days. Dates back to the epoch—January 1, 1970—are easy to handle in JavaScript by subtracting milliseconds. Anything before that won’t fly.
- Julian Days
- or Julian Day Number is the time that has elapsed since January 1, 4713 BCE. It is the generally preferred algorithm for tracking relative time since current machine implementations of dates generally only handle dates back to January 1, 1970. [more at Wikipedia]
Any Gregorian date, our regular calendar, can be converted to Julian Days. This is ideal for delta measures since it’s irrelevant on which calendar 1,000 days passed. It’s still 1,000 days difference.
DateExt.js
Date.prototype.julianDays = function () { if ( this.jd ) return this.jd; var y = this.getFullYear(); var m = this.getMonth() + 1; var d = this.getDate(); // this is a fairly well know algorithm for converting if ( m < 3 ) { m += 12; y--; } // Julian Days conversion algorithm is from "Astronomical Algorithms," // 1991, Jean Meeus. Found via Peter Baum. this.jd = Math.floor( 365.25 * ( y + 4716 ) ) + Math.floor( 30.6001 * ( m + 1 ) ) + d + 2 - Math.floor( y / 100 ) + Math.floor( Math.floor( y / 100 ) / 4 ) - 1524.5; return this.jd; } Date.prototype.deltaDays = function ( date2 ) { // might want to try/throw custom errors here return Math.abs( this.julianDays() - date2.julianDays() ); }
Now to extend Number()
objects. Once this is imported,
every positive integer Number()
will be able to use the
method commify()
.
NumberExt.js
// decide whether commify() should really periodify() thousandths var tmpNum = new Number(0.1); // thousandth marker is opposite of decimal marker Number.localeThousandth = tmpNum.toLocaleString().match(/,/) ? '.' : ','; Number.prototype.commify = function () { var numStr = this.toString(); var num = numStr.split(''); // simplify by only accepting integers longer than 3 digits if ( numStr.match(/\D/) || num.length < 3 ) return numStr; num.reverse(); numStr = num.join(''); numStr = numStr.replace(/(\d\d\d)/g, "$1_marker_"); // if we did one too many, take it back numStr = numStr.replace(/_marker_$/g, ''); // replace the thousandth _marker_ with ',' or '.' numStr = numStr.replace(/_marker_/g, Number.localeThousandth); num = numStr.split(''); num.reverse(); return num.join(''); }
There are a few different approaches to this problem. We’ve kept it
brief and only dealt with positive integers. That means something like
-1000 won’t work and neither will 98.6, nor special numbers like
infinity, NaN, and exponents. They’ll just return the
toString()
value of the number.
We’ve also done a bit of mucking with the thousandth marker in the
_localeThousandth
variable. We prefer to allow the user’s
locale to set whether our thousands separator is a comma or a period.
It’s important to think internationally when writing your code. It makes the code rougher but we’ll never have to see it again if it’s working correctly. That’s the point of what we’re doing.
The daysOld service itself
All the heavy lifting is done. We no longer have to think about getting args from the query string or how to do a delta between two different dates or how to commify the result. We’re all ready to go.
Remember that though the client calls daysOld.js
, the
real service is in daysOldProper.js
. Here it is.
daysOldProper.js
var args = new Args(); if ( args.birthday && args.birthday.match(/^\d\d\d\d-\d\d-\d\d$/) ) { var yyyy_mm_dd = args.birthday.split('-'); var bday = new Date( yyyy_mm_dd[0] ,yyyy_mm_dd[1] - 1 ,yyyy_mm_dd[2] ); var now = new Date(); var deltaDays = bday.deltaDays( now ); document.write( deltaDays.commify() ); }
With just those 8 or so lines of code we’ve done quite a lot. The magic, of course, is that it’s supported by a couple hundred lines of code in our libraries. We’ve made them reusable and simple to get at though so we’ll never have to think much about it again.
And it’s every bit as easy for the client.
Refresher: a client call to daysOld.js with some text added
Marlo Thomas is <script type="text/javascript" src="http://elektrum.org/js/daysOld.js?birthday=1937-11-21"> </script> days old today.
And the output
Conclusion
You now have techniques, a mini-arsenal, to bring JavaScript up to par with its more sophisticated cousins. The endless extensibility and facility of languages like Perl, PHP, Ruby, and Python is now less of a fantasy for your JavaScript. It still has a more limited scope, but a lack of reusable code libraries is no longer holding you back.
Also, we only developed object oriented solutions in our classes and extensions. It’s just as easy, if not easier, to do functional libraries without objects.
The hassle of writing HTML with endless document.write()
s,
for example, could be abstracted away into an HTML.js
lib with functions named
p
, br
, h1
, and the like to
output HTML
directly. The possibilities, within the realm of the DOM, are only bounded by your
imagination and creativity.
Whew! That’s enough mind bending with JavaScript for now. We’ve just got one more project to go: On target with WhiskerBiscuit.