I'm trying to do something fairly simple that I understand how to do in
XSLT, but not in Ruby.
I have a list of film recommendations, with comments, in a yaml file
that looks like this:
···
-
title: Lost in translation
setting: Tokyo
comments: Some comments..
-
title: Chungking Express
setting: Hong Kong
comments: >
Some comments.
Since a film may have more than one entry, I want to create a list that
groups the comments by film. So I'd like output like:
Some Film
Comment 1.
Comment 2.
However, while I figured out how to sort the list correctly, I don't
really understand how to do the grouping. Could someone please explain?
Current code:
require 'yaml'
films = YAML::load(File.open("film.yaml"))
sorted = films.sort_by{|film| film["title"]}
sorted.each do |item|
# I want to put the title for a group here,
# then list the comments.
if item["comments"] : puts item["comments"]
else ''
end
end
Bruce
bdarcus@gmail.com wrote:
However, while I figured out how to sort the list correctly, I don't
really understand how to do the grouping. Could someone please explain?
Inject is your friend:
require 'yaml'
films = YAML::load(File.open("film.yaml"))
grouped_comments = films.inject({}) do |grouped, film|
(grouped[film['title']] ||= []) << film['comments']
grouped
end
grouped_comments.keys.sort.each do |title|
puts "Comments for #{title}:"
grouped_comments[title].each do |comment|
puts "\t#{comment}"
end
end
Ryan
bdarcus@gmail.com wrote:
Since a film may have more than one entry, I want to create a list that
groups the comments by film.
Have a look at my implementation of Enumerable#group_by at
http://www.ruby-talk.org/cgi-bin/scat.rb/ruby/ruby-core/4311
Thanks Ryan. Certainly less verbose than xslt!
So when you;'re using inject there, you're just creating a hash whose
key is the title?
What is the "||=[]" bit doing?
Bruce
bdarcus@gmail.com wrote:
Thanks Ryan. Certainly less verbose than xslt!
Generally so. Here is an article by Martin Fowler about his switch from XSLT to Ruby for processing XML:
So when you;'re using inject there, you're just creating a hash whose
key is the title?
Correct. You give inject the initial value, which it injects into the block along with each value in the array. So we start with an empty hash and use the film titles as keys. The return value from the block is used to re-inject into the block (that is why I have the grouped hash at the bottom of the block.)
What is the "||=[]" bit doing?
That is a bit of a Ruby idiom (that comes from Perl I think.) It is equivalent to var = var || []. If var is initially nil, it becomes a new array, and whenever the above code is called again, var is just re-assigned to itself. It just makes the code shorter. The verbose version from the code I sent is this:
grouped_comments = films.inject({}) do |grouped, film|
if grouped[film['title']].nil?
grouped[film['title']] = []
end
grouped[film['title']] << film['comments']
grouped
end
As you can see there is a lot of repetition, so it is a good idea to become familiar with this idiom.
Ryan
Ryan Leavengood wrote:
> Thanks Ryan. Certainly less verbose than xslt!
Generally so. Here is an article by Martin Fowler about his switch from
XSLT to Ruby for processing XML:
http://www.martinfowler.com/bliki/MovingAwayFromXslt.html
I saw that, though it'd be nice to have a fully functional example to
try out. I couldn't manage how to get it working.
Also, while Fowler's comments may apply well to XSLT 1.0, 2.0 adds a
lot of power to the language. Still, it only goes so far of course,
which is why I'm interested in Ruby and Python.
> What is the "||=[]" bit doing?
That is a bit of a Ruby idiom (that comes from Perl I think.) It is
equivalent to var = var || []. If var is initially nil, it becomes a new
array, and whenever the above code is called again, var is just
re-assigned to itself.
E.g. it only creates a new "group" if one doesn't already exist? Was
wondering how to do that, so thanks for this!
Bruce
···
bdarcus@gmail.com wrote:
bdarcus@gmail.com wrote:
E.g. it only creates a new "group" if one doesn't already exist? Was
wondering how to do that, so thanks for this!
That is one way. Hashes can also take a block in their constructors which can be used for default values:
h = Hash.new {|h,k| h[k] = 'default'}
h['foo'] = 'bar'
p h['foo'] # => "bar"
p h['baz'] # => "default"
For the code I wrote before, it would be like this:
grouped_comments = films.inject(Hash.new {|h,k| h[k] = []}) do |grouped, film>
grouped[film['title']] << film['comments']
grouped
end
But in this case I prefer the ||= syntax.
Ryan