The Bungee Blog

News, updates and rants around The Bungee Book (the landmark book on Prototype and script.aculo.us)

Neuron Workout Solutions #5

Welcome to this fifth installment of Neuron Workout Solutions™, a series that answers the questions and challenges at the end of many chapters in the book. Today we’ll address the challenges closing chapter 10: More Useful Helper Objects. And with this, we’ll wrap up the Neuron Workouts for the Prototype part of the book. The next installment will begin the challenges about script.aculo.us!

Produce a sorted list of all the properties in an object

Oh, the easy one-liner:

Object.keys(obj).sort()

Check this out:

var obj = { name: 'Sam', position: 'author',
  lib: 'Prototype', company: '37signals' };
Object.keys(obj).sort()
// => ['company', 'lib', 'name', 'position']

Reduce it to methods only

Ah-ha! That’s what the Object.isFunction method can help us with! Combined with the select method we get from the Enumerable mix-in in Array, we’ve got the tools we need. There are many ways to go about this, here are two:

var obj = { name: 'Sam', position: 'author',
  codeUp: Prototype.emptyFunction, echo: Prototype.K };
 
// One way: don't lookup again on each key, but
// double-iterate by chaining pluck over the intermediate
// pair list…
$H(obj).select(function(pair) {
  return Object.isFunction(pair.value);
}).pluck('key').sort()
// => ['name', 'position']
 
// Another way: lookup the keys (which means we need
// the object reference in the filter function), but iterate
// just once…
Object.keys(obj).select(function(key) {
  return Object.isFunction(obj[key]);
}).sort()
// => ['name', 'position']

Reduce it instead to nonmethod properties that are strings and whose names begin with a vowel

It’s just a matter of changing our filter function, for instance:

var VOWELS = /^[aeiouy]/i;
 
var obj = { name: 'Élodie', age: 27,
  codeUp: Prototype.emptyFunction, echo: Prototype.K };
Object.keys(obj).select(function(key) {
  return VOWELS.test(key) && !Object.isFunction(obj[key]);
}).sort()
// => ['age']

Notice that we’re making it as efficient as we can on the regex’s by pre-compiling it and using its test method instead of match-like stuff, as we don’t care about groups and such: we just want to know whether we match or not, so test is our best option.

Change String#succ() so it wraps at the end of the ASCII alphabet; for example, "wiz".succ() yields "wja" instead of "wi{". Check out $A($R('abc', 'bzz')) then.

Ah, now there’s some need for actual coding. Let’s look at how Prototype implements the original String#succ():

succ: function() {
  return this.slice(0, this.length - 1) +
    String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
},

Hmmm, okay, so we just care about the last character. That just won’t do once we impose ASCII limits. We’ll need some form of recursion, and a sentinel value beyond which we can’t get a successor value (that sentinel would be a full set of z’s).

1
2
3
4
5
6
7
8
9
10
String.prototype.succ = function() {
  var lastChar = this[this.length - 1];
  if ('z' == lastChar)
    return (this.length == 1 ? '' + this :
      this.slice(0, this.length - 1).succ() + 'a');
  return this.slice(0, this.length - 1) +
    String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
};
'wiz'.succ()
// => 'wja'

The algorithm is pretty simple:

  • If we end up with a ‘z’, grab the text before and succ() it (unless this ‘z’ is all we have)
  • Otherwise, leave the text before alone and grab the next char along the character table (we maintain regular succ behavior outside the lowercase ASCII alphabet)

If necessary, adjust it to wrap over from 9 to 0, and from Z to A, too.

“If necessary”?! What was I thinking? OK, ignore this: let’s just adjust the code. It’s a fairly basic refactoring of the previous code:

1
2
3
4
5
6
7
8
9
10
11
String.prototype.succ = function() {
  var lastChar = this[this.length - 1], fx = arguments.callee;
  if (fx.mapper[lastChar])
    return (this.length == 1 ? '' + this :
      this.slice(0, this.length - 1).succ() + fx.mapper[lastChar]);
  return this.slice(0, this.length - 1) +
    String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
};
String.prototype.succ.mapper = { 'z': 'a', 'Z': 'A', '9': '0' };
'az9Z'.succ()
// => 'ba0A'

Isn’t this sweet? We use a custom store for cataloguing our boundary values, and just check whether the last char is one of them, at which point we wrap accordingly.

And that’s a wrap!

I’ll look forward to any questions, comments or wild guesses you may have. And stay tuned for the sixth installment and its first script.aculo.us tricks!

2 Comments so far

  1. kangax April 13th, 2008 10:51 pm

    It’s funny, but plain for..in is just as small as “heavy” keys/select:

    results = []; for (var p in obj) { if (VOWELS.test(p) && typeof obj[p] != ‘function’) results.push(p); }

  2. Christophe April 14th, 2008 8:44 am

    Ha ha, good point.

    Past a certain code size, going for…in results in negligible extra code compared to the whole thing.

    In the present instance, it might even be slightly faster, considering we have, among other things, one less function call per iteration.

Leave a reply