Working With Structured Data in Ruby

Working With Structured Data in Ruby

Many Ruby programs work with collections of information. A program may manage book titles, member records, task lists, order entries, or imported text. Ruby provides several useful structures for storing and processing this information.

Arrays and hashes are central to this work. Files allow data to remain available between program runs. Validation helps prevent incomplete or incorrect values from entering later processing steps.

Arrays for Ordered Collections

An array stores values in order.

topics = ["variables", "methods", "classes"]

Each item has a position.

puts topics[0]
puts topics[1]

Ruby arrays provide methods for adding and removing values.

topics << "modules"
topics.delete("variables")

They can also be filtered:

long_names = topics.select { |topic| topic.length > 7 }

Or converted:

labels = topics.map { |topic| topic.upcase }

These methods return new arrays, which makes it easier to create clear processing steps.

Hashes for Related Values

A hash stores values under keys.

member = {
  name: "Marlow Reed",
  city: "Portland",
  active: true
}

Values are read through their keys:

puts member[:name]
puts member[:city]

Hashes work well when one record contains several named fields.

A collection of records may be stored as an array of hashes:

members = [
  { name: "Marlow Reed", active: true },
  { name: "Tessa Cole", active: false },
  { name: "Orin Blake", active: true }
]

Active records can be selected:

active_members = members.select { |member| member[:active] }

Names can then be collected:

names = active_members.map { |member| member[:name] }

Build Processing Steps

Data processing is easier to review when each step has a clear name.

records = [
  "  ruby basics ",
  "",
  "methods",
  "  classes"
]

A crowded expression may work:

result = records.map(&:strip).reject(&:empty?).map(&:capitalize)

A step-by-step version is often easier to inspect:

cleaned = records.map(&:strip)
non_empty = cleaned.reject(&:empty?)
formatted = non_empty.map(&:capitalize)

Both approaches are valid. The second version helps when debugging or explaining the process.

Grouping Data

Ruby’s group_by method creates groups based on a rule.

words = ["array", "class", "object", "attribute", "method"]

grouped = words.group_by { |word| word[0] }

The result is a hash where each key contains a group of words.

puts grouped["a"]

Records can also be grouped by status:

tasks = [
  { title: "Read chapter", status: "open" },
  { title: "Write notes", status: "done" },
  { title: "Review code", status: "open" }
]

by_status = tasks.group_by { |task| task[:status] }

Sorting Records

Arrays can be sorted by one field.

books = [
  { title: "Objects", pages: 180 },
  { title: "Files", pages: 120 },
  { title: "Modules", pages: 210 }
]

sorted = books.sort_by { |book| book[:pages] }

For reverse order:

descending = books.sort_by { |book| -book[:pages] }

Reading Data From Files

Text files are a simple way to store information.

Suppose topics.txt contains:

arrays
hashes
files
validation

Ruby can read the file into an array:

topics = File.readlines("topics.txt", chomp: true)

The chomp: true option removes newline characters.

The program can then process the values:

cleaned = topics.map(&:strip).reject(&:empty?)
puts cleaned

Writing Data to Files

Ruby can write a string to a file:

File.write("summary.txt", "Ruby study notes")

To write several lines:

topics = ["arrays", "hashes", "files"]

File.open("topics.txt", "w") do |file|
  topics.each do |topic|
    file.puts topic
  end
end

The block automatically closes the file when the operation is finished.

Appending New Records

To add information without replacing existing content, use append mode:

File.open("topics.txt", "a") do |file|
  file.puts "classes"
end

Validate Before Processing

Validation checks whether data follows the expected rules.

def valid_name?(name)
  !name.to_s.strip.empty?
end

A record can be reviewed before it is stored:

record = { name: "Elian", age: 28 }

if valid_name?(record[:name])
  puts "Record accepted"
else
  puts "Name is required"
end

Several checks can be collected:

def validate_member(member)
  errors = []

  errors << "Name is required" if member[:name].to_s.strip.empty?
  errors << "Age must be positive" unless member[:age].to_i > 0

  errors
end

Usage:

errors = validate_member(name: "", age: -2)

if errors.empty?
  puts "Record accepted"
else
  puts errors
end

Convert Hashes Into Objects

As a program expands, hashes may be replaced with objects.

class Member
  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
end

A hash can be converted:

data = { name: "Ronan Vale", age: 34 }
member = Member.new(data[:name], data[:age])

Objects provide a clear place for behavior related to the data.

Create a Data Pipeline

A small data pipeline may include:

  1. reading raw lines;
  2. cleaning values;
  3. validating records;
  4. converting records into objects;
  5. grouping or sorting them;
  6. preparing output.

Each step can be placed in its own method or class.

class TopicImporter
  def initialize(path)
    @path = path
  end

  def call
    File.readlines(@path, chomp: true)
        .map(&:strip)
        .reject(&:empty?)
  end
end

Practice Exercise

Create a small member directory that:

  • reads names and cities from a text file;
  • converts each line into a hash;
  • rejects incomplete records;
  • sorts records by name;
  • groups records by city;
  • writes a summary into another file.

Begin with input such as:

Avery Stone,Denver
Milo West,Austin
Tessa Lane,Denver

Structured data work is easier when each stage is visible. Use arrays for ordered collections, hashes for named fields, objects for data with behavior, and files for stored information. Add validation before later processing. These habits make Ruby programs easier to understand and maintain.

Back to blog