Strings7 min readExamples run on PHP 8.5.10

count_chars()

Learn how PHP 8.4 count_chars() tallies byte values in a string, what each of its five modes returns, and which mistakes to avoid.

Signature

count_chars(string $string, int $mode = 0): array|string

Purpose

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

  • $string is the string to examine.
  • $mode selects the shape of the result. It defaults to 0.

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.

Example
<?php
$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";
Output
{"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.

Example
<?php
$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";
Output
{"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.

Example
<?php
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";
Output
{"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 1 returns an empty array. With mode 0 it returns 256 entries, all zero.
  • Mode 3 on an empty string returns an empty string, while mode 4 returns all 256 byte values as a string.
  • Counting is case-sensitive: A (65) and a (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

  1. Treating keys as characters. Mode 0, 1 and 2 keys are integers. Wrap them in chr() to display the character.
  2. 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.
  3. Using mode 0 and then testing for presence. Every key exists in mode 0, so isset() is always true for those keys. Check the value is greater than zero, or use mode 1.
  4. Passing an invalid mode. Only integers 0 to 4 are valid; other values raise a ValueError.
  5. Confusing it with substr_count(). count_chars() counts single bytes across the whole string, while substr_count() counts occurrences of one specific substring.
  • strpos() finds the position of the first occurrence of a substring.
  • substr_count() counts how many times a particular substring occurs.
  • chr() and ord() convert between byte values and one-byte strings.
  • str_split() and array_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

Frequency table with readable characters
<?php
$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";
Output
{"a":3,"b":1,"n":2}
Unique and unused bytes with modes 3 and 4
<?php
$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";
Output
{"unique":" dehlorw","uniqueCount":8,"unusedCount":248}
Anagram check and the byte caveat
<?php
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";
Output
{"anagram":true,"different":false,"bytes":2,"distinctBytes":2}