Entries from February 2009 ↓

Advanced Twitter Queries with the Twitter Gem

twitter_post.pngI wanted a reliable way to find out a few things about my twitter account (for example which of the users I am following are not following me back) - unfortunately the third party apps out there are not always very reliable, do not exactly do what I want/as I want so I decided to check out how easy it is to hack up a more advanced query using a Ruby twitter API wrapper. It turned out that it couldn’t be easier!

There are several gems wrapping the Twitter API out there - I started to use the ‘twitter’ gem from John Nunemaker and I am perfectly happy with it so far. John did a great job supporting all the features offered by the API - it’s a different question that the API, like twitter itself, is quite minimalistic. For example I have not found a way to get all my followers/friends easily (drop me a comment if I am missing something) so I monkey-patched this into the module in a generic way:

module Twitter
  class Base
    def all_entries(method, options = {})
      all_entries = []  
      next_100 = self.send method, {:page => (current_page = 1)}.merge(options)
      while (next_100.size != 0) do
        all_entries << next_100
        next_100 = self.send method, {:page => (current_page += 1)}.merge(options)
      end  
      all_entries.flatten
    end
  end
end
  1. module Twitter
  2.   class Base
  3.     def all_entries(method, options = {})
  4.       all_entries = [] 
  5.       next_100 = self.send method, {:page => (current_page = 1)}.merge(options)
  6.       while (next_100.size != 0) do
  7.         all_entries << next_100
  8.         next_100 = self.send method, {:page => (current_page += 1)}.merge(options)
  9.       end 
  10.       all_entries.flatten
  11.     end
  12.   end
  13. end

for example you can call connection.all_entries(:friends) to get all of your friends, given that you set up a connection to your account (I found only a method which returns your first 100 friends - didn’t spend too much time with the documentation though - agin, drop me a message if I overlooked something).

I have added a bit of syntactic sugar to be able to call connection.all_friends instead of connection.all_entries(:friends):

module Twitter
  class Base
    alias_method :throw_method_missing, :method_missing

    def method_missing(method_name, *args, &bloke)
      if (method_name.to_s =~ /^all_.+/)
        all_entries(method_name.to_s[/all_(.+)/,1], args[0] || {})
      else
        throw_method_missing(method_name, *args, &bloke)
      end
    end
  end
end
  1. module Twitter
  2.   class Base
  3.     alias_method :throw_method_missing, :method_missing
  4.  
  5.     def method_missing(method_name, *args, &bloke)
  6.       if (method_name.to_s =~ /^all_.+/)
  7.         all_entries(method_name.to_s[/all_(.+)/,1], args[0] || {})
  8.       else
  9.         throw_method_missing(method_name, *args, &bloke)
  10.       end
  11.     end
  12.   end
  13. end

Here we ensure that only method calls that start with all_ are handled by all_entries, the rest is throwing a method_missing since we are not interested in handling those messages.

Now it could not be easier to implement the function I originally intended to build: list of users who are not following back.

class Array
  def names
    self.map{|u| u.screen_name}
  end
end

module Twitter
  class Base
    def not_following_back
      all_friends.names - all_followers.names
    end
  end
end 
  1. class Array
  2.   def names
  3.     self.map{|u| u.screen_name}
  4.   end
  5. end
  6.  
  7. module Twitter
  8.   class Base
  9.     def not_following_back
  10.       all_friends.names - all_followers.names
  11.     end
  12.   end
  13. end

That’s all there’s to it (I am not a big fan of monkey patching core classes btw ; but in this case, adding the names() method to the Array class just made the method I intended to originally implement much cleaner so I rolled with it). Note that since subtraction is a non-commutative operation, all_friends.names - all_followers.names is not necessarily the same as allfollowers.names - allfriends.names. This is how the final code looks like:

require 'rubygems'
require 'twitter'

connection =  Twitter::Base.new('yourname', 'yourpass')

class Array
  def names
    self.map{|u| u.screen_name}
  end
end

