From 33ac3b84c89695e947f714809bc581921d8faec4 Mon Sep 17 00:00:00 2001 From: Aaqibhafeez Khan Date: Wed, 28 Oct 2026 06:34:33 +0530 Subject: [PATCH] feat: add Romanian phone number validation --- CHANGELOG.md | 4 ++++ README.md | 18 +++++++++++++----- src/index.ts | 2 ++ src/phone-ro.ts | 39 +++++++++++++++++++++++++++++++++++++++ test/phone-ro.test.ts | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 src/phone-ro.ts create mode 100644 test/phone-ro.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c3f050d..d986813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project are documented here. The format follows [Kee ## [Unreleased] +### Added + +- `validatePhoneRo` / `isValidPhoneRo` for Romanian mobile and landline numbers, normalized to E.164 + ## [0.1.0] - 2026-10-27 ### Added diff --git a/README.md b/README.md index a82e679..011c30e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Try it in your browser

-Validate and decode Romanian identifiers (**CNP**, **CUI/CIF**, **IBAN** and postal codes) with error messages you can show directly to users. Zero dependencies, fully typed, runs in Node and in the browser. +Validate and decode Romanian identifiers (**CNP**, **CUI/CIF**, **IBAN**, phone numbers and postal codes) with error messages you can show directly to users. Zero dependencies, fully typed, runs in Node and in the browser. ```bash npm install github:Robert-GeorgeDev/ValidRO @@ -31,7 +31,7 @@ The package is not on the npm registry yet; this installs it straight from GitHu ## Usage ```ts -import { validateCnp, validateCui, validateIbanRo } from "validro"; +import { validateCnp, validateCui, validateIbanRo, validatePhoneRo } from "validro"; validateCnp("3000101221147"); // { @@ -51,6 +51,12 @@ validateIbanRo("ro49 aaaa 1b31 0075 9384 0000"); // { valid: true, error: null, // data: { iban: "RO49AAAA1B31007593840000", formatted: "RO49 AAAA 1B31 0075 9384 0000", // bankCode: "AAAA", treasury: false } } + +validatePhoneRo("0723 456 789"); +// { valid: true, error: null, data: { normalized: "+40723456789" } } + +validatePhoneRo("0040723456789"); +// { valid: true, error: null, data: { normalized: "+40723456789" } } ``` Every `validate*` function returns the same shape, so you can handle all of them the same way: @@ -61,7 +67,7 @@ type ValidationResult = | { valid: false; error: string; data: null }; ``` -Only need true or false? Use `isValidCnp`, `isValidCui`, `isValidIbanRo` and `isValidPostalCode`. +Only need true or false? Use `isValidCnp`, `isValidCui`, `isValidIbanRo`, `isValidPostalCode` and `isValidPhoneRo`. ## API @@ -71,7 +77,8 @@ Only need true or false? Use `isValidCnp`, `isValidCui`, `isValidIbanRo` and `is | `validateCui(cui)` | `ValidationResult` | | `validateIbanRo(iban)` | `ValidationResult` | | `validatePostalCode(postalCode)` | `ValidationResult`; exactly 6 digits | -| `isValidCnp`, `isValidCui`, `isValidIbanRo`, `isValidPostalCode` | `boolean` | +| `validatePhoneRo(phone)` | `ValidationResult`; `data.normalized` is in E.164 format | +| `isValidCnp`, `isValidCui`, `isValidIbanRo`, `isValidPostalCode`, `isValidPhoneRo` | `boolean` | | `cnpControlDigit(first12)` | the CNP control digit | | `cuiControlDigit(body)` | the CUI control digit | | `ibanChecksumOk(iban)` | mod-97 check, works for any country | @@ -85,6 +92,7 @@ Only need true or false? Use `isValidCnp`, `isValidCui`, `isValidIbanRo` and `is | CUI / CIF | optional `RO` prefix, 2–10 digits, no leading zero, control digit (key `753217532`) | | IBAN (RO) | 24 characters: `RO`, 2 check digits, 4-letter bank code, 16 alphanumerics, ISO 13616 mod-97 | | Postal code | exactly 6 digits | +| Phone (RO) | Romanian mobile (`07`) and landline (`02` / `03`) formats; accepts national, `+40` and `0040` prefixes; normalizes to `+40` E.164 | Spaces, dots and dashes in the input are ignored. @@ -103,7 +111,7 @@ All examples in this repository are fictitious: sample CNPs use birth dates in t ## Roadmap - [ ] Romanian licence plates -- [ ] Phone numbers, normalized to `+40` +- [x] Phone numbers, normalized to `+40` - [x] Postal codes - [ ] Python port diff --git a/src/index.ts b/src/index.ts index 6758db9..3155707 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,5 +6,7 @@ export { validateIbanRo, isValidIbanRo, ibanChecksumOk } from "./iban.js"; export type { IbanInfo } from "./iban.js"; export { validatePostalCode, isValidPostalCode } from "./postal-code.js"; export type { PostalCodeInfo } from "./postal-code.js"; +export { validatePhoneRo, isValidPhoneRo } from "./phone-ro.js"; +export type { PhoneRoInfo } from "./phone-ro.js"; export { CNP_COUNTIES } from "./counties.js"; export type { ValidationResult } from "./result.js"; diff --git a/src/phone-ro.ts b/src/phone-ro.ts new file mode 100644 index 0000000..940792d --- /dev/null +++ b/src/phone-ro.ts @@ -0,0 +1,39 @@ +import { clean, fail, ok, type ValidationResult } from "./result.js"; + +export interface PhoneRoInfo { + /** Romanian phone number normalized to E.164 format. */ + normalized: string; +} + +/** + * Validates a Romanian mobile or landline number and normalizes it to E.164. + * Accepts national numbers (07..., 02... or 03...) and +40 / 0040 prefixes. + * Spaces, dots and dashes are ignored. + */ +export function validatePhoneRo(input: string): ValidationResult { + if (typeof input !== "string") return fail("Numărul de telefon trebuie să fie un șir de caractere"); + + const phone = clean(input); + let nationalNumber: string; + + if (phone.startsWith("+40")) { + nationalNumber = phone.slice(3); + } else if (phone.startsWith("0040")) { + nationalNumber = phone.slice(4); + } else if (/^0/.test(phone)) { + if (!/^0[237]\d{8}$/.test(phone)) { + return fail("Numărul de telefon trebuie să fie un număr mobil sau fix din România"); + } + nationalNumber = phone.slice(1); + } else { + return fail("Numărul de telefon trebuie să înceapă cu 0, +40 sau 0040"); + } + + if (!/^[237]\d{8}$/.test(nationalNumber)) { + return fail("Numărul de telefon trebuie să fie un număr mobil sau fix din România"); + } + + return ok({ normalized: `+40${nationalNumber}` }); +} + +export const isValidPhoneRo = (input: string): boolean => validatePhoneRo(input).valid; diff --git a/test/phone-ro.test.ts b/test/phone-ro.test.ts new file mode 100644 index 0000000..0b04b72 --- /dev/null +++ b/test/phone-ro.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { isValidPhoneRo, validatePhoneRo } from "../src/phone-ro.js"; + +describe("Romanian phone number", () => { + it("normalizes national mobile numbers", () => { + expect(validatePhoneRo("0723 456 789")).toEqual({ + valid: true, + error: null, + data: { normalized: "+40723456789" }, + }); + expect(validatePhoneRo("07-23-456-789").data).toEqual({ normalized: "+40723456789" }); + }); + + it("accepts international prefixes and normalizes to E.164", () => { + expect(validatePhoneRo("+40 723 456 789").data).toEqual({ normalized: "+40723456789" }); + expect(validatePhoneRo("0040723456789").data).toEqual({ normalized: "+40723456789" }); + }); + + it("accepts Romanian landline numbers", () => { + expect(validatePhoneRo("021 345 6789").data).toEqual({ normalized: "+40213456789" }); + expect(validatePhoneRo("0313456789").data).toEqual({ normalized: "+40313456789" }); + }); + + it("rejects unsupported prefixes and malformed lengths", () => { + expect(isValidPhoneRo("0123456789")).toBe(false); + expect(isValidPhoneRo("072345678")).toBe(false); + expect(isValidPhoneRo("+44123456789")).toBe(false); + expect(isValidPhoneRo("not a phone number")).toBe(false); + }); + + it("returns Romanian error messages", () => { + expect(validatePhoneRo("123").error).toContain("trebuie să înceapă"); + }); +});