What Really Happens When Ruby Code Executes?

As Ruby developers, we normally think execution is simple:

ruby app.rb

Ruby runs the file.

But what exactly is ruby?

Does the CPU execute Ruby code directly?

What is the Ruby interpreter?

Where does bytecode come into the picture?

What exactly is the runtime?

And where do C, machine code and the operating system enter the story?

For a developer who wants to understand Ruby beyond the language syntax, these are important questions.

This article follows a small Ruby program from source code all the way down to CPU execution.

Note: The discussion here focuses on CRuby/MRI- the standard Ruby implementation. Details differ in JRuby, TruffleRuby and other implementations. Ruby’s RubyVM APIs are explicitly MRI-specific. (docs.ruby-lang.org)


1. Start with a simple Ruby class

Consider this file:

# person.rb

class Person
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}"
  end
end

person = Person.new("Ruby")
puts person.greet

We execute it:

ruby person.rb

So what happens after we press Enter?


2. ruby is an executable program

When we type:

ruby person.rb

the shell does not understand Ruby syntax.

It finds the ruby executable in your PATH.

For example:

which ruby

might return:

/usr/bin/ruby

or perhaps a version-manager path such as:

/Users/me/.rbenv/shims/ruby

That executable is a compiled native program.

This is a crucial distinction:

Ruby source code is not itself executed by the operating system. The operating system starts the Ruby executable, and that program executes your Ruby program.

The flow initially looks like this:

Terminal
   โ”‚
   โ”‚ ruby person.rb
   โ–ผ
Shell
   โ”‚
   โ”‚ locate executable
   โ–ผ
Ruby executable
   โ”‚
   โ–ผ
Operating System creates process

The ruby process is now running.


3. The Ruby interpreter is inside that process

People often say:

“Ruby interprets my code.”

This is useful shorthand, but the reality is more interesting.

The Ruby executable contains the runtime machinery necessary to:

  • read Ruby source
  • parse it
  • compile it
  • create internal structures
  • execute VM instructions
  • manage Ruby objects
  • run garbage collection
  • perform method calls
  • interact with the operating system

So we can think of:

ruby executable
       โ”‚
       โ”œโ”€โ”€ parser
       โ”œโ”€โ”€ compiler
       โ”œโ”€โ”€ VM
       โ”œโ”€โ”€ garbage collector
       โ”œโ”€โ”€ object system
       โ””โ”€โ”€ runtime libraries

This collection of mechanisms is what we generally mean by the Ruby runtime.


4. Source code is first parsed

Our source:

person = Person.new("Ruby")

is not immediately converted into CPU instructions.

Ruby first needs to understand its structure.

The parser turns the source into an internal representation of the program.

Conceptually:

Ruby source
    โ”‚
    โ–ผ
Tokenizer / Parser
    โ”‚
    โ–ผ
Internal syntax representation

For example, Ruby has to understand:

Person.new("Ruby")

as roughly:

receiver: Person
method:    new
argument:  "Ruby"

The exact internal representation is an implementation detail, but the important point is:

Ruby must understand the program before it can execute it.


5. Ruby then compiles the code into VM instructions

This is the part many Ruby developers don’t realize.

CRuby does not normally execute the original Ruby source line-by-line.

The code is compiled into instructions for Ruby’s virtual machine.

These are commonly referred to as YARV instructions or Ruby bytecode.

Ruby exposes this machinery through:

RubyVM::InstructionSequence

For example:

puts RubyVM::InstructionSequence.compile(
  'puts "Hello"'
).disasm
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,12)>
0000 putself                                                          (   1)[Li]
0001 putchilledstring                       "Hello"
0003 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0005 leave
=> nil

You will see VM instructions rather than Ruby source.

The exact output changes between Ruby versions because the instruction set and compiler details are implementation-specific. Ruby documents InstructionSequence specifically as a way to inspect the VM’s compiled instructions.

So our pipeline becomes:

person.rb
   โ”‚
   โ–ผ
Parser
   โ”‚
   โ–ผ
