Control Flow with Numbers

In Chapter 1, we branched on booleans and looped until strings matched. The control flow machinery is the same here. What changes is where the conditions come from. Comparisons take numbers and produce booleans. Those booleans drive the same if statements and while loops you already know.

Numbers open up richer problems. A boolean had two possible values, so if-else covered everything. An integer can be positive, negative, or zero. It can be even or odd. It can fall inside a range or outside it. Each shape calls for its own branch. And because numbers change predictably through arithmetic, loops gain a powerful new tool: the counter.

We start with branching, then move to loops.

Branching on Numeric Ranges

Consider assigning a letter grade based on a test score:

int score = 85;
string grade;
 
if (score >= 90)
{
    grade = "A";
}
else if (score >= 80)
{
    grade = "B";
}
else if (score >= 70)
{
    grade = "C";
}
else if (score >= 60)
{
    grade = "D";
}
else
{
    grade = "F";
}
 
Console.WriteLine(grade);

The translation: “If score is greater than or equal to 90, execute the first scope. Otherwise, if score is greater than or equal to 80, execute the second scope. Otherwise, if score is greater than or equal to 70, execute the third scope. Otherwise, if score is greater than or equal to 60, execute the fourth scope. Otherwise, execute the fifth scope.”

  • Scope 1: bind the string “A” to grade.
  • Scope 2: bind the string “B” to grade.
  • Scope 3: bind the string “C” to grade.
  • Scope 4: bind the string “D” to grade.
  • Scope 5: bind the string “F” to grade.

Output: B

This looks like the if-else if-else chains from Chapter 1, because it is. The only difference is the conditions. Instead of checking boolean variables like isAdmin or isMember, we check comparison expressions like score >= 90.

The conditions are checked in order, top to bottom. The first one that evaluates to true wins. Its scope executes, and the remaining branches are skipped.

Order matters. When score is 85, the first condition score >= 90 evaluates to false. The second condition score >= 80 evaluates to true, so we execute that scope and bind “B” to grade. We never check the remaining conditions.

Notice that the second condition is just score >= 80, not score >= 80 && score < 90. The else-if structure handles the upper bound implicitly. If we reached the second condition, the first must have been false, which means score is already less than 90. Each branch inherits the negation of every condition above it.


Try it yourself.

What does this code display when score is 72?

int score = 72;
string grade;
 
if (score >= 90)
{
    grade = "A";
}
else if (score >= 80)
{
    grade = "B";
}
else if (score >= 70)
{
    grade = "C";
}
else if (score >= 60)
{
    grade = "D";
}
else
{
    grade = "F";
}
 
Console.WriteLine(grade);

Trace through the conditions to determine which branch executes.

Reveal answer

Output: C

The first condition score >= 90 is false (72 is not >= 90). The second condition score >= 80 is false (72 is not >= 80). The third condition score >= 70 is true (72 >= 70). We execute that scope and bind “C” to grade. The remaining conditions are never checked.

If your answer differed, note what you missed before continuing.


Try it yourself.

Write code that classifies an integer variable named temperature. If it is above 100, bind “hot” to a string variable named label. If it is between 50 and 100 (inclusive), bind “moderate”. Otherwise, bind “cold”. Display the result.

Reveal answer
string label;
 
if (temperature > 100)
{
    label = "hot";
}
else if (temperature >= 50)
{
    label = "moderate";
}
else
{
    label = "cold";
}
 
Console.WriteLine(label);

The second condition is temperature >= 50, not temperature >= 50 && temperature <= 100. If we reached that branch, temperature is already 100 or below (the first condition was false).

Compare your answer and note any differences.


Switch Expressions

The grading code works, but look at what it does: it takes a score and produces a grade. One value goes in, one value comes out. Every branch binds a string to the same variable. The structure is a mapping from numeric ranges to string results.

C# has a tool designed for exactly this pattern.

int score = 85;
 
string grade = score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};
 
Console.WriteLine(grade);

Output: B

The translation: “Evaluate score. Return ‘A’ where score is greater than or equal to 90. Return ‘B’ where score is greater than or equal to 80. Return ‘C’ where score is greater than or equal to 70. Return ‘D’ where score is greater than or equal to 60. Otherwise, return ‘F’. Bind the result to a string variable named grade.”

A switch expression is a control structure that evaluates its input against a series of patterns and returns the result paired with the first matching pattern.

