Creating variables on an OpenStruct with dynamic names

Hey guys,

I’m day 3 into The Pragmatic Programmers - Seven Languages in Seven
Days, having previously done a little ruby. I’m from an Actionscript
background and finding Ruby a little challenging, so bear with me.

I’ve been parsing some very simple CSV, I’ve seen there are libraries
out there to handle this, but the book is about learning.

The book gives us some simple code and the task is to elaborate on it:

module ActsAsCsv

def self.included(base)
base.extend ClassMethods
end

module ClassMethods
def acts_as_csv
include InstanceMethods
end
end

module InstanceMethods

def read
  @csv_contents = []
  filename = self.class.to_s.downcase + '.txt'
  file = File.new(filename)
  @headers = file.gets.chomp.split(', ')

  file.each do |row|
    @csv_contents << row.chomp.split(', ')
  end
end

attr_accessor :headers, :csv_contents

def initialize
  read
end

end

end

class RubyCsv # no inheritance! You can mix it in
include ActsAsCsv
acts_as_csv
end

m = RubyCsv.new
puts m.headers.inspect
puts m.csv_contents.inspect

The task is to create an each method and return a CsvRow object
instead of an array.

So I figure I need to create the object first in the file.each loop

This mess is where I got to:

file.each do |row|
@headers.each_with_index do |head, index|
require ‘ostruct’
rowObject = OpenStruct.new
rowObject.#{@headers[index]} = row.chomp.split(’, ')

    end
    @csv_contents << rowObject
  end

But as you can see it’s a mess

I’ve also read some other posts around the internet and on here and
thought this might work but haven’t tried it yet

file.each_with_index do |row, rowIndex|
@headers.each_with_index do |head, headIndex|
require ‘ostruct’
rowObject = OpenStruct.new( :#{@header[headIndex =>
row[rowIndex]}

Or something…

All help and guidance gratefully received.

On Wed, May 4, 2011 at 6:34 PM, Jolyon R. [email protected] wrote:

