The Modulo 11 algorithm for the Chilean RUT, explained
Worked example of the Modulo 11 algorithm for the Chilean RUT verifier digit, the TypeScript implementation, and the edge cases hand-rolled code misses.
Every Chilean RUT carries a single-character checksum — a digit 0–9 or the letter K — computed via a well-defined Modulo 11 procedure. Understanding the math helps when debugging unexpected validation failures or auditing third-party implementations. That said, reaching for validate() from rut.ts is the right move in production: the algorithm is already implemented correctly, hardened against edge cases, and covered by a comprehensive test suite.
Four edge cases hand-rolled Modulo 11 implementations miss: lowercase k verifier, leading zeros in the body, unicode dashes (‐, –, —) in pasted input, and repeated-digit placeholders like 11.111.111-1 that satisfy the checksum but are not real identities. rut.ts handles all four; details at the bottom.
What is a RUT, exactly?#
A RUT (Rol Único Tributario) is Chile's national tax and identity number. Its structure is a numeric body of 7 or 8 digits followed by a single verifier character separated by a hyphen. The canonical display format uses dots to group the body digits and a hyphen before the verifier — for example, 12.345.678-5. The verifier is either a decimal digit (0–9) or the uppercase letter K.
The body is assigned sequentially by the Servicio de Registro Civil e Identificación. The verifier is not assigned alongside the body — it is derived mathematically from the body digits. This design makes the verifier a checksum: it catches single-digit transcription errors and most adjacent-digit transpositions that occur during manual data entry, which is the classic application of a weighted checksum.
The Modulo 11 procedure step by step#
The algorithm processes the body digits from right to left, multiplying each digit by a cycling weight sequence 2, 3, 4, 5, 6, 7, which restarts at 2 once it reaches 7. The products are summed, the sum is reduced modulo 11, and the remainder is subtracted from 11 to produce the raw verifier value.
Here is the full worked example for the body 12345678:
| Digit | Weight | Product |
|---|---|---|
| 8 | 2 | 16 |
| 7 | 3 | 21 |
| 6 | 4 | 24 |
| 5 | 5 | 25 |
| 4 | 6 | 24 |
| 3 | 7 | 21 |
| 2 | 2 | 4 |
| 1 | 3 | 3 |
Sum = 138. Then 138 mod 11 = 6, and 11 − 6 = 5, so the verifier for 12345678 is '5'.
Two special cases apply to the subtraction result. If the result is 11, the verifier is '0' — not '11', since the field is a single character. If the result is 10, the verifier is 'K'. The letter K is the historical representation of the number ten in this context and must always be stored and displayed in uppercase.
The weight sequence cycles 2, 3, 4, 5, 6, 7, 2, 3, … because the Chilean standard specifies those six multipliers applied right-to-left, looping back to 2 for bodies longer than six digits. For an eight-digit body the first two weights from the restart (2 and 3) are applied to the seventh and eighth digits from the right.
The same thing in TypeScript#
Any from-scratch implementation of the above is around 15 lines of code — and most of the bugs live in those 15 lines. The rut.ts library collapses the entire computation to a single function call:
import { calculateVerifier, validate } from "rut.ts";
calculateVerifier("12345678"); // → '5'
validate("12.345.678-5"); // → true
validate("12.345.678-6"); // → false (wrong checksum)calculateVerifier encapsulates the weight cycling, the modular arithmetic, and the K/0 special cases. validate goes further: it checks format, runs the checksum, and enforces canonical dot grouping. The algorithm is implemented once, tested exhaustively, and every consumer benefits automatically — including the regression tests that guard against future changes.
Why you should not ship your own implementation#
Hand-rolled Modulo 11 implementations for the Chilean RUT almost always start from a Spanish-language tutorial that covers the happy path and skips a handful of edge cases that matter in production.
Case sensitivity. The verifier K must be accepted in both k and K from user input, but stored and compared as K. A naive === 'K' check silently rejects lowercase input; a naive lowercase normalisation before the checksum breaks the comparison.
Leading zeros on the body. Older RUT numbers can have a 7-digit body, which in some legacy systems is zero-padded to 8 digits (e.g. 01234567). A hand-written implementation that strips leading zeros before running the algorithm will compute the wrong verifier.
Whitespace and non-standard separators. Users routinely type spaces around the hyphen or paste values from documents that contain a unicode figure dash (‐), an en-dash (–), or an em-dash (—) instead of the ASCII hyphen-minus (-). An implementation that does a simple split('-') will fail silently on these variants, returning an incorrect checksum comparison rather than a clean validation error.
Placeholder RUTs. A set of RUTs with repeated digits — 11.111.111-1, 22.222.222-2, through 99.999.999-9 — happen to satisfy the Modulo 11 check. They are valid checksums but not real identities. They appear in test datasets, demo forms, and occasionally in production data submitted by users who want to avoid registering. A basic Modulo 11 check passes them without complaint.
rut.ts handles all of the above. For the placeholder case specifically, use validate(value, { strict: true }): it rejects the repeated-digit patterns in addition to performing the full checksum and format check. Strict mode is the appropriate gate for any flow where you need a genuine, non-synthetic identity.
Further reading#
- Install rut.ts —
pnpm add rut.ts(zero dependencies, TypeScript-native) - Quick start — five-minute path from install to a hardened validation gate
validate()referencecalculateVerifier()reference- Stop hardcoding "11.111.111-1": generating test RUTs with rut.ts — the placeholder math in practice
- Wikipedia: Rol Único Tributario