Skip to main content
Thoughts from David Cornelius

Category

ruby diamondLast December, I discovered Advent of Code, daily programming puzzles during the first part of month, and wrote my solutions in Delphi. When this year's challenge started, I decided to use a different programming language as an excuse to learn something new. I've had the book "Seven Languages in Seven Weeks" for a while and never even got through the first chapter (it was always something I should read someday). So I finally picked it up and told myself I would use the very first language in that book, no matter what it was (I honestly didn't remember from when I had glanced through it several months earlier). It turns out the first language it discusses is Ruby! Thus started my adventure in learning!

The book is not very big and it's not meant to be an exhaustive tutorial, so I signed up for a Udemy course and quickly went through as much I figured I needed to get started and dove into the AoC challenges. Presented here is my solution to Day 1 of AoC 2025 in two programs, one in Ruby and the other in Delphi, structured as nearly the same as possible so you can see the differences.

The Simple Challenge: Rotate a Dial

Advent of Code 2025's Day 1 challenge involves simulating a dial that rotates left and right, tracking when it lands on or passes through zero. This seemingly simple problem reveals fundamental differences in how Delphi and Ruby approach software design.

Class Structure: Explicit vs. Implicit

Delphi's Interface/Implementation Separation

type
  TDialPassword = class
  private
    const
      DIAL_MAX = 99;
    var
      FCurrPos: Longint;
      FZeroCount: Longint;
      FZeroPassive: Longint;
      FLogit: Boolean;
  public
    constructor Create;
    procedure Rotate(const Amount: Integer);
    property Logit: Boolean read FLogit write FLogit default True;
    property ZeroCount: Longint read FZeroCount write FZeroCount;
    property ZeroPassive: Longint read FZeroPassive write FZeroPassive;
  end;

{ TDialPassword }

constructor TDialPassword.Create;
begin
  FZeroCount := 0;
  FZeroPassive := 0;
  FCurrPos := 50;
  FLogit := True;
end;

Delphi enforces a clear contract: declare everything up front, implement it separately. This separation aids readability in large projects and enables circular unit references, but requires more boilerplate code.

Ruby's Unified Definition

class DialPassword

  attr_accessor :zero_count, :zero_passive, :logit
    
  def initialize
    @zero_count = 0
    @zero_passive = 0
    @curr_pos = 50
    @DIAL_MAX = 99
    @logit = true
  end

  ...

end

Ruby combines declaration and implementation. The attr_accessor line creates both getters and setters in one statement; it's roughly the same as Delphi's public property declarations. Instance variables (prefixed with @) spring into existence when first assigned. This brevity can be refreshing for small programs but may obscure the class interface in larger codebases.

Type Safety: Compile-time vs. Runtime

Delphi's Type Declarations

var
  FCurrPos: Longint;
  FZeroCount: Longint;
  FZeroPassive: Longint;
  FLogit: Boolean;

procedure Rotate(const Amount: Integer);

Every variable has a declared type in Delphi (except for inlined variables with type inference, which still happen at compile-time). The compiler catches type mismatches before runtime. Using const Amount: Integer prevents Amount from being anything other than an Integer. This is a nice safety net if you inadvertenly misspell or pass in a wrong variable but annoying if you're accustomed to languages that automatically convert data to a common type.

Ruby's Dynamic Typing

def rotate(amount)
  @curr_pos += amount
  # ...
end

Ruby infers types at runtime. The amount parameter could be an integer, float, or even a string (which would cause a runtime error). This flexibility enables rapid prototyping and simplifies the code you type but shifts error detection from compile-time to runtime which can be harder to track down.

The Core Algorithm: Similar Logic, Different Expression

Both implementations share the same rotation logic, but express it quite differently:

Delphi's Rotate Method

procedure TDialPassword.Rotate(const Amount: Integer);
begin
  // Special case for left rotation from 0
  if (Amount < 0) and (FCurrPos = 0) then
    Inc(FCurrPos, DIAL_MAX + 1); 

  Inc(FCurrPos, Amount);

  while (FCurrPos < 0) or (FCurrPos > DIAL_MAX) do begin
    if FCurrPos < 0 then begin
      Inc(FZeroPassive);
      Inc(FCurrPos, DIAL_MAX + 1);
    end else begin
      if FCurrPos > (DIAL_MAX + 1) then
        Inc(FZeroPassive);
      Dec(FCurrPos, DIAL_MAX + 1);
    end;
  end;

  if FCurrPos = 0 then
    Inc(FZeroCount);
end;

Ruby's rotate Method

def rotate(amount)
  # Special case for left rotation from 0
  @curr_pos += (@DIAL_MAX + 1) if amount < 0 && @curr_pos == 0

  @curr_pos += amount
  
  while @curr_pos < 0 || @curr_pos > @DIAL_MAX
    if @curr_pos < 0
      @zero_passive += 1
      @curr_pos += (@DIAL_MAX + 1)
    else
      @zero_passive += 1 if @curr_pos > (@DIAL_MAX + 1)
      @curr_pos -= (@DIAL_MAX + 1)
    end
  end
  
  @zero_count += 1 if @curr_pos == 0
end

Notice Ruby's statement modifiers (if at the end of lines) which I thought was interesting and sounded like an English statement in the form, "Only do this if this expression is true." Delphi's Inc and Dec procedures are more explicit about varaible modification but I like Ruby's += and -= operators better (personal preference stemming from studying C/C++ in college and having fun writing very terse code).

I/O: Files and Output

Delphi's File Reading and Array creation

var data := TFile.ReadAllLines('..\..\input01.txt');

By default, Delphi compiles programs to a sub-folder underneath the current project folder based on the platform and bitness, so to get to the same input file as the Ruby program in the same project folder, I had to reference it up a couple of levels.

ReadAllLines is a handy function in the IOUtils unit that returns a TArray<string> which is the inferred type assigned to the variable data at compile time.

Ruby's File Reading and array creation

data = File.read("input01.txt").split("\n")

Ruby chains methods naturally: read the file, split on newlines, saving the result into an array of strings.

String Processing and Control Flow

Delphi's Main Loop

dp.Logit := Length(data) < 100;

for var s in data do begin
  var amount := StrToInt(Copy(s, 2, 10));
  dp.Rotate(if s[1] = 'L' then -amount else amount);
end;

Delphi has come a long ways to support enumerated types with inferred data types for iterating in loops like this. Still, there is so much more "scaffolding" (as I like to call it) and explicit type conversion necessary that the code can look a little murky. I do like the recent addition of the If Conditional Operator which makes the call to Rotate cleaner.

Ruby's Main Loop

dp.logit = data.length < 100

data.each do |elem| 
  amount = elem[1..].to_i
  dp.rotate(elem[0] == 'L' ? -amount : amount)
end

Ruby's each method with a block is quite unique. In fact, you can add .with_index and then it provides two variables inside the iteration loop (i.e. |elem,i|) which I thought was really cool (I didn't need it in this situation but there is another program my repository that does use that technique).

