A Zig port of libphonenumber, Google's library for parsing, formatting and validating international phone numbers.
  • Zig 96.9%
  • Nix 1.6%
  • Python 1.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie e9516a0038
All checks were successful
test / test (push) Successful in 9m25s
test / docs (push) Successful in 8m21s
Begin 0.4.0 development
0.3.0 is tagged, with libphonenumber v9.0.42.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01699fj25qmtgJG6jsB8tcGe
2026-10-23 13:32:20 -05:00
.forgejo/workflows Check formatting with a build step that skips generated and fetched code 2026-09-24 08:38:05 -05:00
LICENSES Compile libphonenumber's metadata into Zig source 2026-09-10 22:18:51 -05:00
src Build for other targets than the host 2026-10-08 16:08:10 -05:00
tests Check the library against every real numbering plan 2026-09-10 22:18:54 -05:00
tools Build for other targets than the host 2026-10-08 16:08:10 -05:00
.gitignore Stop tracking the result symlink from nix build 2026-09-24 08:38:35 -05:00
build.zig Build for other targets than the host 2026-10-08 16:08:10 -05:00
build.zig.zon Begin 0.4.0 development 2026-10-23 13:32:20 -05:00
build.zig.zon.nix Update zon2nix and regenerate build.zig.zon.nix 2026-10-23 10:23:47 -05:00
flake.lock Update zon2nix and regenerate build.zig.zon.nix 2026-10-23 10:23:47 -05:00
flake.nix Put QEMU's user-mode emulators in the devshell 2026-10-08 16:13:51 -05:00
package.nix Begin 0.4.0 development 2026-10-23 13:32:20 -05:00
README.md Put QEMU's user-mode emulators in the devshell 2026-10-08 16:13:51 -05:00
REUSE.toml Name the project after the repository it lives in 2026-09-10 22:18:55 -05:00

zig-phonenumber

A Zig 0.17 port of libphonenumber, Google's library for parsing, formatting and validating international phone numbers. For Zig 0.16 use the zig-0.16 branch, which starts from the 0.1.0 release.

The API documentation is generated from the doc comments, which is where most of the explanation is.

The repository's home is my Forgejo instance at git.jcollie.dev/jeff/zig-phonenumber, which is where CI runs. It is mirrored on Tangled at tangled.org/jcollie.dev/zig-phonenumber, and also published on Radicle, a peer-to-peer forge built on git, where the copy needs no account and no server anyone has to keep running.

$ git clone https://git.jcollie.dev/jeff/zig-phonenumber.git

$ git clone https://tangled.org/jcollie.dev/zig-phonenumber

$ rad clone rad:z3tmDBe7SJj9xhbQbkEP9GHqzFmQH

Any of them is the whole project, on the main branch, with the same history. A Radicle repository is findable only by that identifier, and rad clone looks for seeds through your own node's routing table rather than through a known host, so the node has to be running first:

$ rad node start

What is here

Util Parse, format, validate, and say what type a number is. A port of PhoneNumberUtil.
AsYouTypeFormatter Format a number while it is being typed, one character at a time.
matcher.Matcher Find phone numbers in free text, at four levels of strictness.
ShortNumberInfo Emergency numbers and short codes, which have their own rules and their own metadata.
Geocoder, CarrierMapper, TimeZonesMapper What a number's prefix says about where it is, whose network it is on, and what time it is there.
const phonenumbers = @import("phonenumbers");

var util: phonenumbers.Util = .init(gpa);
defer util.deinit();

const number = try util.parse("(650) 253-0000", "US");
try util.isValidNumber(number);                     // true

var buffer: [64]u8 = undefined;
try util.formatBuf(number, .e164, &buffer);         // "+16502530000"
try util.formatBuf(number, .international, &buffer); // "+1 650-253-0000"

A PhoneNumber is a plain value with no owned memory, so it copies and stores freely and needs no deinit. A Util owns a cache of compiled patterns, so it does.

There is a command-line tool as well, which is the quickest way to see what the library makes of something:

$ zig build run -- "+1 650 253 0000"
  country code    1
  region          US
  type            fixed_line_or_mobile
  valid           true
  possible        is_possible
  e164            +16502530000
  international   +1 650-253-0000
  national        (650) 253-0000
  rfc3966         tel:+1-650-253-0000
  location        Mountain View, CA
  time zones      America/Los_Angeles

How faithful is it

libphonenumber's own unit tests are ported and run against the same deliberately artificial metadata the Java suite uses, so the expectations here are upstream's rather than rewritten: PhoneNumberUtilTest, AsYouTypeFormatterTest, PhoneNumberMatcherTest, ShortNumberInfoTest, and the three mapper suites. ExampleNumbersTest is ported too, and that one runs against the real metadata: every numbering plan carries an example number for each type of number it has, so it exercises the library against two hundred and forty-six real dialling plans at once.

The metadata compiler is checked against ground truth rather than only against itself. Upstream's own compiled metadata sits in its repository for the same revision, and decoding it and comparing field by field gives 246 numbering plans, 241 short-number plans, 46 alternate-format entries and 34 test plans with no differences at all.

Three things differ from Java on purpose:

  • The geocoder's country-name fallback is a hook. Where the prefix data has nothing to say, upstream falls back to the name of the number's country in the caller's language, which it takes from the JDK's locale data. That is CLDR rather than anything libphonenumber ships, so Geocoder.country_name is left for a caller who has such a table.
  • Characters are code points, not UTF-16 units. This matters only for numbers written in mathematical alphanumeric digits, where Java's own normalizer and its own regular expressions already disagree with each other.
  • AsYouTypeFormatter.rememberedPosition is a byte offset, where Java's is a UTF-16 offset. The two agree for anything made of digits and punctuation.

Metadata

Nothing is vendored. libphonenumber is a dependency in build.zig.zon, pinned to a release tag, and the metadata is compiled out of it as part of zig build -- so the version this port speaks for is one line that can be seen and bumped, and there is nothing checked in that could drift from it:

$ zig fetch --save=libphonenumber \
    https://github.com/google/libphonenumber/archive/refs/tags/v9.0.15.tar.gz
$ zig build test

The archive is 13 MB to fetch and 107 MB unpacked in the package cache, most of which is the per-region source the upstream build tooling uses and this does not read. That is the price of not carrying the data in this repository.

zig build metadata writes the generated source to zig-out/metadata/ for reading, which is the first place to look when a number validates unexpectedly.

Every pattern in the metadata is compiled by zregex as it is generated, so a pattern the runtime engine could not handle fails the build rather than turning into a number that quietly refuses to validate.

XML reading is zxml's.

Which languages the mappers cover

English geocoding alone is six and a half megabytes of text and there are thirty-three languages of it, so which ones are compiled in is a build option and only English is there by default:

$ zig build -Dmapper-languages=en,de,fr
$ zig build -Dmapper-languages=all        # twelve megabytes of data

Asking for a language that was left out gives an empty description rather than an error, which is the same answer libphonenumber gives for a language it has no data for.

Building

$ nix develop
$ zig build test
$ zig build docs-serve     # read the API documentation at localhost:8000
$ zig build fmt            # check formatting; -Dfix rewrites instead

Other targets are checked with zig build check -Dtarget=.... On Linux the devshell has QEMU's user-mode emulators, so the tests also run for other architectures, and on x86_64 Linux it has a 64-bit Wine, so they run for Windows too:

$ zig build test -Dtarget=aarch64-linux -fqemu
$ zig build test -Dtarget=x86_64-windows -fwine

Fuzzing

tests/fuzz.zig holds the properties -- parsing anything terminates and reports, a valid number survives a round trip through E.164, every match is a substring of the text it was found in -- and zig build test runs them against a corpus of seeds. To search for new inputs:

$ zig build fuzz-run                        # a minute of each target
$ zig build fuzz-run -- --target roundtrip --seconds 300

That is this project's own loop, which mutates a corpus of real numbers and watches for hangs. Zig's own fuzzer runs the same targets with coverage feedback:

$ zig build fuzz --fuzz                     # until interrupted, with a web interface
$ zig build fuzz --fuzz=1M                  # a bounded run, then a report

Licence

Apache-2.0, the same as libphonenumber, since this is a derivative work of it: the code is a port of upstream's, and the metadata it compiles is upstream's. The project follows the REUSE specification; reuse lint checks it.