Ryan Bigg

⟵ Posts

Decorating arrays with SimpleDelegator

03 Mar 2025

Let’s say that I have a long list of transactions and that I want to apply some filtering steps to these with Ruby. I might have gathered this list from somewhere in particular, or I could generate it with some quick Ruby:

Transaction  = Data.define(:date, :amount, :status)

transactions = 100.times.map do
  Transaction.new(
    date: Date.today - rand(30),
    amount: rand(1.0..250.0).round(2),
    status: rand < 0.9 ? "Approved" : "Declined"
  )
end

These transactions are a list occurring anywhere in the last 30 days, with amounts between $1 and $250, with a status that has a 90% chance of being “Approved” and 10% chance of being “Declined”.

To filter on these I can use common methods like select:

transactions
  .select { it.amount <= 25 }
  .select { it.date == Date.parse("2025-02-26") }

That would find me any transaction with an amount less than $25, occurring on the 26th of February. Easy enough!

But we can bring this code closer to English by using SimpleDelegator to decorate our array, creating a neat DSL:

class Transactions < SimpleDelegator
  def amount_lte(amount)
    select { it.amount <= amount }
  end

  def for_date(date)
    select { it.date == Date.parse(date) }
  end

  def select(&block)
    self.class.new(super(&block))
  end
end

This class inherits from SimpleDelegator and defines methods to provide that simple DSL. Our code from before:

transactions
  .select { it.amount <= 25 }
  .select { it.date == Date.parse("2025-02-26") }

Can now instead be written as:

transactions = Transactions.new(transactions)
transactions
  .amount_lte(25)
  .for_date("2025-02-06")

This has centralized the implementation details of how we query the transactions into one simple class. Anything that needs to massage the input before we run a query on transactions now has a nice place to live. An example of this is to put Date.parse inside for_date. This could be customized further to only do that Date.parse if the object passed in is a string and not a Date already.

As a bit of “homework” here, can you update this class to add methods for finding only approved or declined transactions? Is there a chance you could make outside this Transactions class to make the syntax cleaner?

Could you also support this syntax?

transactions.for_date(date_1).or(transactions.for_date(date_2))

And now can you write that code any shorter as well?