Getting every foo whose name is bar

Published
2013-02-04
Tagged

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

1
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:

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

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

1
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:

1
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:

1
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:

1
subSet = items.filterUsingPredicate(n)

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

1
!/usr/bin/env macruby
2
require 'benchmark'
3
4
framework 'ScriptingBridge'
5
6
of = SBApplication.applicationWithBundleIdentifier('com.omnigroup.omnifocus').defaultDocument
7
8
Benchmark.bm(20) do |x|
9
  x.report('Predicate') do
10
    n = NSPredicate.predicateWithFormat("completed = TRUE")
11
    of.flattenedTasks.filterUsingPredicate(n)
12
  end
13
14
  x.report('Select') do
15
    of.flattenedTasks.select{ |t| t.completed }
16
  end
17
end

The result:

1
                          user     system      total        real
2
Predicate             0.000000   0.000000   0.000000 (  0.002507)
3
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.