@sri-lanka/nic 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Vinod Liyanage
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,343 @@
1
+ <p align="start">
2
+ <img src="https://raw.githubusercontent.com/vinodliyanage/sri-lanka-nic/main/public/sri-lanka-nic-logo.jpg" alt="Sri Lanka NIC Logo" width="200" />
3
+ </p>
4
+
5
+ # @sri-lanka/nic
6
+
7
+ [![NPM Version](https://img.shields.io/npm/v/%40sri-lanka%2Fnic)](https://www.npmjs.com/package/@sri-lanka/nic)
8
+ [![npm bundle size](https://img.shields.io/bundlejs/size/%40sri-lanka%2Fnic)](https://bundlejs.com/?q=%40sri-lanka%2Fnic)
9
+ [![NPM License](https://img.shields.io/npm/l/%40sri-lanka%2Fnic)](./LICENSE)
10
+
11
+ A lightweight, zero-dependency TypeScript library to **parse**, **validate**, **convert**, and **generate** Sri Lankan National Identity Card (NIC) numbers.
12
+
13
+ Supports both the old format (`9 digits + V/X`) and the new format (`12 digits`), with accurate validation logic that goes beyond simple regex matching.
14
+
15
+ **[Live Demo & API Explorer](https://nic.vinodliyanage.me)** · **[NPM Package](https://www.npmjs.com/package/@sri-lanka/nic)**
16
+
17
+ ---
18
+
19
+ ## Table of Contents
20
+
21
+ - [Features](#features)
22
+ - [Installation](#installation)
23
+ - [Quick Start](#quick-start)
24
+ - [Validation](#validation)
25
+ - [Parsing](#parsing)
26
+ - [Format Conversion](#format-conversion)
27
+ - [Random NIC Generation](#random-nic-generation)
28
+ - [Sanitization](#sanitization)
29
+ - [Error Handling](#error-handling)
30
+ - [Integration with Zod](#integration-with-zod)
31
+ - [Error Codes](#error-codes)
32
+ - [Configuration](#configuration)
33
+ - [API Reference](#api-reference)
34
+ - [Module Support](#module-support)
35
+ - [Contributing](#contributing)
36
+ - [License](#license)
37
+
38
+ ---
39
+
40
+ ## Features
41
+
42
+ - **Zero dependencies** — lightweight and self-contained, nothing extra to install
43
+ - **Accurate validation** — goes beyond regex to validate birth year, day-of-year, leap years, and minimum age
44
+ - **Full parsing** — extract birthday, gender, age, serial number, voter status, and more from any valid NIC
45
+ - **Format conversion** — convert between old (9-digit + letter) and new (12-digit) NIC formats
46
+ - **Random generation** — generate structurally valid NIC numbers, useful for testing and mock data
47
+ - **Tiny footprint** — under 6 kB minified with tree-shaking support
48
+ - **Universal module support** — ships ESM, CJS, and TypeScript declarations out of the box
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pnpm add @sri-lanka/nic
56
+ ```
57
+
58
+ ```bash
59
+ npm install @sri-lanka/nic
60
+ ```
61
+
62
+ ```bash
63
+ yarn add @sri-lanka/nic
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Quick Start
69
+
70
+ ```ts
71
+ import { NIC } from "@sri-lanka/nic";
72
+
73
+ const nic = NIC.parse("200012301234");
74
+
75
+ console.log(nic.type); // "new"
76
+ console.log(nic.gender); // "male"
77
+ console.log(nic.birthday); // { year: 2000, month: 5, day: 15 }
78
+ console.log(nic.age); // 25
79
+ ```
80
+
81
+ That's it — one import, one call, and you have everything about the NIC.
82
+
83
+ ---
84
+
85
+ ## Validation
86
+
87
+ Most NIC validation libraries do a simple regex check to see if the string _looks_ like a NIC. This library goes much further and validates the **actual data encoded in the number**.
88
+
89
+ Here's what the validation checks:
90
+
91
+ 1. **Structure** — the string must match either the old format (9 digits followed by `V` or `X`) or the new format (exactly 12 digits). Anything else is immediately rejected.
92
+
93
+ 2. **Birth year range** — the birth year extracted from the NIC must fall between 1901 and a reasonable upper limit. For the new format, the upper limit is dynamically calculated as `current year - 15`, ensuring the NIC holder meets the minimum age.
94
+
95
+ 3. **Day-of-year bounds** — Sri Lankan NICs encode the birthday as a day-of-year value (1–365 for males, 501–865 for females). The library validates that this value falls within the correct range, accounting for whether the birth year is a leap year (allowing day 366 / 866).
96
+
97
+ 4. **Leap year correctness** — day 366 (or 866 for females) is only valid if the birth year is actually a leap year. The library checks this against the real birth year, not a static assumption.
98
+
99
+ 5. **Minimum age requirement** — in Sri Lanka, the legal age to obtain a NIC is **15 years** ([source](https://drp.gov.lk/en/normal.php)). The library doesn't just check the year — it checks down to the **exact day**. If someone was born on day 300 of the maximum allowed year, but today is only day 200, it correctly rejects the NIC because the person hasn't turned 15 yet.
100
+
101
+ ### Using Validation
102
+
103
+ There are two ways to validate:
104
+
105
+ #### Quick boolean check
106
+
107
+ ```ts
108
+ NIC.valid("853400937V"); // true
109
+ NIC.valid("000000000V"); // false
110
+ ```
111
+
112
+ #### Detailed result with error information
113
+
114
+ ```ts
115
+ const result = NIC.validate("853400937V");
116
+
117
+ if (result.valid) {
118
+ console.log("NIC is valid");
119
+ } else {
120
+ console.log(result.error.code); // e.g. "INVALID_NIC_STRUCTURE"
121
+ console.log(result.error.message); // Human-readable error description
122
+ }
123
+ ```
124
+
125
+ `NIC.validate()` never throws. It returns a result object so you can safely check validity and inspect the specific error if the NIC is invalid.
126
+
127
+ ---
128
+
129
+ ## Parsing
130
+
131
+ `NIC.parse()` reads a NIC string and returns a rich `NIC` object with all the extracted information. If the NIC is invalid, it throws a `NIC.Error`.
132
+
133
+ ```ts
134
+ const nic = NIC.parse("853400937V");
135
+ ```
136
+
137
+ Once parsed, you have access to all the details:
138
+
139
+ ```ts
140
+ nic.value; // "853400937V" — the sanitized NIC string
141
+ nic.type; // "old" — format type ("old" or "new")
142
+ nic.gender; // "male" — gender encoded in the NIC
143
+ nic.birthday; // { year: 1985, month: 12, day: 6 } — full date of birth
144
+ nic.age; // 40 — current age in years
145
+ nic.year; // 1985 — birth year
146
+ nic.days; // 340 — day-of-year value from the NIC
147
+ nic.serial; // "093" — serial number portion
148
+ nic.checkdigit; // "7" — check digit
149
+ nic.letter; // "V" — trailing letter (null for new format NICs)
150
+ nic.voter; // true — voter registration status (null for new format NICs)
151
+ ```
152
+
153
+ You can also get a summary object or the plain string:
154
+
155
+ ```ts
156
+ nic.toJSON();
157
+ // { type: "old", gender: "male", birthday: { year: 1985, month: 12, day: 6 }, age: 40 }
158
+
159
+ nic.toString(); // "853400937V"
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Format Conversion
165
+
166
+ Sri Lanka has used two NIC formats over the years. This library lets you convert between them:
167
+
168
+ - **Old format**: 2-digit year + 3-digit day + 4-digit serial + letter (e.g. `853400937V`)
169
+ - **New format**: 4-digit year + 3-digit day + 4-digit serial + check digit (e.g. `198534009370`)
170
+
171
+ ```ts
172
+ // Old → New
173
+ const nic = NIC.parse("853400937V");
174
+ nic.convert(); // "198534009370"
175
+
176
+ // New → Old
177
+ const nic2 = NIC.parse("198534009370");
178
+ nic2.convert(); // "853400937V"
179
+ ```
180
+
181
+ > **Note:** New → Old conversion only works for birth years in the 1900s, since the old format only has 2 digits for the year.
182
+
183
+ ---
184
+
185
+ ## Random NIC Generation
186
+
187
+ Generate a valid, random NIC number. Useful for unit tests, seeding databases, or creating mock data.
188
+
189
+ ```ts
190
+ const randomNIC = NIC.random();
191
+ console.log(randomNIC); // e.g. "941520456V" or "199815204560"
192
+ ```
193
+
194
+ The generated NIC will always pass validation — it uses real date logic to ensure the birth year, day-of-year, and all other fields are structurally correct.
195
+
196
+ ---
197
+
198
+ ## Sanitization
199
+
200
+ Clean up user input before processing. `NIC.sanitize()` trims whitespace and normalizes the letter casing.
201
+
202
+ ```ts
203
+ NIC.sanitize(" 853400937v "); // "853400937V"
204
+ NIC.sanitize(" 200012301234 "); // "200012301234"
205
+ ```
206
+
207
+ Throws `NIC.Error` if the input is completely invalid (not recognizable as a NIC at all).
208
+
209
+ ---
210
+
211
+ ## Error Handling
212
+
213
+ When `NIC.parse()` encounters an invalid NIC, it throws a `NIC.Error` with a specific error code:
214
+
215
+ ```ts
216
+ try {
217
+ NIC.parse("invalid-nic");
218
+ } catch (error) {
219
+ if (error instanceof NIC.Error) {
220
+ console.log(error.code); // "INVALID_NIC_STRUCTURE"
221
+ console.log(error.message); // "Invalid NIC structure in the given NIC number..."
222
+ }
223
+ }
224
+ ```
225
+
226
+ If you prefer not to use try/catch, use `NIC.validate()` or `NIC.valid()` instead — they never throw.
227
+
228
+ ---
229
+
230
+ ## Integration with Zod
231
+
232
+ You can easily use this library as a custom Zod validator for form validation or API input:
233
+
234
+ ```ts
235
+ import { z } from "zod";
236
+ import { NIC } from "@sri-lanka/nic";
237
+
238
+ const schema = z.object({
239
+ nic: z.string().refine((v) => NIC.valid(v), {
240
+ message: "Invalid Sri Lankan NIC",
241
+ }),
242
+ });
243
+
244
+ // Usage
245
+ schema.parse({ nic: "853400937V" }); // ✅ passes
246
+ schema.parse({ nic: "0000000000" }); // ❌ throws ZodError
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Error Codes
252
+
253
+ Every error thrown by the library includes a specific `code` property:
254
+
255
+ | Code | Description |
256
+ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
257
+ | `INVALID_NIC_STRUCTURE` | The input doesn't match either the old format (9 digits + V/X) or the new format (12 digits) |
258
+ | `INVALID_BIRTH_YEAR` | The birth year extracted from the NIC is outside the valid range (before 1901 or too recent) |
259
+ | `INVALID_DAY_OF_YEAR` | The day-of-year value is outside valid bounds for the given birth year |
260
+ | `MINIMUM_AGE_REQUIREMENT_NOT_MET` | The person would be under 15 years old, which is below the legal minimum age for a NIC |
261
+ | `ONLY_19XX_YEAR_NICS_CAN_BE_CONVERTED_TO_OLD_FORMAT` | Attempted to convert a NIC with a birth year of 2000 or later to the old format |
262
+
263
+ ---
264
+
265
+ ## Configuration
266
+
267
+ The library exposes its validation constants through `NIC.Config`, which you can override to suit your needs:
268
+
269
+ ```ts
270
+ // Read the current values
271
+ NIC.Config.MINIMUM_LEGAL_AGE_TO_HAVE_NIC; // 15
272
+ NIC.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC; // 1901
273
+
274
+ // Override them
275
+ NIC.Config.MINIMUM_LEGAL_AGE_TO_HAVE_NIC = 16;
276
+ NIC.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC = 1950;
277
+ ```
278
+
279
+ These values are used internally during validation, generation, and error messages. Changing them affects all subsequent calls to `NIC.parse()`, `NIC.validate()`, `NIC.valid()`, and `NIC.random()`.
280
+
281
+ ---
282
+
283
+ ## API Reference
284
+
285
+ ### Static Methods
286
+
287
+ | Method | Returns | Throws | Description |
288
+ | ------------------- | ---------------------------------------------- | ----------- | ------------------------------------- |
289
+ | `NIC.parse(nic)` | `NIC` instance | `NIC.Error` | Parse a NIC string into a rich object |
290
+ | `NIC.validate(nic)` | `{ valid: true }` or `{ valid: false, error }` | — | Validate without throwing |
291
+ | `NIC.valid(nic)` | `boolean` | — | Quick validity check |
292
+ | `NIC.random()` | `string` | — | Generate a random valid NIC |
293
+ | `NIC.sanitize(nic)` | `string` | `NIC.Error` | Clean up and normalize a NIC string |
294
+
295
+ ### Instance Properties
296
+
297
+ | Property | Type | Description |
298
+ | ------------- | ---------------------- | --------------------------------------------------------------- |
299
+ | `.value` | `string` | The sanitized NIC string |
300
+ | `.type` | `"old" \| "new"` | NIC format type |
301
+ | `.gender` | `"male" \| "female"` | Gender encoded in the NIC |
302
+ | `.birthday` | `{ year, month, day }` | Full date of birth |
303
+ | `.age` | `number` | Current age in years |
304
+ | `.year` | `number` | Birth year |
305
+ | `.days` | `number` | Day-of-year value from the NIC |
306
+ | `.serial` | `string` | Serial number portion |
307
+ | `.checkdigit` | `string` | Check digit character |
308
+ | `.letter` | `string \| null` | Trailing letter — `"V"` or `"X"` for old format, `null` for new |
309
+ | `.voter` | `boolean \| null` | Voter registration status — old format only, `null` for new |
310
+
311
+ ### Instance Methods
312
+
313
+ | Method | Returns | Description |
314
+ | ------------- | -------- | --------------------------------------------------------- |
315
+ | `.convert()` | `string` | Convert between old and new NIC formats |
316
+ | `.toJSON()` | `object` | Get a summary object with type, gender, birthday, and age |
317
+ | `.toString()` | `string` | Get the NIC as a plain string |
318
+
319
+ ---
320
+
321
+ ## Module Support
322
+
323
+ The library ships with both ESM and CommonJS builds, plus TypeScript declarations:
324
+
325
+ ```ts
326
+ // ESM
327
+ import { NIC } from "@sri-lanka/nic";
328
+
329
+ // CommonJS
330
+ const { NIC } = require("@sri-lanka/nic");
331
+ ```
332
+
333
+ Tree-shaking is fully supported.
334
+
335
+ ---
336
+
337
+ ## Contributing
338
+
339
+ Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull request.
340
+
341
+ ## License
342
+
343
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class e{static MINIMUM_LEGAL_AGE_TO_HAVE_NIC=15;static OLDEST_BIRTH_YEAR_FOR_VALID_NIC=1901}const t={INVALID_NIC_STRUCTURE:()=>"Invalid NIC structure in the given NIC number. Old format requires 9 digits followed by 'V' or 'X'. New format requires 12 digits.",INVALID_BIRTH_YEAR:()=>`Invalid birth year in the given NIC number. Old format requires a year between ${e.OLDEST_BIRTH_YEAR_FOR_VALID_NIC} and 1999 (inclusive). New format requires a year between ${e.OLDEST_BIRTH_YEAR_FOR_VALID_NIC} and the current year minus the minimum legal age (${e.MINIMUM_LEGAL_AGE_TO_HAVE_NIC}) to have an NIC (inclusive).`,INVALID_DAY_OF_YEAR:()=>"Invalid day of the year in the given NIC number. Must be between 001 and 365 or 366 (inclusive) for males, or 501 and 865 or 866 (inclusive) for females.",MINIMUM_AGE_REQUIREMENT_NOT_MET:()=>`Minimum age requirement not met in the given NIC number. The legal age to obtain an NIC in Sri Lanka is ${e.MINIMUM_LEGAL_AGE_TO_HAVE_NIC} years.`,ONLY_19XX_YEAR_NICS_CAN_BE_CONVERTED_TO_OLD_FORMAT:()=>"Only 19xx year NICs can be converted to OLD format"};class r extends Error{code;constructor(e){super(t[e]()),Object.setPrototypeOf(this,r.prototype),this.name="NICError",this.code=e}}const i=(e,t)=>Math.floor(Math.random()*(t-e+1))+e;var s=(e=>(e.OLD="old",e.NEW="new",e))(s||{}),n=(e=>(e.MALE="male",e.FEMALE="female",e))(n||{});const a={now(){const e=new Date,t=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Colombo",year:"numeric",month:"numeric",day:"numeric",hourCycle:"h23"}).formatToParts(e),r=e=>Number(t.find(t=>t.type===e)?.value);return{year:r("year"),month:r("month"),day:r("day")}},dayInCurrentYear(){const e=this.now(),t=new Date(e.year,0,1),r=new Date(e.year,e.month-1,e.day);return Math.round((r.getTime()-t.getTime())/864e5)+1},isLeap:e=>e%4==0&&e%100!=0||e%400==0};class o{value;static Error=r;static Config=e;nic;_days;_year;_type;_gender;static STRUCTURE_OLD=/^[0-9]{9}[vVxX]$/;static STRUCTURE_NEW=/^[0-9]{12}$/;constructor(e,t,r,i,s){this.nic=e,this._type=t,this._gender=r,this._days=i,this._year=s,this.value=e}static sanitize(e){const t=o.validator(e);if(!t.success)throw t.error;return t.data.nic}static valid(e){return o.validate(e).valid}static validate(e){const t=o.validator(e);return t.success?{valid:!0}:{valid:!1,error:t.error}}static parse(e){const t=o.validator(e);if(!t.success)throw t.error;const{nic:r,type:i,gender:s,days:n,year:a}=t.data;return new o(r,i,s,n,a)}static random(){let e;const t=Math.random()<.5?s.OLD:s.NEW,r=Math.random()<.5?n.MALE:n.FEMALE,_=a.now().year-o.Config.MINIMUM_LEGAL_AGE_TO_HAVE_NIC;e=t===s.OLD?i(o.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC,1999):i(o.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC,_);let c=a.isLeap(e)?366:365;e===_&&(c=a.dayInCurrentYear());let E=i(1,c);r===n.FEMALE&&(E+=500);const u=String(e),h=String(E).padStart(3,"0"),I=String(i(1,999)).padStart(3,"0"),d=String(i(1,9));return t===s.OLD?`${u.slice(2)}${h}${I}${d}V`:`${u}${h}0${I}${d}`}convert(){if(this._type===s.OLD)return`${this.year}${this.days}0${this.serial}${this.checkdigit}`;if(this.nic.startsWith("19")){const e=this.year.slice(2),t=this.serial.slice(1);return`${e}${this.days}${t}${this.checkdigit}V`}throw new o.Error("ONLY_19XX_YEAR_NICS_CAN_BE_CONVERTED_TO_OLD_FORMAT")}get year(){return String(this._year)}get days(){return String(this._days).padStart(3,"0")}get serial(){return this._type===s.OLD?this.nic.substring(5,8):this.nic.substring(7,11)}get checkdigit(){return this._type===s.OLD?this.nic.substring(8,9):this.nic.substring(11,12)}get letter(){return this._type===s.OLD?this.nic.substring(9,10):null}get type(){return this._type}get gender(){return this._gender}get voter(){return this._type===s.NEW?null:"V"===this.letter}get birthday(){let e=[31,59,90,120,151,181,212,243,273,304,334,365];a.isLeap(this._year)&&(e=e.map((e,t)=>t>0?e+1:e));const t=this._days;let r=0,i=1,s=0;for(let n=0;n<e.length;n++){const a=e[n];if(!(t>a)){s=t-r;break}i++,r=a}return{year:this._year,month:i,day:s}}get age(){const e=a.now(),t=e.year,r=e.month,i=e.day,s=this.birthday;let n=t-s.year;const o=r-s.month,_=i-s.day;return(o<0||0===o&&_<0)&&n--,n}toString(){return this.nic}toJSON(){return{nic:this.nic,year:this.year,days:this.days,serial:this.serial,checkdigit:this.checkdigit,letter:this.letter,type:this.type,gender:this.gender,voter:this.voter,birthday:this.birthday,age:this.age}}static validator(e){if(e=e.trim().toUpperCase(),!o.STRUCTURE_OLD.test(e)&&!o.STRUCTURE_NEW.test(e))return{success:!1,error:new o.Error("INVALID_NIC_STRUCTURE")};const t=10===e.length,r=a.now().year-o.Config.MINIMUM_LEGAL_AGE_TO_HAVE_NIC;let i,_,c;if(t){if(i=1900+Number(e.substring(0,2)),i<o.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC)return{success:!1,error:new o.Error("INVALID_BIRTH_YEAR")}}else{const t=Number(e.substring(0,4));if(t<o.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC)return{success:!1,error:new o.Error("INVALID_BIRTH_YEAR")};if(t>r)return{success:!1,error:new o.Error("MINIMUM_AGE_REQUIREMENT_NOT_MET")};i=t}_=Number(t?e.substring(2,5):e.substring(4,7)),_>500?(_-=500,c=n.FEMALE):c=n.MALE;const E=a.isLeap(i)?366:365;if(_<1||_>E)return{success:!1,error:new o.Error("INVALID_DAY_OF_YEAR")};if(i===r&&_>a.dayInCurrentYear())return{success:!1,error:new o.Error("MINIMUM_AGE_REQUIREMENT_NOT_MET")};return{success:!0,data:{nic:e,type:t?s.OLD:s.NEW,gender:c,days:_,year:i}}}}exports.Gender=n,exports.NIC=o,exports.NICConfig=e,exports.NICError=r,exports.NICType=s;
@@ -0,0 +1,192 @@
1
+ export declare type Birthday = {
2
+ year: number;
3
+ month: number;
4
+ day: number;
5
+ };
6
+
7
+ declare type ErrorCodes = keyof typeof errors;
8
+
9
+ declare const errors: {
10
+ INVALID_NIC_STRUCTURE: () => string;
11
+ INVALID_BIRTH_YEAR: () => string;
12
+ INVALID_DAY_OF_YEAR: () => string;
13
+ MINIMUM_AGE_REQUIREMENT_NOT_MET: () => string;
14
+ ONLY_19XX_YEAR_NICS_CAN_BE_CONVERTED_TO_OLD_FORMAT: () => string;
15
+ };
16
+
17
+ export declare enum Gender {
18
+ MALE = "male",
19
+ FEMALE = "female"
20
+ }
21
+
22
+ export declare class NIC {
23
+ /**
24
+ * The NIC number as a string.
25
+ */
26
+ readonly value: string;
27
+ static readonly Error: typeof NICError;
28
+ static readonly Config: typeof NICConfig;
29
+ private readonly nic;
30
+ private readonly _days;
31
+ private readonly _year;
32
+ private readonly _type;
33
+ private readonly _gender;
34
+ private static readonly STRUCTURE_OLD;
35
+ private static readonly STRUCTURE_NEW;
36
+ private constructor();
37
+ /**
38
+ * Cleans up the given NIC number by removing extra spaces and formatting it properly.
39
+ * Throws an error if the NIC is completely invalid.
40
+ *
41
+ * @param nic - The NIC number string.
42
+ * @returns The cleaned up NIC string.
43
+ */
44
+ static sanitize(nic: string): string;
45
+ /**
46
+ * Quickly checks if a given NIC number string is valid.
47
+ *
48
+ * @param nic - The NIC number string.
49
+ * @returns `true` if it's a valid NIC, otherwise `false`.
50
+ */
51
+ static valid(nic: string): boolean;
52
+ /**
53
+ * Detailed check to see if a NIC number is valid.
54
+ * Instead of throwing an error, it returns an object explaining what went wrong if it's invalid.
55
+ *
56
+ * @param nic - The NIC number string.
57
+ * @returns An object with `valid: true` or `valid: false` along with the specific error.
58
+ */
59
+ static validate(nic: string): {
60
+ valid: boolean;
61
+ error?: undefined;
62
+ } | {
63
+ valid: boolean;
64
+ error: NICError;
65
+ };
66
+ /**
67
+ * Reads a NIC string and extracts all the useful details from it like the birth year and gender.
68
+ * Throws an error if the NIC is invalid.
69
+ *
70
+ * @param nic - The NIC number string.
71
+ * @returns A completely populated `NIC` object.
72
+ */
73
+ static parse(nic: string): NIC;
74
+ /**
75
+ * Generates a completely valid, random NIC number.
76
+ * Great for testing and mocking data!
77
+ *
78
+ * @returns A random NIC string.
79
+ */
80
+ static random(): string;
81
+ /**
82
+ * Switches the NIC number between the old and new formats.
83
+ * Old format -> New format.
84
+ * New format -> Old format (Only works for birth years starting with 19xx).
85
+ *
86
+ * @returns The converted NIC number string.
87
+ */
88
+ convert(): string;
89
+ /**
90
+ * Full birth year (e.g. "1991" or "2001").
91
+ */
92
+ get year(): string;
93
+ /**
94
+ * The number of days passed in the birth year.
95
+ */
96
+ get days(): string;
97
+ /**
98
+ * The unique serial number assigned on the birth day.
99
+ */
100
+ get serial(): string;
101
+ /**
102
+ * The single check digit character of the NIC.
103
+ */
104
+ get checkdigit(): string;
105
+ /**
106
+ * The trailing letter of the old format NIC (usually "V" or "X").
107
+ * Returns null if it's a new format NIC.
108
+ */
109
+ get letter(): string | null;
110
+ /**
111
+ * Identifies whether the format is OLD or NEW.
112
+ */
113
+ get type(): NICType;
114
+ /**
115
+ * The gender (Male or Female) extracted from the NIC.
116
+ */
117
+ get gender(): Gender;
118
+ /**
119
+ * Identifies if the person is a registered voter.
120
+ * Only applicable for old format NICs (letter "V"), returns null for new ones.
121
+ */
122
+ get voter(): boolean | null;
123
+ /**
124
+ * The full date of birth separated into year, month, and day.
125
+ */
126
+ get birthday(): Birthday;
127
+ /**
128
+ * Calculates the person's current age in years.
129
+ */
130
+ get age(): number;
131
+ /**
132
+ * The plain NIC number string.
133
+ */
134
+ toString(): string;
135
+ /**
136
+ * Creates a neat summary object containing all the details of the NIC.
137
+ */
138
+ toJSON(): {
139
+ nic: string;
140
+ year: string;
141
+ days: string;
142
+ serial: string;
143
+ checkdigit: string;
144
+ letter: string | null;
145
+ type: NICType;
146
+ gender: Gender;
147
+ voter: boolean | null;
148
+ birthday: Birthday;
149
+ age: number;
150
+ };
151
+ private static validator;
152
+ }
153
+
154
+ export declare class NICConfig {
155
+ /**
156
+ * The minimum legal age to hold a NIC in Sri Lanka is 15 years.
157
+ * This value is used to validate the birth year.
158
+ * "Every person who is a citizen of Sri Lanka and who has attained or attains the age of 15 years shall apply for a National Identity card."
159
+ * @see https://drp.gov.lk/en/normal.php
160
+ */
161
+ static MINIMUM_LEGAL_AGE_TO_HAVE_NIC: number;
162
+ /**
163
+ * The oldest birth year considered valid. NICs with birth years before this are invalid.
164
+ */
165
+ static OLDEST_BIRTH_YEAR_FOR_VALID_NIC: number;
166
+ }
167
+
168
+ export declare class NICError extends Error {
169
+ readonly code: ErrorCodes;
170
+ constructor(code: ErrorCodes);
171
+ }
172
+
173
+ export declare enum NICType {
174
+ OLD = "old",
175
+ NEW = "new"
176
+ }
177
+
178
+ export declare type ValidatorResult = {
179
+ success: true;
180
+ data: {
181
+ nic: string;
182
+ type: NICType;
183
+ gender: Gender;
184
+ days: number;
185
+ year: number;
186
+ };
187
+ } | {
188
+ success: false;
189
+ error: NICError;
190
+ };
191
+
192
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ class t{static MINIMUM_LEGAL_AGE_TO_HAVE_NIC=15;static OLDEST_BIRTH_YEAR_FOR_VALID_NIC=1901}const e={INVALID_NIC_STRUCTURE:()=>"Invalid NIC structure in the given NIC number. Old format requires 9 digits followed by 'V' or 'X'. New format requires 12 digits.",INVALID_BIRTH_YEAR:()=>`Invalid birth year in the given NIC number. Old format requires a year between ${t.OLDEST_BIRTH_YEAR_FOR_VALID_NIC} and 1999 (inclusive). New format requires a year between ${t.OLDEST_BIRTH_YEAR_FOR_VALID_NIC} and the current year minus the minimum legal age (${t.MINIMUM_LEGAL_AGE_TO_HAVE_NIC}) to have an NIC (inclusive).`,INVALID_DAY_OF_YEAR:()=>"Invalid day of the year in the given NIC number. Must be between 001 and 365 or 366 (inclusive) for males, or 501 and 865 or 866 (inclusive) for females.",MINIMUM_AGE_REQUIREMENT_NOT_MET:()=>`Minimum age requirement not met in the given NIC number. The legal age to obtain an NIC in Sri Lanka is ${t.MINIMUM_LEGAL_AGE_TO_HAVE_NIC} years.`,ONLY_19XX_YEAR_NICS_CAN_BE_CONVERTED_TO_OLD_FORMAT:()=>"Only 19xx year NICs can be converted to OLD format"};class r extends Error{code;constructor(t){super(e[t]()),Object.setPrototypeOf(this,r.prototype),this.name="NICError",this.code=t}}const i=(t,e)=>Math.floor(Math.random()*(e-t+1))+t;var s=/* @__PURE__ */(t=>(t.OLD="old",t.NEW="new",t))(s||{}),n=/* @__PURE__ */(t=>(t.MALE="male",t.FEMALE="female",t))(n||{});const a={now(){const t=/* @__PURE__ */new Date,e=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Colombo",year:"numeric",month:"numeric",day:"numeric",hourCycle:"h23"}).formatToParts(t),r=t=>Number(e.find(e=>e.type===t)?.value);return{year:r("year"),month:r("month"),day:r("day")}},dayInCurrentYear(){const t=this.now(),e=new Date(t.year,0,1),r=new Date(t.year,t.month-1,t.day);return Math.round((r.getTime()-e.getTime())/864e5)+1},isLeap:t=>t%4==0&&t%100!=0||t%400==0};class _{value;static Error=r;static Config=t;nic;_days;_year;_type;_gender;static STRUCTURE_OLD=/^[0-9]{9}[vVxX]$/;static STRUCTURE_NEW=/^[0-9]{12}$/;constructor(t,e,r,i,s){this.nic=t,this._type=e,this._gender=r,this._days=i,this._year=s,this.value=t}static sanitize(t){const e=_.validator(t);if(!e.success)throw e.error;return e.data.nic}static valid(t){return _.validate(t).valid}static validate(t){const e=_.validator(t);return e.success?{valid:!0}:{valid:!1,error:e.error}}static parse(t){const e=_.validator(t);if(!e.success)throw e.error;const{nic:r,type:i,gender:s,days:n,year:a}=e.data;return new _(r,i,s,n,a)}static random(){let t;const e=Math.random()<.5?s.OLD:s.NEW,r=Math.random()<.5?n.MALE:n.FEMALE,o=a.now().year-_.Config.MINIMUM_LEGAL_AGE_TO_HAVE_NIC;t=e===s.OLD?i(_.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC,1999):i(_.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC,o);let c=a.isLeap(t)?366:365;t===o&&(c=a.dayInCurrentYear());let E=i(1,c);r===n.FEMALE&&(E+=500);const h=String(t),u=String(E).padStart(3,"0"),I=String(i(1,999)).padStart(3,"0"),d=String(i(1,9));return e===s.OLD?`${h.slice(2)}${u}${I}${d}V`:`${h}${u}0${I}${d}`}convert(){if(this._type===s.OLD)return`${this.year}${this.days}0${this.serial}${this.checkdigit}`;if(this.nic.startsWith("19")){const t=this.year.slice(2),e=this.serial.slice(1);return`${t}${this.days}${e}${this.checkdigit}V`}throw new _.Error("ONLY_19XX_YEAR_NICS_CAN_BE_CONVERTED_TO_OLD_FORMAT")}get year(){return String(this._year)}get days(){return String(this._days).padStart(3,"0")}get serial(){return this._type===s.OLD?this.nic.substring(5,8):this.nic.substring(7,11)}get checkdigit(){return this._type===s.OLD?this.nic.substring(8,9):this.nic.substring(11,12)}get letter(){return this._type===s.OLD?this.nic.substring(9,10):null}get type(){return this._type}get gender(){return this._gender}get voter(){return this._type===s.NEW?null:"V"===this.letter}get birthday(){let t=[31,59,90,120,151,181,212,243,273,304,334,365];a.isLeap(this._year)&&(t=t.map((t,e)=>e>0?t+1:t));const e=this._days;let r=0,i=1,s=0;for(let n=0;n<t.length;n++){const a=t[n];if(!(e>a)){s=e-r;break}i++,r=a}return{year:this._year,month:i,day:s}}get age(){const t=a.now(),e=t.year,r=t.month,i=t.day,s=this.birthday;let n=e-s.year;const _=r-s.month,o=i-s.day;return(_<0||0===_&&o<0)&&n--,n}toString(){return this.nic}toJSON(){return{nic:this.nic,year:this.year,days:this.days,serial:this.serial,checkdigit:this.checkdigit,letter:this.letter,type:this.type,gender:this.gender,voter:this.voter,birthday:this.birthday,age:this.age}}static validator(t){if(t=t.trim().toUpperCase(),!_.STRUCTURE_OLD.test(t)&&!_.STRUCTURE_NEW.test(t))return{success:!1,error:new _.Error("INVALID_NIC_STRUCTURE")};const e=10===t.length,r=a.now().year-_.Config.MINIMUM_LEGAL_AGE_TO_HAVE_NIC;let i,o,c;if(e){if(i=1900+Number(t.substring(0,2)),i<_.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC)return{success:!1,error:new _.Error("INVALID_BIRTH_YEAR")}}else{const e=Number(t.substring(0,4));if(e<_.Config.OLDEST_BIRTH_YEAR_FOR_VALID_NIC)return{success:!1,error:new _.Error("INVALID_BIRTH_YEAR")};if(e>r)return{success:!1,error:new _.Error("MINIMUM_AGE_REQUIREMENT_NOT_MET")};i=e}o=Number(e?t.substring(2,5):t.substring(4,7)),o>500?(o-=500,c=n.FEMALE):c=n.MALE;const E=a.isLeap(i)?366:365;if(o<1||o>E)return{success:!1,error:new _.Error("INVALID_DAY_OF_YEAR")};if(i===r&&o>a.dayInCurrentYear())return{success:!1,error:new _.Error("MINIMUM_AGE_REQUIREMENT_NOT_MET")};return{success:!0,data:{nic:t,type:e?s.OLD:s.NEW,gender:c,days:o,year:i}}}}export{n as Gender,_ as NIC,t as NICConfig,r as NICError,s as NICType};
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@sri-lanka/nic",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight library for parsing, validating, and generating Sri Lankan National Identity Card (NIC) numbers.",
5
+ "author": "Vinod Liyanage <vinodliyanage.me>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "sideEffects": false,
22
+ "keywords": [
23
+ "sri-lanka",
24
+ "nic",
25
+ "national-identity-card",
26
+ "validator",
27
+ "parser",
28
+ "generator",
29
+ "typescript"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/vinodliyanage/sri-lanka-nic"
34
+ },
35
+ "homepage": "https://nic.vinodliyanage.me",
36
+ "bugs": {
37
+ "url": "https://github.com/vinodliyanage/sri-lanka-nic/issues"
38
+ },
39
+ "devDependencies": {
40
+ "@tailwindcss/vite": "^4.1.18",
41
+ "@types/node": "^25.2.3",
42
+ "@types/react": "^19.2.14",
43
+ "@types/react-dom": "^19.2.3",
44
+ "@vitejs/plugin-react": "^5.1.4",
45
+ "@vitest/coverage-v8": "4.0.18",
46
+ "autoprefixer": "^10.4.24",
47
+ "lucide-react": "^0.564.0",
48
+ "postcss": "^8.5.6",
49
+ "prism-react-renderer": "^2.4.1",
50
+ "react": "^19.2.4",
51
+ "react-dom": "^19.2.4",
52
+ "tailwindcss": "^4.1.18",
53
+ "terser": "^5.46.0",
54
+ "tsx": "^4.21.0",
55
+ "typescript": "^5.9.3",
56
+ "vite": "^7.3.1",
57
+ "vite-plugin-dts": "^4.5.4",
58
+ "vitest": "^4.0.18",
59
+ "zod": "^4.3.6"
60
+ },
61
+ "scripts": {
62
+ "dev": "vite",
63
+ "build": "vite build --config vite.lib.config.ts",
64
+ "preview": "vite preview",
65
+ "test": "vitest",
66
+ "test:coverage": "vitest run --coverage"
67
+ }
68
+ }