Let’s break this down. The value being tested appears before the switch keyword: score switch. Then inside the braces, each line pairs a pattern with a result, separated by =>. The patterns are checked in order, top to bottom, just like if-else if-else. The first pattern that matches wins, and its result becomes the value of the entire expression.

A pattern is a test. The input gets plugged into the test, and if it passes, that pattern matches.

When the pattern is a specific value like 1 or 7, the test is equality: does the input equal this value? When the pattern is a comparison like >= 90, the test has a blank where the input goes. If the input is 85, the test becomes 85 >= 90, which fails. If the input is 95, the test becomes 95 >= 90, which passes.

Think of each pattern as a question about the input. >= 90 asks “is it at least 90?” The value 1 asks “is it exactly 1?” The _ pattern asks nothing. It always matches, which is why it goes last: it catches everything the patterns above missed, like the else at the end of an if-else chain.

The entire switch expression evaluates to a single value. That value gets bound to grade. This is the key distinction from if-else: a switch expression produces a value directly. The if-else version used statements to bind grade inside each scope. The switch expression pairs each pattern with its result on the same line, making the mapping visible at a glance.


Try it yourself.

Translate this code to English:

string day = dayNumber switch
{
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    4 => "Thursday",
    5 => "Friday",
    6 => "Saturday",
    7 => "Sunday",
    _ => "Invalid"
};
Reveal answer

“Evaluate dayNumber. Return ‘Monday’ where dayNumber equals 1. Return ‘Tuesday’ where dayNumber equals 2. Return ‘Wednesday’ where dayNumber equals 3. Return ‘Thursday’ where dayNumber equals 4. Return ‘Friday’ where dayNumber equals 5. Return ‘Saturday’ where dayNumber equals 6. Return ‘Sunday’ where dayNumber equals 7. Otherwise, return ‘Invalid’. Bind the result to a string variable named day.”

If your answer differed, note what you missed before continuing.


Switch expressions shine when the mapping is from specific values to results. The day-of-week example maps seven discrete numbers to seven strings. Writing this with if-else if-else would require seven conditions like dayNumber == 1, dayNumber == 2, and so on. The switch expression reads like a table, which is exactly what it is.

You can also match on multiple values at once. Consider a door with two properties:

bool hasKey = true;
bool doorLocked = true;
 
string result = (hasKey, doorLocked) switch
{
    (true, false) => "Door is open",
    (true, true) => "Unlock and enter",
    (false, false) => "Door is open",
    (false, true) => "Locked out"
};

The translation: “Package hasKey and doorLocked together. Return ‘Door is open’ where hasKey is true and doorLocked is false. Return ‘Unlock and enter’ where hasKey is true and doorLocked is true. Return ‘Door is open’ where hasKey is false and doorLocked is false. Return ‘Locked out’ where hasKey is false and doorLocked is true. Bind the result to a string variable named result.”

The parentheses package two values together. Each pattern matches a combination of values. This scales naturally when decisions depend on multiple values with discrete cases.

We won’t use this form often in this chapter, but it shows that switch expressions handle more than single values. The concept grows with you as you learn more types.


Try it yourself.

Write a switch expression that takes an integer variable named month (1 through 12) and produces a string with the season: “Winter” for months 12, 1, and 2. “Spring” for 3, 4, 5. “Summer” for 6, 7, 8. “Fall” for 9, 10, 11. Use the else pattern for invalid values.

Reveal answer
string season = month switch
{
    12 or 1 or 2 => "Winter",
    3 or 4 or 5 => "Spring",
    6 or 7 or 8 => "Summer",
    9 or 10 or 11 => "Fall",
    _ => "Invalid"
};

The or keyword combines multiple values into a single pattern. If month matches any of the values listed, that pattern matches.

Self-correct against the model above.


Choosing Between if-else and switch

Both tools handle branching over numeric values. Three questions help you choose.

Question 1: Am I producing a value or performing actions?

Switch expressions produce a value. Every arm returns a result, and the whole expression evaluates to that result. If the goal is “given this input, what’s the output?”, a switch expression fits naturally.

If-else statements control behavior. Each branch can print to the console, rebind multiple variables, call functions for their effects, or do anything else. If the branches need to do things rather than produce a thing, if-else is the right tool.

Question 2: Does the decision depend on one value or the relationship between several?

