Subing a program

A redo

Let's have a look at this sub business from another angle. Earlier, Klaus and Jeremy wrote a program to convert pounds and ounces in kilos. Here's some output:

The program would handle three different types of input error. First, if an input was not a valid number.

Second, if the input was negative:

Third, if both pounds and ounces were empty, or zero.

Here's their code:

Private Sub cmdConvert_Click()

'Declare varibles.

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

Dim userInput As String

'Input

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, pounds must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, pounds cannot be negative."

End

End If

pounds = userInput

'Validate ounces

userInput = Cells(4, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, ounces must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, ounces cannot be negative."

End

End If

ounces = userInput

'Check if both are empty.

If pounds = 0 And ounces = 0 Then

MsgBox "Sorry, please enter pounds, ounces, or both."

End

End If

'Processing

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

'Output

Cells(6, 2) = Round(kilos, 3)

End Sub

Lines 2 to 6 declare variables:

Private Sub cmdConvert_Click()

'Declare varibles.

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

Dim userInput As String

Lines 7 to to 29 get and validate the user input. There are three pieces to it. The first one handles the pounds. That's lines 8 to 18:

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, pounds must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, pounds cannot be negative."

End

End If

pounds = userInput

The second part of the input validation handles ounces. That's lines 19 to 29.

'Validate ounces

userInput = Cells(4, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, ounces must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, ounces cannot be negative."

End

End If

ounces = userInput

The third part of the input validation tests whether both inputs are zero:

'Check if both are empty.

If pounds = 0 And ounces = 0 Then

MsgBox "Sorry, please enter pounds, ounces, or both."

End

End If

So, here's the structure of the program so far:

Get input

Get and validate pounds

Get and validate ounces

Test if both inputs are zero

I've left out the part declaring variables, because it doesn't affect the program's logic.

That's all for input. The next part of the program does the processing.

'Processing

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

Not much to it. Like many programs, most of the code is for validation.

The last part of the program does output:

'Output

Cells(6, 2) = Round(kilos, 3)

The complete structure of the program:

Input

Get and validate pounds

Get and validate ounces

Test if both inputs are zero

Processing

Output

There's that good ol' IPO.

Sub it

Let's change the program, using subs. The first version will just makes subs for I, P, and O. Here's the original code, with the chunks marked. We'll make a sub for each one.

Private Sub cmdConvert_Click()

'Declare varibles.

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

Dim userInput As String

'Input

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, pounds must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, pounds cannot be negative."

End

End If

pounds = userInput

'Validate ounces

userInput = Cells(4, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, ounces must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, ounces cannot be negative."

End

End If

ounces = userInput

'Check if both are empty.

If pounds = 0 And ounces = 0 Then

MsgBox "Sorry, please enter pounds, ounces, or both."

End

End If

'Processing

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

'Output

Cells(6, 2) = Round(kilos, 3)

End Sub

Processing

Let's start with the last two chunks. They're easiest. Here's processing:

'Processing

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

Start by giving the code a name:

'Processing

Sub convertToKilos()

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

End Sub

The second thing we need to do is add params (parameters). Params are values that go into and out of the sub. The purpose of the sub is to work out kilos. You can see that in the last executable line of code.

'Processing

Sub convertToKilos()

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

End Sub

So, that's one of the params. We need to know its data type: string, single, integer, whatevs. Look at the declarations in the original code:

Private Sub cmdConvert_Click()

'Declare varibles.

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

Dim userInput As String

kilos is a Single. Let's add that param:

'Processing

Sub convertToKilos(kilos as Single)

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

End Sub

OK, that's the output of convertToKilos(). When I say "output" here, I'm taking about the output of the sub, not the program. We're just doing convertToKilos() now. We can forget the rest of the program for the moment. That's one of the Good Things about subs.

The code needs values for pounds and ounces. Let's add those as params.

'Processing

Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

End Sub

pounds and ounces are inputs to the sub. That is, the sub takes them, and uses them to compute the output value, kilos. Input params are usually listed before output params in a sub's signature.

The last step is to add comments to the sub, explaining what it does, and what the params are. Don't forget this! These comments are what other programmers will use to call your sub.

'Compute kilos.

'Params:

' pounds: pounds (input)

' ounces: ounces (input)

' kilos: result (output)

Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

End Sub

A common mistake is to try to declare params in the sub, like this:

'Compute kilos.

'Params:

' pounds: pounds (input)

' ounces: ounces (input)

' kilos: result (output)

Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

pounds = pounds + ounces / 16

kilos = pounds * 0.453592

End Sub

Won't work. VBA will complain. When you use a param in a sub's signature, you're declaring it right there

How is convertToKilos() used? Here's the main program:

Private Sub cmdConvert_Click()

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

'Input

getInput pounds, ounces

'Processing

convertToKilos pounds, ounces, kilos

'Output

outputKilos kilos

End Sub

(Notice how simple the program is!)

pounds, ounces, and kilos are declared in the main program, and used when calling the subs. So, you Dim variables in the caller. Subs get them as params.

Let's summarize what we've done. Here's the program's structure:

Input

Get and validate pounds

Get and validate ounces

Test if both inputs are zero

Processing

Output

A bunch of steps that are easy to understand. We've taken the processing step…

Input

Get and validate pounds

Get and validate ounces

Test if both inputs are zero

Processing

Output

… made a sub for it. That's the goal. A sub for each chunk of the program.

The output sub

This one is easy, too. Here's the original code for it.

'Output

Cells(6, 2) = Round(kilos, 3)

We need to:

Give the sub a name.

Add params.

Here's the name part:

Sub outputKilos()

Cells(6, 2) = Round(kilos, 3)

End Sub

A good name, that says exactly what the code does.

Now let's do the params. The code only needs one value:

Sub outputKilos()

Cells(6, 2) = Round(kilos, 3)

End Sub

So:

Sub outputKilos(kilos as Single)

Cells(6, 2) = Round(kilos, 3)

End Sub

Now the comments:

'Output kilos.

'Params:

' kilos: kilos (input)

Sub outputKilos(kilos As Single)

Cells(6, 2) = Round(kilos, 3)

End Sub

Line 3 says that kilos is an input. That's correct. kilos is an input to this sub. Even though kilos is an output from the entire program, it's an input for this chunk of code.

Here's the main program again:

Private Sub cmdConvert_Click()

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

'Input

getInput pounds, ounces

'Processing

convertToKilos pounds, ounces, kilos

'Output

outputKilos kilos

End Sub

You can see the call the outputKilos at the bottom.

The input

OK, this is the complicated one. Here's the original code:

'Input

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, pounds must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, pounds cannot be negative."

End

End If

pounds = userInput

'Validate ounces

userInput = Cells(4, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, ounces must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, ounces cannot be negative."

End

End If

ounces = userInput

'Check if both are empty.

If pounds = 0 And ounces = 0 Then

MsgBox "Sorry, please enter pounds, ounces, or both."

End

End If

Let's make it into a sub.

The two steps to making a sub from existing code are:

The name.

The params.

The name is easier.

Sub getInput()

'Input

'Validate pounds

userInput = Cells(3, 2)

...

End Sub

OK, now the params. This code gets input from the worksheet:

bc. userInput = Cells(3, 2)

There are no input params; the sub grabs data from the worksheet, and sends it out. Some input subs have input params, e.g., the names of files to open, or the numbers of cells to read from. We'll leave this one without input params.

What about output params? Here they are:

Sub getInput()

'Input

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, pounds must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, pounds cannot be negative."

End

End If

pounds = userInput

'Validate ounces

userInput = Cells(4, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, ounces must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, ounces cannot be negative."

End

End If

ounces = userInput

'Check if both are empty.

If pounds = 0 And ounces = 0 Then

MsgBox "Sorry, please enter pounds, ounces, or both."

End

End If

End Sub

All that code, just to get values for two params. That's normal in business programming.

Let's add them to the sig, and document them:

'Input and validate pounds and ounces

'Params:

' pounds: value user entered (output).

' ounces: value user entered (output).

Sub getInput(pounds As Single, ounces As Single)

...

pounds and ounces are labeled as output. Why?

Klaus

Because they're output from the sub. The sub hands them back to the main program.

Tara

Right!

A local variable

If we ran the program right now, we'd get an error:

Every variable has to be defined somewhere. For a param, it can be defined in the caller, like this:

Private Sub cmdConvert_Click()

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

'Input

getInput pounds, ounces

'Processing

convertToKilos pounds, ounces, kilos

'Output

outputKilos kilos

End Sub

Sub getInput(pounds As Single, ounces As Single)

...

End Sub

Sub convertToKilos(pounds As Single, ounces As Single, kilos As Single)

...

End Sub

Sub outputKilos(kilos As Single)

...

End Sub

pounds, ounces, and kilos are all defined in the main program, then passed to the subs as params. That works fine.

Here's code from the input sub:

Sub getInput(pounds As Single, ounces As Single)

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

Line 3 has a variable that isn't from a param:

Sub getInput(pounds As Single, ounces As Single)

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

We could add userInput as a param, and define it in the main program.

Private Sub cmdConvert_Click()

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

Dim userInput As String

'Input

getInput pounds, ounces, userInput

'Processing

convertToKilos pounds, ounces, kilos

'Output

outputKilos kilos

End Sub

Sub getInput(pounds As Single, ounces As Single, userInput as String)

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

...

Is that a good idea? Hmm… userInput is not used by any other sub. Declaring it in the main program lets the program run, but it makes the code untidy, harder to understand, and more prone to bugs. And screaming.

userInput is a temporary variable, only used in getInput. So, we should define it in getInput:

Private Sub cmdConvert_Click()

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

'Input

getInput pounds, ounces

'Processing

convertToKilos pounds, ounces, kilos

'Output

outputKilos kilos

End Sub

Sub getInput(pounds As Single, ounces As Single)

Dim userInput As String

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

...

Better! The main program stays clean and tidy. userInput is declared just where it is needed. It doesn't show up anywhere else.

userInput is a local variable. The CPU allocates memory for it when it runs getInput. When the CPU exits the sub, it releases the memory, and userInput ceases to exist.

Use locals when you can. Reserve params for data that needs to pass between subs.

The whole shebang

Here's all the code:

Private Sub cmdConvert_Click()

Dim pounds As Single

Dim ounces As Single

Dim kilos As Single

'Input

getInput pounds, ounces

'Processing

convertToKilos pounds, ounces, kilos

'Output

outputKilos kilos

End Sub

'Input and validate pounds and ounces

'Params:

' pounds: value user entered (output).

' ounces: value user entered (output).

Sub getInput(pounds As Single, ounces As Single)

Dim userInput As String

'Validate pounds

userInput = Cells(3, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, pounds must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, pounds cannot be negative."

End

End If

pounds = userInput

'Validate ounces

userInput = Cells(4, 2)

If Not IsNumeric(userInput) Then

MsgBox "Sorry, ounces must be a number."

End

End If

If userInput < 0 Then

MsgBox "Sorry, ounces cannot be negative."

End

End If

ounces = userInput

'Check if both are zero.

If pounds = 0 And ounces = 0 Then

MsgBox "Sorry, please enter pounds, ounces, or both."

End

End If

End Sub

'Compute kilos.

'Params:

' pounds: pounds (input)

' ounces: ounces (input)

' kilos: result (output)

Sub convertToKilos(pounds As Single, ounces As Single, kilos As Single)

pounds = pounds + ounces / 16

kilos = pounds / 2.20462

End Sub

'Output kilos.

'Params:

' kilos: kilos (input)

Sub outputKilos(kilos As Single)

Cells(6, 2) = Round(kilos, 3)

End Sub

Ah!

Top-down versus bottom-up

Lily

OK, we started this lesson with code that the dudes had written. Then we made some subs, and then the main program.

Is that how you work when you don't have existing code?

Tara

No. What we did here is called bottom-up. We made a sub – computeKilos – and then worked out how to call it.

When I write a program, I don't start with code from the dudes. I start from scratch. I work top-down. I work out the structure, often as bullet points. Then I write the main program, and then the subs.

That's what you should do, too. We did this one bottom up, so you could better understand how subs work. But top-down is the way to go.

The process of taking a chunk of functionality (like getting input, doing processing, or showing output) and making a subroutine out of it is called encapsulation. It's one of the Big Ideas of programming. Encapsulation is key to writing complex software. Take a task, break it into chunks, with a sub for each one. If those chunks are complex, break them up into smaller chunks. And so on.