Ruby internal representation
   โ”‚
   โ–ผ
Compiler
   โ”‚
   โ–ผ
YARV bytecode / InstructionSequence

6. What is bytecode?

Bytecode is an intermediate instruction format designed for a virtual machine.

It is not CPU machine code.

Think of this distinction:

Ruby source
    โ†“
Ruby VM bytecode
    โ†“
CPU machine code

Bytecode might conceptually contain operations such as:

putself
putobject
send
setlocal
getinstancevariable
leave

These aren’t x86 instructions.

They are instructions understood by the Ruby VM.

Ruby’s documentation exposes the compiled instruction sequence and its bytecode specifically for inspecting how YARV works. (docs.ruby-lang.org)


7. Enter the virtual machine

Now we have something like:

Ruby source
     โ†“
Compiler
     โ†“
YARV bytecode
     โ†“
Ruby VM

The VM executes those instructions.

You can think of it as a machine built inside the Ruby process:

             Ruby Process
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                                      โ”‚
โ”‚   Ruby VM                            โ”‚
โ”‚                                      โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚ YARV instructions             โ”‚   โ”‚
โ”‚   โ”‚                              โ”‚   โ”‚
โ”‚   โ”‚ putobject                    โ”‚   โ”‚
โ”‚   โ”‚ send                         โ”‚   โ”‚
โ”‚   โ”‚ getinstancevariable          โ”‚   โ”‚
โ”‚   โ”‚ leave                        โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                  โ”‚                   โ”‚
โ”‚                  โ–ผ                   โ”‚
โ”‚             VM execution             โ”‚
โ”‚                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

CRuby’s interpreter loop and instruction definitions are implemented in the Ruby source tree; the Ruby documentation points to insns.def and vm_exec.c as core pieces of this machinery. (docs.ruby-lang.org)

https://github.com/ruby/ruby/blob/master/vm_exec.c


8. But the VM itself is native code

Here is the important connection to C.

The Ruby VM isn’t written in Ruby.

CRuby itself is implemented primarily in C, with some components implemented in other languages.

So conceptually:

Your Ruby code
      โ†“
Ruby bytecode
      โ†“
CRuby VM
      โ†“
C code
      โ†“
Machine instructions
      โ†“
CPU

This is where learning C becomes incredibly useful for a Ruby developer.

Ruby is high-level.

The Ruby runtime is much closer to the machine.


9. What happens with our Person class?

Take:

class Person
  def greet
    "Hello, #{@name}"
  end
end

Ruby compiles the class and its methods into VM instruction sequences.

There isn’t simply one giant sequence representing the entire application.

Different constructs can have different instruction sequences.

Ruby’s InstructionSequence#type can identify sequences such as:

:class
:method
:block
:rescue
:ensure
:top

among others. (docs.ruby-lang.org)

Conceptually:

Person class
     โ”‚
     โ”œโ”€โ”€ class instruction sequence
     โ”‚
     โ”œโ”€โ”€ initialize method sequence
     โ”‚
     โ””โ”€โ”€ greet method sequence

When:

person.greet

executes, the VM needs to resolve the method call and execute the corresponding instruction sequence.


10. Method calls become VM work

This Ruby:

person.greet

looks tiny.

Internally, Ruby has to determine:

1. What object is `person`?
2. What class does it belong to?
3. Which method is `greet`?
4. Is the method overridden?
5. What arguments are involved?
6. What execution frame should be created?
7. Which instructions should run?

Conceptually:

person.greet
     โ”‚
     โ–ผ
VM method dispatch
     โ”‚
     โ–ผ
Find `greet`
     โ”‚
     โ–ผ
Create/enter execution frame
     โ”‚
     โ–ผ
Execute method instructions

The exact internals are sophisticated, including method caches and object-shape optimizations, but the important thing is that the VM – not your operating system- understands the Ruby method call.


11. Where does the operating system come in?

Eventually, everything has to reach the real machine.

The operating system created the Ruby process.

It provides things such as:

