Building a Commitment Calculator with Test-Driven Development
Here is a fun little test where we create a service object that calculates the highest commitment from an array of prices. The service needed to take in an array of prices, sort them from highest to lowest, multiply each number by its rank, and then return the highest commitment. We are going code two different implementations of this and we will be using RSpec to ensure our code works as expected. I have created two tests:
A look at method one
This is the first test:
require 'rails_helper' RSpec.describe CommitmentCalculatorOne, type: :model do describe "#call" do context "No bid prices" do it "returns 0 commitment" do bid_prices =  commitment = CommitmentCalculator.call(bid_prices) expect(commitment).to eq(0) end end context "first array of prices" do it "returns highest commitment" do bid_prices = [120, 130, 110, 180] commitment = CommitmentCalculator.call(bid_prices) expect(commitment).to eq(440) end end context "second array of prices" do it "returns highest commitment" do bid_prices = [150, 450, 110, 500] commitment = CommitmentCalculator.call(bid_prices) expect(commitment).to eq(900) end end context "third array of prices" do it "returns highest commitment" do bid_prices = [150, 150, 500, 110] commitment = CommitmentCalculator.call(bid_prices) expect(commitment).to eq(500) end end end end
This spec is going to ensure that if no prices are passed, it will return zero, but if an array of prices is passed, it will return the highest commitment. We have an expected output for each array. Let’s start to look at the first implementation.
It’s natural to think about the process and try to break it down step by step. Based on what we need the algorithm to do, we need to:
- Return 0 if there are no prices
- Sort the prices from highest to lowest
- Multiply each price by its rank
- Return the highest commitment
I see the possibility for 4 methods there, but considering a service object should have one public method, we’ll need to create a few private methods as well. A service object should do one thing and do it well, which is why we are going to implement a public
#call method, and a few private methods to perform some of the other operations.
class CommitmentCalculatorOne < ApplicationService def initialize(bid_prices) @bid_prices = bid_prices @results =  end def call if bid_prices.empty? 0 else sort_prices rank_prices highest_commitment end end private attr_reader :bid_prices def sort_prices @bid_prices = bid_prices.sort.reverse end def rank_prices bid_prices.each_with_index do |number, id| result = (id + 1) * number @results.push(result) end end def highest_commitment @results.max end end
Alright! So this code is defining our Ruby class
CommitmentCalculatorOne which is our first implementation of our calculator. It has an
initialize method that takes in a argument for the prices and stores them in an instance variable. It also initializes and empty array to store our results.
call method is the entry point for our service, returning
0 if the array is empty, otherwise calling the private methods.
sort_prices methods sorts the prices in descending order. The
rank_prices goes through each price, multiplies the price by its ranking, as found via the
each_with_index method, and then pushes the result back to the
highest_commitment method just returns the highest number in the array.
Now this is cool, it works. There is nothing wrong with it, but it can be different…more Ruby-ish…
I’ll skip pasting the second spec since it is pretty much identical other than the name…so let’s just get straight to the second implementation.
A look a method numero dos
class CommitmentCalculatorTwo def self.call(prices) return 0 if prices.empty? ranked_commitments(prices).max end private_class_method def self.ranked_commitments(prices) prices.sort.reverse.map.with_index(1) do |price, ranking| price * ranking end end end
It’s amazing how elegant and succinct our program can be! Here the
call action simply returns
0 if the array is empty, and if it is not then it runs the private class method
ranked_commitments(prices) with the prices being passed down as a parameter to be sorted, mapped through, and return with the highest commitment.
I created a repo with the following code so anyone can clone/fork it for those that want to play around with it and get familiar with what’s going on. You can find it here: Commitment Calculator
I’d like to give credit to Stuart P. for sharing this implementation with me a while back! Hope it helps other look at your Ruby code and think “How else can I write this?”