module Twitter
  class Base
    alias_method :throw_method_missing, :method_missing

    def method_missing(method_name, *args, &bloke)
      if (method_name.to_s =~ /^all_.+/)
        all_entries(method_name.to_s[/all_(.+)/,1], args[0] || {})
      else
        throw_method_missing(method_name, *args, &bloke)
      end
    end

    def all_entries(method, options = {})
      all_entries = []  
      next_100 = self.send method, {:page => (current_page = 1)}.merge(options)
      while (next_100.size != 0) do
        all_entries << next_100
        next_100 = self.send method, {:page => (current_page += 1)}.merge(options)
      end  
      all_entries.flatten
    end

    def not_following_back
      all_friends.names - all_followers.names
    end

  end  
end  

p connection.not_following_back
  1. require ‘rubygems’
  2. require ‘twitter’
  3.  
  4. connection =  Twitter::Base.new(‘yourname’, ‘yourpass’)
  5.  
  6. class Array
  7.   def names
  8.     self.map{|u| u.screen_name}
  9.   end
  10. end
  11.  
  12. module Twitter
  13.   class Base
  14.     alias_method :throw_method_missing, :method_missing
  15.  
  16.     def method_missing(method_name, *args, &bloke)
  17.       if (method_name.to_s =~ /^all_.+/)
  18.         all_entries(method_name.to_s[/all_(.+)/,1], args[0] || {})
  19.       else
  20.         throw_method_missing(method_name, *args, &bloke)
  21.       end
  22.     end
  23.  
  24.     def all_entries(method, options = {})
  25.       all_entries = [] 
  26.       next_100 = self.send method, {:page => (current_page = 1)}.merge(options)
  27.       while (next_100.size != 0) do
  28.         all_entries << next_100
  29.         next_100 = self.send method, {:page => (current_page += 1)}.merge(options)
  30.       end 
  31.       all_entries.flatten
  32.     end
  33.  
  34.     def not_following_back
  35.       all_friends.names - all_followers.names
  36.     end
  37.  
  38.   end 
  39. end 
  40.  
  41. p connection.not_following_back

You can download/check out the code here - do not try to copy & paste it from the text as it will be b0rk3d.

In part 2 I’d like to set up a small Sinatra app showing the above users in a list - displaying their avatar, screen name and real name, plus a link to remove them if you decide so.

Vitajte na slovenskom blogu o Ruby a Ruby on Rails

Vitajte na slovenskom blogu o Ruby a Ruby on Rails. Začíname…

Ruby’s Most Underused Keyword

redo.pngI spent the first 2+ years in Ruby-land without even knowing about the probably most underused (and underrated) keyword of the language: redo. Even after I came across the first example and liked it immensely, I could not come up with another use for it, so I threw it to the bottom of my toolbox. Then I found another example, and another one - so I came to the conclusion that redo might be a valuable keyword in your Ruby arsenal after all - it is one of those things which you rarely need, but if you need it, it’s a perfect solution which would be cumbersome to replace with other constructs.

Break vs Next vs Redo

I am sure everyone is familiar with redo’s cousins: next and break. While next and break allow you to skip an iteration, resp. skip the rest of the remaining iterations altogether, redo does not skip anything - it ‘restarts’ the same iteration. In other words, while next transfers control to the end of the iteration block, redo ‘jumps’ to the beginning of the block. Let’s see an example, where redo is used to recover from an input error (code from “The Ruby Programming Language”):

puts "Please enter the first word you can think of"
words =%w(apple banana cherry)
response = words.collect do |word|
  print word + "> "
  response = gets.chop
  if response.size == 0
    word.upcase!
    redo
  end
  response
end
  1. puts "Please enter the first word you can think of"
  2. words =%w(apple banana cherry)
  3. response = words.collect do |word|
  4.   print word + "> "
  5.   response = gets.chop
  6.   if response.size == 0
  7.     word.upcase!
  8.     redo
  9.   end
  10.   response
  11. end

The trick is that whenever the user enters an empty string, the block is restarted with redo, asking the same ‘question’ until the user enters something that makes sense to the system.

A Real Example - Tail Recursion

As of now, Ruby does not support tail recursion optimization, so you either have to write the function in a non-recursive (i.e. iterative) way yourself, or suffer the consequences of a full stack… or use redo!