virtual memory
threads
file descriptors
sockets
timers
process scheduling
system calls

When Ruby needs to write:

puts "Hello"

the operation eventually crosses from Ruby runtime code into OS facilities for output.

Conceptually:

puts
 โ†“
Ruby implementation
 โ†“
C runtime / OS interface
 โ†“
system call
 โ†“
Operating System
 โ†“
terminal / file / pipe

The exact path can vary by platform and implementation, but this is the important architectural boundary.


12. Where does the CPU actually execute instructions?

Here is the complete picture:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚       Ruby Source            โ”‚
โ”‚                              โ”‚
โ”‚  person.greet                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Parser / Compiler             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ YARV Bytecode                โ”‚
โ”‚ Ruby VM instructions         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ CRuby VM                     โ”‚
โ”‚ Native runtime implementationโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Native Machine Instructions  โ”‚
โ”‚ x86-64 / ARM64 / etc.        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ CPU                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

That is the mental model I want to keep as a Ruby developer.


13. And then there is JIT

The previous diagram describes the interpreter path well, but modern Ruby can go further.

CRuby includes YJIT, a Just-In-Time compiler.

Instead of always executing VM bytecode through the interpreter, frequently executed code can be compiled into native machine code.

Conceptually:

             Ruby source
                  โ†“
             VM bytecode
                  โ†“
          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ”‚                โ”‚
          โ–ผ                โ–ผ
     Interpreter         YJIT
          โ”‚                โ”‚
          โ–ผ                โ–ผ
      VM execution     Native code
          โ”‚                โ”‚
          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ–ผ
                 CPU

YJIT became production-ready in Ruby 3.2, and Ruby’s documentation describes the interpreter and YJIT as different execution paths around the VM. (Ruby)

This is an important distinction:

Ruby bytecode is not necessarily the final form of execution.

Depending on how Ruby is running and whether JIT is enabled, execution can involve interpreted VM instructions, JIT-generated native code, or transitions between them.


14. Try it yourself

Check your Ruby implementation:

ruby -v

Check where the executable comes from:

which ruby

Inspect VM instructions:

ruby -e 'p RubyVM::InstructionSequence.compile("1 + 2").disasm'
"== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,5)>
0000 putobject_INT2FIX_1_ ( 1)[Li]
0001 putobject 2
0003 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0005 leave\n"

Try a method:

ruby -e '
class Person
  def greet
    "hello"
  end
end

puts RubyVM::InstructionSequence.compile(
  "Person.new.greet"
).disasm
'

You will see that Ruby source code has already been transformed into a lower-level instruction sequence before execution.

The exact instructions will depend on your Ruby version, so don’t treat a particular disassembly listing as universal. Ruby explicitly warns that instruction sequences are version-dependent. (docs.ruby-lang.org)


15. The complete mental model

As a senior Ruby developer, I find this model much more useful than simply saying “Ruby is interpreted.”

                   Ruby Program
                        โ”‚
                        โ–ผ
                 Ruby Executable
                        โ”‚
                        โ–ผ
                    Parser
                        โ”‚
                        โ–ผ
                    Compiler
                        โ”‚
                        โ–ผ
               YARV Bytecode
                        โ”‚
                        โ–ผ
             โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
             โ”‚     CRuby VM      โ”‚
             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ”‚
             โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
             โ”‚                 โ”‚
             โ–ผ                 โ–ผ
        Interpreter          YJIT
             โ”‚                 โ”‚
             โ–ผ                 โ–ผ
       Native runtime     Native machine code
             โ”‚                 โ”‚
             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ–ผ
                  CPU executes
                      โ”‚
                      โ–ผ
               Memory / OS / I/O

So when I run:

ruby person.rb

the CPU isn’t magically executing Ruby syntax.

The operating system starts a native Ruby process.

That process parses my Ruby source, compiles it into VM instructions, and the CRuby runtime executes those instructions – potentially compiling hot code to native machine code through JIT.

And that brings us right back to why learning C is so valuable.

When you understand C, pointers, memory, functions, stacks, machine instructions and system calls, the Ruby runtime stops looking like a black box.

