DEV Community

Cover image for How to Validate Sri Lankan NIC Numbers, Phone Numbers, and LKR Amounts.
mmsalmanfaris
mmsalmanfaris

Posted on AI-assisted

How to Validate Sri Lankan NIC Numbers, Phone Numbers, and LKR Amounts.

Links: GitHub · npm · API documentation

Short answer: install lanka-validations from npm and call isValidNic('921234567V') from lanka-validations/nic. It returns true for valid old-format (9 digits plus V or X) and new-format (12 digits) Sri Lankan NIC numbers. The same package validates phone numbers, passports, driving licences, vehicle plates, LKR amounts, postal codes, CBSL bank codes, and TIN/VAT numbers. It has no runtime dependencies.

npm install lanka-validations
Enter fullscreen mode Exit fullscreen mode
import { isValidNic, parseNic } from 'lanka-validations/nic';

isValidNic('921234567V'); // true
parseNic('921234567V')?.dobString; // '1992-05-02'
Enter fullscreen mode Exit fullscreen mode

The rest of this guide explains how Sri Lankan identifiers are structured, how to validate each one, how to plug the checks into Zod, React Hook Form, and Express, and what format validation can and cannot prove.

What is lanka-validations?

lanka-validations is an open-source (MIT) TypeScript and JavaScript library for validating, parsing, and formatting Sri Lankan data. It covers National Identity Card (NIC) numbers, phone numbers, passports, driving licences, vehicle number plates, LKR currency, dates of birth, gender, postal codes, CBSL bank codes, TIN/VAT numbers, provinces, and districts. It runs in Node.js 20+, browsers, Deno, and Bun.

Most Sri Lankan web forms ask for some of these values: sign-up pages, KYC onboarding, checkout, payroll, and government-service portals. Without a library, every team writes its own NIC regex and phone-prefix list, and those copies drift out of date. lanka-validations keeps the rules and reference tables in one tested place, and records the official source for each table.

Key facts:

Property Value
Runtime dependencies None
Runtimes Node.js 20+, browsers, Deno, Bun
Module formats ESM and CommonJS, with TypeScript types
Size of isValidNic About 1 kB gzipped (enforced in CI)
Typical validation time About 0.4 to 0.9 µs for NIC and phone checks (local benchmark)
Languages English, Sinhala, and Tamil for currency words, gender, regions, errors
Licence MIT

How a Sri Lankan NIC number is structured

A Sri Lankan NIC number encodes the holder's birth year, birthday, and gender. There are two formats, and both are still in circulation:

Format Example Layout
Old (before 2016) 921234567V 2-digit year, 3-digit day, 3-digit serial, check digit, V or X
New (12 digits) 199212304567 4-digit year, 3-digit day, 4-digit serial, check digit

The day number counts from 1 January using a 366-day year, so day 123 is 2 May. For women, 500 is added to the day, so a woman born on the same day has 623 instead of 123. Converting old to new adds 19 before the year and a 0 before the serial.

That is why a plain regex is not enough. /^\d{9}[VX]$/ accepts 929994567V, which has day 999 and cannot be a real birthday. A real validator has to decode the day number, check it against the calendar, and reject birth dates in the future.

Validate and parse an NIC number

isValidNic returns a boolean. parseNic returns the decoded details, or null if the number is invalid.

import {
  isValidNic,
  parseNic,
  convertOldToNewNic,
  convertNewToOldNic,
  formatNic,
} from 'lanka-validations/nic';

isValidNic('921234567V'); // true
isValidNic('199212304567'); // true
isValidNic('9212345678'); // false

const info = parseNic('921234567V');
info?.year; // 1992
info?.gender; // 'male'
info?.dobString; // '1992-05-02'
info?.isVoter; // true
info?.newNic; // '199212304567'

convertOldToNewNic('921234567V'); // '199212304567'
convertNewToOldNic('199212304567'); // '921234567V'
formatNic('199212304567', 'spaced-new'); // '1992 123 0456 7'
Enter fullscreen mode Exit fullscreen mode