I recently came across Magnus Holm’s article on this problem. Magnus provides an elegant solution using redo:

define_method(:acc) do |i, n, result|
  if i == -1
    result
  else
    i, n, result = i - 1, n + result, n
    redo
  end
end

def fib(i)
  acc(i, 1, 0)
end
  1. define_method(:acc) do |i, n, result|
  2.   if i == -1
  3.     result
  4.   else
  5.     i, n, result = i - 1, n + result, n
  6.     redo
  7.   end
  8. end
  9.  
  10. def fib(i)
  11.   acc(i, 1, 0)
  12. end

Magnus even worked around a redo perk: “The redo statement restarts the current iteration of a loop or iterator”. That means he couldn’t define a method with “def”, since that neither involves a loop nor an iterator. Maybe a less esoteric way relying on the same fact would be to use a lambda:

def fib(i)
  acc = lambda do |i, n, result|
    if i == -1
      result
    else
      i, n, result = i - 1, n + result, n
      redo
    end
  end.call(i, 1, 0)
end
  1. def fib(i)
  2.   acc = lambda do |i, n, result|
  3.     if i == -1
  4.       result
  5.     else
  6.       i, n, result = i - 1, n + result, n
  7.       redo
  8.     end
  9.   end.call(i, 1, 0)
  10. end

Golf Time

Here is a Ruby golf riddle, which I have no idea how could be made this short (42 characters) without redo (via coderrr):

"Generate a random string of length 4 or more consisting of only the following 
set of characters: a-z, A-Z, 0-9"

Here is the winner, Magnus Holm’s solution (The name sounds familiar? Right, the previous example was from him too - he understands the power of redo for sure :-)).

(0..9).map{rand(?z).chr[/[^_\W]/]||redo}*""
  1. (0..9).map{rand(?z).chr[/[^_\W]/]||redo}*""

The above solution might not be instantly crystal clear - so let’s break it down a bit:

  1. rand(?z) is equivalent to rand(122) (but shaves off a character!)
  2. rand(?z).chr turns the random number into a string (equivalent to its ASCII code, i.e. 65.chr == “A” etc)
  3. the regexp /[^_\W]/ means the negation of ‘everything but underscore or non-word character (digits and numbers). To put it another way, accept word characters but not an underscore. (\w includes underscore, so we need to get rid of that)
  4. rand(?z).chr[/[^_\W]/] acts as an identity function for letters and digits, but returns nil for everything else. So “a”[/[^_\W]/] == “a” but “@”[/[^_\W]/] == nil. “_”[/[^_\W]/] also returns nil.
  5. And now comes the redo trick: rand(?z).chr[/[^_\W]/] || redo will restart the block until a random character/letter is not found! w00t!

While one might argue that golfing examples are contrived by definition, I don’t always agree: the [/[^_\W]/] is a neat trick to get numbers/letters from a string only, as well as the use of redo is valid and non-contrived.

Conclusion

As demonstrated above, redo can be used in a variety of situations to produce an elegant solution that would look much more cumbersome without it. Of course all the usual stuff applies: don’t overuse, abuse etc. etc.

If you have any other interesting cases for redo, leave a comment - I’d love to see some more :-)

Ruby Quiz: Gathering Ruby Quiz 2 Data

ruby_quiz.png After running the Ruby Quiz show for almost a year, Mathew Moss, the second Ruby Quiz master had just retired (as a quiz master :-) - great work Matt!), passing the ball to Daniel Moore, who just kicked off the third season with the following problem:

I'm building the new Ruby Quiz website and I need your help...

This week's quiz involves gathering the existing Ruby Quiz 2 data from
the Ruby Quiz website: 

Each quiz entry contains the following information:

 * id
 * title
 * description
 * summary

There are also many quiz solutions that belong to each quiz. The quiz
solutions have the following:

 * quiz_id
 * author
 * ruby_talk_reference
 * text 

For some reason, no one (except me) participated this time - so for the first time in history, I am pretty sure my solution is the best of all! w00t!

Daniel did a really great writeup of my solution (which utilized scRUBYt! and Nokogiri) - check it out here (view the code here), so I am not going to do my own writeup this time.