In Progress
Unit 1, Lesson 21
In Progress

Values At

Video transcript & code

Here's some code I lifted and simplified out of an export script for financial records. The input is a hash of sums, mapping keys to monetary values. If either the :sub_gross or :sub_fee keys have a nonzero value, then a summary line should be printed.

sums = { sub_gross: 0.00, sub_fee: 1.23, pay_gross: 4.56, pay_fee: 3.21 }

if sums[:sub_gross] > 0 || sums[:sub_fee] > 0
  printf "Gross: $%0.2f; Fees: $%0.2f\n", sums[:sub_gross], sums[:sub_fee]
end
# >> Gross: $0.00; Fees: $1.23

There is nothing terrible about this code. But the test for whether a line should be printed is a bit repetitive. We can already see that if we add a new sum key to test for, the statement would start to get unwieldy. Let's see if we can find another way of stating this predicate.

We can start by eliminating the duplication of doing two separate hash subscript operations. Instead, let's just extract the sum values from each key we care about. To do this we'll use the Hash#values_at method. Given a list of keys, it will return an array of the values found under those keys.

sums = { sub_gross: 0.00, sub_fee: 1.23 }

sums.values_at(:sub_gross, :sub_fee)
# => [0.0, 1.23]

Now that we have extracted the values, we need to determine if any of them are greater than zero. A first attempt might involve a boolean reduction, where we check if each value is greater than zero and || the result with the preceding value.

sums = { sub_gross: 0.00, sub_fee: 1.23 }

sums.values_at(:sub_gross, :sub_fee).reduce(false){|a, v| a || v > 0}
# => true

But this is neither short nor particularly intention-revealing. Instead, let's use a method more suited to this specific case: we'll use the Enumerable#any? predicate with a block, testing if any of the values is greater than zero.

sums = { sub_gross: 0.00, sub_fee: 1.23 }

sums.values_at(:sub_gross, :sub_fee).any?{|v| v > 0}
# => true

This is a big improvement. But we could take things one step further if we wanted. Instead of a block, we could use Ruby's Symbol#to_proc trick to pass in the :nonzero? method as replacement for the block. This method is supported by all numeric objects, and does exactly what it suggests: it returns truthy if the value isn't zero.

sums = { sub_gross: 0.00, sub_fee: 1.23 }

sums.values_at(:sub_gross, :sub_fee).any?(&:nonzero?)
# => true

Let's replace the old test with our new expression. Then let's test to make sure it works, by first running the code and seeing that a line is printed, then altering all the relevant values to zero and seeing that when we run the code again there is no output.

sums = { sub_gross: 0.00, sub_fee: 1.23 }

if sums.values_at(:sub_gross, :sub_fee).any?(&:nonzero?)
  printf "Gross: $%0.2f; Fees: $%0.2f\n", sums[:sub_gross], sums[:sub_fee]
end
# >> Gross: $0.00; Fees: $1.23
sums = { sub_gross: 0.00, sub_fee: 0.00 }

if sums.values_at(:sub_gross, :sub_fee).any?(&:nonzero?)
  printf "Gross: $%0.2f; Fees: $%0.2f\n", sums[:sub_gross], sums[:sub_fee]
end

I like this new if-test, because I think it very clearly states the condition we're looking for: "if any of the sum values at keys :sub_gross or :sub_fee are nonzero, then proceed". And if we need to modify the list of keys to check, the edit will be simple and localized.

That's all for now. Happy hacking!

Responses