When you need an error message, use validateNic. It returns either { isValid: true, data } or { isValid: false, error, errors, code }, so TypeScript narrows the type for you:

import { validateNic } from 'lanka-validations/nic';

const result = validateNic('92123456V');
if (result.isValid) {
  result.data.dobString;
} else {
  result.code; // 'NIC_INVALID_FORMAT'
  result.error; // 'Invalid NIC format. Must be 9 digits followed by V/X or 12 digits.'
}
Enter fullscreen mode Exit fullscreen mode

The code is stable across versions, so you can branch on it or translate it without matching on English text.

Validate Sri Lankan phone numbers

isValidMobile accepts Sri Lankan mobile numbers in local or +94 form, with spaces, dashes, brackets, dots, or slashes. parsePhone tells you the line type and the operator that the prefix was originally allocated to. formatPhone converts to E.164, national, or international format.

import { isValidMobile, isValidLandline, parsePhone, formatPhone } from 'lanka-validations/phone';

isValidMobile('077 123 4567'); // true
isValidLandline('0112345678'); // true (Colombo)

const phone = parsePhone('+94 77 123 4567');
phone?.operator; // 'Dialog'
phone?.normalized; // '0771234567'
phone?.type; // 'mobile'

formatPhone('0771234567', 'e164'); // '+94771234567'
formatPhone('0771234567', 'international'); // '+94 77 123 4567'
Enter fullscreen mode Exit fullscreen mode

Store phone numbers in E.164 (+94771234567). It is the format SMS gateways and most APIs expect, and it removes duplicates caused by spacing differences.

Two details matter in production. First, isValidPhone also accepts short codes such as 119 (police emergency). For a "contact number" field, use isValidMobile or isValidContactPhone, which reject them. Second, the operator comes from the prefix's original allocation. Because of number portability, a 077 number may now be on a different network.

Parse and format LKR currency, with lakh grouping and words

Sri Lankan amounts are written in several ways: Rs. 1,500.50, LKR 1,50,000, or with the Sinhala රු. and Tamil ரூ. symbols. parseLKR reads all of them. formatLKR writes either Western grouping or South Asian lakh grouping, and lkrToWords writes the amount in words for cheques and receipts.

import { parseLKR, formatLKR, lkrToWords, parseLKRMinorUnits } from 'lanka-validations/currency';

parseLKR('Rs. 1,500.50'); // 1500.5
parseLKR('LKR 1,50,000'); // 150000

formatLKR(1500000); // 'Rs. 1,500,000.00'
formatLKR(1500000, { lakhScale: true }); // 'Rs. 15,00,000.00'

lkrToWords(1250); // 'One Thousand Two Hundred Fifty Rupees Only'
lkrToWords(1500.5, { language: 'si' }); // 'රුපියල් එක්දහස් පන්සියයයි ශත පනහයි'
lkrToWords(1500.5, { language: 'ta' }); // 'ஆயிரத்து ஐந்நூறு ரூபாய் ஐம்பது சதம்'

parseLKRMinorUnits('Rs. 1,500.50'); // 150050n (cents, as a bigint)
Enter fullscreen mode Exit fullscreen mode

parseLKR returns a JavaScript number, which is fine for display and range checks. For balances, invoices, and anything you add up, use parseLKRMinorUnits, which returns exact cents as a bigint and avoids floating-point rounding errors.

Other Sri Lankan identifiers and reference data

The library uses the same pattern for every identifier: isValidX for a boolean, validateX for a result with an error code, and parseX or a lookup function for details.

Data Example call Result
Passport isValidPassport('N1234567') true
Driving licence isValidDrivingLicense('B1234567') true
Vehicle plate parseVehiclePlate('wp cab 1234')?.formatted 'WP CAB-1234'
Postal code to district lookupPostalCode('20000')?.district 'Kandy'
CBSL bank code getBankByCode('7010')?.name 'Bank of Ceylon'
Taxpayer Identification No. isValidTIN('123456789') true
District in Sinhala or Tamil isValidDistrict('කොළඹ') true
Gender in any language normalizeGender('ஆண்') 'male'

