Getting every foo whose name is bar

Published
4 March 2013
Tagged

There's a particular coding idiom that's prevalent in Applescript:

set subSet to every item whose name is "bar"

Usually I have to surround various bits of the command with parentheses so Applescript knows what the hell I mean, but you get the overall pattern. I can specify a number of conditions, and AppleScript will fetch only the records that match said conditions. In much the same way as you'd put filters on a SQL query, you want to use filters because it's generally faster and easier than trying to do it through "proper" AppleScript.

rb-appscript has a similar setup:

include Appscript
subSet = items[its.name.eq 'bar']

Although this gets messy when you have two or three conditions:

subSet = items[its.name.eq('bar').and(its.done.eq(true).and(its.size.gt(3)))]

Switching to MacRuby recently, one of the things I missed was this "filter" functionality. I couldn't find any examples anywhere of how to select every item whose name was "bar" without making a somewhat time-consuming select loop:

subSet = items.get.select{ |i| i.name == 'bar' }

Thankfully, there is a way to do it in MacRuby, it's just a bit more complex.

The first step is to realise that MacRuby has overloaded the Object#methods method to let you access Objective-C-style methods (more info here). Using the call methods(true,true) on an array got me a whole heap of interesting methods you can now use on arrays, including the filterUsingPredicate method. This method takes a NSPredicate and uses it to filter your list of arrays - which can be considerably faster than the horrid select loop suggested above.

You make a NSPredicate in the following manner:

n = NSPredicate.predicateWithFormat %(name = "bar")

That's a string at the end, I'm just using alternate quotes to look cool and avoid having to escape my double-quotes. You can find out all about predicates here.

Now all I do is feed in my predicate to the array:

subSet = items.filterUsingPredicate(n)

How much faster is this than running a select loop? I tried benchmarking it on my OmniFocus database:

!/usr/bin/env macruby
require 'benchmark'

framework 'ScriptingBridge'

of = SBApplication.applicationWithBundleIdentifier('com.omnigroup.omnifocus').defaultDocument

Benchmark.bm(20) do |x|
  x.report('Predicate') do
    n = NSPredicate.predicateWithFormat("completed = TRUE")
    of.flattenedTasks.filterUsingPredicate(n)
  end

  x.report('Select') do
    of.flattenedTasks.select{ |t| t.completed }
  end
end

The result:

                          user     system      total        real
Predicate             0.000000   0.000000   0.000000 (  0.002507)
Select                0.130000   0.070000   0.200000 (  6.323786)

Using select, it takes six seconds just to get the data. With predicate, you don't even notice. Quite an improvement.