Quantcast
Channel: Hey, Scripting Guy! Blog
Viewing all articles
Browse latest Browse all 3333

Fun Formatting Ones—Part 2: The Method

$
0
0

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


Viewing all articles
Browse latest Browse all 3333

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>