Creating variables on an OpenStruct with dynamic names

J

Jolyon R.

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.

Attachments:
http://www.ruby-forum.com/attachment/6164/acts_as_csv_module_original.rb
 
J

Jesús Gabriel y Galán

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

=A0def self.included(base)
=A0 =A0base.extend ClassMethods
=A0end

=A0module ClassMethods
=A0 =A0def acts_as_csv
=A0 =A0 =A0include InstanceMethods
=A0 =A0end
=A0end

=A0module InstanceMethods

=A0 =A0def read
=A0 =A0 =A0@csv_contents =3D []
=A0 =A0 =A0filename =3D self.class.to_s.downcase + '.txt'
=A0 =A0 =A0file =3D File.new(filename)
=A0 =A0 =A0@headers =3D file.gets.chomp.split(', ')

=A0 =A0 =A0file.each do |row|
=A0 =A0 =A0 =A0@csv_contents << row.chomp.split(', ')
=A0 =A0 =A0end
=A0 =A0end

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 =3D "#{self.class.to_s.downcase}.txt"
@headers, *@csv_contents =3D File.readlines(filename).map {|line|
line.chomp.split(",")}
end

=A0 =A0attr_accessor :headers, :csv_contents

=A0 =A0def initialize
=A0 =A0 =A0read
=A0 =A0end

=A0end

end

class RubyCsv =A0# no inheritance! You can mix it in
=A0include ActsAsCsv
=A0acts_as_csv
end

m =3D 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|
=A0 =A0 =A0 [email protected]_with_index do |head, index|
=A0 =A0 =A0 =A0 =A0require 'ostruct'

You should require outside of the loop.
=A0 =A0 =A0 =A0 =A0rowObject =3D OpenStruct.new
=A0 =A0 =A0 =A0 =A0rowObject.#{@headers[index]} =3D row.chomp.split(', ')

=A0 =A0 =A0 =A0end
=A0 =A0 =A0 =A0@csv_contents << rowObject
=A0 =A0 =A0end

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|
=A0 [email protected]_with_index do |head, headIndex|
=A0 =A0 =A0 =A0require 'ostruct'
=A0 =A0 =A0 =A0rowObject =3D OpenStruct.new( :#{@header[headIndex =3D>
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 =3D Struct.new *@headers.map(&:to_sym)
@csv_contents.each do |row|
yield row_struct.new *row
end
end
end

csv =3D 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.
 
C

Christopher Dicely

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

=C2=A0def self.included(base)
=C2=A0 =C2=A0base.extend ClassMethods
=C2=A0end

=C2=A0module ClassMethods
=C2=A0 =C2=A0def acts_as_csv
=C2=A0 =C2=A0 =C2=A0include InstanceMethods
=C2=A0 =C2=A0end
=C2=A0end

=C2=A0module InstanceMethods

=C2=A0 =C2=A0def read
=C2=A0 =C2=A0 =C2=A0@csv_contents =3D []
=C2=A0 =C2=A0 =C2=A0filename =3D self.class.to_s.downcase + '.txt'
=C2=A0 =C2=A0 =C2=A0file =3D File.new(filename)
=C2=A0 =C2=A0 =C2=A0@headers =3D file.gets.chomp.split(', ')

=C2=A0 =C2=A0 =C2=A0file.each do |row|
=C2=A0 =C2=A0 =C2=A0 =C2=A0@csv_contents << row.chomp.split(', ')
=C2=A0 =C2=A0 =C2=A0end
=C2=A0 =C2=A0end

=C2=A0 =C2=A0attr_accessor :headers, :csv_contents

=C2=A0 =C2=A0def initialize
=C2=A0 =C2=A0 =C2=A0read
=C2=A0 =C2=A0end

=C2=A0end

end

class RubyCsv =C2=A0# no inheritance! You can mix it in
=C2=A0include ActsAsCsv
=C2=A0acts_as_csv
end

m =3D 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|
=C2=A0 =C2=A0 =C2=A0 [email protected]_with_index do |head, index|
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0require 'ostruct'
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0rowObject =3D OpenStruct.new
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0rowObject.#{@headers[index]} =3D row.ch= omp.split(', ')

=C2=A0 =C2=A0 =C2=A0 =C2=A0end
=C2=A0 =C2=A0 =C2=A0 =C2=A0@csv_contents << rowObject
=C2=A0 =C2=A0 =C2=A0end

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.
 
7

7stud --

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=C3=BAs Gabriel y Gal=C3=A1n's co=
de =

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:

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 =3D []
filename =3D self.class.to_s.downcase + '.txt'
file =3D File.new(filename)
@headers =3D 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 =3D 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.

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 call the yield() function. =

Here is an example:

class Dog
def each
yield 'dog'
yield 20
yield 'last one'
end
end

d =3D Dog.new

d.each do |x|
puts x
end

--output:--
dog
20
last one

In your case, you will be iterating over the lines of a file. 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

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

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

end

class RowObject
attr_reader :fields

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

Note that initialize() opens the file, so somewhere you will need to =

close the file.

-- =

Posted via http://www.ruby-forum.com/.=
 
7

7stud --

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.
 
7

7stud --

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.
 
R

Robert Klemme

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!
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

=A0def self.included(base)
=A0 =A0base.extend ClassMethods
=A0end

=A0module ClassMethods
=A0 =A0def acts_as_csv
=A0 =A0 =A0include InstanceMethods
=A0 =A0end
=A0end

=A0module InstanceMethods

=A0 =A0def read
=A0 =A0 =A0@csv_contents =3D []
=A0 =A0 =A0filename =3D self.class.to_s.downcase + '.txt'
=A0 =A0 =A0file =3D File.new(filename)
=A0 =A0 =A0@headers =3D file.gets.chomp.split(', ')

=A0 =A0 =A0file.each do |row|
=A0 =A0 =A0 =A0@csv_contents << row.chomp.split(', ')
=A0 =A0 =A0end
=A0 =A0end

=A0 =A0attr_accessor :headers, :csv_contents

=A0 =A0def initialize
=A0 =A0 =A0read
=A0 =A0end

=A0end

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 =3D []
irb(main):026:2> rows {|r| a << r}
irb(main):027:2> a
irb(main):028:2> end
irb(main):029:1> end
=3D> nil
irb(main):030:0> rows {|r| p r}
1
2
3
=3D> 3
irb(main):031:0> rows
=3D> [1, 2, 3]

If there is no block rows invokes itself providing a block which
appends to the Array.
class RubyCsv =A0# no inheritance! You can mix it in
=A0include ActsAsCsv
=A0acts_as_csv
end

m =3D 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|
=A0 =A0 =A0 [email protected]_with_index do |head, index|
=A0 =A0 =A0 =A0 =A0require 'ostruct'
=A0 =A0 =A0 =A0 =A0rowObject =3D OpenStruct.new
=A0 =A0 =A0 =A0 =A0rowObject.#{@headers[index]} =3D 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 =3D row.chomp.split(', ')

# create the row object
row =3D OpenStruct.new

# now assign all values by combining
# headers with columns
@headers.zip columns do |hd, col|
row.send("#{hd}=3D", col)
end

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

irb(main):004:0> h =3D {"foo" =3D> 1, "bar" =3D> 2}
=3D> {"foo"=3D>1, "bar"=3D>2}
irb(main):005:0> o =3D OpenStruct.new(h)
=3D> #<OpenStruct foo=3D1, bar=3D2>
irb(main):006:0> o.foo
=3D> 1
irb(main):007:0> o.bar
=3D> 2

For that you could do

# get all the columns
columns =3D row.chomp.split(', ')

# combine into nested arrays and create Hash
h =3D Hash[@headers.zip columns]

# now create the row object
row =3D OpenStruct.new h
=A0 =A0 =A0 =A0end
=A0 =A0 =A0 =A0@csv_contents << rowObject
=A0 =A0 =A0end

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|
=A0 [email protected]_with_index do |head, headIndex|
=A0 =A0 =A0 =A0require 'ostruct'
=A0 =A0 =A0 =A0rowObject =3D OpenStruct.new( :#{@header[headIndex =3D>
row[rowIndex]}

Or something...

All help and guidance gratefully received.

Hopefully that helped.

Kind regards

robert


--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 
J

Jolyon R.

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 :)
 
