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: commitment_calculator_one_spec.rb
and commitment_calculator_two_spec.rb
.
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.
The call
method is the entry point for our service, returning 0
if the array is empty, otherwise calling the private methods.
The 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 @results
array.
Finally, 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?”