Code too hashy, I think

I came to Ruby from a scattering of Java, a tiny bit of Python, and a
ton of Perl. So the other day I wrote a simple utility script and
afterwards realized it may have been very influenced by all those
years of Perl. Specifically, it seemed to use hashes too much.

How can I cure myself of my hash addiction? :wink:

Seriously ā€“ I had a CSV file from an Excel spreadsheet. Each row
represented a thing, so I made a Struct for that thing. Those things
were in groups, so I hashed them by their common feature, a unique id
from the spreadsheet. But the last character in the unique id on the
spreadsheet was non-unique, and had to be preserved. Each group had to
have a dash-delimited list of those characters, relevant to that
particular group (since different groups would have different terminal
characters in their unique ids). So I hashed them again ā€“ and thatā€™s
what seemed like overkill.

Whatā€™s the most idiomatic Ruby data structure? Would another Struct
have been more maintainable? The code was perfectly functional, and
itā€™s pretty readable, but you can read it and go, oh, this is a
recovering Perl hacker. I picked it up from another programmer I work
with, and looking over what he had initially, I was able to go, oh,
this is a recovering PHP hacker. I want my code to look like, oh, this
is a Ruby hacker. How do I hide my shameful past?

Hi Giles,

Iā€™m a new ruby convert (from primarily Perl, but also Java and PHP
myself). Iā€™d like to offer my observation and see if other more
experienced Ruby programmers have input.

To risk sounding trite, my take on the most idiomatic Ruby data
structure would be the class. When I deal with CSVs I typically
use the CSV object in the standard library (though Iā€™ve read there
may be better ways) and do something like the code below.

At the end of the day, itā€™s a hash cloaked in object clothing. I
use the first line of the CSV to determine the names of the
keys in the hash (and hence the instance method names for the
object instance for each row).

The reason I take this approach is that it avoids locking you to
the hash syntax using [] or fetch/ store. Additionally, it would
allow you to subclass and have instance methods like:

def full_name
first + " " + last
end

Iā€™m still learning ruby, so feed back on the approach is welcome.

#!/usr/bin/ruby

require ā€˜csvā€™

class ObjectCsvParser
attr :attributes

def initialize(fields,values)
    @attributes = Hash[
        *[
            (0..fields.length-1).map {
                |i| [fields[i], values[i]]
            }
        ].flatten
    ]

end

def method_missing(method_name)
    if(@attributes[method_name])
        @attributes[method_name]
    else
        raise NoMethodError.new(method_name)
    end
end

def self.each_record(fn,&block)
    fields = nil
    CSV.open(fn,'r') do |line|
        if(fields == nil)
            fields = line.map { |l| l.downcase.to_sym }
            next
        end

        yield ObjectCsvParser.new(fields,line)
    end
end

end

Assuming your CSV has last, first, and email as fields

ObjectCsvParser.each_record(ARGV.shift) do |record|
puts "Last: " + record.last
puts "First: " + record.first
puts "Email: " + record.email
end

Cheers!

Andy

Nice! Thatā€™s great Brian, I love it. A quick
fix to the code I posted previously makes use of
this class. My class extends OpenStruct, and no longer
has an @attributes attribute, or a method_missing
method.

Thanks!

#!/usr/bin/ruby

require ā€˜csvā€™
require ā€˜ostructā€™

class ObjectCsvParser < OpenStruct
def initialize(fields,values)
super Hash[
*[
(0ā€¦fields.length-1).map {
|i| [fields[i], values[i]]
}
].flatten
]

end

def self.each_record(fn,&block)
    fields = nil
    CSV.open(fn,'r') do |line|
        if(fields == nil)
            fields = line.map { |l| l.downcase.to_sym }
            next
        end

        yield ObjectCsvParser.new(fields,line)
    end
end

end

Assuming your CSV has last, first, and email as fields

ObjectCsvParser.each_record(ARGV.shift) do |record|
puts "Last: " + record.last
puts "First: " + record.first
puts "Email: " + record.email
end

On Thu, Mar 29, 2007 at 03:47:04AM +0900, Andrew L. wrote:

At the end of the day, itā€™s a hash cloaked in object clothing.

Perhaps you want Struct or OpenStruct (both in the standard install; for
the
latter you will need to require ā€˜ostruct.rbā€™)

From /usr/lib/ruby/1.8/ostruct.rb:

OpenStruct allows you to create data objects and set arbitrary

attributes.

For example:

require ā€˜ostructā€™

record = OpenStruct.new

record.name = ā€œJohn S.ā€

record.age = 70

record.pension = 300

puts record.name # -> ā€œJohn S.ā€

puts record.address # -> nil

It is like a hash with a different way to access the data. In fact,

it is

implemented with a hash, and you can initialize it with one.

hash = { ā€œcountryā€ => ā€œAustraliaā€, :population => 20_000_000 }

data = OpenStruct.new(hash)

p data # -> <OpenStruct country=ā€œAustraliaā€

population=20000000>

Both good solutions. My own approach mostly revolved around Struct,
Iā€™d heard of both CSV and OpenStruct but I was coding in a hurry so I
didnā€™t think to use them.

Thing is, there were elements on each line which affected not the
lines themselves but the groups those lines were part of, and to
collect those, I resorted to an additional hash. So I ended up with a
hash of Structs, indexed by group ID, and a hash of collected
elements, also indexed by group ID. I should have probably just made a
multidimensional hash, but even that seems Perl-y. I think the real
thing to do would have been to create another Struct for the groups,
containing both an array of Structs ā€“ one for each line in the group
ā€“ and the collected-data string as well. That might have been better.

On Thu, Mar 29, 2007 at 04:45:37AM +0900, Andrew L. wrote:

