Summary: June Blender explains Doug Finke's multiplication and formatting trick. Today...the method.
Microsoft Scripting Guy, Ed Wilson, is here. This is the second part of a two-part series written by June Blender, Honorary Scripting Guy. In the first part, Fun Formatting Ones—Part 1: The Task, we discussed the task of printing Doug's multiplication table. In this part, we'll talk about the techniques that he used, including formatting the table.
Note If you already understand an element of the method, such as Ranges, you can skip that section. The sections are independent.
Here's June...
Today, I'll explain the techniques that Doug used to solve the task, including an interesting loop and string formatting.
Recap of Part 1
Let's recap what we learned in Part 1.
Doug wanted to create this table of products in Windows PowerShell.
1 * 1 = 1
11 * 11 = 121
111 * 111 = 12321
1111 * 1111 = 1234321
11111 * 11111 = 123454321
111111 * 111111 = 12345654321
1111111 * 1111111 = 1234567654321
11111111 * 11111111 = 123456787654321
Each row of the table multiplies n digits of 1s and shows the product. The number of 1-digits increases with each row. To do this, he creates a loop that goes from 1 to 8 and prints that number of 1s and their product. For each row:
- Print n digits of "1" ("1" * $i)
- An " x "
- Repeat n digits of "1"
- An " = "
- The product of the n digits, as though they were numbers
To get the product of a number string, use the Invoke-Expression cmdlet. It evaluates the string as an arithmetic equation, for example:
PS C:\> "1111 * 1111"
"1111 * 1111"
PS C:\> Invoke-Expression -command "1111 * 1111"
1234321
Doug uses a more succinct form of the same Invoke-Expression command:
"1111 * 1111" | iex
So the Windows PowerShell statement that creates the string for each row is:
"1" * $i + " x " + "1" * $i + " = " = ("1" * $i) * ("1" * $i) | iex
We assign "1" * $i to the variable $n to make this a bit cleaner:
$n = "1" * $i
$n + " x " + $n + " = " + ("$n * $n" | iex)
In a loop from 1 to 8 (inclusive):
for ($i = 1; $i -le 8; $i++)
{
$n = "1" * $i
$n + " x " + $n + " = " + ("$n * $n" | iex)
}
1 x 1 = 1
11 x 11 = 121
111 x 111 = 12321
1111 x 1111 = 1234321
11111 x 11111 = 123454321
111111 x 111111 = 12345654321
1111111 x 1111111 = 1234567654321
11111111 x 11111111 = 123456787654321
That's really close. But in his post, Doug uses a different type of loop and string formatting. Let's discuss those.
Ranges
Instead of using a typical For loop, Doug pipes a range of numbers, 1-8, to the ForEach-Object cmdlet.
1..8 | ForEach-Object {
$n = "1" * $i; $n + " x " + $n + " = " + ("$n * $n" | iex)
}
A range is an array of numbers that starts at a beginning number, stops at (and includes) an ending number, and includes all numbers in between in numeric order. For example, a range of numbers from 1 to 5 is: 1, 2, 3, 4, 5. The syntax for a range is:
$start..$stop
There are no intervening spaces. You can use negative and positive numbers, and you can create ranges that count down or up, for example:
PS C:\> 0..6
0
1
2
3
4
5
6
PS C:\> -4..4
-4
-3
-2
-1
0
1
2
3
4
PS C:\> 8..-3
8
7
6
5
4
3
2
1
0
-1
-2
-3
Best of all, the starting and the stopping numbers can be variables or expressions that can change over time.
PS C:\> $start = 0
PS C:\> $stop = 5
PS C:\> $start..$stop
0
1
2
3
4
5
PS C:\> $start = 6
PS C:\> $start++..$stop--
7
6
Piping the items in the range
Doug creates a range of numbers from 1 to 8 (1..8) and then pipes the range to the ForEach-Object cmdlet.
When you send it a collection of objects (any kind), the pipeline operator ( | ) takes each item from the collection (one at a time) and sends it to the next command:
1..8 | <next thing>
Is equivalent to:
1, 2, 3, 4, 5, 6, 7, 8 | <next thing>
Or:
1| <next thing>
2| <next thing>
…
8| <next thing>
In the pipeline command, the current object coming down the pipeline is represented by good old $_, for example:
PS C:\> 1..8 | ForEach-Object {"I need $_ new servers."}
I need 1 new servers.
I need 2 new servers.
I need 3 new servers.
I need 4 new servers.
I need 5 new servers.
I need 6 new servers.
I need 7 new servers.
I need 8 new servers.
You can use a range controlled ForEach-Object command in place of any For loop. Here's the equivalent For loop. The $i variable starts at 1, continues while $i is less-than or equal-to 8 (because 8 is included in the range), and it increments $i on each pass. In this case, the value that is increasing is $i, not $_:
PS C:\> For ($i = 1; $i -le 8; $i++) {"I need $i new servers."}
I need 1 new servers.
I need 2 new servers.
I need 3 new servers.
I need 4 new servers.
I need 5 new servers.
I need 6 new servers.
I need 7 new servers.
I need 8 new servers.
Let's replace the For loop that creates Doug's 1s table with a statement that pipes a range to a ForEach-Object command. Here's the For loop:
for ($i = 1; $i -le 8; $i++)
{
$n = "1" * $i
$n + " x " + $n + " = " + ("$n * $n" | iex)
}
Here's the new range controlled ForEach loop. The $_ takes the place of $i in the For loop:
1..8 | ForEach {
$n = "1" * $_
$n + " x " + $n + " = " + ("$n * $n" | iex)
}
Doug makes it a one-liner. In a one-liner, we don't have each command on a separate line, so we add a statement terminator ( ; ) to end the assignment statement. The remainder of the command is a single statement:
1..8 | ForEach {$n = "1" * $_; $n + " x " + $n + " = " + ("$n * $n" | iex)}
1 x 1 = 1
11 x 11 = 121
111 x 111 = 12321
1111 x 1111 = 1234321
11111 x 11111 = 123454321
111111 x 111111 = 12345654321
1111111 x 1111111 = 1234567654321
11111111 x 11111111 = 123456787654321
Hanging in there? Now, let's add some formatting.
Formatting strings with -f
Windows PowerShell uses the formatting statements in the .NET library that are designed for strings. They're really powerful, so it's worth learning how to use them.
The primary MSDN doc about this topic starts with Composite Formatting, a really well-written topic that is worth reading and bookmarking. There are lots of blog posts about string formatting in Windows PowerShell, including one by Ed Wilson (Use PowerShell to Format Strings with Composite Formatting) and one by me (String Formatting in Windows PowerShell).
String formatting is powerful and complex, but we can start with the basics. A formatted string statement has the following form:
"{0} and {1}" -f <expressionA>, <expressionB>
The -f (for Format) divides the statement into two parts.
The double-quoted string to the left of the -f includes integers that are placeholders for strings. The placeholder integers must begin with 0 and increase by 1. They're enclosed in curly braces, for example:
{0}
To the right of the -f is a comma-separated array of one or more expressions. The result of each expression takes the place of a placeholder. The first expression takes the place of {0} in the string; the second expression takes the place of {1}, and so on, for example:
PS C:\> "{0} is {1}." -f "PowerShell", "fun"
PowerShell is fun.
PS C:\>"{0} is {1}" -f ("car" * 3), "redun" + "dant"
carcarcar is redundant.
The placeholders can be repeated in the string, but you need to have exactly one expression for each placeholder.
PS C:\>"How much {0} could a {0}{1} {1} if a {0}{1} could {1} {0}?" -f "wood", "chuck"
How much wood could a woodchuck chuck if a woodchuck could chuck wood?
Let's see how Doug used formatting. He started with a string of the form:
ones x ones = product
$n x $n = ("$n * $n" | iex)
He replaced the ones with "{0}" and product with "{1}". Note that this is a single string. We don't need to use string addition to concatenate the x and the =. They're just part of the string:
"{0} x {0} = {1}"
Then he used the -f statement to replace every {0} with $n and every {1} with ("$n * $n | iex"):
"{0} x {0} = {1}" -f $n, ("$n * $n | iex")
So, the original statement:
1..8 | ForEach {$n = "1" * $_; $n + " x " + $n + " = " + ("$n * $n" | iex)}
Becomes:
1..8 | ForEach {$n = "1" * $_; "{0} x {0} = {1}" -f $n, ("$n * $n" | iex)}
The result is the same:
1 x 1 = 1
11 x 11 = 121
111 x 111 = 12321
1111 x 1111 = 1234321
11111 x 11111 = 123454321
111111 x 111111 = 12345654321
1111111 x 1111111 = 1234567654321
11111111 x 11111111 = 123456787654321
The last step is aligning it so it looks even cooler.
Alignment in formatted strings
Among the many things you can do with string formatting is to align the characters in a string. To place characters in a string of a certain size, add an alignment value to the placeholder. Positive values align to the right, and negative values align to the left. The syntax is:
{<placeholder>,<alignment>}
For example, this statement declares placeholder {0} and places the result in a 5-character string where the "a" character is in position 5:
PS C:\> "{0, 5}" -f "a"
a
This statement uses a negative value to left-align the "a" in a 5-character space. I've put an "x" in the next position, so you can see the alignment more clearly:
PS C:\ps-test> "{0, -5}{1}" -f "a", "x"
a x
In his "Fun with Ones" post, Doug uses an alignment value of 9 (+9) to right-align the $n strings in a 9-character field. The alignment changes this:
1..8 | ForEach {$n = "1" * $_; "{0} x {0} = {1}" -f $n, ("$n * $n" | iex)}
1 x 1 = 1
11 x 11 = 121
111 x 111 = 12321
1111 x 1111 = 1234321
11111 x 11111 = 123454321
111111 x 111111 = 12345654321
1111111 x 1111111 = 1234567654321
11111111 x 11111111 = 123456787654321
To this:
1..8 | ForEach {$n = "1" * $_; "{0, 9} x {0, 9} = {1}" -f $n, ("$n * $n" | iex)}
1 x 1 = 1
11 x 11 = 121
111 x 111 = 12321
1111 x 1111 = 1234321
11111 x 11111 = 123454321
111111 x 111111 = 12345654321
1111111 x 1111111 = 1234567654321
11111111 x 11111111 = 123456787654321
So, what did we learn from taking some time to interpret Doug's work? Here we go...
- Ranges
PS C:\>1..8
1
2
3
4
5
6
7
8
- Replacing a For loop with a range piped to a ForEach-Object command
for ($i = 1; $i -le 8; $i++) {"1" * $i}
1
11
111
1111
11111
111111
1111111
11111111
1..8 | ForEach-Object {"1" * $_}
1
11
111
1111
11111
111111
1111111
11111111
- Multiplying strings
PS C>"1" * 3
111
- Adding strings
PS C:\> ("1" * 3) + " x " + ("1" * 3) + " = "
111 x 111 =
- Using Invoke-Expression to evaluate number strings
PS C:\ps-test> "111 * 111"
111 * 111
PS C:\ps-test> Invoke-Expression -Command "111 * 111"
12321
PS C:\ps-test> "111 * 111" | Invoke-Expression
12321
PS C:\ps-test> "111 * 111" | iex
12321
- Formatting strings
PS C:\> $n = "111"
PS C:\> $n + " + " + $n
111 + 111
PS C:\> "{0} + {0}" -f $n
111 + 111
- Aligning formatted strings
PS C:\> "{0} + {0}" -f $n
111 + 111
PS C:\> "{0, 5} + {0, 5}" -f $n
111 + 111
Not bad lessons for quick Facebook post. Thanks, Doug!
We invite you to follow us on Twitter and Facebook. If you have any questions, send email to scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow.
June Blender, Honorary Scripting Guy