# Phone numbers and postal codes

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:

```jadpo
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)
}
```

```jadpo
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 ...` or `011 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](objects.md)), 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.