J

Jesús Gabriel y Galán

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 =3D 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
=A0attr_reader :fields, :headersPattern

Convention in Ruby is snake_case, so headers_pattern is more typical.
=A0def initialize(line)
=A0 =A0@fields =3D line.chomp.split(', ')
=A0end

=A0def headerPattern( pattern )
=A0 =A0@headersPattern =3D pattern
=A0end

Who is calling this method? If you use attr_accessor instead of
reader, you get a method called header_pattern=3D, 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.
=A0def self.method_missing name, *args
=A0 [email protected]_with_index |header, index|
=A0 =A0 =A0if header =3D name.to_s yield @fields.send(@headers[index])
=A0 =A0end
=A0end
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 =3D=3D name.to_s
@fields[index]
break
end
end

Which could be also written as:

def method_missing name, *args
if index =3D @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
 
J

Jesús Gabriel y Galán

Sorry, pressed wrong key...

2011/5/5 Jes=FAs Gabriel y Gal=E1n said:
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 =3D 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
=A0attr_reader :fields, :headersPattern

Convention in Ruby is snake_case, so headers_pattern is more typical.
=A0def initialize(line)
=A0 =A0@fields =3D line.chomp.split(', ')
=A0end

=A0def headerPattern( pattern )
=A0 =A0@headersPattern =3D pattern
=A0end

Who is calling this method? If you use attr_accessor instead of
reader, you get a method called header_pattern=3D, 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.
=A0def self.method_missing name, *args
=A0 [email protected]_with_index |header, index|
=A0 =A0 =A0if header =3D name.to_s yield @fields.send(@headers[index])
=A0 =A0end
=A0end
end

method_missing is an instance method, so drop the self there. =A0And 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
=A0@header_pattern.each_with_index do |header, index|
=A0 =A0if header =3D=3D name.to_s
=A0 =A0 =A0@fields[index]
=A0 =A0 =A0break
=A0 =A0end
end

Which could be also written as:

def method_missing name, *args
=A0if index =3D @header_pattern.index name
=A0 =A0@fields[index]
=A0end
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

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

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

Hope this helps,

Jesus.
 
R

Robert Klemme

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 =3D line.chomp.split(', ')

Is that an array or hash?

Array. You can try that easily in IRB. It's really a helpful tool.
Using method_missing and it's an array will this work:

class CsvRow
=A0attr_reader :fields, :headersPattern

=A0def initialize(line)
=A0 =A0@fields =3D line.chomp.split(', ')
=A0end

=A0def headerPattern( pattern )
=A0 =A0@headersPattern =3D pattern
=A0end

=A0def self.method_missing name, *args
=A0 [email protected]_with_index |header, index|
=A0 =A0 =A0if header =3D name.to_s yield @fields.send(@headers[index])
=A0 =A0end
=A0end
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 "=3D" instead of "=3D=3D". 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 =3D=3D name
end
super # default error handling
end

Or you could do

def method_missing name, *args, &b
idx =3D @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 :)

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

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,770
Messages
2,569,584
Members
45,075
Latest member
MakersCBDBloodSupport

Latest Threads

Top