# Loops & Control Flow

So far our programs have run straight from top to bottom. Real programs need to make decisions ("if this customer is overdue, flag the invoice") and repeat work ("for every row in the dataset, clean it up"). That's what control flow is for.

## Making decisions with `if`

```{code-block} julia
balance = 1200

if balance > 1000
    println("Premium account")
end
```

Add `elseif` to test more conditions, and `else` for the fallback:

```{code-block} julia
function classify(balance)
    if balance > 10_000
        println("Status: VIP")
    elseif balance > 1000
        println("Status: Premium")
    else
        println("Status: Standard")
    end
end

classify(25_000)   # Status: VIP
classify(1200)     # Status: Premium
classify(50)       # Status: Standard
```

Three beginner notes that trip people up:

- The condition does **not** need parentheses, and there's no colon at the end of the line.
- Indentation is for humans, not the compiler. Julia uses the **`end`** keyword to close blocks. Coming from Python this feels like extra typing; coming from anywhere else it feels normal. Either way, indent consistently — four spaces is the community convention.
- It's `elseif`, one word. Not `elif`, not `else if`.

:::{warning} Conditions must actually be `Bool`
Julia will not accept a number where a boolean belongs:

```{code-block} julia
julia> if 1
           println("nope")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context
```

No truthiness. `0` isn't false, an empty array isn't false. Write the comparison you actually mean: `if count > 0`, `if !isempty(items)`. It's stricter than Python, and it catches real bugs.
:::

### The ternary operator

For picking one of two values, there's a compact form:

```{code-block} julia
a, b = 7, 12
smaller = a < b ? a : b       # 7
```

Read it as: "is `a < b`? then `a`, otherwise `b`." The spaces around `?` and `:` are required.

### Short-circuit operators as statements

This is idiomatic Julia and you'll see it constantly in real code:

```{code-block} julia
x > 0 || error("x must be positive")
isvalid(record) && process(record)
```

`||` only evaluates its right side if the left is `false`; `&&` only if the left is `true`. So the first line reads "either `x` is positive, or we throw an error" — a guard clause in one line. Perfectly readable once you've seen it a few times.

## Repeating with `for`

A `for` loop walks through the items of a collection, one at a time. This is the loop you'll use most.

```{code-block} julia
temps = [20.5, 22.3, 19.8, 25.1]

for temp in temps
    println("$temp °C")
end
```

Each time around, `temp` holds the next value. You can write `∈` instead of `in` if you're feeling mathematical (`\in` + Tab); `=` also works and you'll see it in older code.

### Counting with ranges

```{code-block} julia
for i in 1:4
    println(i)        # prints 1, 2, 3, 4
end
```

`1:4` is a **range**, and it's worth knowing that it's not a list — it's a tiny object that stores its start, step, and stop, and generates values as needed. `1:1_000_000_000` costs almost no memory.

Ranges take a step in the middle:

```{code-block} julia
for x in 9:-3:3           # start:step:stop
    println(x)            # 9, 6, 3
end
```

Note the ordering: `start:step:stop`, with the step in the *middle*. Python programmers reach for `range(9, 0, -3)` and get it wrong the first time. I certainly did.

### Looping over data with its index

Three patterns, each right in a different situation:

```{code-block} julia
temps = [20.5, 22.3, 19.8, 25.1]

# 1. You only need the value
for t in temps
    println(t)
end

# 2. You need position and value — this is usually what you want
for (day, t) in enumerate(temps)
    println("Day $day → $t °C")
end

# 3. You need the raw indices (e.g. to write back into the array)
for i in eachindex(temps)
    temps[i] = temps[i] * 1.8 + 32
end
```

Prefer `eachindex(temps)` over `1:length(temps)`. They're identical for ordinary arrays, but `eachindex` also does the right thing for arrays with unusual index ranges, and it lets the compiler skip some bounds checks. It's a free habit — pick it up now.

### Nested loops, the Julia way

Julia lets you flatten nested loops into a single `for`:

```{code-block} julia
for i in 1:2, j in 1:3
    println("($i, $j)")
end
```

That's exactly equivalent to two nested loops, just less indented. Note that `break` inside it exits *both* levels, since it's really one loop.

## Repeating with `while`

Use `while` when you don't know in advance how many times you'll loop.

```{code-block} julia
countdown = 5
while countdown > 0
    println(countdown)
    countdown -= 1
end
println("Liftoff!")
```

The danger with `while` is the *infinite loop*: if the condition never becomes false, your program runs forever. Here, `countdown -= 1` is what guarantees we eventually stop. Always make sure something inside the loop moves you toward the exit. (And if you do hang the REPL, **Ctrl-C** is your friend.)

