Category
Last 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
-
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
interfacesection 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. -
Compile-Time vs. Runtime: Delphi catches errors early; Ruby provides flexibility.
-
Explicit vs. Implicit: Delphi makes everything visible (types, memory management, property access). Ruby hides complexity behind conventions.
-
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