# validation.uk-sort-code-account
Checks a UK sort code and account number against the modulus rules banks
publish through Vocalink (for Pay.UK), the same check Bacs recommends before a
Direct Credit or Direct Debit instruction is submitted.
A PASS IS NOT AN ACCOUNT. The specification is explicit: a valid result means
the account number is a possible account at that sorting code, not that it is
open, in use or belongs to the payee. Use Confirmation of Payee for that. Use
this to catch a mistyped digit before it becomes a returned payment.
## You supply the weight table
This package is the algorithm only. The modulus weight table it checks
against, Vocalink's **VALACDOS.txt**, and exception 5's substitution table,
**SCSUBTAB.txt**, are Vocalink's copyright and are not redistributed here.
1. Download both files from Vocalink's modulus checking page, run for
Pay.UK: .
2. Parse them with `parseUkModulusTable` from
`validation.uk-modulus-table`, once, when your application starts.
3. Pass the table to every call.
Vocalink updates VALACDOS.txt several times a year as banks add and retire
sort codes; the page gives the date of each release. Fetch the new file when
it changes. An out-of-date table answers "unchecked" for a new sort code
rather than rejecting it.
TypeScript:
```ts
import { readFileSync } from "node:fs";
import { parseUkModulusTable } from "#fune/validation.uk-modulus-table@^1";
import { validateUkSortCodeAccount } from "#fune/validation.uk-sort-code-account@^3";
const table = parseUkModulusTable(
readFileSync("data/valacdos.txt", "utf8"),
readFileSync("data/scsubtab.txt", "utf8"),
);
const result = validateUkSortCodeAccount("08-99-99", "66374958", table);
if (!result.valid) console.log(result.reason); // "bad-check-digit", ...
```
Python:
```python
from pathlib import Path
from fune.validation.uk_modulus_table import parse_uk_modulus_table # validation.uk-modulus-table@^1
from fune.validation.uk_sort_code_account import validate_uk_sort_code_account # validation.uk-sort-code-account@^3
table = parse_uk_modulus_table(
Path("data/valacdos.txt").read_text(encoding="utf-8"),
Path("data/scsubtab.txt").read_text(encoding="utf-8"),
)
result = validate_uk_sort_code_account("08-99-99", "66374958", table)
if not result.valid:
print(result.reason)
```
Rust:
```rust
fune!(validation.uk-modulus-table@^1);
fune!(validation.uk-sort-code-account@^3);
fn main() -> std::io::Result<()> {
let table = parse_uk_modulus_table(
&std::fs::read_to_string("data/valacdos.txt")?,
&std::fs::read_to_string("data/scsubtab.txt")?,
);
let result = validate_uk_sort_code_account("08-99-99", "66374958", &table);
if !result.valid {
println!("{}", result.reason.unwrap_or_default());
}
Ok(())
}
```
## The result
`valid`, the standardised `sortCode` (six digits) and `accountNumber` (eight
digits), `checked`, and a `reason` when invalid. The two numbers are null when
`valid` is false.
`checked` is false in two cases where the specification says to presume the
account valid because nothing can be checked: the sort code is in no range of
the weight table, or exception 6 applies (a foreign currency account at a
sorting code that holds them). Callers who want to treat "unchecked" more
cautiously can. An empty table leaves everything unchecked, which is one
reason the parser refuses an empty VALACDOS.txt.
| reason | meaning |
|---|---|
| `bad-sort-code` | not six digits once spaces and hyphens are removed (checked first) |
| `bad-account-number` | not 6 to 10 digits once spaces and hyphens are removed |
| `non-standard-account` | 9 or 10 digits: must be standardised per bank first (below) |
| `bad-check-digit` | the modulus check failed |
A bad sort code or account number is an answer, never an error. The function
throws only for a malformed table (one built or edited by hand: a row without
14 weights, an unknown algorithm, a range that is not two six-digit sort
codes, a negative DBLAL weight, an exception outside 1 to 14, or a
substitution that is not two six-digit sort codes), naming the row.
## Account number length
Six and seven digit account numbers are left-padded with zeros, which the
specification applies to every bank. Nine and ten digit numbers are refused
with `non-standard-account`, because their standardisation depends on the bank
(NatWest keeps the last eight digits, Co-operative and Leeds the first eight,
Santander moves the first digit into the sort code) and nothing here knows
which bank a sort code belongs to without the EISCD. Standardise them and call
again.
## The algorithm
Find the row(s) of the weight table whose range contains the sort code. Each
names MOD10, MOD11 or DBLAL (double alternate), fourteen weights for the digits
u-z (sort code) and a-h (account), and an optional exception. Multiply digit by
weight; MOD10 and MOD11 add the products, DBLAL adds the digits of the
products; the total must divide by the modulus. With two rows, both checks
must pass, except:
- 1: add 27 to the DBLAL total.
- 2 and 9: if a is not 0 replace the weights (two sets, by whether g is 9); if
that fails, check again against sort code 309634 (Lloyds euro accounts).
Either passing is enough.
- 3: skip the second check when c is 6 or 9.
- 4: the MOD11 remainder must equal the two-digit number gh.
- 5: substitute the sort code from the table's substitutions (SCSUBTAB.txt)
for both checks; the MOD11 check digit is g and the DBLAL check digit is h:
a MOD11 remainder of 0 needs g = 0, of 1 is always invalid, otherwise
g = 11 - remainder; a DBLAL remainder of 0 needs h = 0, otherwise
h = 10 - remainder.
- 6: a in 4-8 with g equal to h is a foreign currency account: unchecked.
- 7: if g is 9, zero the weights u to b.
- 8: check against sort code 090126.
- 10 and 11: either passing is enough; for 10, if ab is 09 or 99 and g is 9,
zero the weights u to b.
- 12 and 13: either passing is enough.
- 14 (Coutts): if MOD11 fails and h is 0, 1 or 9, drop h, put a 0 in front
and check again.
A MOD11 row may carry a negative weight; its remainder is always taken as 0
to 10, so the three languages agree.
## Tests
The vectors use small invented tables, not Vocalink's: made-up sort code
ranges (110000-119999, 220000-220099 and so on) with weights chosen so each
algorithm and each exception is exercised, passing and failing, and every
expected answer worked by hand from the specification's description of the
algorithm. They are not the test cases in chapter 3 of the specification,
which depend on the real table. To check your own copy of VALACDOS.txt, run
those cases against it in your application's tests.
## Changes
3.0.0 takes the weight table as a third argument instead of bundling
Vocalink's VALACDOS.txt and SCSUBTAB.txt, which may not be redistributed.
Parse the files with `validation.uk-modulus-table`. The result and its
reasons are unchanged; the function now throws for a malformed table.
## Sources
- Vocalink, "Validating account numbers: UK Modulus Checking", section 2 (the
algorithms and exceptions), via the modulus checking page:
The specification also says to confirm the sort code exists in the EISCD
first. That directory is licensed and not shipped here.