Refactoring JavaScript to Ruby

March 10, 2022   • refactoring

So this evening I started my third reading of Martin Fowler’s great book Refactoring: Improving the Design of Existing Code. The refactoring skills this book teaches are timeless, and this is not a book that you read once and put it away. I strongly think that you have to review them regularly and apply on your codebase. Hence, I have aimed to read this book at least once a year.

However, this time I thought I’d do something different. All the examples in this book are in JavaScript. However, I have been spoiled by Ruby’s elegance and simplicity since I started learning it last year, and wanted to practice these refactorings in Ruby, instead of JavaScript. So I decided to rewrite the JavaScript examples in Ruby and work through them.

This blog post shows my rewrite attempt of the first big example that’s in JavaScript.

JavaScript

function statement (invoice, plays) {
  let totalAmount = 0;
  let volumeCredits = 0;
  let result = `Statement for ${invoice.customer}\n`;
  const format = new Intl.NumberFormat("en-US",
                        { style: "currency", currency: "USD",
                          minimumFractionDigits: 2 }).format;
  for (let perf of invoice.performances) {
    const play = plays[perf.playID];
    let thisAmount = 0;

    switch (play.type) {
    case "tragedy":
      thisAmount = 40000;
      if (perf.audience > 30) {
        thisAmount += 1000 * (perf.audience - 30);
      }
      break;
    case "comedy":
      thisAmount = 30000;
      if (perf.audience > 20) {
        thisAmount += 10000 + 500 * (perf.audience - 20);
      }
      thisAmount += 300 * perf.audience;
      break;
    default:
        throw new Error(`unknown type: ${play.type}`);
    }

    // add volume credits
    volumeCredits += Math.max(perf.audience - 30, 0);
    // add extra credit for every ten comedy attendees
    if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);

    // print line for this order
    result += `  ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;
    totalAmount += thisAmount;
  }
  result += `Amount owed is ${format(totalAmount/100)}\n`;
  result += `You earned ${volumeCredits} credits\n`;
  return result;
}

After I rewrote it in Ruby, this is what it looks like:

Ruby

require "money"

module ProgrammingRuby
  module Refactoring
    class InvoicePrinter
      def statement(invoice, plays)
        total_amount = 0
        volume_credits = 0
        result = "Statement for #{invoice[:customer]}\n"

        invoice[:performances].each do |perf|
          play = plays.fetch(perf[:playID].to_sym)
          amount = 0

          case play[:type]
          when "tragedy"
            amount = 40000
            if perf[:audience] > 30
              amount += 1000 * (perf[:audience] - 30)
            end
          when "comedy"
            amount = 30000
            if perf[:audience] > 20
              amount += 10000 + 500 * (perf[:audience] - 20)
            end
            amount += 300 * perf[:audience]
          else
            raise StandardError.new "Unknown Type: #{play[:type]}"
          end

          # add volume credits
          volume_credits += [perf[:audience] - 30, 0].max
          # add extra credits for every ten comedy attendees
          if play[:type] == "comedy"
            volume_credits += (perf[:audience] / 5).floor
          end

          # print line for this order
          result += "   #{play[:name]}: #{Money.us_dollar(amount).format} (#{perf[:audience]} seats)\n"
          total_amount += amount
        end

        result += "Amount owed is #{Money.us_dollar(total_amount).format}\n"
        result += "You earned #{volume_credits} credits\n"
        result
      end
    end
  end
end

Refactored Version

require "money"

Money.locale_backend = nil
Money.rounding_mode = BigDecimal::ROUND_HALF_UP

module ProgrammingRuby
  module Refactoring
    class InvoicePrinter
      attr_reader :invoice, :plays

      def initialize(invoice, plays)
        @invoice = invoice
        @plays = plays
      end

      def statement
        result = "Statement for #{invoice[:customer]}\n"

        invoice[:performances].each do |perf|
          result += "   #{play_for(perf)[:name]}: #{usd(amount_for(perf))} (#{perf[:audience]} seats)\n"
        end

        result += "Amount owed is #{usd(total_amount)}\n"
        result += "You earned #{total_volume_credits} credits\n"
        result
      end

      private

      def amount_for(performance)
        play = play_for(performance)

        case play[:type]
        when "tragedy"
          result = 40000
          if performance[:audience] > 30
            result += 1000 * (performance[:audience] - 30)
          end
        when "comedy"
          result = 30000
          if performance[:audience] > 20
            result += 10000 + 500 * (performance[:audience] - 20)
          end
          result += 300 * performance[:audience]
        else
          raise StandardError.new "Unknown Type: #{play_for(perf)[:type]}"
        end

        result
      end

      def play_for(perf)
        plays.fetch(perf[:playID].to_sym)
      end

      def total_volume_credits
        invoice[:performances].sum { |perf| volume_credits_for(perf) }
      end

      def volume_credits_for(perf)
        result = [perf[:audience] - 30, 0].max

        # add extra credits for every ten comedy attendees
        play = play_for(perf)
        result += (perf[:audience] / 5).floor if play[:type] == "comedy"

        result
      end

      def total_amount
        invoice[:performances].sum { |perf| amount_for(perf) }
      end

      def usd(amount)
        Money.us_dollar(amount).format
      end
    end
  end
end