1. Technology
You can opt-out at any time. Please refer to our privacy policy for contact information.

Worked Example: Tabular Data

By

Worked Example: Tabular Data

Ruby provides simple data structures for 1 dimensional arrays, as well as for dictionary tables (hashes). However, tabular data is normally represented as a 2 dimensional array. While there is no native support for this in the base language or standard library, it's easy to emulate using an array of arrays.

Constructing Arrays

Normally, arrays are constructed using the array literal. This is fine if you already know the contents of the array, but in our case we'll be making an array of arrays of specific size, so we need to do something a bit different. The Array constructor allows you to provide two method parameters that are of use here. First is the size parameter. In most uses of the Array class, you either create an array from a literal, or create an empty array and add elements to it. However, you can create an array of known size like so.


a = Array.new(10)  # 10 elements long, elements are nil

How can we use this to create a table of numbers? The second parameter can define the default value for the array members. We can use this to create an array of arrays. You can pass any object as the second parameter to the array constructor, so you can do thing like this, which will create an array 10 elements long, with each element equal to 0.


a = Array.new(10, 0)  # Elements are 0

However, the second parameter can be a block as well. Each time a new object needs to be created to put into the array, the block will be called and the yielded object will be the array element. The following example will create a 10x10 array. The outer array holds the rows, each row holds another array 10 elements long, and each element in that array will be initialized to 0.


table = Array.new(10) { Array.new(10, 0) }

Indexing this array is quite easy. Indexing the table variable will return an array that represents one of the rows. Indexing that array will select a column from that row. So, for example, to access the element at row 3, column 4 you would use table[3][4]. Do not confuse this with a similar syntax, table[3,4]. This index operation will retrieve two entire rows, 3 and 4, from the table.

Loading and Computing Data

A teacher needs to compute the grades for her students. She keeps the test scores for each student in a text file. This text file is comma separated, with the student name in the first column and the test scores in the following eight columns. To load this data, she simply needs to create the 10x10 array (one row for every student, 10 columns for name, scores and the average). Then she just needs to load the file, split on commas and merge it with each row in the table.


Alice,71,77,81,83,96,91,98,75
Bob,97,75,74,99,81,65,76,71
Carol,68,93,91,90,70,83,84,100
David,92,78,75,79,98,71,78,75
Elle,72,83,81,70,95,71,74,74
Frank,98,79,92,71,70,94,96,78
Gerald,65,65,84,89,74,100,72,74
Helen,100,71,84,86,79,97,100,91
Isabelle,94,71,88,70,69,66,89,75
Jake,94,86,83,81,97,76,78,68

Once this data is loaded, all that remains to be done is to compute the average and print the grades. The following is the complete program.


#!/usr/bin/env ruby

# Read the grades
grades = Array.new(10){ Array.new(10,0) }
File.readlines('grades.txt').each_with_index do|line,idx|
  student = line.chomp.split(',')
  # Merge into existing array
  grades[idx][0..student.length - 1] = student
end

# Convert from strings to integers
grades.each do|g|
  (1..9).each do|i|
    g[i] = g[i].to_i
  end
end

# Compute the averages
grades.each do|g|
  g[g.length-1] = g[1..g.length-2].inject(&:+) / (g.length-2)
end

# Output the final grades
grades.each do|g|
  puts "%-14s %4d" % [ g.first, g.last ]
end

There are a few tricks used here. The first is an array merge. If we created our 10x10 array with all 0's, what we want to do is just merge the loaded data into this 0 data, leaving any 0's that are there already. If you have an array [0,0,0,0] and want to merge [1,2] into it to get [1,2,0,0], you would do something like [0,0,0,0][0..1] = [1,2]. Assigning with the range operator will replace any section of an array with another. This is employed in the code as follows.


grades[idx][0..student.length - 1] = student

The next trick is used in computing the average. By "injecting" the symbol :+ converted to a proc using to_proc (implicitly called using the & syntax), you can arbitrarily sum any array or portions of an array. In this program, we're summing everything but the first and last of the array.


g[g.length-1] = g[1..g.length-2].inject(&:+) / (g.length-2)

And finally in order to print the table in an orderly manner, we use a few format strings. The format string %-15s will left-justify a string to 15 characters. Similarly, the %4d format string will right-align an integer to 4 characters.


puts "%-14s %4d" % [ g.first, g.last ]

And finally, the output of the program looks like this.


Alice            84
Bob              79
Carol            84
David            80
Elle             77
Frank            84
Gerald           77
Helen            88
Isabelle         77
Jake             82
  1. About.com
  2. Technology
  3. Ruby
  4. Beginning Ruby
  5. Worked Example: Tabular Data

©2014 About.com. All rights reserved.