It becomes another program.

A very sophisticated program – but still a program running on a machine.

And that is exactly where I want to go next: inside the Ruby object model itself – VALUE, RBasic, object headers, heap allocation and how a simple Person.new becomes a real object in memory.

The natural next article is “What does Person.new actually create inside CRuby?” – connecting the Ruby object model to C structs, VALUE, object headers, heap slots and garbage collection.

Happy Rubying! ~

๐Ÿƒโ€โ™‚๏ธ Solving LeetCode Problems the TDDย Way (Test-First Ruby): The Two Sum Problem

Welcome to my new series where I combine the power of Ruby with the discipline of Test-Driven Development (TDD) to tackle popular algorithm problems from LeetCode! ๐Ÿง‘โ€๐Ÿ’ป๐Ÿ’Ž Whether you’re a Ruby enthusiast looking to sharpen your problem-solving skills, or a developer curious about how TDD can transform the way you approach coding challenges, youโ€™re in the right place. In each episode, I’ll walk through a classic algorithm problem, show how TDD guides my thinking, and share insights I gain along the way. Letโ€™s dive in and discover how writing tests first can make us better, more thoughtful programmers – one problem at a time! ๐Ÿš€

๐ŸŽฏ Why I chose this approach

When I decided to level up my algorithmic thinking, I could have simply jumped into solving problems and checking solutions afterward. But I chose a different path – Test-Driven Development with Ruby – and here’s why this combination is pure magic โœจ. Learning algorithms through TDD forces me to think before I code, breaking down complex problems into small, testable behaviors. Instead of rushing to implement a solution, I first articulate what the function should do in various scenarios through tests.

This approach naturally leads me to discover edge cases I would have completely missed otherwise – like handling empty arrays, negative numbers, or boundary conditions that only surface when you’re forced to think about what could go wrong. Ruby’s expressive syntax makes writing these tests feel almost conversational, while the red-green-refactor cycle ensures I’m not just solving the problem, but solving it elegantly. Every failing test becomes a mini-puzzle to solve, every passing test builds confidence, and every refactor teaches me something new about both the problem domain and Ruby itself. It’s not just about getting the right answer – it’s about building a robust mental model of the problem while writing maintainable, well-tested code. ๐Ÿš€

๐ŸŽฒ Episode 1: The Two Sum Problem

#####################################
#   Problem 1: The Two Sum Problem
#####################################

# Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

# You may assume that each input would have exactly one solution, and you may not use the same element twice.

# You can return the answer in any order.
# Example 1:

# Input: nums = [2,7,11,15], target = 9
# Output: [0,1]
# Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].
# Example 2:

# Input: nums = [3,2,4], target = 6
# Output: [1,2]
# Example 3:

# Input: nums = [3,3], target = 6
# Output: [0,1]

# Constraints:
# Only one valid answer exists.

# We are not considering following concepts for now:
# 2 <= nums.length <= 104
# -109 <= nums[i] <= 109
# -109 <= target <= 109

# Follow-up: Can you come up with an algorithm that is less than O(n2) time complexity?

๐Ÿ”ง Setting up the TDD environment

Create a test file first and add the first test case.

mkdir two_sum
touch test_two_sum.rb
touch two_sum.rb
# frozen_string_literal: true

require 'minitest/autorun'
require_relative 'two_sum'

###############################
# This is the test case for finding the index of two numbers in an array
# such that adding both numbers should be equal to the target number provided
#
#  Ex:
#    two_sum(num, target)
#    num: [23, 4, 8, 92], tatget: 12
#    output: [1, 2] => index of the two numbers whose sum is equal to target
##############################
class TestTwoSum < Minitest::Test
  def setup
    ####
  end

  def test_array_is_an_empty_array
    assert_equal 'Provide an array with length 2 or more', two_sum([], 9)
  end
end

Create the problem file: two_sum.rb with empty method first.

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
end

โŒ Red: Writing the failing test

Run the test:

ruby test_two_sum.rb

