UNPKG

zellige.js

Version:

A Moroccan utility library for working with CIN, phone numbers, currency, addresses, dates, and more.

291 lines (290 loc) 9.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ERRORS = void 0; exports.validatePhone = validatePhone; exports.formatPhone = formatPhone; exports.getPhoneDetails = getPhoneDetails; exports.extractPhoneNumbers = extractPhoneNumbers; exports.maskPhone = maskPhone; exports.arePhoneNumbersEqual = arePhoneNumbersEqual; exports.sanitizePhone = sanitizePhone; const phone_number_1 = require("../types/phone-number"); Object.defineProperty(exports, "ERRORS", { enumerable: true, get: function () { return phone_number_1.ERRORS; } }); const phone_number_2 = require("../constants/phone-number"); /** * LRU Cache for efficient memory management * @template K Cache key type * @template V Cache value type */ class LRUCache { constructor(maxSize, cleanupInterval = phone_number_2.CONFIG.CACHE_CLEANUP_INTERVAL) { this.lastCleanup = Date.now(); this.cache = new Map(); this.maxSize = maxSize; this.cleanupInterval = cleanupInterval; } get(key) { const value = this.cache.get(key); if (value) { // Refresh item position this.cache.delete(key); this.cache.set(key, value); } return value; } shouldCleanup() { const now = Date.now(); if (now - this.lastCleanup >= this.cleanupInterval) { this.lastCleanup = now; return true; } return false; } set(key, value) { if (this.shouldCleanup()) { this.clear(); } if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== undefined) { this.cache.delete(firstKey); } } this.cache.set(key, value); } clear() { this.cache.clear(); } } // Initialize caches const validationCache = new LRUCache(phone_number_2.CONFIG.MAX_CACHE_SIZE); const detailsCache = new LRUCache(phone_number_2.CONFIG.MAX_CACHE_SIZE); // Pre-compile regex patterns for better performance const COMPILED_PATTERNS = { GENERAL: new RegExp(phone_number_2.PATTERNS.GENERAL), FAKE: new RegExp(phone_number_2.PATTERNS.FAKE), // ... existing code ... }; /** * Normalizes a phone number by removing non-numeric characters and standardizing format * @param input Raw phone number * @returns 9-digit normalized number or null if invalid */ function normalizeInput(input) { if (!input) { return null; } if (typeof input !== 'string' || input.trim().length === 0) { return null; } if (input.length > phone_number_2.CONFIG.MAX_INPUT_LENGTH) { return null; } const normalized = input.replace(/[\s-.()+]/g, '').replace(/\D/g, ''); if (normalized.startsWith('00212')) return normalized.slice(5); if (normalized.startsWith('212')) return normalized.slice(3); if (normalized.startsWith('0')) return normalized.slice(1); return normalized; } /** * Determines telecom operator from phone number prefix * @param normalizedNumber 9-digit phone number * @returns Detected operator or 'UNKNOWN' */ function detectOperator(normalizedNumber) { const { OPERATORS } = phone_number_2.PATTERNS; if (OPERATORS.IAM.FIXED.test(normalizedNumber) || OPERATORS.IAM.MOBILE.test(normalizedNumber)) { return 'IAM'; } if (OPERATORS.ORANGE.FIXED.test(normalizedNumber) || OPERATORS.ORANGE.MOBILE.test(normalizedNumber)) { return 'ORANGE'; } if (OPERATORS.INWI.FIXED.test(normalizedNumber) || OPERATORS.INWI.MOBILE.test(normalizedNumber)) { return 'INWI'; } return 'UNKNOWN'; } /** * Determines if number is mobile or fixed line * Mobile starts with 6/7, fixed with 5 * @param normalizedNumber 9-digit phone number * @returns 'MOBILE', 'FIXED', or 'UNKNOWN' */ function getPhoneType(normalizedNumber) { const { TYPES } = phone_number_2.PATTERNS; if (TYPES.MOBILE.test(normalizedNumber)) return 'MOBILE'; if (TYPES.FIXED.test(normalizedNumber)) return 'FIXED'; return 'UNKNOWN'; } /** * Validates a Moroccan phone number * @param phone Phone number to validate * @returns Validation result with details */ function validatePhone(phone) { if (!phone) { return { isValid: false, error: phone_number_1.ERRORS.EMPTY_INPUT }; } const normalized = normalizeInput(phone); if (!normalized) { return { isValid: false, error: phone_number_1.ERRORS.INVALID_FORMAT }; } if (normalized.length !== 9) { return { isValid: false, error: phone_number_1.ERRORS.INVALID_FORMAT }; } // Check cache const cacheKey = normalized; const cached = validationCache.get(cacheKey); if (cached) return cached; if (!COMPILED_PATTERNS.GENERAL.test(`0${normalized}`)) { return { isValid: false, error: phone_number_1.ERRORS.INVALID_FORMAT }; } const type = getPhoneType(normalized); const operator = detectOperator(normalized); const isFake = COMPILED_PATTERNS.FAKE.test(normalized); const result = { isValid: true, normalizedNumber: normalized, type, operator, isFake, }; validationCache.set(cacheKey, result); return result; } /** * Formats a phone number to specified format (NATIONAL, INTERNATIONAL, E164, RFC3966) * @param phone Phone number to format * @param format Desired format * @returns Formatted number or null if invalid */ function formatPhone(phone, format = 'INTERNATIONAL') { if (!phone) return null; const { isValid, normalizedNumber } = validatePhone(phone); if (!isValid || !normalizedNumber) return null; switch (format) { case 'NATIONAL': return `0${normalizedNumber}`; case 'E164': return `+${phone_number_2.CONFIG.COUNTRY_CODE}${normalizedNumber}`; case 'RFC3966': return `tel:+${phone_number_2.CONFIG.COUNTRY_CODE}-${normalizedNumber}`; case 'INTERNATIONAL': return `+${phone_number_2.CONFIG.COUNTRY_CODE} ${normalizedNumber.replace(/(\d{2})(\d{2})(\d{2})(\d{3})/, '$1 $2 $3 $4')}`; default: return null; } } /** * Gets comprehensive details about a phone number including validation, type, formats * @param phone Phone number to analyze * @returns Detailed phone information */ function getPhoneDetails(phone) { const normalized = normalizeInput(phone); if (!normalized) { return { type: 'UNKNOWN', operator: 'UNKNOWN', region: phone_number_2.CONFIG.REGION, isValid: false, isFake: true, formats: { national: '', international: '', e164: '', rfc3966: '', }, }; } const cacheKey = normalized; const cached = detailsCache.get(cacheKey); if (cached) return cached; const validation = validatePhone(phone); const result = { type: validation.type || 'UNKNOWN', operator: validation.operator || 'UNKNOWN', region: phone_number_2.CONFIG.REGION, isValid: validation.isValid, isFake: validation.isFake || false, formats: { national: formatPhone(phone, 'NATIONAL') || '', international: formatPhone(phone, 'INTERNATIONAL') || '', e164: formatPhone(phone, 'E164') || '', rfc3966: formatPhone(phone, 'RFC3966') || '', }, }; detailsCache.set(cacheKey, result); return result; } /** * Extracts valid Moroccan phone numbers from text * @param text Text containing potential phone numbers * @returns Array of valid unique numbers in national format */ function extractPhoneNumbers(text) { if (typeof text !== 'string') return []; const phonePattern = /(?:\+212|00212|0)?[567]\d{8}/g; const matches = text.match(phonePattern) || []; return [...new Set(matches)] .map(phone => { const validation = validatePhone(phone); if (validation.isValid && !validation.isFake && validation.normalizedNumber) { return `0${validation.normalizedNumber}`; } return null; }) .filter((phone) => phone !== null); } /** * Masks a phone number for privacy (+212 XX ****** X) * @param phone Phone number to mask * @param maskChar Character for masking * @returns Masked number or null if invalid */ function maskPhone(phone, maskChar = '*') { const { isValid, normalizedNumber } = validatePhone(phone); if (!isValid || !normalizedNumber) return null; const mask = maskChar.charAt(0).repeat(6); return `+${phone_number_2.CONFIG.COUNTRY_CODE} ${normalizedNumber.slice(0, 2)} ${mask} ${normalizedNumber.slice(-1)}`; } /** * Compares two phone numbers for equality after normalization * @param phone1 First phone number * @param phone2 Second phone number * @returns True if numbers are equivalent */ function arePhoneNumbersEqual(phone1, phone2) { const norm1 = normalizeInput(phone1); const norm2 = normalizeInput(phone2); return norm1 === norm2; } /** * Sanitizes a phone number by removing non-numeric characters * @param phone Phone number to sanitize * @returns Sanitized number or null if invalid */ function sanitizePhone(phone) { if (!phone) return null; const normalized = normalizeInput(phone); if (!normalized) return null; return normalized.replace(/[^\d+]/g, ''); }