- Zig 96.9%
- Nix 1.6%
- Python 1.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
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_nameis 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.rememberedPositionis 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.