Friday Puzzle Solution: Write a Map Function for PowerShell

On Friday, February 24, 2017, I posted the following puzzle on Twitter and Facebook (and here!). We provide a Pester test script and a script with detailed help but no code, and you add the code to the script to make the tests pass.

Special thanks to Andy Cahn at Chrysalis School in Seattle for help with the math.

Pester tests: ConvertTo-Range.Tests.ps1
Script: ConvertTo-Range.ps1

The task is to write a PowerShell function that converts a value in one range to its equivalent value in another range like the map() method in the Java Processing library.

For example, given a value of 5 in the range 0 – 10, where 5 is at the halfway point in the range, and a range of 0 – 100, the function would return a value of 50, which is the halfway point in the second range.

So, a little bit of math, a little bit of Pester, a little bit of PowerShell, and lots of fun.

Test-Driven Development

This exercise gives you a chance to try test-driven development, a programming model where you write tests before you code and then write only enough code to make each of the tests pass.

When the task is complete, the code is fully tested and it’s likely to be simpler than code you write without a test goal. It also encourages innovation, because when you change the code, including re-organizing or refactoring it, you can run your tests again to confirm that the changes haven’t affected the result.

The Pester Tests

The Pester tests for this exercise are contained in a single Describe block, but are organized into four Context blocks. Each Context block contains tests for edge cases, maximum and minimum values, and a mid-range case.

All of the tests return integer values, which, by design, wasn’t specified in the help topic. I added this to cover implicit specifications, those nasty assumptions that should be expressed, but are often forgotten until the code is complete. Specifying the expected values in examples helps you to get the code right the first time.

The first Context block, SmallToLarge_StandardValues, includes tests where the value is within the first range and the first range is smaller than the second range.

For example, this test takes a value of 60 in the range 40-90 and returns the equivalent value in the range 0-255.

It "Mid-range test" {
    # map(60, 40, 90, 0, 255) is 102
    ConvertTo-Range -Value 60 -Range1_Start 40 -Range1_End 90 -Range2_Start 0 -Range2_End 255 | Should Be 102
}

 

The second Context block, LargeToSmall_StandardValues, includes tests where the value is within the first range and the first range is larger than the second range.

For example, this test takes a value of 60 in the range 10-500 and returns the equivalent value in the range 40-90.

It "Mid-range test" {
    # map(60, 10, 500, 40, 90) is 45
    ConvertTo-Range -Value 60 -Range1_Start 10 -Range1_End 500 -Range2_Start 40 -Range2_End 90  | Should Be 45
}

 

The third Context block, SmallToLarge_OutOfRange, includes tests where the value is outside of the first range, the second range, or both.

For example, this test takes a value of 300 in the range 40-90 and returns the equivalent value in the range 0-255.

It "Max-range test" {
    # map(300, 40, 90, 0, 255) is 1326
    ConvertTo-Range -Value 300 -Range1_Start 40 -Range1_End 90 -Range2_Start 0 -Range2_End 255 | Should Be 1326
}

 

The last Context block, FunStuff, tests negative values, ranges where the end is greater than the start, and, a big hint, applying the map method to Celsius-to-Fahrenheit conversions.

For example, this test takes a value of 60 in a range that begins at 90 and ends at 40.

It "Min > Max Range" {
    # map(60, 90, 40, 0, 255) is 153
    ConvertTo-Range -Value 60 -Range1_Start 90 -Range1_End 40 -Range2_Start 0 -Range2_End 255 | Should Be 153
}

 

The Solution

The function name and parameter names are listed in the help and the tests, so you just need to use them in an equation that returns the correct result. There are many ways to do this. I’ll explain just one of them.

To solve this puzzle, you can generalize the familiar Celsius-to-Fahrenheit and Fahrenheit-to-Celsius equations.

$Fahrenheit = $Celsius × 9/5 + 32
$Celsius = ($Fahrenheit - 32) × 5/9

 

First, create a range scale by dividing the second range by the first. This multiplier effectively stretches or shrinks the second scale to the proportions of the first scale

$rangeScale = ($Range2_End - $Range2_Start) / ($Range1_End - $Range1_Start)

To find the position of the value in the first range, subtract the start of the first range from the value.

$Value - $Range1_Start

Multiply the value position by the range scale, and then, to position the value on the second range, add it to start of the second range.

($Value - $Range1_Start) * $rangeScale + $Range2_Start

Finally, cast the result to an integer (that unstated assumption), and return the result.

[int]$result = ($Value - $Range1_Start) * $rangeScale + $Range2_Start
return $result

 

Our inspirational Java map() method uses a very similar technique. That will work, too.

Run the Pester Tests

When you have a potential solution, run the Pester tests. The ConvertTo-Range.Tests.ps1 file begins with code that dot-sources ConvertTo-Range.ps1 file into its scope. This code expects that both files are in the same directory.

If some of the tests fail, fix your code and run the tests again.

Here’s the result in Pester 4.0.2, but any version of Pester will work.

Hope you had fun with this puzzle.


Like this PowerShell puzzle? Our FridayPowerShellPuzzle repository includes all of the SAPIEN Technologies Friday PowerShell puzzles without the solutions, so you’re not even tempted to peek.

To read the solutions to our Friday PowerShell Puzzles, see Friday Puzzle Solutions in the SAPIEN blog.

June Blender is a technology evangelist at SAPIEN Technologies, Inc. and a Microsoft Cloud and Datacenter MVP. You can reach her at juneb@sapien.com or follow her on Twitter at @juneb_get_help.