Kata: Nearest Color
Few weeks ago, with the Software Crafters Lyon, we have tackled the Nearest color.
The basic idea is simple: given a 12-bit RGB color such as FFF, give the closest color between red (F00), green (0F0), and blue (00F).
Let's have a simple start, checking only F, as follows:
-- describe "Nearest color" $ do
-- forM_
-- [ ("F00", "F00"),
-- ("0F0", "0F0"),
-- ("00F", "00F")
-- ]
-- $ \(input, expected) ->
-- it (show input <> " should be " <> show expected) $
-- nearestColor input `shouldBe` expected
type Color = String
nearestColor :: Color -> Color
nearestColor = id
id is enough because test cases are direct; if we expect F00 for A00, it will fail.
We should compute the distance between two colors, and then pick the smaller one, as follows:
nearestColor :: Color -> Color
nearestColor target =
fst $ minimumBy (comparing snd) [(color, diff intsTarget $ intsColor color) | color <- ["F00", "0F0", "00F"]]
where
intsTarget = intsColor target
diff (xr, xg, xb) (yr, yg, yb) = abs (xr - yr) + abs (xg - yg) + abs (xb - yb)
intsColor [r, g, b] = (digitToInt r, digitToInt g, digitToInt b)
This is a three-step process:
- Convert
Colorto a triplet, e.g.,00Abecomes(0, 0, 10) - Convert each compared color and compare them, storing the result in an array, e.g.,
[("F00", 26), ("0F0", 26), ("00F", 6)] - Pick the tuple with the minimum second element and keep the first element
I'm not happy with the type alias; let's use a proper type, delegating functions to it, as follows:
data Color = Color
{ red :: Int,
green :: Int,
blue :: Int
}
deriving stock (Eq)
instance Show Color where
show color = toUpper . intToDigit . ($ color) <$> [(.red), (.green), (.blue)]
instance IsString Color where
fromString [r, g, b] =
Color
{ red = digitToInt r,
green = digitToInt g,
blue = digitToInt b
}
diffColors :: Color -> Color -> Int
diffColors x y = sum $ (\f -> abs $ on (-) f x y) <$> [(.red), (.green), (.blue)]
nearestColor :: Color -> Color
nearestColor target =
fst $ minimumBy (comparing snd) [(color, diffColors target color) | color <- ["F00", "0F0", "00F"]]
It is clean enough; the next feature is to produce multiple colors when there is distance equality (e.g., 989 is as close to F00 as 00F). It can be done by extracting the minimum distance and filtering on it, as follows:
nearestColors :: Color -> [Color]
nearestColors target =
fst <$> filter ((== smallestDiff) . snd) colors
where
colors = [(color, diffColors target color) | color <- ["F00", "0F0", "00F"]]
smallestDiff = minimum $ map snd colors
At this point, I thought we were done, but it seems that, according to the author, I have cheated, and that colors should be injected as follows:
nearestColors :: Color -> [Color]
nearestColors = nearestColorsWith ["F00", "0F0", "00F"]
nearestColorsWith :: [Color] -> Color -> [Color]
nearestColorsWith compareColors target =
fst <$> filter ((== smallestDiff) . snd) colors
where
colors = [(color, diffColors target color) | color <- compareColors]
smallestDiff = minimum $ map snd colors
It was a quick code kata, but I have enjoyed it.