@aakashwije/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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +616 -0
- package/dist/cli.cjs +282 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +280 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +300 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +279 -0
- package/dist/index.js.map +1 -0
- package/package.json +122 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## 1.0.0 (2026-06-16)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* initial lanka nic package ([ac97912](https://github.com/Aakashwije/lanka-nic/commit/ac97912bc0acdcfe446f9c6401a8a058c81d6485))
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* add conventional commits preset for semantic-release ([ddce03e](https://github.com/Aakashwije/lanka-nic/commit/ddce03ea188f470bb3156798f684dc17ddb7fd62))
|
|
10
|
+
* correct repository metadata for semantic-release ([985235d](https://github.com/Aakashwije/lanka-nic/commit/985235dac9174a81db13de5c32318933f2285445))
|
|
11
|
+
* disable npm publishing in semantic-release to fix CI release job ([23f93b4](https://github.com/Aakashwije/lanka-nic/commit/23f93b419a56ef419f31b01c36d91c1f845805e8))
|
|
12
|
+
* harden release workflow token handling ([1e348b8](https://github.com/Aakashwije/lanka-nic/commit/1e348b8cea175a003ba837f839191cb2fc6186f7))
|
|
13
|
+
* harden semantic-release repo and token env ([af164c9](https://github.com/Aakashwije/lanka-nic/commit/af164c94686d4e1f028cbe8fd097677bc05c9f00))
|
|
14
|
+
|
|
15
|
+
# Changelog
|
|
16
|
+
|
|
17
|
+
All notable changes to this project will be documented in this file.
|
|
18
|
+
|
|
19
|
+
## [0.1.0] - 2026-06-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Validation, parsing, normalization, masking, and old→new conversion for Sri Lankan NIC numbers.
|
|
24
|
+
- `safeParse` returning a discriminated `{ success, data | error }` result with typed error codes.
|
|
25
|
+
- `parseNICOrThrow` variant that throws `NICError` with `.code` and `.input`.
|
|
26
|
+
- Type guards: `isValidNIC`, `isOldNIC`, `isNewNIC`.
|
|
27
|
+
- Age helpers: `getAge`, `getAgeOn`.
|
|
28
|
+
- `equalsNIC` for cross-format equality.
|
|
29
|
+
- `validateBatch`, `parseBatch` for array workflows.
|
|
30
|
+
- `generateNIC` test fixture helper (round-trippable through `parseNIC`).
|
|
31
|
+
- `lanka-nic` CLI binary with `validate`, `parse`, `mask`, `age` subcommands.
|
|
32
|
+
- Dual ESM + CJS distribution with TypeScript declarations.
|
|
33
|
+
- npm provenance attestation on release.
|
|
34
|
+
- 100% coverage gate in CI.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aakash Wijesekara
|
|
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,616 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# `@aakashwije/lanka-nic`
|
|
4
|
+
|
|
5
|
+
**Production-grade Sri Lankan NIC validation, parsing, and generation**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@aakashwije/lanka-nic)
|
|
8
|
+
[](https://github.com/aakashlk/lanka-nic/actions)
|
|
9
|
+
[](#)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://www.typescriptlang.org/)
|
|
12
|
+
[](https://nodejs.org/)
|
|
13
|
+
[](#)
|
|
14
|
+
[](#)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
*Engineered for correctness and developer ergonomics. UTC-safe date math, leap-year aware, typed error codes, and a first-class CLI — all with zero runtime dependencies.*
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
- [Overview](#overview)
|
|
27
|
+
- [Technology Stack](#technology-stack)
|
|
28
|
+
- [NIC Format Specification](#nic-format-specification)
|
|
29
|
+
- [Architecture](#architecture)
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Quick Start](#quick-start)
|
|
32
|
+
- [API Reference](#api-reference)
|
|
33
|
+
- [CLI](#cli)
|
|
34
|
+
- [Contributing](#contributing)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
`@aakashwije/lanka-nic` is a zero-dependency, fully-typed TypeScript library for working with Sri Lankan **National Identity Card (NIC)** numbers. It handles both the legacy 9-digit format (`YYXXXSSSSV`) and the modern 12-digit format (`YYYYXXXSSSSS`), covering:
|
|
42
|
+
|
|
43
|
+
- **Parsing** — extract birth year, birth date, day-of-year, gender
|
|
44
|
+
- **Validation** — format and semantic correctness including leap-year safety
|
|
45
|
+
- **Normalization** — whitespace trimming, suffix casing
|
|
46
|
+
- **Masking** — safe display in logs and UIs
|
|
47
|
+
- **Age derivation** — UTC-correct age calculation at any reference date
|
|
48
|
+
- **Format conversion** — old → new best-effort conversion
|
|
49
|
+
- **Equality** — compare NICs across formats
|
|
50
|
+
- **Batch operations** — validate or parse arrays efficiently
|
|
51
|
+
- **Test data generation** — generate syntactically valid NICs for seeding
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Technology Stack
|
|
56
|
+
|
|
57
|
+
| Layer | Technology | Purpose |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Language |  | Type-safe implementation |
|
|
60
|
+
| Runtime |  | Server-side execution |
|
|
61
|
+
| Build |  | Dual ESM + CJS output |
|
|
62
|
+
| Test |  | Unit tests + coverage |
|
|
63
|
+
| Lint |  | Code quality |
|
|
64
|
+
| Format |  | Consistent style |
|
|
65
|
+
| Release |  | Automated versioning |
|
|
66
|
+
| CI/CD |  | Continuous integration |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## NIC Format Specification
|
|
71
|
+
|
|
72
|
+
Sri Lanka uses two NIC formats, both encoding birth year, day-of-year, gender, and serial number in a compact string.
|
|
73
|
+
|
|
74
|
+
### Old Format — `YYXXXSSSSV`
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
┌─ 2 ─┬──── 3 ────┬──── 4 ────┬─ 1 ─┐
|
|
78
|
+
│ YY │ DDD │ SSSS │ V │
|
|
79
|
+
└──────┴───────────┴───────────┴──────┘
|
|
80
|
+
Year Day code Serial Check letter
|
|
81
|
+
(1900+) (001–866) (0000–9999) (V or X)
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
950012345V → born 1995, day 001, male, serial 2345
|
|
85
|
+
956512345V → born 1995, day 001, female (001 + 500 = 501... see gender encoding)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### New Format — `YYYYXXXSSSSSSS`
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
┌──── 4 ────┬──── 3 ────┬─────── 5 ───────┐
|
|
92
|
+
│ YYYY │ DDD │ SSSSS │
|
|
93
|
+
└───────────┴───────────┴──────────────────┘
|
|
94
|
+
Birth year Day code Serial
|
|
95
|
+
(4 digits) (001–866) (00000–99999)
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
199500123456 → born 1995, day 001, male, serial 23456
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Gender Encoding
|
|
102
|
+
|
|
103
|
+
The day-of-year is encoded directly for males. For females, **500 is added** to the raw day value:
|
|
104
|
+
|
|
105
|
+
| Gender | Day code range | Resolved day-of-year |
|
|
106
|
+
|--------|---------------|---------------------|
|
|
107
|
+
| Male | `001` – `366` | value as-is |
|
|
108
|
+
| Female | `501` – `866` | value − 500 |
|
|
109
|
+
| Invalid | `000`, `367`–`500`, `867`+ | rejected |
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Architecture
|
|
114
|
+
|
|
115
|
+
### Module Dependency Graph
|
|
116
|
+
|
|
117
|
+
```mermaid
|
|
118
|
+
graph TD
|
|
119
|
+
subgraph Utilities["⚙️ Utilities"]
|
|
120
|
+
constants["constants\nregex patterns · day offsets"]
|
|
121
|
+
date["date utils\nleap year · day→date · format"]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
subgraph Parsing["🔍 Parsing & Normalization"]
|
|
125
|
+
normalize["normalizeNIC\ntrim · compact · uppercase suffix"]
|
|
126
|
+
parse["safeParse · parseNIC\nparseNICOrThrow\ngetNICType · getBirthDate · getGender"]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
subgraph Derived["📦 Derived Operations"]
|
|
130
|
+
validate["validateNIC"]
|
|
131
|
+
mask["maskNIC"]
|
|
132
|
+
age["getAge · getAgeOn"]
|
|
133
|
+
equals["equalsNIC"]
|
|
134
|
+
convert["convertOldToNew"]
|
|
135
|
+
guards["isValidNIC · isOldNIC · isNewNIC"]
|
|
136
|
+
batch["validateBatch · parseBatch"]
|
|
137
|
+
generate["generateNIC"]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
subgraph CLI["💻 CLI"]
|
|
141
|
+
cli_core["runCommand\n(testable core)"]
|
|
142
|
+
cli_bin["lanka-nic binary"]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
constants --> normalize & parse & generate
|
|
146
|
+
date --> parse & generate
|
|
147
|
+
normalize --> parse & mask & convert
|
|
148
|
+
|
|
149
|
+
parse --> validate & mask & age & equals & guards & batch
|
|
150
|
+
validate --> guards & batch & mask
|
|
151
|
+
|
|
152
|
+
validate & parse & mask & age --> cli_core
|
|
153
|
+
cli_core --> cli_bin
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Parse Pipeline
|
|
157
|
+
|
|
158
|
+
```mermaid
|
|
159
|
+
flowchart LR
|
|
160
|
+
A([raw input]) --> B{is string?}
|
|
161
|
+
B -- ❌ no --> E1([NON_STRING])
|
|
162
|
+
B -- ✅ yes --> C[normalizeNIC\ntrim · compact\nuppercase suffix]
|
|
163
|
+
C --> D{empty\nafter trim?}
|
|
164
|
+
D -- ❌ yes --> E2([EMPTY])
|
|
165
|
+
D -- ✅ no --> F{matches\nold or new regex?}
|
|
166
|
+
F -- ❌ neither --> E3([INVALID_FORMAT])
|
|
167
|
+
F -- old\n9d+V/X --> G[extract yy · dayCode]
|
|
168
|
+
F -- new\n12d --> H[extract yyyy · dayCode]
|
|
169
|
+
G & H --> I{birthYear\n≤ today UTC?}
|
|
170
|
+
I -- ❌ no --> E4([FUTURE_YEAR])
|
|
171
|
+
I -- ✅ yes --> J{dayCode in\n1–366 or 501–866?}
|
|
172
|
+
J -- ❌ no --> E5([INVALID_DAY_CODE])
|
|
173
|
+
J -- ✅ yes --> K{day exists\nin that year?}
|
|
174
|
+
K -- ❌ no\ne.g. day 366\nnon-leap --> E6([INVALID_DAY_FOR_YEAR])
|
|
175
|
+
K -- ✅ yes --> L([✅ ParsedNIC])
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### CLI Interaction Flow
|
|
179
|
+
|
|
180
|
+
```mermaid
|
|
181
|
+
sequenceDiagram
|
|
182
|
+
actor User
|
|
183
|
+
participant CLI as lanka-nic
|
|
184
|
+
participant Core as runCommand
|
|
185
|
+
participant Lib as @aakashwije/lanka-nic
|
|
186
|
+
|
|
187
|
+
User->>CLI: lanka-nic parse 950012345V
|
|
188
|
+
|
|
189
|
+
alt NIC passed as argument
|
|
190
|
+
CLI->>Core: argv=['parse','950012345V'], stdin=null
|
|
191
|
+
else NIC from stdin
|
|
192
|
+
User->>CLI: echo 950012345V | lanka-nic parse
|
|
193
|
+
CLI->>CLI: readStdin()
|
|
194
|
+
CLI->>Core: argv=['parse'], stdin='950012345V'
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
Core->>Lib: safeParse('950012345V')
|
|
198
|
+
Lib-->>Core: { success: true, data: ParsedNIC }
|
|
199
|
+
Core-->>CLI: { code: 0, stdout: JSON }
|
|
200
|
+
CLI-->>User: prints JSON · exits 0
|
|
201
|
+
|
|
202
|
+
Note over CLI,Lib: On invalid NIC → exits 1 with error JSON on stderr
|
|
203
|
+
Note over CLI,Lib: On bad command → exits 2 with usage text on stderr
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Installation
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# npm
|
|
212
|
+
npm install @aakashwije/lanka-nic
|
|
213
|
+
|
|
214
|
+
# pnpm
|
|
215
|
+
pnpm add @aakashwije/lanka-nic
|
|
216
|
+
|
|
217
|
+
# yarn
|
|
218
|
+
yarn add @aakashwije/lanka-nic
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Requires **Node.js ≥ 18**. Zero runtime dependencies. Dual ESM + CommonJS output.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Quick Start
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { parseNIC, validateNIC, getAge, maskNIC } from '@aakashwije/lanka-nic';
|
|
229
|
+
|
|
230
|
+
const nic = '950012345V';
|
|
231
|
+
|
|
232
|
+
validateNIC(nic); // true
|
|
233
|
+
getAge(nic); // 30 (computed against today UTC)
|
|
234
|
+
maskNIC(nic); // '95001****V'
|
|
235
|
+
|
|
236
|
+
const parsed = parseNIC(nic);
|
|
237
|
+
// {
|
|
238
|
+
// input: '950012345V',
|
|
239
|
+
// normalized: '950012345V',
|
|
240
|
+
// type: 'old',
|
|
241
|
+
// valid: true,
|
|
242
|
+
// birthYear: 1995,
|
|
243
|
+
// dayOfYear: 1,
|
|
244
|
+
// birthDate: '1995-01-01',
|
|
245
|
+
// gender: 'male'
|
|
246
|
+
// }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## API Reference
|
|
252
|
+
|
|
253
|
+
### Parsing
|
|
254
|
+
|
|
255
|
+
#### `safeParse(nic: unknown): SafeParseResult<ParsedNIC>`
|
|
256
|
+
|
|
257
|
+
The primary entry point. Returns a discriminated union — **never throws**. Accepts `unknown` so it is safe to call directly on unvalidated external input.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { safeParse } from '@aakashwije/lanka-nic';
|
|
261
|
+
|
|
262
|
+
const result = safeParse(req.body.nic);
|
|
263
|
+
|
|
264
|
+
if (result.success) {
|
|
265
|
+
const { birthDate, gender, birthYear } = result.data;
|
|
266
|
+
} else {
|
|
267
|
+
// result.error is a NICError with .code and .message
|
|
268
|
+
console.error(result.error.code); // 'INVALID_FORMAT'
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### `parseNIC(nic: string): ParsedNIC | null`
|
|
273
|
+
|
|
274
|
+
Returns the parsed result or `null` for any invalid input. Useful with optional chaining.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const age = parseNIC(nic)?.birthDate ?? 'unknown';
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `parseNICOrThrow(nic: string): ParsedNIC`
|
|
281
|
+
|
|
282
|
+
Throws a `NICError` on invalid input. Use when you control the input and want to fail fast.
|
|
283
|
+
|
|
284
|
+
#### `getNICType(nic: string): 'old' | 'new' | 'invalid'`
|
|
285
|
+
|
|
286
|
+
Detects the format without full semantic validation. Useful for routing logic before parsing.
|
|
287
|
+
|
|
288
|
+
#### `getBirthDate(nic: string): string | null`
|
|
289
|
+
|
|
290
|
+
Returns the birth date as `YYYY-MM-DD` (UTC) or `null`.
|
|
291
|
+
|
|
292
|
+
#### `getGender(nic: string): 'male' | 'female' | null`
|
|
293
|
+
|
|
294
|
+
Resolves gender from the NIC day code.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### Validation
|
|
299
|
+
|
|
300
|
+
#### `validateNIC(nic: string): boolean`
|
|
301
|
+
|
|
302
|
+
Returns `true` only for fully valid NICs — format, day code range, and date integrity all checked.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
### Normalization
|
|
307
|
+
|
|
308
|
+
#### `normalizeNIC(nic: string): string`
|
|
309
|
+
|
|
310
|
+
Strips surrounding whitespace, collapses internal whitespace, uppercases the check letter for old-format NICs.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
normalizeNIC(' 950012345v '); // '950012345V'
|
|
314
|
+
normalizeNIC('1995 001 23456'); // '199500123456'
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
### Masking
|
|
320
|
+
|
|
321
|
+
#### `maskNIC(nic: string): string`
|
|
322
|
+
|
|
323
|
+
Masks serial digits for safe logging and display. Returns the (normalized) input as-is for invalid NICs.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
maskNIC('950012345V'); // '95001****V' (masks 4 serial digits)
|
|
327
|
+
maskNIC('199500123456'); // '1995001*****' (masks 5 serial digits)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### Age Calculation
|
|
333
|
+
|
|
334
|
+
#### `getAge(nic: string, opts?: { now?: Date }): number | null`
|
|
335
|
+
|
|
336
|
+
Calculates the person's age as of today (UTC). Pass `opts.now` to fix the reference date — useful in tests and audits.
|
|
337
|
+
|
|
338
|
+
#### `getAgeOn(nic: string, date: Date): number | null`
|
|
339
|
+
|
|
340
|
+
Calculates age as of a specific reference date. Returns `null` if the date precedes the birth date.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { getAge, getAgeOn } from '@aakashwije/lanka-nic';
|
|
344
|
+
|
|
345
|
+
getAge('950012345V');
|
|
346
|
+
// → current age as of today UTC
|
|
347
|
+
|
|
348
|
+
getAgeOn('950012345V', new Date('2025-06-01'));
|
|
349
|
+
// → 30
|
|
350
|
+
|
|
351
|
+
getAge('950012345V', { now: new Date('2030-01-01') });
|
|
352
|
+
// → 35
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
### Format Conversion
|
|
358
|
+
|
|
359
|
+
#### `convertOldToNew(nic: string): string | null`
|
|
360
|
+
|
|
361
|
+
Converts a valid old-format NIC to an 11-character new-format representation. Returns `null` for non-old-format input.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
convertOldToNew('950012345V'); // '19950012345'
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
> The official 12th check digit cannot be derived from old NIC data. The output is a best-effort 11-digit form; do not treat it as an authoritative new NIC.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### Equality
|
|
372
|
+
|
|
373
|
+
#### `equalsNIC(a: string, b: string): boolean`
|
|
374
|
+
|
|
375
|
+
Returns `true` when two NICs (old or new format, any casing) refer to the same person and serial — matching on birth year, day-of-year, gender, and serial digits.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
equalsNIC('950012345V', '950012345v'); // true (case normalized)
|
|
379
|
+
equalsNIC('950012345V', '950012346V'); // false (different serial)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
### Type Guards
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { isValidNIC, isOldNIC, isNewNIC } from '@aakashwije/lanka-nic';
|
|
388
|
+
|
|
389
|
+
isValidNIC('950012345V'); // true — any valid NIC (narrows to string)
|
|
390
|
+
isOldNIC('950012345V'); // true — valid old-format NIC
|
|
391
|
+
isNewNIC('199500123456'); // true — valid new-format NIC
|
|
392
|
+
|
|
393
|
+
// TypeScript narrowing
|
|
394
|
+
function process(value: unknown) {
|
|
395
|
+
if (isValidNIC(value)) {
|
|
396
|
+
// value: string
|
|
397
|
+
parseNIC(value);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
### Batch Operations
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { validateBatch, parseBatch } from '@aakashwije/lanka-nic';
|
|
408
|
+
|
|
409
|
+
validateBatch(['950012345V', 'invalid', '199500123456']);
|
|
410
|
+
// [true, false, true]
|
|
411
|
+
|
|
412
|
+
parseBatch(['950012345V', 'bad']);
|
|
413
|
+
// [ParsedNIC, null]
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### Test Data Generation
|
|
419
|
+
|
|
420
|
+
#### `generateNIC(opts: NICGenerateOptions): string`
|
|
421
|
+
|
|
422
|
+
Generates a syntactically valid NIC for a given birth year, day, gender, and format. Designed for seeding test fixtures.
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
import { generateNIC } from '@aakashwije/lanka-nic';
|
|
426
|
+
|
|
427
|
+
generateNIC({ year: 1995, dayOfYear: 1, gender: 'male', format: 'old' });
|
|
428
|
+
// '950011234V'
|
|
429
|
+
|
|
430
|
+
generateNIC({ year: 1995, dayOfYear: 1, gender: 'female', format: 'new' });
|
|
431
|
+
// '199550100001' (dayCode = 1 + 500 = 501)
|
|
432
|
+
|
|
433
|
+
generateNIC({ year: 1995, dayOfYear: 1, gender: 'male', format: 'old', serial: 7 });
|
|
434
|
+
// '950010007V'
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
| Option | Type | Required | Default | Description |
|
|
438
|
+
|--------|------|----------|---------|-------------|
|
|
439
|
+
| `year` | `number` | ✓ | — | Birth year ≥ 1900. Old format requires 1900–1999. |
|
|
440
|
+
| `dayOfYear` | `number` | ✓ | — | Day of year (1–365/366). |
|
|
441
|
+
| `gender` | `'male' \| 'female'` | ✓ | — | Determines day-code offset (+500 for female). |
|
|
442
|
+
| `format` | `'old' \| 'new'` | ✓ | — | Output format. |
|
|
443
|
+
| `serial` | `number` | — | `1234` | Serial digits. Must be ≥ 0. |
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
### Error Handling
|
|
448
|
+
|
|
449
|
+
`NICError` extends `Error` with a structured `code` for programmatic branching:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { NICError, parseNICOrThrow } from '@aakashwije/lanka-nic';
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
parseNICOrThrow('not-a-nic');
|
|
456
|
+
} catch (e) {
|
|
457
|
+
if (e instanceof NICError) {
|
|
458
|
+
e.code; // 'INVALID_FORMAT'
|
|
459
|
+
e.input; // 'not-a-nic'
|
|
460
|
+
e.message; // 'NIC does not match old or new format'
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
| Code | Trigger |
|
|
466
|
+
|------|---------|
|
|
467
|
+
| `NON_STRING` | Input is not a string |
|
|
468
|
+
| `EMPTY` | Input is empty or whitespace-only |
|
|
469
|
+
| `INVALID_FORMAT` | Does not match old (`\d{9}[VvXx]`) or new (`\d{12}`) pattern |
|
|
470
|
+
| `INVALID_DAY_CODE` | Day code outside `001–366` and `501–866` |
|
|
471
|
+
| `INVALID_DAY_FOR_YEAR` | Day code maps to a day that does not exist in the year (e.g. day 366 on a non-leap year) |
|
|
472
|
+
| `FUTURE_YEAR` | Birth year is after the current UTC year |
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### Type Reference
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
type ParsedNIC = {
|
|
480
|
+
input: string; // original input as provided
|
|
481
|
+
normalized: string; // whitespace-stripped, suffix uppercased
|
|
482
|
+
type: 'old' | 'new';
|
|
483
|
+
valid: true;
|
|
484
|
+
birthYear: number;
|
|
485
|
+
dayOfYear: number; // 1–366, always male-normalised
|
|
486
|
+
birthDate: string; // ISO 8601, UTC e.g. '1995-01-01'
|
|
487
|
+
gender: 'male' | 'female';
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
type SafeParseResult<T> =
|
|
491
|
+
| { success: true; data: T }
|
|
492
|
+
| { success: false; error: NICError };
|
|
493
|
+
|
|
494
|
+
type NICErrorCode =
|
|
495
|
+
| 'EMPTY'
|
|
496
|
+
| 'NON_STRING'
|
|
497
|
+
| 'INVALID_FORMAT'
|
|
498
|
+
| 'INVALID_DAY_CODE'
|
|
499
|
+
| 'INVALID_DAY_FOR_YEAR'
|
|
500
|
+
| 'FUTURE_YEAR';
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## CLI
|
|
506
|
+
|
|
507
|
+
The `lanka-nic` CLI is bundled with the package.
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
npm install -g @aakashwije/lanka-nic
|
|
511
|
+
# or one-off
|
|
512
|
+
npx @aakashwije/lanka-nic <command> <nic>
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Commands
|
|
516
|
+
|
|
517
|
+
```
|
|
518
|
+
Usage:
|
|
519
|
+
lanka-nic validate <nic>
|
|
520
|
+
lanka-nic parse <nic>
|
|
521
|
+
lanka-nic mask <nic>
|
|
522
|
+
lanka-nic age <nic>
|
|
523
|
+
|
|
524
|
+
If <nic> is omitted, the value is read from stdin.
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Examples
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
# Validate
|
|
531
|
+
$ lanka-nic validate 950012345V
|
|
532
|
+
true
|
|
533
|
+
# exit 0
|
|
534
|
+
|
|
535
|
+
$ lanka-nic validate bad-input
|
|
536
|
+
false
|
|
537
|
+
# exit 1
|
|
538
|
+
|
|
539
|
+
# Parse
|
|
540
|
+
$ lanka-nic parse 950012345V
|
|
541
|
+
{
|
|
542
|
+
"input": "950012345V",
|
|
543
|
+
"normalized": "950012345V",
|
|
544
|
+
"type": "old",
|
|
545
|
+
"valid": true,
|
|
546
|
+
"birthYear": 1995,
|
|
547
|
+
"dayOfYear": 1,
|
|
548
|
+
"birthDate": "1995-01-01",
|
|
549
|
+
"gender": "male"
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
# Parse failure → JSON on stderr, exit 1
|
|
553
|
+
$ lanka-nic parse not-a-nic
|
|
554
|
+
{"error":"INVALID_FORMAT","message":"NIC does not match old or new format"}
|
|
555
|
+
|
|
556
|
+
# Mask
|
|
557
|
+
$ lanka-nic mask 950012345V
|
|
558
|
+
95001****V
|
|
559
|
+
|
|
560
|
+
# Age
|
|
561
|
+
$ lanka-nic age 950012345V
|
|
562
|
+
30
|
|
563
|
+
|
|
564
|
+
# Stdin pipeline
|
|
565
|
+
$ echo 950012345V | lanka-nic parse
|
|
566
|
+
$ cat nics.txt | xargs -I{} lanka-nic validate {}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Exit codes:** `0` success · `1` invalid NIC · `2` usage error
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Edge Cases
|
|
574
|
+
|
|
575
|
+
| Input | Behaviour |
|
|
576
|
+
|-------|-----------|
|
|
577
|
+
| Empty / whitespace-only | Invalid — `EMPTY` |
|
|
578
|
+
| Non-string | Invalid — `NON_STRING` |
|
|
579
|
+
| Day code `000` | Invalid — `INVALID_DAY_CODE` |
|
|
580
|
+
| Day code `367–500` | Invalid — `INVALID_DAY_CODE` |
|
|
581
|
+
| Day code `> 866` | Invalid — `INVALID_DAY_CODE` |
|
|
582
|
+
| Day `366` on a non-leap year | Invalid — `INVALID_DAY_FOR_YEAR` |
|
|
583
|
+
| Future birth year | Invalid — `FUTURE_YEAR` |
|
|
584
|
+
| Lowercase suffix `v` / `x` | Normalized to uppercase |
|
|
585
|
+
| Internal whitespace `93 123 4567 V` | Collapsed and parsed correctly |
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Contributing
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
git clone https://github.com/aakashlk/lanka-nic.git
|
|
593
|
+
cd lanka-nic
|
|
594
|
+
npm install
|
|
595
|
+
|
|
596
|
+
npm test # run all tests
|
|
597
|
+
npm run coverage # generate V8 coverage report
|
|
598
|
+
npm run typecheck # TypeScript type-check only (no emit)
|
|
599
|
+
npm run lint # ESLint
|
|
600
|
+
npm run format # Prettier
|
|
601
|
+
npm run build # tsup dual ESM+CJS build
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Commits must follow **Conventional Commits** (`feat:`, `fix:`, `chore:`, `docs:` …). Versioning and changelog are fully automated via `semantic-release`.
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## License
|
|
609
|
+
|
|
610
|
+
[MIT](LICENSE) © 2024 Contributors
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
<div align="center">
|
|
615
|
+
<sub>Built with precision. Maintained with intent.</sub>
|
|
616
|
+
</div>
|