Switch expressions test one value (or a packaged group) against patterns. They read cleanly when you’re classifying a single variable.

If-else chains can use arbitrary boolean expressions. When the decision involves relationships between multiple variables, like if (balance > 0 && attempts < 3), if-else handles this naturally.

Question 3: How many cases?

Two cases? If-else is natural. A two-arm switch feels like overhead.

Many discrete cases? A switch expression reads like a lookup table. Seven days of the week, twelve months, letter grades from scores. The tabular layout makes the mapping visible at a glance.

Neither tool is always better. The problem tells you which reads more clearly.


Try it yourself.

For each problem, decide which tool fits better and explain why. Then write the code.

  1. Given an integer n, produce the string “positive”, “negative”, or “zero”.

  2. Given an integer score and a boolean extraCredit, display “Bonus!” if the score is above 90 and extraCredit is true. Display “Great job!” if the score is above 90 without extra credit. Display “Keep trying” otherwise.

Reveal answer

Problem 1: Switch expression. We are producing a single string value from one integer. Three cases, pure mapping.

string label = n switch
{
    > 0 => "positive",
    < 0 => "negative",
    _ => "zero"
};

Or equivalently, the last pattern could be 0 => "zero" since zero is the only remaining possibility. The else pattern _ works too.

Problem 2: If-else. We are performing an action (displaying to the console), not producing a value. The decision also involves two variables, score and extraCredit, in combination.

if (score > 90 && extraCredit)
{
    Console.WriteLine("Bonus!");
}
else if (score > 90)
{
    Console.WriteLine("Great job!");
}
else
{
    Console.WriteLine("Keep trying");
}

Check your reasoning and code against the model. Note any differences.


Reading Numeric Input

Before we move to loops, we need one practical tool. In Chapter 1, we used Console.ReadLine() to get input from the user. Recall its type signature: ReadLine: () → string. It returns a string, not an integer. So what happens if we try this?

int n = Console.ReadLine();  // Error!

The compiler stops us. ReadLine returns a string, and we are trying to bind it to an integer variable. The types do not match.

We need a way to convert a string that contains a number into an actual integer. int.Parse does this.

Console.WriteLine("Enter a number:");
int n = int.Parse(Console.ReadLine());

The translation: “Display the string ‘Enter a number:’ to the console. Call ReadLine, parse the result as an integer, and bind that to an integer variable named n.”

int.Parse takes a string and converts it to an integer. If the string contains a valid integer like “42” or “-7”, the conversion succeeds and we get the number. If the user types something that is not a valid integer, like “hello” or “3.5”, the program crashes. For now, we assume the user types valid integers. Handling bad input gracefully is a separate topic.

We can combine this with a loop to validate ranges:

Console.WriteLine("Enter a number between 1 and 100:");
int n = int.Parse(Console.ReadLine());
 
while (n < 1 || n > 100)
{
    Console.WriteLine("Out of range. Try again:");
    n = int.Parse(Console.ReadLine());
}

The translation: “While n is less than 1 or greater than 100, execute the scope.”

The scope: display the string ‘Out of range. Try again:’ to the console. Call ReadLine, parse the result as an integer, and bind that to n.

This is the loop-until-valid pattern from Chapter 1, now applied to numeric values. We read a number, check if it falls in the acceptable range, and keep asking until it does.


Try it yourself.

Write code that asks the user for a positive integer. If they enter zero or a negative number, display “Must be positive. Try again:” and ask again. When they enter a valid positive integer, display “Thank you!” to the console.

Reveal answer
Console.WriteLine("Enter a positive integer:");
int n = int.Parse(Console.ReadLine());
 
while (n <= 0)
{
    Console.WriteLine("Must be positive. Try again:");
    n = int.Parse(Console.ReadLine());
}
 
Console.WriteLine("Thank you!");

If your answer differed, note what you missed before continuing.


Counting Loops

In Chapter 1, loops depended on user input or boolean flags. The program waited for the user to type “quit” or for some condition to change. With numbers, loops gain a new capability: counting.

int i = 1;
 
while (i <= 5)
{
    Console.WriteLine(i);
    i = i + 1;
}

The translation: “While i is less than or equal to 5, execute the scope.”

The scope: evaluate i and display the result to the console. Add 1 to i and bind the result to i.

Output:

1
2
3
4
5

