Signature
count_chars(string $string, int $mode = 0): array|stringPurpose
count_chars() looks at every byte in a string and reports how often each byte value (0 to 255) occurs. Depending on the mode you choose, you get a frequency table, a list of the bytes that appear, or a list of the bytes that never appear. It is handy for quick character-frequency analysis, simple anagram checks and sanity checks on raw data.
The important word is byte. The function does not know about UTF-8 characters, so a multibyte character is counted as several separate byte values.
Signature
Parameters and defaults
$stringis the string to examine.$modeselects the shape of the result. It defaults to0.
The five modes:
| Mode | Result |
|---|---|
0 |
An array with all 256 byte values as keys and their frequencies as values, including zeros |
1 |
The same array, but only byte values with a frequency above zero |
2 |
The same array, but only byte values with a frequency of exactly zero |
3 |
A string made of every unique byte that appears, in ascending byte order |
4 |
A string made of every byte value that does not appear, in ascending byte order |
Return value and failure cases
Modes 0, 1 and 2 return an array whose keys are integers. Modes 3 and 4 return a string. That is why the declared return type is array|string.
An out-of-range $mode raises a ValueError in PHP 8. The manual's changelog notes that before PHP 8.0 the function returned false on failure, so older code that checks for false is no longer relevant.
Example 1: a readable frequency table
The keys returned by mode 1 are byte numbers, not letters. Use chr() to turn them back into characters when you want readable output.
$counts = count_chars("banana", 1);
$result = [];
foreach ($counts as $byte => $count) {
$result[chr($byte)] = $count;
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"a":3,"b":1,"n":2}
The entries come out in ascending byte order, so a precedes b, which precedes n.
Example 2: unique and unused bytes
Mode 3 gives you the distinct bytes as one string. Mode 4 gives you everything that is missing, which for ordinary text is almost the whole byte range.
$text = "hello world";
$unique = count_chars($text, 3);
$unused = count_chars($text, 4);
echo json_encode([
'unique' => $unique,
'uniqueCount' => strlen($unique),
'unusedCount' => strlen($unused),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"unique":" dehlorw","uniqueCount":8,"unusedCount":248}
The unique string begins with a space because byte 32 sorts before the letters. The eight unique bytes and the 248 unused ones add up to 256.
Example 3: anagram check and the byte caveat
Two strings with identical mode 1 results contain the same bytes with the same frequencies. Because both arrays are built in key order, a strict === comparison works.
function isAnagram(string $a, string $b): bool {
return count_chars($a, 1) === count_chars($b, 1);
}
echo json_encode([
'anagram' => isAnagram("listen", "silent"),
'different' => isAnagram("listen", "listens"),
'bytes' => strlen("é"),
'distinctBytes' => count(count_chars("é", 1)),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"anagram":true,"different":false,"bytes":2,"distinctBytes":2}
The last two entries show the byte-oriented nature of the function. In UTF-8 the single character é occupies two bytes, so the string reports two distinct byte values rather than one character.
Edge cases
- An empty string with mode
1returns an empty array. With mode0it returns 256 entries, all zero. - Mode
3on an empty string returns an empty string, while mode4returns all 256 byte values as a string. - Counting is case-sensitive:
A(65) anda(97) are different keys. Normalize the case first if you need case-insensitive counts. - Spaces, punctuation and control characters such as newlines are counted like any other byte.
- Strings holding binary data work fine, since the function simply reads bytes.
Common mistakes
- Treating keys as characters. Mode
0,1and2keys are integers. Wrap them inchr()to display the character. - Expecting character counts from UTF-8 text. For multibyte-aware counting you need a different approach, such as splitting the string with multibyte functions first.
- Using mode
0and then testing for presence. Every key exists in mode0, soisset()is always true for those keys. Check the value is greater than zero, or use mode1. - Passing an invalid mode. Only integers 0 to 4 are valid; other values raise a
ValueError. - Confusing it with
substr_count().count_chars()counts single bytes across the whole string, whilesubstr_count()counts occurrences of one specific substring.
Related functions
strpos()finds the position of the first occurrence of a substring.substr_count()counts how many times a particular substring occurs.chr()andord()convert between byte values and one-byte strings.str_split()andarray_count_values()offer a more flexible, if slower, route to frequency tables.
Recap
count_chars() summarizes the bytes of a string. Modes 0 to 2 return arrays keyed by byte value, mode 3 returns the used bytes as a string and mode 4 returns the unused ones. Remember that it counts bytes, not characters, that keys are integers, and that invalid modes throw a ValueError in PHP 8.
Tested examples
$counts = count_chars("banana", 1);
$result = [];
foreach ($counts as $byte => $count) {
$result[chr($byte)] = $count;
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
{"a":3,"b":1,"n":2}
$text = "hello world";
$unique = count_chars($text, 3);
$unused = count_chars($text, 4);
echo json_encode([
'unique' => $unique,
'uniqueCount' => strlen($unique),
'unusedCount' => strlen($unused),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
{"unique":" dehlorw","uniqueCount":8,"unusedCount":248}
function isAnagram(string $a, string $b): bool {
return count_chars($a, 1) === count_chars($b, 1);
}
echo json_encode([
'anagram' => isAnagram("listen", "silent"),
'different' => isAnagram("listen", "listens"),
'bytes' => strlen("é"),
'distinctBytes' => count(count_chars("é", 1)),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
{"anagram":true,"different":false,"bytes":2,"distinctBytes":2}