Nice! Thatā€™s great Brian, I love it. A quick
fix to the code I posted previously makes use of
this class. My class extends OpenStruct, and no longer
has an @attributes attribute, or a method_missing
method.

Thanks!

NP. Depending on how dynamic you want to be, itā€™s smaller with Struct:

#!/usr/bin/ruby

require ā€˜csvā€™

class ObjectCsvParser
def self.each_record(fn,&block)
klass = nil
CSV.open(fn,ā€˜rā€™) do |line|
if klass.nil?
klass = Struct.new( *line.map { |l| l.downcase.to_sym }
)
next
end

        yield klass.new(*line)
    end
end

end

Assuming your CSV has last, first, and email as fields

ObjectCsvParser.each_record(ARGV.shift) do |record|
puts "Last: " + record.last
puts "First: " + record.first
puts "Email: " + record.email
end

Thing is, there were elements on each line which affected not the
lines themselves but the groups those lines were part of, and to
collect those, I resorted to an additional hash. So I ended up with a
hash of Structs, indexed by group ID, and a hash of collected
elements, also indexed by group ID.

Any chance you could show us some trivial example data (just ten
lines or so is fine and we only need the key fields) and how you want
to access it. We might have better ideas when we see the specificsā€¦

I think thereā€™s no harm in that. Should be able to this evening.

Since starting this thread, Iā€™ve looked at another developerā€™s code
and seen very plainly that they were writing Python before they
started writing Ruby. Itā€™d be nice to make my Ruby so idiomatic that
people didnā€™t believe me when I told them I knew other languages. Kind
of like learning to speak a foreign language with a perfect accent.

On Mar 28, 2007, at 5:01 PM, Giles B. wrote:

Thing is, there were elements on each line which affected not the
lines themselves but the groups those lines were part of, and to
collect those, I resorted to an additional hash. So I ended up with a
hash of Structs, indexed by group ID, and a hash of collected
elements, also indexed by group ID.

Any chance you could show us some trivial example data (just ten
lines or so is fine and we only need the key fields) and how you want
to access it. We might have better ideas when we see the specificsā€¦

James Edward G. II

On 3/28/07, Giles B. [email protected] wrote:

I think thereā€™s no harm in that. Should be able to this evening.

Since starting this thread, Iā€™ve looked at another developerā€™s code
and seen very plainly that they were writing Python before they
started writing Ruby. Itā€™d be nice to make my Ruby so idiomatic that
people didnā€™t believe me when I told them I knew other languages. Kind
of like learning to speak a foreign language with a perfect accent.

My Ruby is super sexy. Just thought Iā€™d throw that in there. If you
want to form a shrine for me, I wonā€™t object.

Pat

Since starting this thread, Iā€™ve looked at another developerā€™s code
and seen very plainly that they were writing Python before they
started writing Ruby. Itā€™d be nice to make my Ruby so idiomatic that
people didnā€™t believe me when I told them I knew other languages. Kind
of like learning to speak a foreign language with a perfect accent.

My Ruby is super sexy. Just thought Iā€™d throw that in there. If you
want to form a shrine for me, I wonā€™t object.

Thank you, Pat. Iā€™ll keep that in mind.

Any chance you could show us some trivial example data (just ten
lines or so is fine and we only need the key fields) and how you want
to access it. We might have better ideas when we see the specificsā€¦

OK, hereā€™s sample data and the script. I changed unique ID numbers and
made some subtle text changes as well to prevent the data from being
an NDA violation.

http://gilesbowkett.com/blog_code_samples/muppet.csv
http://gilesbowkett.com/blog_code_samples/muppetimport.rb

You can probably actually run this code with only a few modifications;
namely, thereā€™s stuff which expects Rails models to be defined, you
can comment that out and uncomment the ā€œ# reporting!ā€ code, and youā€™re
good to go.

(This was a quick script, done in a hurry, so itā€™s not the best thing
Iā€™ve ever done.)

On Mar 29, 2007, at 8:58 PM, James Edward G. II wrote:

or group the data by size

by_size = all_stuff.inject(Hash.new { |size, a| size[a] = [] }) do |
grouped, s|
grouped[s.size_code] << s
grouped
end
pp by_size

Set has a nice method, classify, that can help with this if you arenā€™t
concerned with duplicates.

require ā€˜setā€™

by_size = all_stuff.to_set.classify { |s| s.size_code }

Iā€™m surprised classify isnā€™t part of Enumerable. It is a
generalization of
Enumerable#partition.

Gary W.

On Mar 29, 2007, at 4:44 PM, Giles B. wrote:

http://gilesbowkett.com/blog_code_samples/muppetimport.rb
Usually we approach these problems with iterators.

First I show some ways you might select subsets of data. This isnā€™t
as fast as Hash based access, but can be useful when you need to be
able to view the data several different ways.

If you still need the Hash indexes, I show how I would go about
building those next.

My hope is that something in here will give you some fresh ideas:

#!/usr/bin/env ruby -w

require ā€œppā€

$/ = ā€œ\rā€ # switch to the unusual line endings

read in the data

Stuff = Struct.new(:family, :description, :color, :size_code, :sku)
all_stuff = ARGF.inject(Array.new) do |rows, row|
next rows unless row =~ /\S/
rows.push(Stuff.new(row.split(/\s,\s*/)[0ā€¦4]))
end

find all by an sku when needed

pp all_stuff.select { |s| s.sku.include? ā€œ35860466ā€ }
puts

find all entries of a certain size

pp all_stuff.select { |s| s.size_code == ā€œMā€ }
puts

or group the data by size

by_size = all_stuff.inject(Hash.new { |size, a| size[a] = [] }) do |
grouped, s|
grouped[s.size_code] << s
grouped
end
pp by_size

END

Hope that helps.

James Edward G. II