This loop displays the numbers 1 through 5. A counting loop is a loop that uses a counter variable to step through a range of values.

Let’s trace the state with an iteration table. The table tracks how the variable changes each iteration, nothing else.

int i = 1;
 
while (i <= 5)
{
    Console.WriteLine(i);
    i = i + 1;
}
iterationi (start)i 5i (end)
11true2
22true3
33true4
44true5
55true6
6false

The last row shows what happens after the fifth iteration: i is now 6, the condition 6 <= 5 is false, and the loop exits.

Each iteration, the loop displays the current value of i before updating it. So the output is 1, 2, 3, 4, 5, matching the “i (start)” column.

Every counting loop has three parts that must work together:

  1. Initialize the counter before the loop: int i = 1;
  2. Check the condition before each iteration: i <= 5
  3. Update the counter inside the loop: i = i + 1;

If any part is wrong, the loop misbehaves. Missing the update produces an infinite loop (i stays 1 forever). A wrong condition produces the wrong number of iterations.

An off-by-one error is a bug where a loop’s boundary is wrong by exactly one: the counter starts one too high or low, or the condition uses < when it should use <= (or vice versa), causing the loop to execute one too many or one too few times.

Off-by-one errors are the most common loop bug. Should the condition be < or <=? Should the counter start at 0 or 1? These choices depend on the problem. Tracing with an iteration table catches them. When your loop produces unexpected results, trace it.


Try it yourself.

Write a loop that displays the numbers 1 through n, where n is an integer variable. Trace your iteration table to verify the loop is correct.

Reveal answer
int i = 1;
 
while (i <= n)
{
    Console.WriteLine(i);
    i = i + 1;
}

The translation: “While i is less than or equal to n, execute the scope.”

The scope: evaluate i and display the result to the console. Add 1 to i and bind the result to i.

For n = 3, the iteration table:

iterationi (start)i 3i (end)
11true2
22true3
33true4
4false

Three iterations. The loop displays i at the start of each iteration, so the output is 1, 2, 3.

If your answer differed, note what you missed before continuing.


We can also count down:

int i = 5;
 
while (i >= 1)
{
    Console.WriteLine(i);
    i = i - 1;
}

The translation: “While i is greater than or equal to 1, execute the scope.”

The scope: evaluate i and display the result to the console. Subtract 1 from i and bind the result to i.

Output:

5
4
3
2
1

The three parts still coordinate: initialize at 5, check i >= 1, subtract 1 each iteration.

Shorthands for Updating

Writing i = i + 1 gets repetitive in loops. C# provides shorter ways to say the same thing.

i++ adds 1 to i. It means exactly i = i + 1.

i-- subtracts 1 from i. It means exactly i = i - 1.

For other amounts, compound assignment operators combine an operation with binding:

ShorthandMeaning
i += 3i = i + 3
i -= 2i = i - 2
i *= 4i = i * 4
i /= 2i = i / 2
i %= 3i = i % 3

These are shorthands, not new operations. The mechanism is the same in each case: evaluate the expression on the right, then bind the result back to the variable. For example, i += 3 evaluates i + 3 and binds the result to i.

From here on, we use i++ and i-- in loops.

The Accumulator Pattern

Counting loops do more than display numbers. A common pattern uses the loop to build up a result across iterations.

Problem: compute the sum of the integers 1 through n.

int n = 5;
int sum = 0;
int i = 1;
 
while (i <= n)
{
    sum += i;
    i++;
}
 
Console.WriteLine(sum);

The translation: “While i is less than or equal to n, execute the scope.”

The scope: add i to sum and bind the result to sum. Increment i.

Output: 15

The variable sum is an accumulator. It builds up a result across iterations. Before the loop, sum starts at 0. Each iteration adds the current value of i to sum.

An accumulator is a variable in memory that builds up a result across loop iterations.

An accumulator loop is a counting loop that builds up a result by combining each value into an accumulator.

Let’s trace the state:

int n = 5;
int sum = 0;
int i = 1;
 
while (i <= n)
{
    sum += i;
    i++;
}
iterationisum (start)sum += isum (end)
1100 + 11
2211 + 23
3333 + 36
4466 + 410
551010 + 515

After five iterations, sum holds 15. That is 1 + 2 + 3 + 4 + 5.

