…DO IT. DO IT AND SEE WHO DARES OBJECT. FUCK THEIR LITTLE SCOOP. BARK MENACINGLY AT ANYONE WHO TRIES TO APPROACH YOU. BREAK THE NECKS OF ALL WHO IMPEDE YOUR PROGRESS. BURN THE WHOLE PLACE DOWN IF SOMEONE SO MUCH AS GLANCES AT YOU.
FUCK A GROCERY STORE, FUCK FREE RANGE FAIR TRADE HAND-MASSAGED…
If the Wu-Tang Clan were British, would “Watch your step, kid” be “Mind the gap, kid”? Because that is far less intimidating.
Yesterday (11/17 if I don’t finish this tonight), I walked through creating a somewhat friendlier wrapper around setInterval to make an API that could be used like cron. Its standard syntax is:
var cron = require('./cron'), sys = require('sys');
cron.Every((2).seconds(), function() { sys.puts('Working!'); });
While that is very expressive for the standards of code, my co-worker suggested that it was a little ugly/unfriendly for users. While there is certainly benefit to creating scheduled jobs programmatically, it’s possible that most users would just have predetermined jobs and intervals. In this installment, I’m going to try to rectify this situation.
Our starting files are at http://gist.github.com/238286. These are cleaned up a little bit from the last iteration, and github user fwg found an area where I forgot to include a ‘var’ keyword. Always run your code through jslint; a quick run I just did revealed that I left out some semicolons, and those errors can be hard to track down if your codebase gets big enough.
But I digress.
We want to establish some basic string patterns that represent time in a way that’s simple for users. If this is to be a cron replacement, it should ostensibly be easier to use. Luckily, crontab has one of the worst file formats in the universe (quick: what does * * 12 * 2 * 3 mean?). I propose recognition for the following interval patterns:
- ‘5 weeks’, ‘5 days’, ‘5 hours’, ‘5 minutes’, ‘5 seconds’
- the singular conjugations of #1, even if written as ‘2 second’
- any combination of #1 interspersed with ’ and ’ or ‘&’ e.g. ‘5 weeks and 2 days’, ‘3 hours & 30 minutes’, ‘2 months and 1 week and 3 days’
- ‘month’ ‘week’, ‘day’, ‘hour’, ‘minute’, or ‘second’, meaning once every month, week, day, hour, minute, or second respectively.
We’ll also allow these positional patterns:
- Day of week for a weekly task (‘[Ss]aturday’ implies every Saturday)
- ‘h?h:mm’ 24-hour clock time for a once/daily task
- ‘h?h:mm(p|a)m?’ for 12-hour clock time for a once/daily task
Let’s start with specifying text for intervals. We’re going to create a exports.Time() function that parses these strings, and converts them to TimeInterval structures. While a pure, single regex solution might be possible, I think it’s cleaner to just divide the cases. Here’s a definition that covers the intervals we were already doing:
exports.Time = function(time) {
if(time instanceof TimeInterval)
{
return time;
}
else if(typeof(time) == 'string')
{
var interval = new TimeInterval();
if(time.match(/^(\d+) (month|week|day|hour|minute|second)s?/))
{
var clauses = time.split(/, | and | & | /);
var clauses_length = clauses.length;
for(var i = 0; i < clauses_length; i++)
{
var clause_parsed = clauses[i].match(/(\d+) (month|week|day|hour|minute|second)s?/);
if(clause_parsed != null)
{
var time = clause_parsed[1];
var unit = clause_parsed[2];
switch(unit)
{
case 'minute':
interval.addTo((+time).minutes());
break;
case 'hour':
interval.addTo((+time).hours());
break;
case 'second':
interval.addTo((+time).seconds());
break;
case 'day':
interval.addTo((+time).days());
break;
}
}
}
}
else
{
var unit_parse = time.match(/month|week|day|hour|minute|second/);
if(unit_parse != null)
{
switch(unit_parse[0])
{
case 'minute':
interval.addTo((1).minutes());
break;
case 'hour':
interval.addTo((1).hours());
break;
case 'second':
interval.addTo((1).seconds());
break;
case 'day':
interval.addTo((1).days());
break;
}
}
}
return interval;
}
}
This shouldn’t be too surprising if you’re used to regular expressions.This type of polymorphism might seem a little ugly to you coming from a Java or C/C++ statically typed world, but remember that in more functional languages, verbs are king. The action determines what to do with its input, not its input itself. Think of JavaScript objects as simple, flat structures.
Now, however, we get to do something cool. We get to change our definition of exports. Every to take a string input, and be completely transparent to the user and the change is even mostly transparent to us as well. Look at our new definition of exports.Every:
exports.Every = function(timeInterval, callback) {
var parsed_time_interval = exports.Time(timeInterval);
setInterval(callback, parsed_time_interval.seconds * 1000);
};
And here’s a basic call to it:
cron.Every('2 minutes', function() { sys.puts("It's been 2 minutes since you last saw this message"); });
All of our imaginary users’ old code works, but now we support the new, friendly syntax. Plus, since we’re exporting the time function, we’re making it easier for users to build a program on top of our library that takes input in the same format. And assuming this parsing engine ever gets reasonably sophisticated, that could add a lot of value.
I was going to include code to parse with specific offsets, (Every Saturday, Every 3:30 PM, etc.) but it appears I’ve written way too goddamn much already. So stay tuned, there will be a second subsection of this blog post.
Code available at: http://github.com/onewland/blog/tree/master/js-files/pt-2/ . Happy coding!
So I checked out node.js from a HackerNews post, and I thought I’d try messing around a bit on my own, and create a program that does something similar to a cron daemon. If you’re not familiar, it’s just a program on UNIX that runs commands at given times intervals, usually hourly or daily, for system maintenance tasks. It’s a simple enough task, and it seems to be a good fit at what node.js is good for. (Check out http://www.nakedjavascript.com/going-evented-with-nodejs?c=1 for some really simple instructions on installing node.js).
We’ll start off with a really simple definition of time interval, that only contains the number of seconds in that time interval (in a file named cron.js):
function TimeInterval() {
this.seconds = 0;
this.addTo = function(ti) {
this.seconds += ti.seconds;
return this;
}
}
Right now, we’re assuming this object will be part of a module and it will not be exposed to the user of our module. We “return this;” in addTo() to allow call chaining like:
timeInterval = timeInterval1.addTo(timeInterval2).addTo(timeInterval3);
We’d like a nice idiomatic way to construct these time intervals. Borrowing from a Ruby on Rails tactic, we extend the number prototype:
Number.prototype.seconds = function() {
interval = new TimeInterval();
interval.seconds = this;
return interval;
}
Number.prototype.minutes = function() {
interval = new TimeInterval();
interval.seconds = this * 60;
return interval;
}
Number.prototype.hours = function() {
interval = new TimeInterval();
interval.seconds = this * 60 * 60;
return interval;
}
Remember that every variable in JavaScript comes from a prototype, and extensions of that prototype are propagated in new objects. Now that we’ve extended the prototype of numbers,
(5).minutes()
returns a TimeInterval such that
(5).minutes().seconds
is equal to 300. For the purpose of simplicity, I am not discussing the dangers of assuming that a user is passing you a correct number. JavaScript makes weird assumptions/choices about input, and it is not uncommon to have a string that you think should be a number. The parentheses force evaluation to a number in this specific example.
Now, there’s a really simple way to refactor the seconds/minutes/hours functions. To DRY it up, we create a function which generates these functions:
function generate_multiplier(multiplier)
{
return function() {
interval = new TimeInterval();
interval.seconds = this * multiplier;
return interval;
}
}
Number.prototype.seconds = generate_multiplier(1);
Number.prototype.minutes = generate_multiplier(60);
Number.prototype.hours = generate_multiplier(3600);
Functional programming is here to make your life easier. It’s one of the Good Parts of JavaScript.
Next, we want to be able to routinely schedule a job. This is actually embarrassingly easy, with the builtin setInterval function in node.js which calls a function callback every time a certain number of milliseconds passes.
To add to the exported functions in the namespace of this module, we extend the exports object:
exports.Every = function(timeInterval, callback) {
setInterval(callback, timeInterval.seconds * 1000);
}
Now anybody who uses require() to include our cron module will have access. This gives us a nice fluid interface for our users (if they don’t mind the parentheses on the number thing). An example of a file which uses our module is here (assuming cron.js is in the same directory as said file):
var cron = require('./cron'), sys = require('sys');
cron.Every((2).seconds(), function() { sys.puts('Working!'); });
does exactly what it looks like it does. Every 2 seconds, it puts ‘Working!’ (quotes not included) on the standard output. Note that the user has no access to the TimeInterval structure. To use Every properly, he must use your extensions to the Number prototype. I like how this separates policy from mechanism in a clean and understandable way. Also, when Every is called it does not take control of the program but immediately sets the interval timer and returns.
If there is any interest, I definitely can improve on this with a second installment. Possible topics could be a more flexible way of specifying times with strings and regex parsing (“4:30”, “6.5h”, “2d”, “8 minutes”, could all be supported), integrating with the command-line using node.js’ builtin modules, thread safety and communication, unit testing, or being more type-aware. I look forward to any feedback as this is my first foray into serious blogging!
Thanks for reading!
I’ve posted cron.js and cron-test.js on git. cron.js is a little crude because I just did my testing inline, but it should function fine.
| Michael Goldman: | dont throw drain cleaner at a co-worker though |
"
— Henri Bergson, Laughter: An Essay on the Meaning of the Comic