Comparing Two Temperatures
It doesn't matter what temperature the room is — it's always room temperature.
— Steven Wright
We're at work, and we are tasked with writing two functions that compare any two temperatures:
- One that gets the minimum value
- One that gets the maximum value
We're not sure why they don't use Math.min
and Math.max
themselves, but we
do what they ask.
const minTemperature = (a, b) => Math.min(a, b)
const maxTemperature = (a, b) => Math.max(a, b)
minTemperature(100, 200) // 100
maxTemperature(100, 200) // 200
Once we share that solution, we are reminded that task is to compare
any two temperatures; for example, 100
, 200
, '50F'
, '25C'
.
Dangit... now we look silly. Okay, let's try this again to make it handle strings:
const minTemperature = (a, b) => a > b ? b : a
const maxTemperature = (a, b) => a < b ? b : a
minTemperature(100, 200) // 100
maxTemperature(100, 200) // 200
minTemperature('200F', '100F') // '100F'
maxTemperature('200F', '100F') // '200F'
Then we realize that Ramda has min
and
max
functions, so we use those and
pat ourselves on the back:
import { max, min } from 'ramda'
min(100, 200) // 100
max(100, 200) // 200
min('200F', '100F') // '100F'
max('200F', '100F') // '200F'
We go back and tell our team to use Ramda's functions, but we're wrong again, and we realize that we probably should have asked for a thorough list of inputs.
But what could we have missed?
Compare any two temperatures.
Oh, no... They might not be part of the same measurement system! This
means that '50F'
could be compared with '15C'
! Luckily, we're told that we don't
have to worry about Kelvin, Rankine, nor Réaumur; we only care about Celsius
and Fahrenheit. There will also never be any unit-less numbers, so we can
ignore that case.
This problem just took on a whole new dimension!
Good news: in the last
chapter, we wrote
our celsiusToFahrenheit
and fahrenheitToCelsius
functions, so we have those
functions we can use to do those calculations for us.
Let's try again:
import { max, min } from 'ramda'
const celsiusToFahrenheit =
compose(add(32), multiply(divide(9, 5)))
const minTemperature = (a, b) => {
const unitA = a.slice(-1) // get the last character, like C or F
const unitB = b.slice(-1)
const floatA = parseFloat(a)
const floatB = parseFloat(b)
// same temperature type
if (unitA === unitB) {
const maxT = min(floatA, floatB)
return maxT === floatA ? a : b
}
// a is Fahrenheit but b is Celsius
if (unitA === 'F') {
const floatBAsF = celsiusToFahrenheit(floatB)
const minF = min(floatA, floatBAsF)
return minF === floatA ? a : b
}
// a is Celsius but b is Fahrenheit
const floatAAsF = celsiusToFahrenheit(floatA)
const minF = min(floatAAsF, floatB)
return minF === floatB ? b : a
}
const maxTemperature = (a, b) => {
const unitA = a.slice(-1) // get the last character, like C or F
const unitB = b.slice(-1)
const floatA = parseFloat(a)
const floatB = parseFloat(b)
// same temperature type
if (unitA === unitB) {
const maxT = max(floatA, floatB)
return maxT === floatA ? a : b
}
// a is Fahrenheit but b is Celsius
if (unitA === 'F') {
const floatBAsF = celsiusToFahrenheit(floatB)
const maxF = max(floatA, floatBAsF)
return maxF === floatA ? a : b
}
// a is Celsius but b is Fahrenheit
const floatAAsF = celsiusToFahrenheit(floatA)
const maxF = max(floatAAsF, floatB)
return maxF === floatB ? b : a
}
minTemperature('200F', '100F') // '100F'
maxTemperature('200F', '100F') // '200F'
minTemperature('50F', '25C') // '50F'
maxTemperature('50F', '25C') // '25C'
(View this large min/max temperature example in the Ramda REPL)
Goodness gracious! While we could extract some common functionality into many small functions, there must be a simpler way. After all, isn't functional programming supposed to help us simplify things?
(Hint: yes!)
Ramda has minBy
and
maxBy
functions that will compare two
values after they have been transformed by some transformation function.
Here's their example from the docs:
import { maxBy } from 'ramda'
const square = n => n * n
maxBy(square, -3, 2) // -3
In this example, maxBy
will call square
with -3
and 2
, and it will then
compare each of those results. Whatever value has the largest result after
being applied to square
will be the returned value. Here, -3 * -3
is 9
,
whereas 2 * 2
is 4
, so since 9 > 4
, -3
is our result.
Let's refactor our functions:
import { maxBy, minBy } from 'ramda'
const asF = x => {
if (x.slice(-1) === 'C') {
return celsiusToFahrenheit(parseFloat(x))
}
return x
}
const minTemperature = (a, b) => minBy(asF, a, b)
const maxTemperature = (a, b) => maxBy(asF, a, b)
minTemperature('200F', '100F') // '100F'
maxTemperature('200F', '100F') // '200F'
minTemperature('50F', '25C') // '50F'
maxTemperature('50F', '25C') // '25C'
maxTemperature('50C', '25C') // '50C'
(View this v1 minBy/maxBy temperature example in the Ramda REPL)
By casting all temperatures as a single unit (I chose Fahrenheit here), we can
compare any temperatures and get back their original values! 25C
is indeed
hotter than 50F
!
But wait – there's more.
Just like we noticed in the last chapter, our minTemperature
and
maxTemperature
functions are taking in (a, b)
and are merely forwarding
those arguments on. If you recall, Ramda doesn't care when you provide
arguments, so check this out...
// before
const minTemperature = (a, b) => minBy(asF, a, b)
const maxTemperature = (a, b) => maxBy(asF, a, b)
// after
const minTemperature = minBy(asF)
const maxTemperature = maxBy(asF)
(View this v2 minBy/maxBy temperature example in the Ramda REPL)
Oh, and one more thing; here's a preview of some upcoming chapters' content
retroactively applied to making our asF
function cleaner:
// before
const asF = x => {
if (x.slice(-1) === 'C') {
return celsiusToFahrenheit(parseFloat(x))
}
return x
}
// after
// Celsius = String // "100C"
// Fahrenheit = String // "100F"
// Temperature = Celsius | Fahrenheit
// isC :: Temperature -> Bool
const isC = compose(equals('C'), slice(-1))
// stringCToF :: Celsius -> Number
const stringCToF = compose(celsiusToFahrenheit, parseFloat)
// asF :: Temperature -> Fahrenheit
const asF = when(isC, stringCToF)
And here is what all of the new functions together could look like!
// Celsius = String // "100C"
// Fahrenheit = String // "100F"
// Temperature = Celsius | Fahrenheit
// isC :: Temperature -> Bool
const isC = compose(equals('C'), slice(-1))
// stringCToF :: Celsius -> Number
const stringCToF = compose(celsiusToFahrenheit, parseFloat)
// asF :: Temperature -> Fahrenheit
const asF = when(isC, stringCToF)
// minTemperature :: Temperature -> Temperature -> Temperature
const minTemperature = minBy(asF)
// maxTemperature :: Temperature -> Temperature -> Temperature
const maxTemperature = maxBy(asF)
(View this v3 minBy/maxBy temperature example in the Ramda REPL)
Cool, right?
Fear not! We'll cover when
in the
"Unshakeable Logic" section, slice
in the
"All About ['L','i','s','t','s']" section, and the pseudo-type signatures in the
"Core Ramda Ideas" section, but you should know the pseudo-types are optional,
not exact, and aren't a substitute for tools like jsdoc.
Wrapping Up
The person who requested these functions is going to be blown away!
What started as a simple "which temperature is smaller or larger?" question turned out to be an exercise in asking good questions about requirements up front.
We also walked through implementing a naïve solution and refactored it to a more elegant one by leveraging Ramda and functional programming.