The accumulator starts at 0 because 0 is the identity value for addition, which means adding 0 to any number gives that number back. So 0 + n is always just n. Starting at 0 means the first iteration simply sets the accumulator to the first value being added. For multiplication, the identity value is 1, because multiplying any number by 1 gives that number back. For counting, the starting value is 0 because we have counted nothing yet.


Try it yourself.

Write code that computes the factorial of n. The factorial of n (written n!) is the product of all integers from 1 through n. For example, 5! = 1 × 2 × 3 × 4 × 5 = 120.

Hint: what should the accumulator start at, and what operation do you apply each iteration?

Reveal answer
int n = 5;
int product = 1;
int i = 1;
 
while (i <= n)
{
    product *= i;
    i++;
}
 
Console.WriteLine(product);

Output: 120

The accumulator starts at 1 because 1 is the identity for multiplication, which means multiplying any number by 1 gives that number back. Each iteration multiplies the current i into the product.

iterationiproduct (start)product *= iproduct (end)
1111 × 11
2211 × 22
3322 × 36
4466 × 424
552424 × 5120

Compare your code and trace to the model. Note any differences.


Filtering with Modulo

Loops become more interesting when we add a condition inside the body. Modulo is the workhorse here.

Problem: count how many even numbers exist between 1 and n.

int n = 10;
int count = 0;
int i = 1;
 
while (i <= n)
{
    if (i % 2 == 0)
    {
        count++;
    }
    i++;
}
 
Console.WriteLine(count);

The translation: “While i is less than or equal to n, execute the scope.”

The scope: if the remainder of i divided by 2 equals 0, increment count. Increment i.

Output: 5

This combines three things we’ve learned: a counting loop, an accumulator, and a conditional inside the body. The loop visits every number from 1 to n. The if statement tests each number with modulo. Only the even ones increment the counter.

A filter loop is a counting loop that tests each value against a condition and acts only on values that pass.

Let’s trace:

int n = 10;
int count = 0;
int i = 1;
 
while (i <= n)
{
    if (i % 2 == 0)
    {
        count++;
    }
    i++;
}
iterationii % 2 == 0count
11false0
22true1
33false1
44true2
55false2
66true3
77false3
88true4
99false4
1010true5

The count only increases on even iterations. After 10 iterations, count holds 5.

The pattern generalizes: loop over a range, test each value with a condition, accumulate the ones that pass. Change i % 2 == 0 to i % 3 == 0 to count multiples of 3. Change count++ to sum += i to sum the values instead of counting them.


Try it yourself.

Write code that computes the sum of all multiples of 3 between 1 and n (inclusive). For n = 12, the answer is 3 + 6 + 9 + 12 = 30.

Reveal answer
int n = 12;
int sum = 0;
int i = 1;
 
while (i <= n)
{
    if (i % 3 == 0)
    {
        sum += i;
    }
    i++;
}
 
Console.WriteLine(sum);

Output: 30

Self-correct against the model above.


FizzBuzz

Here is a classic problem that ties branching, looping, and modulo together.

For each number from 1 to n: if the number is divisible by both 3 and 5, display “FizzBuzz”. If divisible by 3 only, display “Fizz”. If divisible by 5 only, display “Buzz”. Otherwise, display the number.

int n = 15;
int i = 1;
 
while (i <= n)
{
    if (i % 3 == 0 && i % 5 == 0)
    {
        Console.WriteLine("FizzBuzz");
    }
    else if (i % 3 == 0)
    {
        Console.WriteLine("Fizz");
    }
    else if (i % 5 == 0)
    {
        Console.WriteLine("Buzz");
    }
    else
    {
        Console.WriteLine(i);
    }
 
    i++;
}

The translation: “While i is less than or equal to n, execute the scope.”

The scope: if the remainder of i divided by 3 equals 0 and the remainder of i divided by 5 equals 0, display “FizzBuzz” to the console. Otherwise, if the remainder of i divided by 3 equals 0, display “Fizz”. Otherwise, if the remainder of i divided by 5 equals 0, display “Buzz”. Otherwise, display the value of i. Increment i.

Output for n = 15:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

The order of conditions matters. We check “divisible by both” first. If we checked “divisible by 3” first, the number 15 would match that condition and display “Fizz” instead of “FizzBuzz”. The most specific condition must come first.

Notice that this uses if-else, not a switch expression. Each branch performs an action (displaying to the console), and the conditions involve relationships (i % 3 == 0) rather than matching the value of a single variable against patterns.