An NIC is not a TIN. Individuals register with the Inland Revenue Department using their NIC and receive a separate TIN, so isValidTIN('921234567V') returns false. Validate NIC fields with isValidNic.

Check that a form's date of birth and gender match the NIC

KYC forms often ask for the NIC, date of birth, and gender separately. Because the NIC encodes the other two, you can catch typos and mismatched entries before they reach a manual review queue:

import { matchesNicDob } from 'lanka-validations/dob';
import { matchesNicGender } from 'lanka-validations/gender';

const nic = '921234567V'; // encodes a man born on 1992-05-02

matchesNicDob('1992-05-02', nic); // true
matchesNicDob('1995-01-01', nic); // false
matchesNicGender('male', nic); // true
matchesNicGender('female', nic); // false
Enter fullscreen mode Exit fullscreen mode

Validate a whole form with the schema validator

For forms with several Sri Lankan fields, createSchemaValidator validates the whole object in one call, runs cross-field rules, and infers the parsed output type in TypeScript:

import { createSchemaValidator, rules } from 'lanka-validations/schema';

const signUp = createSchemaValidator({
  nic: rules.nic(),
  phone: rules.mobile(),
  dob: [rules.dob({ minAge: 18 }), rules.matchesNicDob('nic')],
  gender: [rules.gender(), rules.matchesNicGender('nic')],
  postalCode: rules.postalCode(),
});

const result = signUp.validate({
  nic: '921234567V',
  phone: '123',
  dob: '1992-05-02',
  gender: 'male',
  postalCode: '00100',
});

if (!result.isValid) {
  result.errorCodes; // { phone: 'PHONE_UNSUPPORTED' }
  result.errors.phone; // 'Phone number prefix or short code is not in the bundled reference data.'
}
Enter fullscreen mode Exit fullscreen mode

Show errors in Sinhala or Tamil

Every failure carries a stable code, so you can show messages in the user's language. localizeValidationResult translates a single result, and localizeSchemaResult translates a whole form:

import { validateNic } from 'lanka-validations/nic';
import { localizeValidationResult } from 'lanka-validations/i18n';

localizeValidationResult(validateNic('abc'), 'si').error;
// 'ජාතික හැඳුනුම්පත් ආකෘතිය වලංගු නොවේ.'
Enter fullscreen mode Exit fullscreen mode

Use it with Zod, React Hook Form, and Express

The isValidX functions return plain booleans, so they work as custom checks in any validation library.

Zod

import { z } from 'zod';
import { isValidNic } from 'lanka-validations/nic';
import { isValidMobile } from 'lanka-validations/phone';

const SignUp = z.object({
  nic: z.string().refine(isValidNic, { message: 'Enter a valid Sri Lankan NIC number' }),
  phone: z.string().refine(isValidMobile, { message: 'Enter a Sri Lankan mobile number' }),
});
Enter fullscreen mode Exit fullscreen mode

React Hook Form

import { useForm } from 'react-hook-form';
import { isValidNic } from 'lanka-validations/nic';

const { register } = useForm<{ nic: string }>();

<input {...register('nic', { validate: (value) => isValidNic(value) || 'Invalid NIC number' })} />;
Enter fullscreen mode Exit fullscreen mode

Express

import express from 'express';
import { validateNic } from 'lanka-validations/nic';

const app = express();
app.use(express.json());

app.post('/customers', (req, res) => {
  const nic = validateNic(req.body.nic);
  if (!nic.isValid) {
    res.status(400).json({ code: nic.code, error: nic.error });
    return;
  }
  res.json({ dateOfBirth: nic.data.dobString, gender: nic.data.gender });
});
Enter fullscreen mode Exit fullscreen mode

Run the same checks on the server even when the browser already ran them. Client-side validation helps the user; server-side validation protects your data.

Safe on untrusted input

Form fields and API bodies can contain anything. Every validator in lanka-validations accepts unknown and never throws:

isValidNic(null); // false
isValidNic({}); // false
isValidNic('x'.repeat(2000)); // false
Enter fullscreen mode Exit fullscreen mode

