Groups with the baseline only
Many standard types are a value with a check and nothing more: an IBAN, a
card number, a barcode, an NHS number. They have no operations of their
own. They do what every standard type does
(Every standard type meets one contract):
each is built and checked from text or from its parts, sent, stored and
shown to people. Their rules are how they are built, which text.parse
and the type's data entry already cover.
Here a marketplace pays its sellers into the account they type, takes a patient's NHS number from a referral, and shows a card on file by its last four digits:
type FormText = Text {}type NotAnIban = Failure { kind: FailureKind.invalid_value}type NotAnNhsNumber = Failure { kind: FailureKind.invalid_value}/// The account a seller is paid into, as they typed it: checked by its/// country's length and its mod-97 check digits.function payout_account(raw: FormText) fails NotAnIban -> Iban { var account: Iban = attempt Iban(raw) { format: NotAnIban } return account}/// A patient's NHS number, as a referral sends it.function patient_number(raw: FormText) fails NotAnNhsNumber -> NhsNumber { var number: NhsNumber = attempt text.parse(raw, as: NhsNumber) { malformed: NotAnNhsNumber } return number}/// An account as a statement prints it.function account_label(account: Iban, viewer: Locale) -> PresentationText { return text.format(account, locale: viewer)}/// A card on file as a receipt shows it: its last four only.function card_label(card: PaymentCardNumber, viewer: Locale) -> PresentationText { return text.format(card, locale: viewer)}
test "payment and identity numbers are checked as they are built" { var account: Iban = attempt payout_account("GB29NWBK60161331926819") assert account == Iban("GB29NWBK60161331926819") assert payout_account("GB29NWBK60161331926818") fails NotAnIban assert payout_account("GB29 NWBK 6016 1331 9268 19") fails NotAnIban var patient: NhsNumber = attempt patient_number("9434765919") assert patient == NhsNumber("9434765919") assert patient_number("9434765918") fails NotAnNhsNumber}test "they are shown as they are printed" { var account: Iban = Iban("GB29NWBK60161331926819") assert account_label(account, Locale.en_gb) == "GB29 NWBK 6016 1331 9268 19" assert card_label(PaymentCardNumber("4242424242424242"), Locale.en_gb) == "•••• 4242" assert text.format(NhsNumber("9434765919"), locale: Locale.en_gb) == "943 476 5919"}
Checks beyond a pattern
Most of these types are a pattern:. Where a rule is more than a pattern
can say, such as a check digit, the type names one validator:
| Group | Validators beyond a pattern |
|---|---|
| People and contact | PhoneNumber (E.164 per country), PostalCode (per country), PostalAddress and PersonName (CLDR order for text.format) |
| Places | CountrySubdivision against ISO 3166-2, UnLocode by its pattern and its country, its list not shipped |
| Payment and identity numbers | Iban (mod-97), PaymentCardNumber (Luhn), UkSortCode with UkAccountNumber (modulus check), AbaRoutingNumber, Lei, Isin, NhsNumber (mod-11), UkNationalInsuranceNumber (prefix rules) |
| Tax and identity documents | VatNumber, TaxId, PassportNumber, DrivingLicenceNumber and IdentityDocument, each number by its country's published form and check digit (VIES, the OECD's and the EU's TIN pages, each issuer's own), by length where a country publishes none |
| Commerce and logistics | Ean13, UpcA, Isbn, Sscc (GS1 check digits), Vin (check digit, for North American manufacturers), ContainerNumber (ISO 6346) |
| Health | SnomedConcept (Verhoeff); Icd10Code by pattern, its release not shipped |
| Travel | IataAirport, IcaoAirport, IataAirline by pattern, their lists not shipped |
| Other | Orcid (ISO 7064 MOD 11-2), Mpan (Elexon's check digit) |
Payment and identity numbers
Seventeen payment and identity numbers are held as text:
PaymentCardNumber, CardBin, PaymentReference, InvoiceNumber,
Iban, Bic, UkSortCode, UkAccountNumber, AbaRoutingNumber,
Lei, Isin, UkUtr, UkCompanyNumber, UkNationalInsuranceNumber,
UsSsn, UsEin and NhsNumber.
Held as text. Each is a type built on Text that the standard
library declares; its stub is public type Iban = Text {}. So everything
Values and types says of text applies:
construction, literals checked while compiling, attempt, JSON (a
string), storage (a text column) and ==. Your own type can be built
on one and add rules: type SterlingIban = Iban { pattern: "GB.*" }.
Its pattern, matched against the whole text, and its validator are one
check. Text that fails it breaks format, as text that isn't an Email
does, so there is no new rule name. text.parse fails only as
malformed. text.from is type.mismatch, because the value is text
already. None of them is ordered.
The canonical text is the electronic form, in upper case with no
spaces or dashes: GB29NWBK60161331926819, the sort code 089999, the
SSN 123456789 and the EIN 123456789. Only that form is read, as
text.parse reads only canonical text everywhere, so a printed IBAN and
lower case are refused. Strip a form's spaces before you build one, with
text.replace. An SSN or an EIN with dashes is the printed form.
Shown to people in the printed form, whatever the locale:
- an IBAN in groups of four;
- a sort code in pairs with dashes,
08-99-99; - a National Insurance number in pairs,
AB 12 34 56 C; - an NHS number in groups of 3, 3 and 4,
943 476 5919; - an EIN as 2 then 7 digits;
- the others as their canonical text.
The runtime sees only text, so the checker gives text.format the name of
the standard type the value is held as, as an argument you don't write.
Masked by text.format, to be safe. A PaymentCardNumber and a
UsSsn are shown as their last four only, •••• 4242 and •••-••-6789.
That is as much of a card number as PCI DSS lets a page show, and how the
SSA and the IRS shorten an SSN. Code that must show the whole number has
it as text already.
Personal data. Each type names the personal data category its values are in, whatever field holds them:
financialfor a card number, an IBAN and a UK account number;government_idfor a National Insurance number, an SSN and a UTR;healthfor an NHS number.
Personal data treats a field of such a type as
marked with that category: hashed in logs, kept out of caches when the
category is sensitive, and its reads recorded. Logs carry no values,
only a failure's code and a fault, so nothing writes one in clear. The
check of what reaches a model
already reads the category. A failed assertion in jadpo test still
shows the value it compared, which is test data.
The validators:
- an IBAN's mod-97, with each country's length from SWIFT's IBAN Registry. A country the registry adds later is refused until a release adds it, as a new currency is;
- an LEI's ISO 7064 mod 97-10;
- a card number's Luhn check;
- an ISIN's Luhn check over its letters as numbers;
- an ABA routing number's 3-7-1 checksum;
- an NHS number's mod-11. A check digit of 10 is never issued;
- a National Insurance number's prefix rules, from HMRC's NIM39110, with a suffix of A to D only;
- a BIC's country, its fifth and sixth letters, which must be a current
ISO 3166-1 code as
Countryholds them, or Kosovo's user-assignedXK, which SWIFT issues BICs under as it does IBANs. SoYU, which was withdrawn, is refused.
The rest are patterns: a card BIN of 6 or 8 digits; a payment reference of up to 18 of Bacs's characters; an invoice number of 1 to 200 characters with no control character and no space at either end; a sort code; an account number; a UTR; a company number; an EIN; and an SSN, with the areas, groups and serials the SSA never issues refused.
The UK modulus check needs a sort code and an account number together, so
it belongs to BankAccount, under Bank accounts.
Test values are published ones where a scheme publishes them: SWIFT's registry IBANs, Stripe's test cards, the NHS Number standard's example and its 999 test range, Vocalink's modulus checking test cases and GLEIF's LEIs. Where a scheme publishes none, they are made up to the rules. Each type's entry says which.
Personal data's handling of these categories isn't built yet; see What is built.
Markdown for agents: /docs/groups-with-the-baseline-only.md.Built from Jadpo main at a707efb.