Wrapping Loops in Functions

The sum-from-1-to-n computation is useful enough to deserve a name. We can package it as a function.

Func<int, int> SumTo = n =>
{
    int sum = 0;
    int i = 1;
 
    while (i <= n)
    {
        sum += i;
        i++;
    }
 
    return sum;
};

Translation: “Define a function named SumTo that takes an integer n and returns the result of the scope.”

The scope: bind 0 to an integer variable named sum. Bind 1 to an integer variable named i. While i is less than or equal to n, add i to sum and bind the result to sum, then increment i. Return sum.

Now we can use it anywhere:

int total = SumTo(100);
Console.WriteLine(total);  // 5050

The translation: “Call SumTo with the value 100 and bind the returned value to an integer variable named total. Evaluate total and display the result to the console.”

The loop runs inside the function. The caller sees only the input and the result. This is the same function pattern from Chapter 1, applied to loops.


Try it yourself.

Write a function named CountDivisible that takes two integer parameters, n and d, and returns how many numbers from 1 to n are divisible by d.

Reveal answer
Func<int, int, int> CountDivisible = (n, d) =>
{
    int count = 0;
    int i = 1;
 
    while (i <= n)
    {
        if (i % d == 0)
        {
            count++;
        }
        i++;
    }
 
    return count;
};

CountDivisible(20, 4) returns 5, because the multiples of 4 up to 20 are 4, 8, 12, 16, and 20.

Compare your answer to the model. Note any differences.


The Search Pattern

Sometimes we are not accumulating a result across all values. We want to find the first value that meets a condition.

A search loop is a loop that examines values one at a time and stops when it finds one that meets a condition.

Problem: find the smallest number from 1 to n that is divisible by both 3 and 5.

int n = 100;
int i = 1;
bool found = false;
 
while (i <= n && !found)
{
    if (i % 3 == 0 && i % 5 == 0)
    {
        found = true;
    }
    else
    {
        i++;
    }
}
 
if (found)
{
    Console.WriteLine("Found: " + i);
}
else
{
    Console.WriteLine("Not found");
}

The translation: “While i is less than or equal to n and found is not true, execute the loop scope.”

The loop scope: if the remainder of i divided by 3 equals 0 and the remainder of i divided by 5 equals 0, bind true to found. Otherwise, increment i.

“If found evaluates to true, execute the first scope. Otherwise, execute the second scope.”

  • Scope 1: display “Found: ” joined with the value of i to the console.
  • Scope 2: display “Not found” to the console.

Output: Found: 15

The loop condition i <= n && !found encodes two constraints: we have not run out of numbers, and we have not found what we are looking for. The loop exits when either constraint is violated.

After the loop, we check found to determine which exit happened. If found is true, i holds the answer. If found is false, no number in the range satisfied the condition.

Notice that when we find the answer, we set found = true but do not increment i. The value of i at that point is the answer we want to keep.

A break keyword exists in C# that exits a loop immediately. We prefer encoding the exit condition in the loop’s condition itself. The condition is the loop’s contract with the reader. It says exactly when and why the loop continues.


Try it yourself.

Write code that finds the first power of 2 greater than 1000. Start with int power = 1; and double it each iteration.

Reveal answer
int power = 1;
 
while (power <= 1000)
{
    power *= 2;
}
 
Console.WriteLine(power);

Output: 1024

This is a simpler search. The condition itself encodes everything we need: keep going while the power is not yet above 1000. When the loop exits, power holds the first value that exceeded 1000. No found flag is needed because we know the loop will terminate (doubling always eventually exceeds any bound).

iterationpower (start)power *= 2power (end)
111 × 22
222 × 24
344 × 28
488 × 216
51616 × 232
63232 × 264
76464 × 2128
8128128 × 2256
9256256 × 2512
10512512 × 21024

After 10 iterations, power is 1024, which exceeds 1000. The loop exits.

Check your answer against the model.


Digit Manipulation

Here is a different kind of loop. Instead of counting from 1 to n, the loop processes a number by peeling off digits.

Two operators work as a pair for this:

  • % 10 extracts the last digit
  • / 10 removes the last digit
int n = 7364;
Console.WriteLine(n % 10);  // 4 (last digit)
Console.WriteLine(n / 10);  // 736 (everything except the last digit)

