We will process many sequences or collections. At this point the only collection we have discussed is a string - a sequence of characters that we can index.
Consider the following silly function description and heading as a start:
// Print the characters of s, one per line. static void OneCharPerLine(string s)
OneCharPerLine("bug") would print:
b u g
We are accessing one character at a time. We can do that with
the indexing notation. Thinking concretely about the example above,
we are looking to print,
s[0],
s[1],
s[2]. If
we knew we would always have three characters, we could do this
with three explicit print statements, but we are looking to write a general
definition for an arbitrary length string:
This requires a loop.
For now our only option is a
while loop. We can follow our basic
loop planning rubric,
one step at a time:
The index is changing in a simple repetitive sequence.
We can call the
index
i. Its initial value is clearly 0.
That is our initialization. We need a
while loop continuation
condition.
For the 3-character string example, the last index above is 2.
In general we want all the characters. Recall the index of the last
character is the length - 1, or with our parameter
s,
s.Length - 1
The
while loop condition needs to allow indices through
s.Length - 1. We could write a condition with
<= or more
concisely:
while (i < s.Length) {
In the body of the loop, the main thing is to print the next character,
and the next character is
s[i]:
Console.WriteLine(s[i]);
We also need to remember the part to get ready for the next time
through the loop. We have dealt with regular sequence of values
before. We change
i with:
i = i+1;
This change is so common, there is a simpler syntax:
i++;
This increases the value of the numeric variable i by 1.
(The reverse is
i--;) [1]
So all together:
// Print the characters of s, one per line. static void OneCharPerLine(string s) { int i = 0; while (i < s.Length) { Console.WriteLine(s[i]); i++; } }
You can test this with example char_loop1/char_loop1.cs.
This is a very common pattern. We could do anything we want with each individual character, not just print it.
Here is a variation:
/// Print s in reverse order; no extra newlines static void PrintReversed(string s)
There are a few changes:
Write, not
WriteLine.
Our code with driver is in reversed_print/reversed_print.cs.
Let us get more complicated. Consider the function described:
/// Print the vowels (aeiou) in s, one per line. static void PrintVowels(string s)
For instance PrintVowels(“computer”) would print:
o u e
We have seen that we can go through the whole string and do the same
thing each time through the loop, using
s[i] in some specific way.
This new description seems problematic. We do not appear to want to do
the same thing each time: We only want to print some of the
characters. Again your eyes and mind are so fast, you likely miss what you
need to do when you go through
PrintVowels by hand. Your
eyes let you just grab the vowels easily, but think, what is actually
happening? You are checking each character to see if it is a vowel,
and printing it if it is: You are doing the same thing each time -
testing if the character is a vowel. The pseudocode is
if (s[i] is a vowel) { print s[i] }
We do want to do this each time through the loop. We can use
a
while statement.
Next task: convert the pseudocode “s[i] is a vowel” to C#.
There are multiple approaches. The one you get by following your nose is just to consider all the cases where it is true:
s[i] == 'a' s[i] == 'e' s[i] == 'i' s[i] == 'o' s[i] == 'u'
How do you combine them into a condition? The letter can be a or e or i or o or u. We get the code:
/// Print the vowels (aeiou) in s, one per line. static void PrintVowels(string s) { int i = 0; while (i < s.Length) { if (s[i] == 'a' || s[i] == 'e' || s[i] == 'i' || s[i] == 'o' || s[i] == 'u') { Console.WriteLine(s[i]); } i = i+1; } }
That has a long condition! Here is a nice trick to shorten that:
We want to check if a character is in a group of letters. We have
already seen the string method IndexOf. Recall we can use it to see if
a character is in or not in a string. We can use
"aeiou".IndexOf(s[i]).
We do not care where
s[i] comes in the string of vowels.
All we care is that
"aeiou".IndexOf(s[i]) >= 0.
This is still a bit of a mouthful. Often it is just important if a
character or string is contained in another string, not where it appears,
so it is easier to
use the string method
Contains. Though IndexOf takes either a string
or a character as parameter,
Contains only takes a string. There is a
nice quick idiom to convert anything to a string: use
""+.
The condition could be
"aeiou".Contains(""+s[i]).
The
"" + s[i] adds
the string version of
s[i] to the empty string.
The function is still not as general as it might be:
Only lowercase vowels are listed. We could do something with
ToLower, or just use the condition:
"aeiouAEIOU".Contains(""+s[i])
This variation is in example vowels2/vowels2.cs.
/// Print the vowels (aeiou) in s, one per line. static void PrintVowels(string s) { int i = 0; string vowels = "aeiouAEIOU"; while (i < s.Length) { if (vowels.Contains(""+s[i])) { Console.WriteLine(s[i]); } i++; } }
Consider a variation, determining if all the characters in a string are vowels. We could work on that, but it is not very useful. Instead let us consider if all the characters are digits. This is a true-false question, so the function to determine this would return a Boolean result:
There are several ways to check if a character is a digit. We could use the
Contains idiom from above, but here is another option:
The integer codes for digits are sequential,
and since characters are technically a kind of integer, we can
compare: The character
s[i] is a digit if it is in the range from
'0'
to
'9', so the condition can be written:
'0' <= s[i] && s[i] <= '9'
Similarly the condition
s[i] is not a digit, can be written
negating the compound condition as in Compound Boolean Expressions:
s[i] < '0' || s[i] > '9'
If you think of going through by hand and checking, you would check through the characters sequentially and if you find a non-digit, you would want to remember that the string is not only digits.
One way to do this is have a variable holding an answer so far:
bool allDigitsSoFar = true;
Of course initially, you have not found any non-digits, so it starts off true.
As you go through
the string, you want to make sure that answer is changed to
false
if a non-digit is encountered:
if ('0' > s[i] || s[i] > '9') { allDigitsSoFar = false; }
When we get all the way through the string, the answer so far is the final answer to be returned:
/// Return true if s contains one or more digits /// and nothing else. Otherwise return false. static bool IsDigits(string s) { bool allDigitsSoFar = true; int i = 0; while (i < s.Length) { if (s[i] < '0' || s[i] > '9') { allDigitsSoFar = false; } i++; } return allDigitsSoFar; }
Remember something to always consider: edge cases. In the description it says it is true for a string of one or more digits.
Check examples of length 1 and 0.
Length 1 is fine, but it fails for the empty string,
since the loop is skipped and the initial answer,
true is returned.
There are many ways to fix this. We will know right up front that the answer
is false if the length is 0, and we could immediately set
allDigitsSoFar to false. We would need to change the initialization
so it checks the length and chooses the right value for
allDigitsSoFar,
true or false. Since we are selecting between two values,
an
if statement should occur to you:
bool allDigitsSoFar; if (s.Length > 0) { allDigitsSoFar = true; } else { allDigitsSoFar = false; }
If we substitute this initialization for
allDigitsSoFar,
the code will satisfy the edge case, and the code will always
work. Still, this code can be improved:
Examine the
if statement more closely:
true,
allDigitsSoFaris
true;
false,
allDigitsSoFaris
false;
See the symmetry: the value assigned to
allDigitsSoFar is always
the value of the condition.
A much more concise and still equivalent initialization is just:
bool allDigitsSoFar = (s.Length > 0);
In more generality this
conciseness comes from the fact that it is a Boolean value that
you are trying to set each time, based on a Boolean condition: You do not
need to do that with an
if statement! You just need an
assignment statement. If you use an
if statement in such a situation,
you being verbose and marking yourself as a novice!
It could even be slightly more concise: The precedence of assignment is
very low, lower than the comparison
>,
so the parentheses could be omitted. We think the
code is easier to read with the parentheses left in, as written above.
The whole function would be:
/// Return true if s contains one or more digits /// and nothing else. Otherwise return false. static Boolean IsDigits(string s) { Boolean allDigitsSoFar = (s.Length > 0); int i = 0; while (i < s.Length) { if (s[i] < '0' || s[i] > '9') { allDigitsSoFar = false; } i++; } return allDigitsSoFar; }
You can try this code in example check_digits1/check_digits1.cs.
Note that we earlier made an improvement by replacing an
if-
else statement
generating a Boolean value by a simple Boolean assignment.
In the most recent sample code, there is an
if statement setting a Boolean
value:
if (s[i] < '0' || s[i] > '9') { allDigitsSoFar = false; }
You might be tempted to replace this
if statement by a simple Boolean
assignment:
allDigitsSoFar = (s[i] < '0' || s[i] > '9'); // bad!
Play computer with this change to see for yourself why it is bad, before looking at our explanation below....
The place where we originally said to use a simple Boolean assignment was
replacing an
if-
else statement, that always set a Boolean value. In the more
recent correct code for digits, we had a simple
if statement,
and were only setting the boolean variable
to
false some of the time: when we had not found a digit. The bad code
sets the variable for each character in the string,
so it can change an earlier
false value back to
true for a later digit.
The final value
always comes from the the last character in the string! We want the
function to come up with an answer
false if any character is not a digit,
not just the last character. The bad code would give the wrong answer
with the string “R2D2”. If you do not see that, play computer with this string
and the bad code variation
that sets
allDigitsSoFar every time through the loop.
There is a less commonly useful way to make an assignment without
if
work here [2], but a much more important,
improved approach follows:
The last correct code is still inefficient. If an early character in a long string is not a digit, we already know the final answer, but this code goes through and still checks all the other characters in the string! People checking by hand would stop as soon as they found a non-digit. We can do that in several ways with C#, too. Since this is a function, and we would know the final answer where we find a non-digit, the simplest thing is to use the fact that a return statement immediately terminates the function (even if in a loop).
Instead of setting a variable to
false to later be returned,
we can return right away, using the loop:
while (i < s.Length) { if (s[i] < '0' || s[i] > '9') { return false; } i++; }
What if the loop terminates normally (no return from inside)?
That means no
non-digit was found, so if there are any characters at all,
they are all digits. There are
one or more digits as long as the string length is positive.
Again we do not need an
if-
else statement to check the length and
set the Boolean result.
Look in the full
code for the function:
/// Return true if s contains one or more digits /// and nothing else. Otherwise return false. static Boolean IsDigits(string s) { int i = 0; while (i < s.Length) { if (s[i] < '0' || s[i] > '9') { return false; } i++; } return (s.Length > 0); }
The full code with a
Main testing program is in
example check_digits2/check_digits2.cs.
Returning out of a loop is a good pattern to remember when you are searching for something, and you know the final answer for your function as soon as you find it.
We have not given you a chance to play computer with a loop. Here is some simple silly code, loop_steps/loop_steps.cs, also using a sequence:
Play computer, completing the table. You fill in the line numbers, carefully. The sequence is not 9, 10, 11, 12, 13!
Create a file
double_char_test.cs,
and write and test a function with the documentation and heading below:
/// If two consecutive characters in s are the same, return true. /// Return false otherwise. Examples: /// HasDoubleChar("bigfoot") and HasDoubleChar("aaah!") are true; /// HasDoubleChar("treated") and HasDoubleChar("haha!") are false. static bool HasDoubleChar(string s)
You may want to play computer on a short example - there is an easy mistake to make.