Two weeks ago on Wednesday morning, I went to the latest edition of Alt.Net Coding Breakfast, hosted by Damien Thouvenin of CLT Services. It was my first time there, and I was very pleased with it. The principle is quite simple: several .Net coders meet in the morning, and practice their skills by attempting a Coding Kata during an hour. We then review together what was done, what went well and what went wrong. It is an exercise that I find very useful (and fun), that I usually do during Arolla’s Code Jam on Thursdays. This coding breakfast’s main advantage is to meet with other .Net practitioners (as usually the Java guys are predominant) and that might be the only place in Paris where you can see several people coding in F#. That was also true last month, as you can see on Robert Pickering’s blog.
This month, the kata was about printing user-supplied pictures:
- of any size (in pixels),
- in a given format (10×13 cm, 11×13 cm, 13x15cm, 15×18 cm, 18×24 cm…)
- with a given filling mode (“full-paper” or “full-image”)
The printer always prints at 300 dots-per-inch, so the images must be resized in order to fit in the available printing space.
The expected output data, needed for printing the images, would be:
- the output printer (13 cm width or 18 cm width printer),
- the rotation applied to the picture (0° or 90°, the direction doesn’t matter),
- the expansion/reduction ratio to apply to the picture,
- the position where the picture must be drawn.
The kata’s description also involved some drawings in order to explain the meaning of the filling modes. There was, for instance, a picture of Winston Churchill. Unfortunately, there was not a single kitten picture. I fixed this, in order to provide samples illustrating the two filling modes.
Here is a train picture in full-image mode:
The same picture in full-paper mode, where the red areas will be cropped at printing:
And the same with the kitten, full-image:
And full-paper, with the cropped areas in red:
The exercise chosen by Damien for this kata is really an interesting one, as it is in fact quite simple, but there are many ways to misunderstand it and make it more complicated than it really is. Although we were not so many coders, there were several different techniques, problems and outcomes:
some people paired, some worked alone,
some worked in TDD, some didn’t (but we all wrote tests!),
some of us didn’t consider the print format as an input but rather as an output (trying to determine the most appropriate format for a given picture),
some of us took much time trying to define data types, and in the end had not enough time to handle the core business problem of the image resizing,
some others (amongst the C# devs) used dynamic types for prototyping, so they were able to write the logic first, rather than focusing on the types.
As to myself, I had come to this breakfast with the goal to code in F#, so this is what I’ve done. I paired with Peter Even, who I hope is now addicted to F#! In order to focus on the core problem, we stated that:
- selecting the good output printer given the print format is trivial,
- selecting whether or not we must rotate is not very complicated (although we could get it wrong),
- the ratio computation is the tricky part.
Here is what we came up with (with the French words translated) for the ratio computation. First the unit tests:
[<TestClass>] type UnitTest() = [<TestMethod>] member x.``A 2.54*2.54cm square is 300*300 pixels`` () = let converted = (2.54, 2.54) |> toPixels Assert.AreEqual((300.0, 300.0), converted) [<TestMethod>] member x.``Winston churchill in 18*24cm full-image`` () = let ratio = getResizeRatio (664, 1000) (18.0, 24.0) FullImage Assert.AreEqual(18.0 * 300.0 / 2.54 / 664.0, ratio) [<TestMethod>] member x.``Winston churchill in 18*24cm full-paper`` () = let ratio = getResizeRatio (664, 1000) (18.0, 24.0) FullPaper Assert.AreEqual(24.0 * 300.0 / 2.54 / 1000.0, ratio)
And here is the implementation:
type Mode = | FullImage | FullPaper let toPixels (x, y) = (x * 300.0 / 2.54, y * 300.0 / 2.54) let getResizeRatio (photoHeight, photoWidth) (formatHeight, formatWidth) mode = let (photoHeight, photoWidth) = (float photoHeight, float photoWidth) let (paperWidth, paperHeight) = toPixels (formatWidth, formatHeight) let photoRatio = photoWidth / photoHeight let formatRatio = formatWidth / formatHeight let isPhotoWider = photoRatio > formatRatio match (isPhotoWider, mode) with | (true, FullImage) | (false, FullPaper) -> paperWidth / photoWidth | (false, FullImage) | (true, FullPaper) -> paperHeight / photoHeight
Jérémie Chassaing, who also worked in F#, had a more engineered solution where he:
determines, from the filling-mode, which operator to use in order to compare the ratios,
performs the comparison with the operator and determines from that which side of the image must be used to get the correct ratio (but the return type here is directly a function, where a less-functional approach would have returned a boolean for instance),
uses the resulting function to extract the length and compute the ratio.
It sounds more complicated (to my mind it is, in fact), but I still find that beautiful from a functional perspective!
While writing this blog post, I could not resist the will to go further and investigate the other parts of the problem, because I so much wanted to be able to pipe my functions! In the end, the assumptions we made during the kata were not wrong, but reasoning about the position where the image must be drawn is not as simple as I first thought!
Finally, here is my code:
let printers = [ 13.0; 18.0 ] let findPrinter (formatShortSide, formatLongSide) = let printerWidth = printers |> List.tryFind (fun w -> w = formatShortSide || w = formatLongSide) Option.bind (fun w -> Some(w, w = formatShortSide)) printerWidth let getResizeRatio (photoShortSide, photoLongSide) (frameShortSide, frameLongSide) mode = let isPhotoRatioHigher = (photoLongSide / photoShortSide) > (frameLongSide / frameShortSide) match (isPhotoRatioHigher, mode) with | (true, FullImage) | (false, FullPaper) -> frameLongSide / photoLongSide | (false, FullImage) | (true, FullPaper) -> frameShortSide / photoShortSide let getPrintData (photoWidth, photoHeight) (formatShortSide, formatLongSide) mode = let printer = findPrinter (formatShortSide, formatLongSide) match printer with | Some(printerWidth, withFormatRotation) -> let (withPhotoRotationInFrame, photoShortSide, photoLongSide) = if photoHeight > photoWidth then (true, float photoWidth, float photoHeight) else (false, float photoHeight, float photoWidth) let (frameLongSide, frameShortSide) = (formatLongSide, formatShortSide) |> toPixels let ratio = getResizeRatio (photoShortSide, photoLongSide) (frameShortSide, frameLongSide) mode let xInFrame = (frameLongSide - photoLongSide * ratio) / 2.0 let yInFrame = (frameShortSide - photoShortSide * ratio) / 2.0 let (x, y) = if withFormatRotation then (yInFrame, xInFrame) else (xInFrame, yInFrame) let withRotation = withFormatRotation <> withPhotoRotationInFrame Some(printerWidth, (x, y) |> toCentimeters, withRotation) | None -> None
Notes: the images used in the samples are under a creative commons license, and can be found at http://commons.wikimedia.org/wiki/File:Antigo_Tren_Old_Train._Santiago_de_Compostela.jpg and http://commons.wikimedia.org/wiki/File:Red_Kitten_01.jpg.