These two operations let us take a number apart, one digit at a time.

Problem: compute the sum of a number’s digits. For 7364, that is 7 + 3 + 6 + 4 = 20.

int n = 7364;
int digitSum = 0;
 
while (n > 0)
{
    digitSum += n % 10;
    n /= 10;
}
 
Console.WriteLine(digitSum);

The translation: “While n is greater than 0, execute the scope.”

The scope: add the remainder of n divided by 10 to digitSum and bind the result to digitSum. Divide n by 10 and bind the result to n.

Output: 20

Each iteration peels off the last digit with % 10, adds it to the accumulator, then removes it with /= 10. The number shrinks each iteration, guaranteed to reach 0.

Let’s trace:

int n = 7364;
int digitSum = 0;
 
while (n > 0)
{
    digitSum += n % 10;
    n /= 10;
}
iterationn (start)n % 10digitSumn /= 10
1736444736
273661073
3733137
477200

After four iterations, n is 0, the condition n > 0 is false, and the loop exits. The digit sum is 20.

This is a convergence loop. The number of iterations is not fixed in advance, but termination is guaranteed because n shrinks toward 0 every iteration. Integer division by 10 always produces a smaller non-negative value (for positive n), so the loop must eventually exit.


Try it yourself.

Write a function named CountDigits that takes an integer n and returns how many digits it has. For example, CountDigits(7364) returns 4.

Hint: the structure is similar to digit sum, but instead of accumulating the digits themselves, count how many times you can divide by 10 before reaching 0.

Reveal answer
Func<int, int> CountDigits = n =>
{
    int count = 0;
 
    while (n > 0)
    {
        count++;
        n /= 10;
    }
 
    return count;
};

For n = 7364:

iterationn (start)countn /= 10
173641736
2736273
37337
4740

After 4 iterations, n is 0 and count is 4. Note: this returns 0 for n = 0. If that case matters, handle it separately.

If your answer differed, note what you missed before continuing.


Review

Before continuing, test yourself on what you’ve learned. Use the protocol from Chapter 0: attempt each exercise from memory, then search this section to check your answers, then note what you missed.

Part 1: Definitions

Write the definitions from memory, then find them in this section to check.

  1. What is a switch expression?
  2. What are the two forms a pattern can take in a switch expression?
  3. What does the _ pattern match?
  4. What is a counting loop?
  5. What is an off-by-one error?
  6. What is an accumulator?
  7. What is an accumulator loop?
  8. What is a filter loop?
  9. What is a search loop?

If any of your answers differed from the definitions in this section, note what you missed and write the corrected version.

Part 2: Translations

Translate each piece of code to English.

  1.  string size = weight switch
     {
         > 100 => "heavy",
         > 50 => "medium",
         _ => "light"
     };
  2.  while (i <= n)
     {
         sum += i;
         i++;
     }
  3.  int n = int.Parse(Console.ReadLine());

Check your translations against the patterns shown in this section.

Part 3: Writing Code

Write C# code for each description.

  1. Write a switch expression that takes an integer variable named score and produces the string “pass” if score is 60 or above, and “fail” otherwise.

  2. Write a loop that computes the sum of all odd numbers from 1 to n. (An odd number has a remainder of 1 when divided by 2.)

  3. Write a function named SumDigits that takes an integer n and returns the sum of its digits.

Check your code against the examples in this section.

Part 4: Iteration Tables

Complete the iteration table for this code with n = 4:

int n = 4;
int result = 1;
int i = 1;
 
while (i <= n)
{
    result *= i;
    i++;
}
iterationiresult (start)result *= iresult (end)
1
2
3
4

Trace through each iteration. What value does result hold after the loop exits?

Part 5: Complete Programs

Write complete programs for these specifications.

  1. A program that asks the user for a positive integer and displays the sum of its digits. If the user enters a non-positive number, ask again until they provide a valid one.

  2. A program that asks the user for a number between 1 and 7 and displays the corresponding day of the week. Use a switch expression for the mapping. If the number is out of range, display “Invalid day”.

Check your programs by tracing through them with sample inputs.


You now know how to branch over numeric ranges, map values to results with switch expressions, count with loops, accumulate results, filter with modulo, search for values, and take numbers apart digit by digit. In the next section, we’ll define our own numeric computations as functions.

Previous: Section 2 - Computation

Next: Section 4 - Functions