AidanofVT
AidanofVT

Reputation: 57

Trying to modify method_missing, out of ideas

Here's the assignment:

Modify the CSV application to support an each method to return a CsvRow object. Use method_missing on that CsvRow to return the value for the column for a given heading. For example, for the file:

one, two

lions, tigers

allow an API that works like this:

csv = Ruby.Csv

csv.each {|row| puts row.one}

This should print 'lions'.

My attempt is shown below. No errors, it just returns... nothing. I've done my best to check the components piece by piece, and it seems like everything is behaving as expected except that last step, the method_missing method.

I'd appreciate it if somebody could help me understand where I took a wrong turn.

module ActsAsCsv

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

  module ClassMethods
    def acts_as_csv
      include InstanceMethods
    end
  end

  module InstanceMethods
    attr_accessor :headers, :csv_contents

    def initialize
     read
    end

    def read
      filename = self.class.to_s.downcase + '.txt'
      file = File.new(filename)
      @headers = file.gets.chomp.split(', ')
      @csv_contents = []
      file.each do |aRow|
        @csv_contents << aRow.chomp.split(', ')
      end
      i = 0
      @rows = {}
      @headers.each do |a|
        column = []
        @csv_contents.each {|b| column << b[i]}
        @rows[a] = column
        i = i+1
        end
      @reader = CSVRow.new(@rows)
      return @reader 
    end
  end

  class CSVRow
    attr_accessor  :content

    def initialize (content)
      @content = content
    end

    def method_missing (name)
      @content[name.to_str]
    end

  end

end

class RubyCsv
  include ActsAsCsv
  acts_as_csv
end

m = RubyCsv.new
puts m.headers.inspect
m.each {|rows| puts rows.a}

I also wondered: why not just have the each method return rows, and modify rows.missing_method to return the value associated to the key of the missing method's name?. I tried to implement this various ways, none of which worked. This most recent version at least doesn't return any errors:

    def each
      def @rows.method_missing (name)
        return self[name.to_str]
      end
      return @rows
    end

Upvotes: 0

Views: 77

Answers (1)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121020

Basically, the idea would be to lookup headers on the missing method call and if there is such a header, find the value that corresponds the index of this header in the row and return it.

Let’s start with preparing CSV.

content =
  CSV.parse(<<~ROWS, headers: true)
    one,two
    lions,tigers
  ROWS
#⇒ #<CSV::Table mode:col_or_row row_count:2>

So far, so good. Let’s monkeypatch the opened class CSV::Row to respond to method_missing in a custom way.

class CSV::Row
  def method_missing(m, *_args, &_cb)
    (idx = headers.index(m.to_s)) ? self[idx] : super
  end
end

Here we check if there is such a header, assigning its index to the local variable if found. If not found, we fall back to the default response with super.

Let’s see how it works.

content.first.one
#⇒ "lions"
content.first.two
#⇒ "tigers"
content.first.three
# NoMethodError: undefined method `three' for 
     #<CSV::Row one:"lions" two:"tigers"

The code above monkeypatches the core ruby CSV class, but it should not be hard to port to any custom CSV implementation.

Upvotes: 2

Related Questions