Run options: --seed 58910
# Running:
F
Finished in 0.008429s, 118.6380 runs/s, 118.6380 assertions/s.

  1) Failure:
TestTwoSum#test_array_is_an_empty_array [test_two_sum.rb:21]:
--- expected
+++ actual
@@ -1 +1 @@
-"Provide an array with length 2 or more"
+nil

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

โœ… Green: Making it pass

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  'Provide an array with length 2 or more' if nums.empty?
end

โ™ป๏ธ Refactor: Optimizing the solution

โŒ
# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more' if nums.empty?

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      if selected_index != index
        sum = selected_num[selected_index] + num[index]
        return [selected_index, index] if sum == target
      end
    end
  end
end

โŒ
# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more' if nums.empty?

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      next if selected_index == index

      sum = selected_num[selected_index] + num[index]
      return [selected_index, index] if sum == target
    end
  end
end

โœ… 
# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more' if nums.empty?

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      next if index <= selected_index

      return [selected_index, index] if selected_num + num == target
    end
  end
end

Final

# frozen_string_literal: true

require 'minitest/autorun'
require_relative 'two_sum'

###############################
# This is the test case for finding the index of two numbers in an array
# such that adding both numbers should be equal to the target number provided
#
#  Ex:
#    two_sum(num, target)
#    num: [23, 4, 8, 92], tatget: 12
#    output: [1, 2] => index of the two numbers whose sum is equal to target
##############################
class TestTwoSum < Minitest::Test
  def setup
    ####
  end

  def test_array_is_an_empty_array
    assert_equal 'Provide an array with length 2 or more elements', two_sum([], 9)
  end

  def test_array_with_length_one
    assert_equal 'Provide an array with length 2 or more elements', two_sum([9], 9)
  end

  def test_array_with_length_two
    assert_equal [0, 1], two_sum([9, 3], 12)
  end

  def test_array_with_length_three
    assert_equal [1, 2], two_sum([9, 3, 4], 7)
  end

  def test_array_with_length_four
    assert_equal [1, 3], two_sum([9, 3, 4, 8], 11)
  end

  def test_array_with_length_ten
    assert_equal [7, 8], two_sum([9, 3, 9, 8, 23, 20, 19, 5, 30, 14], 35)
  end
end

# Solution 1 โœ… 

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more elements' if nums.length < 2

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      already_added = index <= selected_index
      next if already_added

      return [selected_index, index] if selected_num + num == target
    end
  end
end

Let us analyze the time complexity of Solution 1 โœ… algorithm:
Our current algorithm is not less than O(n^2) time complexity. In fact, it is exactly O(n^2). This means for an array of length n, you are potentially checking about n(nโˆ’1)/2 pairs, which is O(n^2).

๐Ÿ” Why?
  • You have two nested loops:
  • The outer loop iterates over each element (nums.each_with_index)
  • The inner loop iterates over each element after the current one (nums.each_with_index)
  • For each pair, you check if their sum equals the target.
โ™ป๏ธ Refactor: Try to find a solution below n(^2) time complexity
# Solution 2 โœ… 

#####################################
# Solution 2
# TwoSum.new([2,7,11,15], 9).indices
#####################################
class TwoSum
  def initialize(nums, target)
    @numbers_array = nums
    @target = target
  end

  # @return [index_1, index_2]
  def indices
    return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

    @numbers_array.each_with_index do |num1, index1|
      next if num1 > @target # number already greater than target

      remaining_array = @numbers_array[index1..(@numbers_array.length - 1)]
      num2 = find_number(@target - num1, remaining_array)

      return [index1, @numbers_array.index(num2)] if num2
    end
  end

  private

  def find_number(number, array)
    array.each do |num|
      return num if num == number
    end
    nil
  end
end

Let us analyze the time complexity of Solution 2 โœ… algorithm:

  1. In the indices method:
  • We have an outer loop that iterates through @numbers_array (O(n))
  • For each iteration:
    => Creating a new array slice remaining_array (O(n) operation)
    => Calling find_number which is O(n) as it iterates through the remaining array
    => Using @numbers_array.index(num2) which is another O(n) operation