String slicing with [1..] implies the length of the array as the end of the range which is more concise than Copy where you have to explicitly define the length. The to_i method converts strings to integers, returning 0 for invalid input rather than raising an exception.

Output and String Interpolation

Delphi's Formatted Output

Writeln(Format('Number of zero positions encountered: directly = %d, passively = %d; total = %d', 
  [dp.ZeroCount, dp.ZeroPassive, dp.ZeroCount + dp.ZeroPassive]));

Delphi's Writeln is the standard way to send simple text to the console. I had to use the Format function to provides embed variables in a similar manner to Ruby's default output.

Ruby's String Interpolation

puts "Number of zero positions encountered: directly = #{dp.zero_count}, passively = #{dp.zero_passive}; total = #{dp.zero_count + dp.zero_passive}"

Using puts is the common output function for Ruby and implements string interpolation with #{}; this is arguably more readable than format strings, though it can make very long strings harder to maintain because they embed the variables directly in the string whereas Delphi's Format function declares place-holders that are filled in at run-time.

Memory Management: Explicit vs. Automatic

Delphi

var dp := TDialPassword.Create;
// No explicit Free needed here, but in a larger program you'd want:
// try
//   ... use dp ...
// finally
//   dp.Free;
// end;

Ruby

dp = DialPassword.new
# Automatic garbage collection handles cleanup

Delphi requires explicit object creation and (usually) destruction. Ruby's garbage collector eliminates this concern but at the cost of less predictable performance.

Key Comparisons

  1. Verbosity vs. Brevity: The Delphi version is nearly twice as long as the identical functionality in Ruby, partly because of the need to have an interface section and Delphi's standard paradigm of declaring private variables behind the public properties. Each approach has merit: Delphi's verbosity adds clarity and maintainability; Ruby's brevity aids rapid development.

  2. Compile-Time vs. Runtime: Delphi catches errors early; Ruby provides flexibility.

  3. Explicit vs. Implicit: Delphi makes everything visible (types, memory management, property access). Ruby hides complexity behind conventions.

  4. Performance vs. Productivity: Delphi compiles to native code with predictable performance. Ruby prioritizes developer happiness and speed of development.

Conclusion

Being exposed to the Ruby programming language and environment and using very concise code to solve problems got me to thinking about how to shorten what I do in Delphi. Perhaps I'll use inline variables and conditional expressions more often, chain objects with Spring4D's collection methods, or write helper functions for slicing strings.

Solving Advent of Code challenges in both languages revealed that Delphi and Ruby aren't competitors, they serve different purposes. Delphi's structure and safety make it ideal for large, long-lived applications. The fact it compiles to native executables is necessary for large-scale deployment and intellectual property protection. Ruby's expressiveness and flexibility make it great for exploration and scripting (and it's really fun to solve AoC puzzles in Ruby!).

For fellow Delphi developers: don't learn Ruby to replace Delphi. Learn it to become a better programmer who happens to use Delphi. The mental flexibility gained from switching between static and dynamic typing, between compiled and interpreted execution, and between explicit and implicit styles will make you more effective regardless of the language you're using.

After implementing dozens of Advent of Code solutions in both languages, I can confidently say that the time invested in learning Ruby has made me a better Delphi developer. And that's the real prize: not just knowing more languages, but thinking more broadly about problems and solutions.

You can find the full source for this and other Ruby programs I wrote in my Advent of Code 2025 Github repository.

Add new comment

The content of this field is kept private and will not be shown publicly.