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:

1. Return 0 if there are no prices
2. Sort the prices from highest to lowest
3. Multiply each price by its rank
4. 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

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?”

Back