So the total complexity is:

  • O(n) for the outer loop
  • For each iteration:
  • O(n) for array slicing
  • O(n) for find_number
  • O(n) for index lookup

This gives us:

O(n * (n + n + n)) = O(n * 3n) = O(3nยฒ) = O(nยฒ)

The main bottlenecks are:

  1. Creating a new array slice in each iteration
  2. Using index method to find the second number’s position
  3. Linear search in find_number

Solution 3 โœ…

To make this truly O(n), we should:

# Use a hash map to store numbers and their indices

# Solution 3 โœ…  - Use Hash Map

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

class TwoSum
  def initialize(nums, target)
    @numbers_array = nums
    @target = target
  end

  # @return [index_1, index_2]
  def indices
    return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

    hash = {}

    @numbers_array.each_with_index do |num, index|
      complement = @target - num

      # store first number to hash
      if index == 0
        hash[num] = index
      else
        # if not first number check store has
        return [hash[complement], index] if hash.key?(complement)

        # if not found store the num
        hash[num] = index
      end
    end
  end
end

Let us analyze the complexity of the current code:

def indices
  return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

  hash = {}

  @numbers_array.each_with_index do |num, index|
    complement = @target - num

    # store first number to hash
    if index == 0
      hash[num] = index 
    else
      # if not first number check store has 
      if hash.key?(complement)
        return [hash[complement], index]
      else
        # if not found store the num
        hash[num] = index
      end
    end
  end
end

The complexity is O(n) because:

  1. Single pass through the array: O(n)
  2. For each iteration:
  • Hash lookup (hash.key?(complement)): O(1)
  • Hash insertion (hash[num] = index): O(1)
  • Basic arithmetic (@target - num): O(1)

Total complexity = O(n) * O(1) = O(n)

The code is still efficient because:

  1. We only traverse the array once
  2. All operations inside the loop are constant time
  3. We don’t have any nested loops or array slicing
  4. Hash operations (lookup and insertion) are O(1)

โ™ป๏ธ Refactor Solution 3 โœ…

This is still O(n):

  1. Use a hash map to store numbers and their indices
  2. Avoid array slicing
  3. Avoid using index method
  4. Make a single pass through the array
# โ™ป๏ธ Refactor Solution 3 โœ…  - Use Hash Map

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

class TwoSum
  def initialize(nums, target)
    @numbers_array = nums
    @target = target
  end

  # @return [index_1, index_2]
  def indices
    return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

    hash = {}

    @numbers_array.each_with_index do |num, index|
      complement = @target - num

      return [hash[complement], index] if hash.key?(complement)

      hash[num] = index
    end
  end
end

This refactored solution has O(n) time complexity because:

  1. Single pass through the array: O(n)
  2. Hash operations (insertion and lookup) are O(1)
  3. No array slicing or linear searches
  4. Total complexity is O(n)

The algorithm works by:

  1. For each number, calculate its complement (target – current_number)
  2. Check if the complement exists in our hash
  3. If found, return both indices
  4. If not found, store the current number and its index in the hash

The key differences:

  • Instead of searching for complements in the remaining array, we store numbers we’ve seen in a hash
  • When we see a new number, we check if its complement exists in our hash
  • If found, we return both indices
  • If not found, we store the current number and its index

Detailed explanation of refactored solution 3

I’ll explain how the hash map solution works step by step using the example:

# Input Sample
TwoSum.new([2,7,11,15], 9)
  1. Initial State:
   hash = {}  # Empty hash map
   target = 9
  1. First Iteration (num = 2, index = 0):
   complement = 9 - 2 = 7
   hash = {}  # Empty, so complement 7 not found
   hash[2] = 0  # Store 2 with its index 0
  1. Second Iteration (num = 7, index = 1):
   complement = 9 - 7 = 2
   hash = {2 => 0}  # Found complement 2 in hash!
   return [hash[2], 1]  # Returns [0, 1]

Let’s break down what happens in each iteration:

@numbers_array.each_with_index do |num, index|
  complement = @target - num  # Calculate what number we need

  if hash.key?(complement)   # Check if we've seen the number we need
    return [hash[complement], index]  # If found, return both indices
  end

  hash[num] = index  # If not found, store current number and its index
end

Key points:

  1. We only need to store each number once in the hash
  2. The hash stores numbers as keys and their indices as values
  3. We check for complements before storing the current number
  4. We only need one pass through the array

This is efficient because:

  • Hash lookups are O(1)
  • We only traverse the array once
  • We don’t need to search through the array multiple times
  • We don’t need to create array slices

Why the index order has complement index first?

The order of indices in the return statement [hash[complement], index] is important because:

  1. hash[complement] gives us the index of the first number we found (the complement)
  2. index gives us the current position (the second number)

We return them in this order because:

  • The complement was stored in the hash earlier in the array
  • The current number is found later in the array
  • This maintains the order of appearance in the original array

For example, with [2,7,11,15] and target 9:

  1. When we see 7 at index 1:
  • We look for complement 2 (9-7)
  • 2 was stored at index 0
  • So we return [0, 1] (indices of [2,7])

If we returned [index, hash[complement]], we would get [1, 0] instead, which would be the reverse order. While the problem allows returning the answer in any order, returning them in the order they appear in the array is more intuitive and matches the example outputs in the problem description.

โœ… Solution 4

# Solution 4 โœ…  - Use Hash Map
# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}
def two_sum(nums, target)
  return 'Provide an array with length 2 or more elements' if nums.length < 2

  # number index store, use hash map, store first number in store
  store = { nums[0] => 0}
  
  # check the pair from second element
  nums.each_with_index do |num, index|
    next if index == 0 # already stored first
    pair = target - num

    return [store[pair], index] if store[pair]

    store[num] = index
  end
end

Check my LeetCode progress:

The Problem: https://leetcode.com/problems/two-sum/description/

Solution: https://leetcode.com/problems/two-sum/submissions/1662877573/

๐Ÿง  Lessons learned

  1. Solution 1 โœ… – We found our first solution which is working fine. But has o(n^2)
  2. Solution 2 โœ… – We refactored and found our second solution which is working fine. But also has o(n^2)
  3. Solution 3 โœ… – We refactored to hash_map which is working fine and has time complexity o(n)! ๐Ÿ’ฅ

Happy Algo Coding! ๐Ÿš€

Regular Expressions ๐ŸŽฐ in Ruby: A Step-by-Step Guide

Regular expressions (regex) are powerful tools for pattern matching and text manipulation. In Ruby, they’re implemented through the Regexp class. Let’s start with the basics and gradually build up to more complex patterns.

1. Basic Matching

Literal Characters

The simplest regex matches exact text:

"hello".match(/hello/)  #=> #<MatchData "hello">

Special Characters

Some characters have special meaning and need escaping with \:

# Matching a literal dot
"file.txt".match(/file\.txt/)  #=> #<MatchData "file.txt">

2. Character Classes

Simple Character Sets

Match any one character from a set:

# Match either 'a', 'b', or 'c'
"bat".match(/[abc]/)  #=> #<MatchData "b">

Ranges

Match any character in a range:

# Match any lowercase letter
"hello".match(/[a-z]/)  #=> #<MatchData "h">

# Match any digit
"Room 101".match(/[0-9]/)  #=> #<MatchData "1">

Negated Character Sets

Match any character NOT in the set:

# Match any character that's not a vowel
"hello".match(/[^aeiou]/)  #=> #<MatchData "h">

3. Shorthand Character Classes

Ruby provides shortcuts for common character classes:

\d  # Any digit (0-9)
\D  # Any non-digit
\w  # Word character (letter, digit, underscore)
\W  # Non-word character
\s  # Whitespace (space, tab, newline)
\S  # Non-whitespace

Examples:

"Price: $100".match(/\d+/)  #=> #<MatchData "100">
"hello_world".match(/\w+/)  #=> #<MatchData "hello_world">