## Steering loops: `break` and `continue`

`break` exits the loop immediately:

```{code-block} julia
invoices = [200, 450, -1, 700]

for amount in invoices
    if amount < 0
        println("Bad record found, stopping.")
        break
    end
    println("Processing invoice: $amount")
end
```

`continue` skips the rest of the current pass and jumps to the next item:

```{code-block} julia
invoices = [200, 450, -1, 700]
total = 0

for amount in invoices
    amount < 0 && continue        # ignore bad records, keep going
    total += amount
end

println("Clean total: $total")    # 1350
```

`break` says "I'm done with this loop entirely." `continue` says "skip this one, but keep looping."

## When things go wrong: `try` / `catch`

Sometimes a line of code fails and you'd rather handle it than crash.

```{code-block} julia
raw = ["1200", "800", "n/a", "1500"]
total = 0

for s in raw
    try
        total += parse(Int, s)
    catch e
        println("Skipping unparseable value: $s")
    end
end

println("Total: $total")     # 3500
```

Two words of caution, because this is easy to overuse.

First, `try`/`catch` in Julia has real runtime cost, and — more importantly — it blocks some compiler optimizations in the surrounding code. Don't put it inside a hot loop that runs a million times. For the parsing case above, `tryparse` returns `nothing` on failure instead of throwing, and is much cheaper:

```{code-block} julia
for s in raw
    v = tryparse(Int, s)
    v === nothing && continue
    global total += v
end
```

Second, catching *everything* hides bugs. If you only expect a parse failure, catch that specifically. `finally` is available too, for cleanup that must happen either way:

```{code-block} julia
f = open("ledger.csv")
try
    process(f)
finally
    close(f)
end
```

Though for files specifically, the block form handles this for you:

```{code-block} julia
open("ledger.csv") do f
    process(f)
end                        # closed automatically, even if process() throws
```

## A small, real example

Let's tie it together. Imagine we're scanning a week of account balances and want to count how many days fell below a threshold — the kind of quick check you'd do before feeding data to a model.

```{code-block} julia
function count_low_days(balances, threshold)
    low_days = 0
    for (day, balance) in enumerate(balances)
        if balance < threshold
            low_days += 1
            println("Day $day is low: $balance")
        end
    end
    return low_days
end

balances = [1200, 800, 1500, 300, 950, 2000, 600]
n = count_low_days(balances, 1000)
println("Total low-balance days: $n")
```

Run it and you'll see exactly which days fell short, plus a final count. That single pattern — *loop over data, test a condition, accumulate a result* — is the backbone of an enormous amount of data work.

:::{note} Why did I wrap that in a function?
I could have written the loop at the top level of a script and it would have produced the same output. But code inside a function gets **compiled and type-specialized**; code at the top level of a script does not, because its variables are globals whose types could change at any moment.

This is the most consequential piece of Julia advice in the whole book, so I'm planting it here in chapter four rather than saving it for the performance chapter: **put your real work inside functions.** The same loop can be an order of magnitude faster purely by living inside a `function`. We'll dig into exactly why in the *Performance* chapter.
:::

## Try it yourself

1. Print the numbers 1 through 10, but skip every multiple of 3.
2. Given `prices = [9.99, 19.50, 4.25, 30.00]`, compute and print the total using a loop.
3. Starting from 100.0, repeatedly halve a number with a `while` loop, printing each step, until it drops below 1.
4. Given `readings = [20.5, -99.0, 22.3, -99.0, 19.8]` where `-99.0` means "sensor failed", compute the average of only the valid readings.

### Solutions

```{code-block} julia
# 1.
for n in 1:10
    n % 3 == 0 && continue
    println(n)
end

# 2.
function total_price(prices)
    total = 0.0
    for p in prices
        total += p
    end
    return total
end
println(total_price([9.99, 19.50, 4.25, 30.00]))

# 3.
value = 100.0
while value >= 1.0
    println(value)
    global value /= 2.0        # `global` needed only at the top level of a script
end

# 4.
function valid_mean(readings, sentinel = -99.0)
    total = 0.0
    count = 0
    for r in readings
        r == sentinel && continue
        total += r
        count += 1
    end
    return count == 0 ? NaN : total / count
end
println(valid_mean([20.5, -99.0, 22.3, -99.0, 19.8]))   # 20.866...
```

You can now branch and repeat, which means you can express real logic. Next we'll package that logic into reusable **functions** — and in Julia, functions are considerably more interesting than they first appear.
