Skip to content

Instantly share code, notes, and snippets.

@zerowidth
Created February 14, 2013 03:23
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zerowidth/4950374 to your computer and use it in GitHub Desktop.
Save zerowidth/4950374 to your computer and use it in GitHub Desktop.
Script to parse a text table and convert to a reasonable output. Demonstration of an idiomatic ruby script for a friend.
require "pp"
def convert_to_offsets(widths)
offsets = []
position = 0
widths.each.with_index do |width, index|
offsets << (position...(width+position)) # range does not include the end
position += width + 1 # assume spacing of 1 character
end
offsets
end
def extract_values(line, offsets)
offsets.map { |range| line[range].strip }
end
def read_table(table)
lines = table.split "\n"
fields = lines[0].split /\s+/
widths = lines[1].split(" ").map(&:length)
offsets = convert_to_offsets widths
lines[2..-1].map do |line|
values = extract_values line, offsets
Hash[*fields.zip(values).flatten]
end
end
# this reads from the bottom of the file, after the __END__:
DATA.read.split("***\n").each do |table|
puts "-" * 20
data = read_table table
data.each do |row|
# do a bit o' massaging with the data before displaying it:
port_id = row.delete "Port"
row["PortID"] = port_id
row["Module"], row["PortNum"] = port_id.split("/").map(&:to_i)
pp row
end
end
__END__
Port Name Status Vlan Duplex Speed Type
----- -------------------- ---------- ---------- ------ ----- ------------
1/1 LcacdcC1-CU08 7/12 connected trunk full 1000 1000Base SX
1/2 disable 1 full 1000 1000Base SX
3/1 not used? to 8/12 connected trunk full 1000 1000BaseSX
***
Port Name Status Vlan Duplex Speed Type
----- ----------------- ---------- ------------ ------ ----- ------------
1/1 Lcac1-CU08 7/12 connected trunk full 1000 1000BaseSX
1/2 disable 1 full 1000 1000 BaseSX
3/1 nosed? to 8/12 connected trunk full 1000 1000 BaseSX
@zerowidth
Copy link
Author

I would wrap this up in a class if this needed to be a reusable component. Additionally, for example, if using this within a rails app (String#underscore is available), I'd do something like:

fields = lines[0].split(/\s+/).map(&:underscore)

so the field names are ruby style, that is, "port_num" instead of "PortNum"

I'd also consider returning an OpenStruct instead of a hash:

require "ostruct"
row = OpenStruct.new( Hash[ *fields.zip(values).flatten ] )
# row.port
# row.name
# etc.

@zerowidth
Copy link
Author

Aaaand because I appear to have nothing better to do, let's go so far overboard we can't see the boat anymore. Here's an example of that script extracted into a class and documented with TomDoc.

# Assumes ActiveSupport (Rails) is loaded, for String#underscore
require "ostruct"

class TextTable

  # Public: return a list of fields for this table
  attr_reader :fields

  # Public: return a list of values for each row of this table
  attr_reader :row_values

  # Create a new TextTable from a textual representation of a table
  #
  # table - a string containing the table data, e.g.
  #
  #   Field 1   Field 2
  #   --------- -------
  #   value 1   asdf
  #   value 2   jkl
  #
  def initialize(table)
    @lines = table.split "\n"
    @fields = @lines[0].split /\s+/
    @pretty_fields = @fields.map(&:underscore)

    offsets = convert_widths_to_offsets(@lines[1].split(" ").map(&:length))

    @row_values = @lines[2..-1].map do |line|
      extract_values line, offsets
    end
  end

  # Public: return a list of rows from this table
  #
  #
  # The fields are converted to underscored names, e.g. "Field 1" would become
  # "field_1".
  #
  # Example:
  #   text_table.rows
  #   # => [ #<OpenStruct field_1="value 1", field_2="asdf">, ... ]
  #
  # Returns OpenStructs for each row, with the table fields as accessors.
  def rows
    @row_values.map do |values|
      OpenStruct.new Hash[*@pretty_fields.zip(values).flatten]
    end
  end

  # Public: return a Hash for each row from this table
  #
  # Example:
  #
  #   text_table.row_hashes
  #   # => [ {"Field 1" => "value 1", "Field 2" => "asdf"}, ... ]
  #
  # Returns an array of hashes representing each row, without field name
  # conversion.
  def row_hashes
    @row_values.map do |values|
      Hash[*@fields.zip(values).flatten]
    end
  end

  # Internal: convert a list of widths to a list of offsets
  #
  # widths - an Array of column widths
  #
  # Assumes a spacing of one character between fields.
  #
  # Example:
  #
  #  convert_widths_to_offsets [5, 5, 10]
  #  # => [0..4, 6..10, 12..21]
  #
  # Returns a list of offset Ranges.
  def convert_widths_to_offsets(widths)
    offsets = []
    position = 0

    widths.each.with_index do |width, index|
      offsets << (position...(width+position))
      position += width + 1
    end

    offsets
  end

  # Internal: extract the values from a line
  #
  # line    - the input line of a table row
  # offsets - an array of Ranges for each field's offsets
  #
  # Returns a list of values, whitespace stripped.
  def extract_values(line, offsets)
    offsets.map { |range| line[range].strip }
  end
end

And the script:

require "pp"
DATA.read.split("***\n").each do |input|
  puts "-" * 20
  table = TextTable.new input
  table.rows.each do |row|
    pp row
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment