Developing Feather-Weight Webservices with JavaScript

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.

  1. Argument handling and finding the right arguments in the script stack. We dealt with this difficulty in part 1.
  2. 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.
  3. 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-in toLocaleString 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() and deltaDays().

Three phases of execution

The client still enjoys the simplicity of a single phase but we have to take care of three.

  1. Receive the client’s service call. We’ll call this script, daysOld.js.
  2. daysOld.js writes out <script> tags to bring in JavaScript.
    1. It writes the HTML to import the libraries–ArgsObj.js, DateExt.js, and NumberExt.js–into the page’s code space.
    2. It writes the HTML to import the actual service, daysOldProper.js, which will use the libraries.
  3. Execute daysOldProper.js, the real service, now that it has access to the library code written into the page by daysOld.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

Marlo Thomas is days old today.

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.

« Using JS code libraries · On target with WhiskerBiscuit »
Google
 
Web Developing Featherweight Web Services with JavaScript
This is version 0.57b of this manual. It is a beta version with some gaps. We are grateful for feedback.

The code is the manual has not yet been fully tested against Internet Explorer. Bug reports are welcome.
An Elektrum Press Online