Objects and arrays are rejected without calling their toString. Strings longer than 1,024 characters are rejected rather than truncated, which keeps validation time bounded. Property-based tests check these guarantees across thousands of generated inputs. The one deliberate exception is lkrToWords, which throws a RangeError for amounts too large to write in words.

When you log identifiers, mask them:

import { maskNic, maskPhone } from 'lanka-validations/privacy';

maskNic('921234567V'); // '******567V'
maskPhone('0771234567'); // '******4567'
Enter fullscreen mode Exit fullscreen mode

What format validation does not prove

A valid format is not proof of identity. lanka-validations checks that a value is well-formed and consistent with published rules. It does not check that:

  • an NIC, passport, or licence number was issued, or belongs to the person entering it;
  • a phone number is assigned and reachable;
  • a bank account exists or is open;
  • a TIN is registered with the IRD.

NIC check digits are also not verified, because the Department for Registration of Persons has not published the algorithm. For KYC and payments, treat these checks as input hygiene, then confirm identity and accounts with the issuing authority or a verification provider.

Where the reference data comes from

Bank codes, postal codes, phone prefixes, and district names change over time, so each bundled table records its official source and the date it was checked:

Table Official source
Provinces and districts Department of Census and Statistics (Census 2024)
Bank codes Central Bank of Sri Lanka (CBSL) bank reference manual
Postal codes Sri Lanka Post code book
Phone prefixes, short codes TRCSL National Numbering Plan
Driving licence classes Department of Motor Traffic (DMT)
TIN and VAT Inland Revenue Department (IRD) registration guidelines

getReferenceDataMetadata() returns the source, as-of date, coverage, and known gaps for each table at runtime, so you can show or log which data version made a decision. The full list, with links to each source, is in the README on GitHub.

When to use something else

lanka-validations is for Sri Lanka-specific data. For phone numbers from many countries, use libphonenumber-js. For generic checks such as email addresses and URLs, use Zod or a general-purpose validator. The libraries combine well: Zod for the form shape, lanka-validations for the Sri Lankan fields.

Approach Old and new NIC Decodes DOB and gender LKR lakh format and words Sinhala and Tamil Sources recorded
Hand-written regex Partly No No No No
libphonenumber-js No (phones only) No No No Not applicable
lanka-validations Yes Yes Yes Yes Yes

Frequently asked questions

How do I validate a Sri Lankan NIC number in JavaScript?

Install lanka-validations and call isValidNic(value) from lanka-validations/nic. It returns true for valid old-format (9 digits plus V or X) and new-format (12 digits) numbers. Use validateNic(value) to also get the date of birth, age, gender, and an error code when the number is invalid.

How do I get the date of birth from a Sri Lankan NIC?

Call parseNic(nic). It returns dobString (for example '1992-05-02'), dob as a UTC Date, age, and gender. The birth year and the day of the year are encoded in the number. For women, 500 is added to the day, so 927234567V decodes to a woman born on 10 August 1992.

How do I convert an old NIC to the new 12-digit format?

Use convertOldToNewNic('921234567V'), which returns '199212304567'. It prefixes 19 to the year and adds a 0 before the serial number. convertNewToOldNic converts back for people born between 1900 and 1999.

Does it work in React, Next.js, Deno, and Bun?

Yes. It has no runtime dependencies and uses no Node.js-only APIs, so it runs in browsers, React and Next.js (client and server components), Node.js 20+, Deno, and Bun. Import from subpaths such as lanka-validations/nic to keep the browser bundle small.

Does validating an NIC confirm someone's identity?

No. It checks the number's format and whether the encoded birth date is a real date in the past. It cannot tell whether the number was issued or who it belongs to. For KYC, verify identity with the issuing authority or a verification provider.

Is the bank and postal code data up to date?

Each table records its official source and the date it was last checked. Call getReferenceDataMetadata() to read them at runtime. If you find an outdated entry, open an issue on GitHub with a link to the current official source.

Get started

npm install lanka-validations
Enter fullscreen mode Exit fullscreen mode

lanka-validations is maintained by M.M. Salman Faris. If it saves you time, star the repository on GitHub. Bug reports and pull requests are welcome.

Top comments (0)