@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 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
+ [![npm](https://img.shields.io/npm/v/@aakashwije/lanka-nic?color=%230060c7&label=npm&logo=npm&style=flat-square)](https://www.npmjs.com/package/@aakashwije/lanka-nic)
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/aakashlk/lanka-nic/ci.yml?branch=main&logo=github-actions&logoColor=white&style=flat-square&label=CI)](https://github.com/aakashlk/lanka-nic/actions)
9
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?logo=vitest&style=flat-square)](#)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-22c55e?style=flat-square)](LICENSE)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white&style=flat-square)](https://www.typescriptlang.org/)
12
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?logo=node.js&logoColor=white&style=flat-square)](https://nodejs.org/)
13
+ [![ESM + CJS](https://img.shields.io/badge/module-ESM%20%2B%20CJS-f97316?style=flat-square)](#)
14
+ [![Zero deps](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](#)
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 | ![TypeScript](https://img.shields.io/badge/-TypeScript_5.x-3178C6?logo=typescript&logoColor=white&style=flat-square) | Type-safe implementation |
60
+ | Runtime | ![Node.js](https://img.shields.io/badge/-Node.js_%E2%89%A518-339933?logo=node.js&logoColor=white&style=flat-square) | Server-side execution |
61
+ | Build | ![tsup](https://img.shields.io/badge/-tsup-f97316?style=flat-square) | Dual ESM + CJS output |
62
+ | Test | ![Vitest](https://img.shields.io/badge/-Vitest-6E9F18?logo=vitest&logoColor=white&style=flat-square) | Unit tests + coverage |
63
+ | Lint | ![ESLint](https://img.shields.io/badge/-ESLint_9.x-4B32C3?logo=eslint&logoColor=white&style=flat-square) | Code quality |
64
+ | Format | ![Prettier](https://img.shields.io/badge/-Prettier-F7B93E?logo=prettier&logoColor=black&style=flat-square) | Consistent style |
65
+ | Release | ![semantic-release](https://img.shields.io/badge/-semantic--release-e10079?logo=semantic-release&logoColor=white&style=flat-square) | Automated versioning |
66
+ | CI/CD | ![GitHub Actions](https://img.shields.io/badge/-GitHub_Actions-2088FF?logo=github-actions&logoColor=white&style=flat-square) | 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>