Nov 20 2009

Cleaning Up an iTunes Library with MacRuby

For a little more than a year now, I’ve been meaning to write a script to rename all of the files in my iTunes library so that they’re in proper English title case. In large part, this project was inspired by reading John Gruber’s post about a Perl script that he’d written to convert text strings into title case programmatically. After reading Gruber’s post, I grabbed a copy of Sam Souder’s translation of Gruber’s Title Case script into Ruby and set about trying to use it as part of a home-grown Ruby script to clean up my iTunes library. During my first pass, I tried to combine Sam’s convenient utility method with RubyCocoa to produce a script that could access the iTunes methods directly and rename my files automatically without editing the MP3 files directly. Unfortunately, the RubyCocoa documentation was too sparse at the time for me to figure out the relevant method calls to be making.

Thankfully, I spent some time this week reading the MacRuby documentation and realized in the process how to write my desired script. The results are now on GitHub. The only original bit of code is below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/macruby
 
# We iterate over every track, stripping bounding whitespace and putting things into proper title case.
 
require 'titlecase'
 
framework 'cocoa'
 
load_bridge_support_file 'ITunes.bridgesupport'
 
framework("ScriptingBridge")
 
itunes = SBApplication.applicationWithBundleIdentifier("com.apple.itunes")
 
music_playlist_tracks = itunes.sources.objectWithName("Library").userPlaylists.objectWithName("Music").fileTracks
 
music_playlist_tracks.each do |track|
  old_artist = track.artist
  new_artist = track.artist.strip.titlecase
  if old_artist != new_artist
    track.artist = new_artist
    puts "Artist: #{old_artist} => #{new_artist}"
  end
 
  old_album = track.album
  new_album = track.album.strip.titlecase
  if old_album != new_album
    track.album = new_album
    puts "Album: #{old_album} => #{new_album}"
  end
 
  old_name = track.name
  new_name = track.name.strip.titlecase
  if old_name != new_name
    track.name = new_name
    puts "Name: #{old_name} => #{new_name}"
  end
end

Outside of this little snippet of MacRuby code that I had to write, I made two small changes to Sam Souder’s original code:

  1. I defined a titlecase method for the NSString class rather than the String class.
  2. I pulled the list of English prepositions out of the main code and put it into a separate YAML file to make it easier for a non-programmer to edit the list.

I know from experience with checking the results in a half gleeful and half paranoid frenzy that this code works properly on my own two machines, both of which are running MacRuby 0.5. That said, I won’t vouch for the reliability of this program in any way. If you notice any bugs, please do let me know, so that I can fix my own iTunes library with a revised version of the script.


Aug 20 2008

Why You Shouldn’t Be Clever

Today I started reading the Ruby Snips website, which has a pretty good sample of interesting snippets of Ruby code on it. I was particularly intrigued by the following snippet from a post on Prime Numbers dating back to March 23rd, 2007:

1
2
3
4
5
class Fixnum
  def prime?
    ('1' * self) !~ /^1?$|^(11+?)\1+$/
  end
end

At first, I was convinced this code was broken. Before I had given much thought to the algorithm, I was ready to assume that the “prime?” name was a misnomer: I assumed the code was actually testing for members of a specific class of palindromes. After a minute or two, I pieced together what was really happening when testing a number:

  1. The number, n, is expanded into a string of 1’s of length n.
  2. The string of 1’s is testing using a regex with back-references that finds the presence of a repeated divisor, a technique that works because a string of length a * b = n is composed of b copies of a string of length a.

At that point, I began to marvel at the simplicity of the algorithm relative to the obscurity of the code. And while I was so amazed by this, it occurred to me: this is an absolutely terrible way to test for primality. Because you have no control over the loop bounds for the regex, you effectively test every number up to n as a possible divisor if n is a prime. But you really only need to test up to the square root of n to determine if n is prime. So your code runs for the square of the time it should run. And the expansion of n into a string, at least theoretically, requires exponentially more space than an integer expressed in binary would. This seemed like one of the worst possible ways to test for primality I had ever seen.

Still, I wanted to verify this empirically as well as theoretically, so I typed in

1
399839483.prime?

during an IRB session. The CPU and memory usage were absurd for a primality test on such a small number. I didn’t even bother letting the code complete after a few seconds of the watching interpreter hanging there, silently wasting CPU cycles. In contrast, the simpler, clearer code,

1
2
3
4
5
6
7
8
9
10
class Fixnum
  def prime?
    (2..Math.sqrt(self).to_i).each do |i|
      if i * (self / i) == self
        return false
      end
    end
    return true
  end
end

runs fairly quickly.

The lesson I think everyone should take from this is a simple one: don’t be clever when writing code. At the very least, don’t be clever in the way the author of this snippet was. Your code is likely to end up being unclear, slow and wasteful with memory.

Perhaps I should be clearer about the problems of cleverness. Clever code is a mistake; clever algorithms are wonderful. Quick sort is a clever algorithm that always confuses me until I think carefully about it — and then I marvel at its superiority over other less efficient sorting algorithms; this snippet is a mediocre algorithm written in a style of code that’s needlessly confusing.