end
@csv_contents << row.chomp.split(’, ')
end
end

Here you are not closing the file handler. As you are reading the
whole file in memory anyway, I’d do it like this:

def read
filename = “#{self.class.to_s.downcase}.txt”
@headers, *@csv_contents = File.readlines(filename).map {|line|
line.chomp.split(",")}
end

class RubyCsv # no inheritance! You can mix it in

So I figure I need to create the object first in the file.each loop

This mess is where I got to:

file.each do |row|
@headers.each_with_index do |head, index|
require ‘ostruct’

You should require outside of the loop.

thought this might work but haven’t tried it yet

file.each_with_index do |row, rowIndex|
@headers.each_with_index do |head, headIndex|
require ‘ostruct’
rowObject = OpenStruct.new( :#{@header[headIndex =>
row[rowIndex]}

Or something…

All help and guidance gratefully received.

Then, for the CsvRow stuff: I don’t get why you need an OpenStruct,
because you are using the same headers for all rows. So maybe a normal
Struct could work for you:

class RubyCsv
include ActsAsCsv
acts_as_csv

def each
row_struct = Struct.new *@headers.map(&:to_sym)
@csv_contents.each do |row|
yield row_struct.new *row
end
end
end

csv = RubyCsv.new
csv.read
csv.each do |row|
puts row
end

Maybe you would like to change how the file is read, maybe lazily in
the each method, or in an initialize method so that all instances read
the file at creation time. Having to call read to correctly initialize
the object looks a bit awkward.

Hope this helps,

Jesus.

On Wed, May 4, 2011 at 9:34 AM, Jolyon R. [email protected] wrote:

end
@csv_contents << row.chomp.split(’, ')

     rowObject = OpenStruct.new
     rowObject.#{@headers[index]} = row.chomp.split(', ')

   end
   @csv_contents << rowObject
 end

But as you can see it’s a mess

I think that (looking at the Day 3 self-study in the book), your best
approach will be not to modify any of the “prewritten” methods that
you are given:

define your CsvRow class first, and make it so that it is something
you can populate with the @headers array in ActsAsCsv and one item
(row) from @csv_contents. (Pay attention, here, to the book’s call to
use method_missing!)

Then in your new ActsAsCsv#each method, you just need to iterate over
the @csv_contents collection, creating and yielding CsvRow objects.

I think that’s the simplest route to do what you are looking for, and
once you’ve done that bit you can explore variations. You don’t need
to use OpenStruct (or Struct) – you could do it with them, but you’ll
miss out on what the book is getting at with the call to use
method_missing.

7stud – wrote in post #996724:

Note that initialize() opens the file, so somewhere you will need to
close the file. Also, if you read() the entire file, then the file
pointer will be pointing to the end of the file, so if you then want to
each() the file, you will have to rewind() the file to the beginning.

On the other hand, if read() only reads one line at a time, you could
call read() or each() depending on whether you wanted the next line from
the file returned as an array or a row object.

Jolyon R. wrote in post #996621:

Hey guys,

I’m day 3 into The Pragmatic Programmers - Seven Languages in Seven
Days, having previously done a little ruby. I’m from an Actionscript
background and finding Ruby a little challenging, so bear with me.

Then I wouldn’t worry too much about Jesús Gabriel y Galán’s code
because it contains some advanced stuff.

I’ve been parsing some very simple CSV, I’ve seen there are libraries
out there to handle this, but the book is about learning.

The book gives us some simple code and the task is to elaborate on it:

The task is to create an each method and return a CsvRow object
instead of an array.

Typically, you call an each() method like this:

obj.each do |x|
puts x
end

That syntax uses what’s called a ‘block’, which is this part:

do |x|
puts x
end

The block tells the each() method to iterate over the elements of obj,
and send them to the block one by one. The element will get assigned to
the parameter variable x of the block. In order for the each() method
to send elements to the block, each() must itself call the yield()
method. Here is an example:

class Dog
def each
yield ‘dog’
yield 20
yield ‘last one’
end
end

d = Dog.new

d.each do |x|
puts x
end

–output:–
dog
20
last one

In your case, your each() method will be iterating over the lines of a
file. So inside your each() method, you will need to get a line from
the file and create a row object, and then yield() the row object.

Also, to me it seems like you need to organize your code along these
lines:

class RubyCsv # no inheritance! You can mix it in
include ActsAsCsv #bring in the read() method

def initialize(fname)
@file = File.new(fname)
end

def each
yield RowObject.new(@file.gets)
end

end

class RowObject
attr_reader :fields

def initialize(line)
@fields = line.chomp.split(’, ')
end
end

Note that initialize() opens the file, so somewhere you will need to
close the file. Also, if you read() the entire file, then the file
pointer will be pointing to the end of the file, so if you then want to
each() the file, you will have to rewind() the file to the beginning.

I read the Prolog section of that book. Interesting. Good luck.

rowObject.#{@headers[index]}

If you have the name of a method as a string, then you can use send() to
call the method:

rowObject.send(@headers[index])

That even works with private methods, which violates encapsulation, but
hey that’s ruby.

On Wed, May 4, 2011 at 6:34 PM, Jolyon R. [email protected] wrote:

Hey guys,

I’m day 3 into The Pragmatic Programmers - Seven Languages in Seven
Days, having previously done a little ruby. I’m from an Actionscript
background and finding Ruby a little challenging, so bear with me.

Welcome to the wonderful world of Ruby!

filename = self.class.to_s.downcase + ‘.txt’
def initialize
read
end

end

end

Note that the approach above has the disadvantage to read the whole
file into memory and store it there. This could use up a lot memory
if the file is large - or even fail. Often a better approach is to
use Ruby’s block mechanism to yield one row at a time. Then you can
still use that to create an Array of all rows. Silly example (a row
is just a number here):

irb(main):019:0> def rows
irb(main):020:1> if block_given?
irb(main):021:2> yield 1
irb(main):022:2> yield 2
irb(main):023:2> yield 3
irb(main):024:2> else
irb(main):025:2* a = []
irb(main):026:2> rows {|r| a << r}
irb(main):027:2> a
irb(main):028:2> end
irb(main):029:1> end
=> nil
irb(main):030:0> rows {|r| p r}
1
2
3
=> 3
irb(main):031:0> rows
=> [1, 2, 3]

If there is no block rows invokes itself providing a block which
appends to the Array.

instead of an array.

So I figure I need to create the object first in the file.each loop

This mess is where I got to:

file.each do |row|
@headers.each_with_index do |head, index|
require ‘ostruct’
rowObject = OpenStruct.new
rowObject.#{@headers[index]} = row.chomp.split(’, ')

This will assigne the whole row as an Array to a single member of
OpenStruct. I don’t think this is what you want. Rather, you want
multiple assignments. You could do it like this:

get all the columns

columns = row.chomp.split(’, ')

create the row object

row = OpenStruct.new

now assign all values by combining

headers with columns

@headers.zip columns do |hd, col|
row.send("#{hd}=", col)
end

Another approach would first create a Hash and use OpenStruct’s Hash
construction:

irb(main):004:0> h = {“foo” => 1, “bar” => 2}
=> {“foo”=>1, “bar”=>2}
irb(main):005:0> o = OpenStruct.new(h)
=> #
irb(main):006:0> o.foo
=> 1
irb(main):007:0> o.bar
=> 2

For that you could do

get all the columns

columns = row.chomp.split(’, ')

combine into nested arrays and create Hash

h = Hash[@headers.zip columns]

now create the row object

row = OpenStruct.new h

@headers.each_with_index do |head, headIndex|
require ‘ostruct’
rowObject = OpenStruct.new( :#{@header[headIndex =>
row[rowIndex]}

Or something…

All help and guidance gratefully received.

Hopefully that helped.

Kind regards

robert

Wow, thanks for the plentiful replies!

I think I was averse to creating multiple classes in one file, so
thought it best to try and create the CsvRow inline, it had crossed my
mind but I got hung up on the semantics of the book asking for an
object.

I create a CsvRow class and am now getting an each to return a CsvRow,
AWESOME!

The next bit I’m getting a little caught up on is how to get CsvRow to
return a single value in a row based on a headers value.

When I store a line in my CsvRow like this:

@fields = line.chomp.split(’, ')

Is that an array or hash?

Using method_missing and it’s an array will this work:

class CsvRow
attr_reader :fields, :headersPattern

def initialize(line)
@fields = line.chomp.split(’, ')
end

def headerPattern( pattern )
@headersPattern = pattern
end

def self.method_missing name, *args
@headersPattern.each_with_index |header, index|
if header = name.to_s yield @fields.send(@headers[index])
end
end
end

Thanks again for your help, it really makes a difference knowing a
language has a community of helpful people behind it :slight_smile:

Sorry, pressed wrong key…

2011/5/5 Jess Gabriel y Galn [email protected]:

return a single value in a row based on a headers value.

def self.method_missing name, *args
def method_missing name, *args
if index = @header_pattern.index name
when you construct the CsvRow, I think it’s cleaner:

class CsvRow
attr_reader :header_pattern, :values

def initialize headers, line
@header_pattern = headers
@values = line.chomp.split(",")
@hash = Hash[*@header_pattern.zip(@values).flatten]
end

def method_missing name, *args
@hash[name]
end
end

Hope this helps,

Jesus.

On Thu, May 5, 2011 at 4:19 PM, Jolyon R. [email protected] wrote:

The next bit I’m getting a little caught up on is how to get CsvRow to
return a single value in a row based on a headers value.

When I store a line in my CsvRow like this:

@fields = line.chomp.split(’, ')

Is that an array or hash?

Array. You can try that easily in IRB. It’s really a helpful tool.

@headersPattern = pattern
end

def self.method_missing name, *args
@headersPattern.each_with_index |header, index|
if header = name.to_s yield @fields.send(@headers[index])
end
end
end

Not sure what the method above is intended to do. First odd thing is
that it is not defined in your CsvRow class as instance method.
That’s likely not what you want.

Then, there is a typo: you have “=” instead of “==”. If you
identified a match you rather want to return the value instead of
using yield.

Maybe you had something in mind like

def method_missing name, *args, &b
@header_pattern.each_with_index do |hd, idx|
return @fields[idx] if hd == name
end
super # default error handling
end

Or you could do

def method_missing name, *args, &b
idx = @header_pattern.index name.to_s and return @fields[idx]
super # default error handling
end

A general remark: conventionally we use underscore_case for method and
variable names. CamelCase is only used for constants (class names and
the like).

One last remark: you can make your live easier and do

class CsvRow
attr_reader :fields
attr_accessor :headers_pattern
end

Thanks again for your help, it really makes a difference knowing a
language has a community of helpful people behind it :slight_smile:

Absolutely! I’d say Ruby community is one of the friendliest places
you can hang out.

Kind regards

robert

On Thu, May 5, 2011 at 4:19 PM, Jolyon R. [email protected] wrote:

When I store a line in my CsvRow like this:

@fields = line.chomp.split(’, ')

Is that an array or hash?

You can read about String#split in the documentation and you will see
that it returns an array.

Using method_missing and it’s an array will this work:

class CsvRow
attr_reader :fields, :headersPattern

Convention in Ruby is snake_case, so headers_pattern is more typical.

def initialize(line)
@fields = line.chomp.split(’, ')
end

def headerPattern( pattern )
@headersPattern = pattern
end

Who is calling this method? If you use attr_accessor instead of
reader, you get a method called header_pattern=, which sets the value,
as you are doing, so that would be more convenient. Also, you are
rewriting the method headerPattern created by attr_reader with this
one. So, remove it and use attr_accessor :header_pattern.

def self.method_missing name, *args
@headersPattern.each_with_index |header, index|
if header = name.to_s yield @fields.send(@headers[index])
end
end
end

method_missing is an instance method, so drop the self there. And I
think that you want to access the corresponding field of the header in
the @fields array, so no need to send anything, just:

def method_missing name, *args
@header_pattern.each_with_index do |header, index|
if header == name.to_s
@fields[index]
break
end
end

Which could be also written as:

def method_missing name, *args
if index = @header_pattern.index name
@fields[index]
end
end

Which will find the name in the header_pattern, if it finds it indexes
the @fields array at that index. Anyway, if the numbers of headers
could be big, or the number of times you are going to call this method
is relevant, I would think of having a different data structure. I
would have a hash whose keys are the header names, and whose values
are what you have in @fields. If possible, I would pass the headers
when you construct the CsvRow, I think it’s cleaner:

class CsvRow
attr_reader :header_pattern, :values

end

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs