Phone numbers and postal codes
On this page
A phone number is kept in its international form and a postal code in its country's form, but people type both in their own way. These types read what people type, given the country they typed it in, and show a phone number as the reader's region dials it.
Here a shop takes a customer's phone number and postcode as they type them in their own country, and shows the number to its staff as their region dials it:
type TypedText = Text {}type PhoneUnreadable = Failure { kind: FailureKind.invalid_value}type PostcodeUnreadable = Failure { kind: FailureKind.invalid_value}/// A customer's phone number as they typed it in their country: national/// or international, with any spaces and brackets.function typed_phone(raw: TypedText, typed_in: Country) fails PhoneUnreadable -> PhoneNumber { var phone: PhoneNumber = attempt text.parse(raw, as: PhoneNumber, country: typed_in) { malformed: PhoneUnreadable } return phone}/// A customer's postcode as they typed it, in their country's own form.function typed_postcode(raw: TypedText, typed_in: Country) fails PostcodeUnreadable -> PostalCode { var postcode: PostalCode = attempt text.parse(raw, as: PostalCode, country: typed_in) { malformed: PostcodeUnreadable } return postcode}/// A number as someone in the staff_locale's region dials it.function dialled_as(phone: PhoneNumber, staff_locale: Locale) -> PresentationText { return text.format(phone, locale: staff_locale)}
test "a phone number is read as it is typed and kept in E.164" { var mobile: PhoneNumber = attempt typed_phone("07400 123456", Country.gb) assert mobile == PhoneNumber("+447400123456") var from_abroad: PhoneNumber = attempt typed_phone("+44 (0)7400 123456", Country.fr) assert from_abroad == mobile assert typed_phone("07400 12345", Country.gb) fails PhoneUnreadable assert typed_phone("07400 123456", Country.us) fails PhoneUnreadable}test "a phone number is shown as the staff_locale's region dials it" { var mobile: PhoneNumber = PhoneNumber("+447400123456") assert dialled_as(mobile, Locale.en_gb) == "07400 123456" assert dialled_as(mobile, Locale.fr_fr) == "+44 7400 123456" assert dialled_as(PhoneNumber("+12015550123"), Locale.en_us) == "(201) 555-0123"}test "a postcode is in its country's form" { var royal_code: PostalCode = attempt typed_postcode("sw1a1aa", Country.gb) assert royal_code == PostalCode("SW1A 1AA") var us_code: PostalCode = attempt typed_postcode("221621010", Country.us) assert us_code == PostalCode("22162-1010") assert typed_postcode("75004", Country.gb) fails PostcodeUnreadable assert typed_postcode("2A004", Country.fr) fails PostcodeUnreadable}
PhoneNumber, CallingCode, PostalCode and UnLocode are held as
text, as the payment numbers are. Each is a pattern and, where it needs
one, a named validator, and its canonical text is the value. None is
ordered.
Phone numbers
A PhoneNumber is its E.164 text, the international form with no
spaces: +447400123456. It is valid when libphonenumber's
isValidNumber, the stricter of its two checks, says so: the national
number matches one of its region's number types (fixed line, mobile,
freephone and so on) with that type's lengths, not merely a length the
region uses.
Its data is libphonenumber's PhoneNumberMetadata.xml, release v9.0.41
(2026-10-08, Apache-2.0), generated once into
jadpo_std::standard::phones_data. A range a regulator opens later is
refused until a release brings newer data, as a new currency is. A phone
number is contact personal data.
text.parse reads E.164 only, as it reads only canonical text
everywhere. To read a number as people write it, pass country:, a
Country value, the country it was typed in. Then text.parse reads:
- a national number with its prefix,
07400 123456; - an international number after
+or the region's international prefix,00 44 ...or011 44 ...; - a national prefix written after the country code,
+44 (0)7400 ..., which it leaves out; - spaces, dashes, dots, slashes and brackets anywhere.
A number valid in another region with the same calling code is taken,
such as a Jersey number typed in Britain. An extension belongs in a
PhoneExtension, so text with one is malformed. country: on a type
other than PhoneNumber and PostalCode is type.temporal_option_unused,
as an option that means nothing for a type is.
text.format picks a phone number's format from the locale, with no
option to set. A reader whose region has the number's calling code gets
the national format, 07400 123456. The region is the locale's, or its
likeliest one, as for an address's reader. Any other reader gets the
international format, +44 7400 123456. Both use libphonenumber's formats
for the calling code's main region.
A CallingCode is a code ITU-T assigned, with its +: +44, +1
for the whole North American plan, +800 and the other non-geographic
codes. The list is the one libphonenumber's data holds. It isn't
personal data.
Postal codes
A PostalCode on its own is text any country's postal code could be:
1 to 12 capitals, digits, spaces and dashes, with a letter or digit at
each end. Its country's rule applies where the country is known: in a
PostalAddress, and in text.parse(raw, as: PostalCode, country:).
The patterns are libaddressinput's, from the commit the address formats
come from (Apache-2.0), in postal_codes_data. Where a pattern leaves a
separator optional but the country's own examples all write it one way,
the separator is fixed: a space in SW1A 1AA, a dash in 154-0023 and
22162-1010, none in 75004. So each postal code has one text, and ==
holds.
With country:, a postal code is read as people type it: in capitals,
with one space between words and the separators where the country writes
them, so sw1a1aa is SW1A 1AA. Text that could be read two ways is
refused. A country libaddressinput has no pattern for, such as Ireland,
whose Eircodes it doesn't hold, takes any postal code. A postal code is
contact personal data, as an address is.
A PostalAddress's postal code must fit its country. The rule is
postal_code_of_country. An object literal never fails
(Objects), so the rule is proved while
compiling where a literal writes both the postal code and the country
(type.invalid_literal). An address built in code from other values is
built with attempt. JSON, in a request or through json.read, breaks
the rule by name at the address's path. Stored addresses are read as
they were written.
UN/LOCODEs
A UnLocode is a UN code for a trade or transport location: five
characters with no space, GBLON, its first two a current ISO 3166-1
country code. It is printed with a space, GB LON.
The UN/LOCODE list isn't shipped, so a code isn't checked against it.
UNECE's site terms allow only personal, non-commercial use without
redistribution, while its footer says UN/CEFACT standards are under
CC BY 4.0, and it doesn't say which covers the code list. The
datasets/un-locode package records this.
No shapes: type.standard_exists matches none of these four types,
because each is a single value.
Markdown for agents: /docs/phone-numbers-and-postal-codes.md.Built from Jadpo main at a707efb.