Data Types #
One thing that makes Ruby feel different from languages like Java or C is that you never need to declare data types explicitly. Write x = 42 and Ruby knows it’s an Integer. Write x = "hello" and Ruby knows it’s a String. But “dynamic” doesn’t mean “typeless” — every value in Ruby is an object with a clear type, and every type has different behavior and methods. Understanding the characteristics of each data type, when to use them, and how to convert between them is the foundation that determines the quality of the Ruby code you write.
Every Value Is an Object #
Before discussing types one by one, there’s an important concept to understand: in Ruby, every value is an object — including numbers, booleans, and even nil. There are no primitive types like in Java or C. This means every value has methods you can call.
42.class # => Integer
3.14.class # => Float
"hello".class # => String
:symbol.class # => Symbol
true.class # => TrueClass
false.class # => FalseClass
nil.class # => NilClass
[].class # => Array
{}.class # => Hash
(1..10).class # => Range
# Even numbers have methods
42.even? # => true
42.to_s # => "42"
42.times { |i| print "#{i} " } # => 0 1 2 3 ... 41
flowchart TD
A[Every value in Ruby] --> B[Object]
B --> C[Numeric]
B --> D[String]
B --> E[Symbol]
B --> F[Array]
B --> G[Hash]
B --> H[NilClass]
B --> I[TrueClass / FalseClass]
B --> J[Range]
C --> K[Integer]
C --> L[Float]
C --> M[Rational]
C --> N[Complex]Integer #
Integer stores whole numbers — positive, negative, or zero — with no size limit. Ruby automatically handles extremely large numbers without overflow.
# Declaring various Integers
age = 25
temperature = -10
zero = 0
population = 270_000_000 # underscore as thousands separator
really_big = 10 ** 100 # a googol — Ruby handles it with no problem
# Common Integer methods
puts 17.even? # => false
puts 17.odd? # => true
puts -5.abs # => 5
puts 255.to_s(2) # => "11111111" (convert to binary)
puts 255.to_s(16) # => "ff" (convert to hexadecimal)
puts 10.gcd(6) # => 2 (greatest common divisor)
puts 10.lcm(6) # => 30 (least common multiple)
Integer Literals in Different Bases #
Ruby supports writing integer literals in decimal, binary, octal, and hexadecimal directly in code:
decimal = 255 # base 10 (standard)
binary = 0b11111111 # base 2 — 0b prefix
octal = 0377 # base 8 — 0 or 0o prefix
hexadecimal = 0xFF # base 16 — 0x prefix
puts decimal == binary # => true (all are 255)
puts decimal == octal # => true
puts decimal == hexadecimal # => true
Caution: Integer Division #
Dividing two Integers produces an Integer — the decimal remainder is discarded, not rounded.
# ANTI-PATTERN: expecting a decimal result from integer division
puts 7 / 2 # => 3 (not 3.5!)
puts 1 / 3 # => 0 (not 0.333!)
# CORRECT: use a Float in one of the operands
puts 7.0 / 2 # => 3.5
puts 7 / 2.0 # => 3.5
puts 7.fdiv(2) # => 3.5 (the most idiomatic way)
# Or convert explicitly
puts 7.to_f / 2 # => 3.5
Float #
Float stores decimal numbers using double-precision floating-point representation (64-bit IEEE 754). It’s suitable for scientific and light financial calculations, but it has precision limitations you need to understand.
pi = 3.14159265358979
temperature = -10.5
percent = 0.15
e_notation = 1.5e3 # scientific notation: 1.5 × 10³ = 1500.0
# Common Float methods
puts 3.7.ceil # => 4 (round up)
puts 3.7.floor # => 3 (round down)
puts 3.7.round # => 4 (round to nearest)
puts 3.14159.round(2) # => 3.14 (round to n decimals)
puts -3.7.abs # => 3.7
puts 2.0.infinite? # => nil (false)
puts (1.0/0).infinite? # => 1 (positive infinity)
puts (0.0/0).nan? # => true (Not a Number)
Float Precision Issues #
Float can’t represent every decimal number exactly — this isn’t a Ruby bug, but a fundamental limitation of binary floating-point representation.
# ANTI-PATTERN: comparing Floats directly
puts 0.1 + 0.2 == 0.3 # => false !
puts 0.1 + 0.2 # => 0.30000000000000004
# ANTI-PATTERN: using Float for money calculations
price = 19.99
qty = 3
total = price * qty
puts total # => 59.97000000000001 (not 59.97!)
# CORRECT: use BigDecimal for financial calculations
require 'bigdecimal'
price = BigDecimal("19.99")
qty = 3
total = price * qty
puts total.to_s('F') # => "59.97"
# CORRECT: compare Floats with a tolerance (epsilon)
def floats_equal?(a, b, epsilon = 1e-9)
(a - b).abs < epsilon
end
puts floats_equal?(0.1 + 0.2, 0.3) # => true
Never use Float to store or calculate money values. Use BigDecimal (from the standard library) or store money as Integers in the smallest unit (cents or rupiah), then format for display. Float precision errors in a financial context can cause real discrepancies at large transaction scales.String #
A String is a sequence of characters that can be manipulated with hundreds of built-in methods. It’s the most frequently used data type in almost every program.
# Two ways to create a string
double_quoted = "Welcome, #{Time.now.year}!" # interpolation active
single_quoted = 'Welcome, #{year}!' # interpolation inactive
# Heredoc — for long multi-line strings
message = <<~HEREDOC
Hello, this is a string
that is very long and
spans multiple lines.
HEREDOC
puts message.strip
Frequently Used String Methods #
text = " Ruby is a beautiful programming language! "
# Cleaning whitespace
text.strip # => "Ruby is a beautiful programming language!"
text.lstrip # => "Ruby is..." (left only)
text.rstrip # => " Ruby is..." (right only)
# Letter transformations
text.upcase # => " RUBY IS A BEAUTIFUL..."
text.downcase # => " ruby is a beautiful..."
text.capitalize # => " ruby..." → " Ruby..." (first letter only)
text.swapcase # => uppercase becomes lowercase and vice versa
# Content checks
text.include?("Ruby") # => true
text.start_with?(" Ruby") # => true
text.end_with?("!") # => true
text.empty? # => false
text.match?(/beautiful/) # => true (regex)
# Manipulation
text.gsub("Ruby", "Python") # replace all occurrences
text.sub("language", "tongue") # replace only the first
text.split(" ") # split into an array
text.reverse # reverse the character order
text.chars # split into an array of characters
text.bytes # split into an array of bytes
# Size
text.length # => 45 (number of characters)
text.size # alias for length
text.bytesize # number of bytes (differs for multibyte characters)
String Immutability with freeze #
# frozen_string_literal: true ← enable at the top of the file for better performance
name = "Rina"
name.frozen? # => true (if the magic comment is active)
name << " Wijaya" # => FrozenError if frozen
name += " Wijaya" # => OK — creates a new String, doesn't modify the old one
Symbol #
A Symbol is a lightweight, immutable identifier. It looks like a String, but has a unique characteristic: every Symbol with the same name refers to exactly the same object in memory, no matter how many times you write it.
# Declaring Symbols
status = :active
role = :admin
method = :get
# Symbol vs String — object identity differences
puts "active".object_id == "active".object_id # => false (two different objects)
puts :active.object_id == :active.object_id # => true (the same object!)
# Symbol methods
puts :hello.to_s # => "hello" (convert to String)
puts "hello".to_sym # => :hello (convert to Symbol)
puts :hello.upcase # => :HELLO
puts :hello_world.length # => 11
When to Use Symbol vs String #
# ANTI-PATTERN: using Strings as Hash keys
config = {
"host" => "localhost",
"port" => 5432,
"database" => "app_db"
}
# Every "host" access creates a new String object in memory
# CORRECT: use Symbols as Hash keys — more efficient
config = {
host: "localhost",
port: 5432,
database: "app_db"
}
# :host is a single, always-the-same object
# ANTI-PATTERN: Symbols for text displayed to the user
puts :welcome # not ideal — Symbols aren't for UI text
# CORRECT: Symbols for internal identifiers, Strings for displayed text
active_status = :active # internal identifier
ui_message = "Welcome!" # text for the user
Guide to choosing Symbol vs String:
Use a Symbol when:
✓ Used as a Hash key
✓ Internal identifier (status, role, event name)
✓ Method name passed as an argument
✓ Value being compared, not displayed
Use a String when:
✓ Text displayed to the user
✓ Content that needs manipulation (split, gsub, etc.)
✓ Data read from input or external files
✓ Values that change or have an unlimited number of them
Array #
An Array is an ordered collection that can hold objects of any type, including a mix of different types at once. Indexes start at 0, and negative indexes count from the end.
# Creating Arrays
fruits = ["apple", "mango", "orange", "pineapple"]
numbers = [1, 2, 3, 4, 5]
mixed = [1, "two", :three, 4.0, nil, true]
empty = []
words = %w[one two three four] # shorthand for a string array
# Accessing elements
puts fruits[0] # => "apple"
puts fruits[-1] # => "pineapple" (from the end)
puts fruits[1..2] # => ["mango", "orange"] (range)
puts fruits[1, 2] # => ["mango", "orange"] (start at index 1, take 2 elements)
puts fruits.first # => "apple"
puts fruits.last # => "pineapple"
puts fruits.first(2) # => ["apple", "mango"]
Array Manipulation #
list = [3, 1, 4, 1, 5, 9, 2, 6]
# Adding and removing elements
list.push(7) # add to the end
list << 8 # push alias — most idiomatic
list.unshift(0) # add to the front
list.pop # remove and return the last element
list.shift # remove and return the first element
list.delete(1) # remove all elements with value 1
# Transformation — all return a new Array (non-destructive)
list.sort # => [2, 3, 4, 5, 6, 9]
list.sort.reverse # => [9, 6, 5, 4, 3, 2]
list.uniq # => [3, 4, 5, 9, 2, 6] (remove duplicates)
list.flatten # flatten nested arrays
list.compact # remove all nils
list.map { |n| n * 2 } # transform every element
list.select { |n| n > 4 } # filter elements meeting the condition
list.reject { |n| n > 4 } # the opposite of select
list.reduce(:+) # sum all elements
list.min # smallest element
list.max # largest element
list.sum # total (shortcut for reduce(:+))
list.count { |n| n > 3 } # count those meeting the condition
Set Operations #
a = [1, 2, 3, 4, 5]
b = [3, 4, 5, 6, 7]
puts (a | b).inspect # => [1, 2, 3, 4, 5, 6, 7] (union)
puts (a & b).inspect # => [3, 4, 5] (intersection)
puts (a - b).inspect # => [1, 2] (difference)
puts (a + b).inspect # => [1, 2, 3, 4, 5, 3, 4, 5, 6, 7] (concat)
Hash #
A Hash is a collection of key-value pairs, similar to a dictionary or map in other languages. In modern Ruby, Hashes preserve insertion order.
# Creating a Hash — two valid syntaxes
# Old syntax (hash rocket):
profile = { "name" => "Budi", "age" => 30 }
# Modern syntax (symbol keys) — more concise and common:
profile = { name: "Budi", age: 30, city: "Jakarta" }
# Accessing values
puts profile[:name] # => "Budi"
puts profile[:age] # => 30
puts profile[:email] # => nil (key doesn't exist, no error)
puts profile.fetch(:email) # => KeyError!
puts profile.fetch(:email, "N/A") # => "N/A" (default if missing)
Hash Manipulation #
data = { a: 1, b: 2, c: 3, d: 4 }
# Add, change, delete
data[:e] = 5 # add a new key
data[:a] = 10 # change a value
data.delete(:b) # delete key :b
# Checks
data.key?(:c) # => true
data.value?(3) # => true
data.empty? # => false
data.size # => 4
data.keys # => [:a, :c, :d, :e]
data.values # => [10, 3, 4, 5]
# Iteration
data.each { |k, v| puts "#{k}: #{v}" }
data.each_key { |k| puts k }
data.each_value { |v| puts v }
# Transformation
data.map { |k, v| [k, v * 2] }.to_h # double all values
data.select { |k, v| v > 3 } # only pairs with value > 3
data.reject { |k, v| v > 3 } # the opposite of select
data.any? { |k, v| v > 3 } # does any pair match?
data.all? { |k, v| v > 0 } # do all pairs match?
data.min_by { |k, v| v } # pair with the smallest value
data.sort_by { |k, v| v } # sort by value
# Merge — combine two hashes
defaults = { timeout: 30, retries: 3, debug: false }
overrides = { timeout: 60, debug: true }
result = defaults.merge(overrides)
# => { timeout: 60, retries: 3, debug: true }
Hashes with Default Values #
# ANTI-PATTERN: manual nil checks before accessing nested hashes
config = {}
config[:db] ||= {}
config[:db][:host] = "localhost"
# CORRECT: Hash with a default value
counter = Hash.new(0) # default value 0 for any key
counter[:apple] += 1
counter[:mango] += 3
puts counter.inspect # => {apple: 1, mango: 3}
# Default value with a block — more powerful
groups = Hash.new { |h, k| h[k] = [] }
["Rina", "Budi", "Ani"].each_with_index do |name, i|
groups[i % 2] << name
end
puts groups.inspect # => {0=>["Rina", "Ani"], 1=>["Budi"]}
Boolean — TrueClass and FalseClass #
Ruby doesn’t have a single Boolean type. Instead, there are two separate classes: TrueClass (for the value true) and FalseClass (for the value false). There’s only one instance of each class in the entire program.
puts true.class # => TrueClass
puts false.class # => FalseClass
# Truthy and Falsy in Ruby
# Only nil and false are falsy — EVERYTHING else is truthy!
puts !!nil # => false (falsy)
puts !!false # => false (falsy)
puts !!0 # => true (0 is truthy in Ruby, unlike C/JS!)
puts !!"" # => true (empty string is truthy in Ruby!)
puts !![] # => true (empty array is truthy in Ruby!)
puts !!:sym # => true
# ANTI-PATTERN: comparing booleans with == true or == false
if user.active? == true # redundant
# ...
end
if value == false # better to use unless
# ...
end
# CORRECT: use truthy/falsy values directly
if user.active?
# ...
end
unless value
# ...
end
This difference matters when coming from JavaScript or Python. In JavaScript,0,"", and[]are falsy. In Python,0,"", and[]are also falsy. In Ruby, onlynilandfalseare falsy — every other value, including0,"", and[], is truthy. This is a common source of bugs for developers new to Ruby.
NilClass #
nil is the only instance of NilClass and represents the absence of a value. Unlike 0, false, or "" — nil explicitly means “no value”.
puts nil.class # => NilClass
puts nil.nil? # => true
puts nil.to_i # => 0
puts nil.to_f # => 0.0
puts nil.to_s # => "" (empty string, not "nil")
puts nil.to_a # => [] (empty array)
puts nil.inspect # => "nil"
# nil as the default return value
def find_user(id)
# if not found, there's no explicit return
end
result = find_user(999)
puts result.nil? # => true
Handling nil Safely #
# ANTI-PATTERN: calling methods without nil checks — vulnerable to NoMethodError
def display_email(user)
puts user.email.upcase # crashes if user or email is nil
end
# Method 1: explicit nil checks
def display_email(user)
if user && user.email
puts user.email.upcase
else
puts "Email not available"
end
end
# Method 2: safe navigation operator &. (most idiomatic)
def display_email(user)
puts user&.email&.upcase || "Email not available"
end
# Method 3: nil? check
def display_email(user)
return "User not found" if user.nil?
return "Email not set" if user.email.nil?
user.email.upcase
end
Range #
A Range represents a sequence of values with a start and end point. It can hold Integers, Floats, Strings, or other objects that implement comparison methods.
# Two kinds of Ranges
inclusive = 1..10 # includes 10
exclusive = 1...10 # excludes 10 (1 through 9)
puts inclusive.include?(10) # => true
puts exclusive.include?(10) # => false
# Converting to an Array
puts (1..5).to_a.inspect # => [1, 2, 3, 4, 5]
puts ('a'..'e').to_a.inspect # => ["a", "b", "c", "d", "e"]
# Common Range methods
r = (1..100)
puts r.min # => 1
puts r.max # => 100
puts r.sum # => 5050
puts r.count # => 100
puts r.include?(50) # => true
puts r.cover?(50.5) # => true (cover? is faster, doesn't iterate)
# Practical use
score = 75
case score
when 90..100 then puts "A"
when 80..89 then puts "B"
when 70..79 then puts "C"
else puts "D or E"
end
# => "C"
# Step — range with a custom increment
(0..20).step(5) { |n| print "#{n} " }
# => 0 5 10 15 20
Converting Between Types #
Ruby provides two kinds of conversion methods that behave differently — it’s important to understand the difference.
# "Soft" conversion (to_i, to_f, to_s, to_a) — no error, produces a default
"42".to_i # => 42
"3.14".to_f # => 3.14
"abc".to_i # => 0 (no error, takes whatever digits it finds at the start)
"123abc".to_i # => 123 (stops at the first non-digit)
nil.to_i # => 0
nil.to_s # => ""
nil.to_a # => []
42.to_s # => "42"
42.to_f # => 42.0
# "Hard" conversion (Integer(), Float(), String()) — raises an error on failure
Integer("42") # => 42
Integer("abc") # => ArgumentError: invalid value for Integer()
Float("3.14") # => 3.14
Float("abc") # => ArgumentError
Integer(nil) # => TypeError
# ANTI-PATTERN: to_i for input validation — hides invalid input
def process_age(input)
age = input.to_i # "abc".to_i => 0 — looks valid when it isn't
raise "Invalid age" if age <= 0
end
# CORRECT: Integer() for conversion with validation
def process_age(input)
age = Integer(input) # raises ArgumentError if not a valid number
raise ArgumentError, "Age must be positive" if age <= 0
age
rescue ArgumentError
raise ArgumentError, "Invalid age input: #{input}"
end
| Method | Behavior on invalid input | Use for |
|---|---|---|
to_i | Returns 0 | Conversions that may silently fail |
to_f | Returns 0.0 | Conversions that may silently fail |
to_s | Returns "" | Almost always safe |
Integer() | Raises ArgumentError | Validating user input |
Float() | Raises ArgumentError | Validating user input |
Duck Typing #
Ruby uses the duck typing approach — what matters isn’t the object’s type, but whether it has the methods you need. The name comes from the idiom: “If it walks like a duck and quacks like a duck, it’s a duck.”
# ANTI-PATTERN: checking types explicitly — not idiomatic in Ruby
def print_length(collection)
if collection.is_a?(Array)
puts collection.length
elsif collection.is_a?(String)
puts collection.length
elsif collection.is_a?(Hash)
puts collection.length
end
end
# CORRECT: duck typing — just call the method you need
def print_length(collection)
puts collection.length # works for Array, String, Hash, and anything with .length
end
print_length([1, 2, 3]) # => 3
print_length("hello") # => 5
print_length({a: 1, b: 2}) # => 2
# Check capability, not identity — respond_to? is more idiomatic than is_a?
def save(object)
if object.respond_to?(:to_json)
save_as_json(object.to_json)
else
raise ArgumentError, "Object cannot be converted to JSON"
end
end
flowchart TD
A["Object received as an argument"] --> B{is_a? or respond_to??}
B --> C["is_a?(Type)\n— checks class identity"]
B --> D["respond_to?(:method)\n— checks capability"]
C --> E["Rigid — only works\nfor exactly the same class"]
D --> F["Flexible — works for\nall objects with that method"]
F --> G["✓ The idiomatic Ruby way\n(Duck Typing)"]
E --> H["✗ Avoid unless you really\nneed a specific type check"]Summary #
- Every value in Ruby is an object — Integer, Float, String, even
nilandtrueall have callable methods.- Integer division discards decimals —
7 / 2produces3, not3.5. Use7.fdiv(2)or make sure one operand is a Float.- Float isn’t for money — use
BigDecimalfor financial calculations that need exact precision.- Symbols are singletons —
:nameis always the same object in memory, unlike"name"which creates a new object every time it’s written. Use Symbols for Hash keys and internal identifiers.- Only
nilandfalseare falsy —0,"", and[]are truthy in Ruby, unlike JavaScript and Python.nilrepresents the absence of a value — not0or"". Use safe navigation&.to preventNoMethodErrorwhen a value might benil.- Ranges have two forms —
..is inclusive (includes the end point),...is exclusive (excludes it).cover?is faster thaninclude?for non-integer ranges.- “Hard” vs “soft” conversion —
Integer()raises an error on invalid input (good for validation), whileto_isilently returns 0 (dangerous for user input).- Duck typing beats type checks — use
respond_to?(:method)instead ofis_a?(Class)to keep code flexible and generic.