Question
Why does the following PHP loop continue printing values such as aa, ab, and yz instead of stopping after z? How can it be changed to print only the letters A through Z?
<?php
for ($i = 'a'; $i <= 'z'; $i++) {
echo "$i\n";
}
Short Answer
You will learn how PHP increments alphabetic strings, why the loop condition still accepts values such as aa, and how to use numeric loops with chr() or range() when you need exactly one alphabet letter at a time.
Concept
PHP's increment operator (++) does more than add one to numbers. When applied to an ASCII letter string, it follows an alphabetic carry rule:
'a'++becomes'b''y'++becomes'z''z'++becomes'aa''az'++becomes'ba'
That makes the values resemble spreadsheet columns or an odometer. Therefore, incrementing 'z' does not produce a value that ends the loop; it produces 'aa'.
The condition also uses string comparison:
$i <= 'z'
For non-numeric strings, PHP compares them lexicographically (dictionary-like, character by character). A value beginning with a through y, such as 'aa' or 'yz', compares as less than 'z'. The loop finally stops when the incremented value becomes 'za', because 'za' is greater than 'z'.
This matters because code that appears to use letters as a simple sequence is actually using two string features at once: string incrementing and string comparison. They are usually less clear than using numeric indexes for a fixed alphabet range.
Mental Model
Think of a string being incremented as an odometer with letters instead of digits.
- After
a, the next value isb. - After
z, there is no next single-letter position, so PHP adds another position:aa. - After
az, the rightmost letter rolls over toaand the left letter advances:ba.
The loop's stop sign says, “continue while the value is no greater than z.” Values beginning with a through y are still before z, so the stop sign does not stop the loop immediately after z.
Syntax and Examples
Use a numeric counter when you want a known number of letters. ord() converts a character to its character code, and chr() converts a character code back to a character.
<?php
for ($code = ord('A'); $code <= ord('Z'); $code++) {
echo chr($code) . PHP_EOL;
}
This starts at the code for A, increases one number at a time, and stops at the code for Z. chr($code) turns each code back into its corresponding letter.
For this exact task, range() is also concise:
<?php
foreach (range('A', 'Z') as $letter) {
echo $letter . PHP_EOL;
}
range('A', 'Z') creates an array containing the one-letter strings from A to Z.
The original behavior can be seen directly:
<?php
$letter = 'z';
$letter++;
echo $letter; // aa
Step by Step Execution
Consider a smaller version of the original loop:
<?php
for ($i = 'x'; $i <= 'z'; $i++) {
echo $i . ' ';
}
Execution proceeds as follows:
$istarts as'x'. Since'x' <= 'z', PHP printsx.$i++changes'x'to'y'.'y' <= 'z', so PHP printsy.$i++changes'y'to'z'.'z' <= 'z', so PHP printsz.$i++changes'z'to'aa', not to a value afterzin the usual single-letter sense.'aa' <= 'z'is true becauseacomes beforez, so PHP printsaa.- The loop continues through values such as
ab,az,ba, andyz. - After
'yz', string increment produces'za'. Since'za' > 'z', the condition is false and the loop ends.
A numeric version has a much more direct trace:
<?php
for ($code = ord('X'); $code <= ord('Z'); $code++) {
echo chr($code) . ' ';
}
It prints X Y Z because the numeric values increase until they exceed the code for Z.
Real World Use Cases
Alphabetic sequences appear in several practical situations:
- Spreadsheet-style labels: Generate
A,B, ...,Z,AA,ABfor columns or sections. PHP string increment can be useful when this extended sequence is actually desired. - Multiple-choice labels: Display answer labels from
AtoDorAtoZ. A numeric loop pluschr()makes the fixed limit explicit. - Report headings: Create numbered sections with alphabetic subheadings.
- Data imports: Map column positions to letters while reading or exporting CSV and spreadsheet-like data.
- Test data: Generate predictable one-letter category codes for fixtures or demonstrations.
Choose the approach based on the requirement: use a fixed numeric range for exactly A–Z, and use an extended label generator if AA, AB, and beyond are valid output.
Real Codebase Usage
In production code, developers usually avoid relying on a string's ++ behavior for a fixed alphabet range. Numeric boundaries are easier to review and less likely to produce accidental extra labels.
A reusable helper makes intent clear:
<?php
function alphabetLetters(string $start = 'A', string $end = 'Z'): array
{
if (strlen($start) !== 1 || strlen($end) !== 1) {
throw new InvalidArgumentException('Start and end must each be one character.');
}
if (ord($start) > ord($end)) {
return [];
}
$letters = [];
for ($code = ord($start); $code <= ord($end); $code++) {
$letters[] = chr($code);
}
return $letters;
}
print_r(alphabetLetters('A', 'D'));
Common project patterns include:
- Validation: Check that inputs are single letters before converting them with
ord(). - Guard clauses: Return early when the start boundary is greater than the end boundary.
- Data transformation: Use
range('A', 'Z')witharray_map()orforeachto build select options, labels, or configuration values. - Explicit bounds: Store the number of choices or the final letter in a named variable instead of relying on an implicit string rollover rule.
For spreadsheet-style labels beyond Z, use a dedicated function and test boundary cases such as Z → AA and AZ → BA.
Common Mistakes
Assuming z++ produces an invalid or terminal value
<?php
$letter = 'z';
$letter++;
echo $letter; // aa
PHP treats alphabetic strings as incrementable sequences. Do not use 'z' as a stopping boundary if string incrementing is also used and only single letters are allowed.
Expecting <= 'z' to mean “one letter up to z”
<?php
for ($i = 'a'; $i <= 'z'; $i++) {
echo $i;
}
The comparison is about string order, not string length. Values such as 'aa' can still compare as less than 'z'.
Using range() when you need labels beyond Z
<?php
print_r(range('A', 'Z'));
This is correct for one-letter labels, but it does not create AA, AB, and later spreadsheet-style labels. Use a specific multi-letter label algorithm when that is the actual requirement.
Mixing uppercase and lowercase boundaries
<?php
for ($code = ord('a'); $code <= ord('Z'); $code++) {
echo chr($code);
}
The start and end codes are in the wrong order, so the loop does not run. Keep the same case, such as A to Z or a to z.
Forgetting that ASCII ranges contain non-letters between Z and a
Do not attempt to print both uppercase and lowercase letters with one continuous character-code loop from A to z. That interval includes punctuation characters such as [, \, and _. Use two separate ranges instead.
Comparisons
| Approach | Best for | Result after Z | Notes |
|---|---|---|---|
String increment: $letter++ | Spreadsheet-like labels | AA | Convenient for extended labels, but boundaries need care. |
Numeric loop with ord() and chr() | A precise character-code range | Loop ends at the chosen code | Explicit and suitable for exactly A–Z. |
range('A', 'Z') | A simple fixed list of letters | Array ends at Z | Readable when you want to iterate over the letters. |
| Array of labels | Custom or non-contiguous labels | Defined by the array | Best for labels such as A, B, D, N/A. |
for is useful when you are advancing a numeric index. foreach is usually clearer when you already have the values to process, such as the array returned by range().
Cheat Sheet
$value++can increment alphabetic ASCII strings in PHP.'a'++→'b''z'++→'aa''az'++→'ba'- String comparisons such as
'aa' <= 'z'are lexicographic for non-numeric strings. - Use this for exactly uppercase letters:
for ($code = ord('A'); $code <= ord('Z'); $code++) {
echo chr($code) . PHP_EOL;
}
- Or iterate over a fixed range:
foreach (range('A', 'Z') as $letter) {
echo $letter . PHP_EOL;
}
- Do not use a continuous
ord('A')toord('z')range; it includes non-letter ASCII characters. - Use string increment deliberately when
AA,AB, and later labels are expected.
FAQ
Why does PHP turn z into aa?
PHP applies an alphabetic carry rule when incrementing ASCII letter strings. The z rolls over to a, and a new leading a is added.
Why is aa less than or equal to z in PHP?
Non-numeric strings are compared lexicographically. Since the first a in aa comes before z, aa compares as less than z.
How do I print only A through Z in PHP?
Use range('A', 'Z') in a foreach, or loop from ord('A') through ord('Z') and convert each value with chr().
Is for ($i = 'A'; $i <= 'Z'; $i++) safe for printing A to Z?
No. After Z, PHP increments the value to AA, which can still satisfy the string comparison and continue the loop.
Can I use string increment for spreadsheet column names?
Yes, it can produce a sequence such as A through Z, then AA, AB, and so on. Still, put an explicit, tested ending rule around it.
Does range('A', 'Z') include AA after Z?
No. It creates the fixed one-character range from A through Z.
Should I use "\n" or PHP_EOL for new lines?
"\n" is common for output intended for Unix-style text. PHP_EOL uses the platform's native line ending and is often convenient in command-line scripts.
Mini Project
Description
Build a command-line-style letter label generator for a quiz, report, or form. It should create exactly the requested uppercase labels without accidentally continuing to AA after Z.
Goal
Create a function that returns uppercase labels from a starting letter through an ending letter, with validation for invalid ranges.
Requirements
Requirement 1
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.