
The Origin Story
Every Sri Lankan citizen carries a National Identity Card. The NIC number isn't just an ID — it's a compressed data structure that encodes birth year, birthday, gender, and voter status into 9 or 12 digits.
I needed to validate and parse these numbers in a project, and quickly discovered that every existing library just slaps a regex on it and calls it a day. A string that looks like a NIC passes validation — even if the encoded birthday is February 30th or the person would be 3 years old.
That wasn't good enough. So I built a library that actually understands what a NIC number means.
Beyond Regex
Most NIC validation libraries do a simple structure check. This library goes five layers deeper:
- Structure — matches old format (9 digits + V/X) or new format (exactly 12 digits)
- Birth year range — extracted year must fall between 1901 and a dynamically calculated upper limit
- Day-of-year bounds — validates the encoded day value (1–365 for males, 501–865 for females), accounting for leap years
- Leap year correctness — day 366 (or 866 for females) is only valid if the birth year is actually a leap year
- Minimum age requirement — checks down to the exact day, not just the year. If someone born on day 300 of the cutoff year hasn't turned 15 yet because today is day 200, the NIC is correctly rejected
This is validation that understands the domain, not just the format.
Parsing
One call to NIC.parse() gives you everything encoded in the number:
import { NIC } from "@sri-lanka/nic";
const nic = NIC.parse("853400937V");
nic.type; // "old"
nic.gender; // "male"
nic.birthday; // { year: 1985, month: 12, day: 6 }
nic.age; // 40
nic.serial; // "093"
nic.checkdigit; // "7"
nic.letter; // "V"
nic.voter; // trueA 10-character string becomes a fully typed object with birthday, gender, age, serial number, check digit, and voter registration status.
Format Conversion
Sri Lanka has used two NIC formats over the years. The old format uses a 2-digit year, the new format uses 4 digits. Converting between them is a single method call:
// Old → New
NIC.parse("853400937V").convert(); // "198534009370"
// New → Old
NIC.parse("198534009370").convert(); // "853400937V"The library handles all the encoding math internally — day-of-year offsets, gender encoding, serial numbers, check digits.
Random NIC Generation
Need test data? Generate structurally valid NIC numbers that will always pass validation:
const randomNIC = NIC.random();
// e.g. "941520456V" or "199815204560"Every generated NIC uses real date logic — valid birth years, correct day-of-year ranges, proper leap year handling. Perfect for unit tests, database seeding, or mock data.
Error Handling
When something is invalid, you get a specific error — not a generic "invalid NIC" message:
try {
NIC.parse("invalid-nic");
} catch (error) {
if (error instanceof NIC.Error) {
error.code; // "INVALID_NIC_STRUCTURE"
error.message; // descriptive error with context
}
}Error codes include INVALID_NIC_STRUCTURE, INVALID_BIRTH_YEAR, INVALID_DAY_OF_YEAR, and MINIMUM_AGE_REQUIREMENT_NOT_MET. If you prefer not to throw, use NIC.valid() for a boolean or NIC.validate() for a result object.
Zod Integration
Drop it straight into your form validation or API input schemas:
import { z } from "zod";
import { NIC } from "@sri-lanka/nic";
const schema = z.object({
nic: z.string().refine((v) => NIC.valid(v), {
message: "Invalid Sri Lankan NIC",
}),
});
schema.parse({ nic: "853400937V" }); // ✅ passes
schema.parse({ nic: "0000000000" }); // ❌ throws ZodErrorThe Technical Details
| Metric | Value |
|---|---|
| Bundle Size | < 6 kB minified |
| Dependencies | Zero |
| Module Formats | ESM, CJS, TypeScript declarations |
| Tree-shaking | Fully supported |
| Test Coverage | Comprehensive with Vitest |
Try It
Live Demo & API Explorer
Try the library in an interactive playground with real-time parsing and validation.
View on npm
Check out the package, versions, and installation guide.
Source on GitHub
Explore the source code, contribute, or open an issue.
pnpm add @sri-lanka/nic