4. Quantifiers

Control how many times a pattern should match:

?     # 0 or 1 times
*     # 0 or more times
+     # 1 or more times
{n}   # Exactly n times
{n,}  # n or more times
{n,m} # Between n and m times

Examples:

# Match between 3 and 5 digits
"12345".match(/\d{3,5}/)  #=> #<MatchData "12345">

# Match 'color' or 'colour'
"colour".match(/colou?r/)  #=> #<MatchData "colour">

5. Anchors

Match positions rather than characters:

^  # Start of line
$  # End of line
\A # Start of string
\Z # End of string
\b # Word boundary

Examples:

# Check if string starts with 'Hello'
"Hello world".match(/^Hello/)  #=> #<MatchData "Hello">

# Check if string ends with 'world'
"Hello world".match(/world$/)  #=> #<MatchData "world">

6. Grouping and Capturing

Parentheses create groups and capture matches:

# Capture date components
match = "2023-05-18".match(/(\d{4})-(\d{2})-(\d{2})/)
match[1]  #=> "2023" (year)
match[2]  #=> "05"   (month)
match[3]  #=> "18"   (day)

7. Alternation

The pipe | acts like an OR operator:

# Match 'cat' or 'dog'
"dog".match(/cat|dog/)  #=> #<MatchData "dog">

8. Modifiers

Change how the regex works:

i  # Case insensitive
m  # Multiline mode (dot matches newline)
x  # Ignore whitespace (for readability)

Examples:

# Case insensitive match
"HELLO".match(/hello/i)  #=> #<MatchData "HELLO">

9. Lookarounds

Assert that a pattern is or isn’t ahead/behind:

(?=pattern)  # Positive lookahead
(?!pattern)  # Negative lookahead
(?<=pattern) # Positive lookbehind
(?<!pattern) # Negative lookbehind

Example:

# Match 'q' not followed by 'u'
"qat".match(/q(?!u)/)  #=> #<MatchData "q">

10. Ruby-Specific Features

Named Captures

match = "2023-05-18".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
match[:year]  #=> "2023"

%r Notation

Alternative syntax for regex literals:

%r{http://example\.com}  # Same as /http:\/\/example\.com/

String Methods Using Regex

Ruby strings have many regex methods:

"hello".gsub(/[aeiou]/, '*')  #=> "h*ll*"
"a,b,c".split(/,/)            #=> ["a", "b", "c"]
"hello".scan(/./)             #=> ["h", "e", "l", "l", "o"]

Practical Examples

  1. Email Validation:
email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
"test@example.com".match?(email_regex)  #=> true
  1. Extracting Phone Numbers:
text = "Call me at 555-1234 or (555) 987-6543"
text.scan(/(\(\d{3}\) \d{3}-\d{4}|\d{3}-\d{4})/)  #=> ["555-1234", "(555) 987-6543"]
  1. HTML Tag Extraction:
html = "<p>Hello</p><div>World</div>"
html.scan(/<(\w+)>(.*?)<\/\1>/)  #=> [["p", "Hello"], ["div", "World"]]

Tips for Effective Regex in Ruby

  1. Use Regexp.escape when matching literal strings:

Returns a new string that escapes any characters that have special meaning in a regular expression:

s = Regexp.escape('\*?{}.')      # => "\\\\\\*\\?\\{\\}\\."
   Regexp.escape("file.txt")  #=> "file\\.txt"
  1. For complex patterns, use the x modifier for readability:
   regex = /
     \A             # Start of string
     [\w+\-.]+      # Local part
     @              # @ symbol
     [a-z\d\-]+     # Domain
     (\.[a-z]+)*    # Subdomains
     \.[a-z]+\z     # TLD
   /xi
  1. Consider using Rubular (https://rubular.com/) for testing your Ruby regular expressions.

Regular expressions can become complex, but starting with these fundamentals will give you a solid foundation for text processing in Ruby.

Happy Ruby Coding! ๐Ÿš€