UNPKG

zellige.js

Version:

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

382 lines (381 loc) 14.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CINFormatter = exports.CINFormattingError = void 0; exports.formatCIN = formatCIN; exports.getAllCINFormats = getAllCINFormats; const cin_1 = require("../validators/cin"); const cin_2 = require("../types/cin"); /** * Error class for CIN (Citizen Identification Number) formatting errors. * Used to throw specific formatting-related errors during CIN processing. * * @extends Error */ class CINFormattingError extends Error { constructor(message) { super(message); this.name = 'CINFormattingError'; } } exports.CINFormattingError = CINFormattingError; /** * A comprehensive formatter for Citizen Identification Numbers (CIN) with extensive configuration options. * Provides various formatting capabilities including: * - Case formatting (upper/lower) * - Digit grouping with custom separators * - Region and issuing office inclusion * - Custom templates * - Masking for sensitive information * * @example * ```typescript * const formatter = new CINFormatter({ * addSpaces: true, * letterCase: 'upper', * includeRegion: true * }); * const result = formatter.format('ABC123456'); * ``` */ class CINFormatter { /** * Creates a new instance of CINFormatter with specified options. * * @param {CINFormatOptions} options - Configuration options for CIN formatting * @throws {CINFormattingError} When provided options are invalid */ constructor(options = {}) { this.validateOptions(options); this.options = Object.assign(Object.assign({}, CINFormatter.DEFAULT_OPTIONS), options); } /** * Validate formatting options */ validateOptions(options) { var _a, _b, _c; if (options.digitGrouping) { if (options.digitGrouping.groupSize <= 0) { throw new CINFormattingError('Digit grouping size must be positive'); } if (options.digitGrouping.separator.length > 1) { throw new CINFormattingError('Digit grouping separator must be a single character'); } } if ((_a = options.mask) === null || _a === void 0 ? void 0 : _a.enabled) { if (((_b = options.mask.start) !== null && _b !== void 0 ? _b : 0) < 0) { throw new CINFormattingError('Mask start position must be non-negative'); } if (((_c = options.mask.length) !== null && _c !== void 0 ? _c : 0) <= 0) { throw new CINFormattingError('Mask length must be positive'); } if (options.mask.character && options.mask.character.length !== 1) { throw new CINFormattingError('Mask character must be a single character'); } } if (options.template && !this.isValidTemplate(options.template)) { throw new CINFormattingError('Invalid template format'); } } /** * Validate template string format */ isValidTemplate(template) { const validTokens = ['{prefix}', '{sequence}', '{region}', '{formatted}']; const templateTokens = template.match(/\{[^}]+\}/g) || []; return templateTokens.every(token => validTokens.includes(token)); } /** * Formats a CIN string according to the specified options. * * @param {unknown} cin - The CIN to format (can be string, number, or other types) * @returns {CINFormatResult} Formatted result containing the formatted CIN and metadata * * @throws {CINFormattingError} When formatting fails due to invalid input or options * * @example * ```typescript * const result = formatter.format('ABC123456'); * console.log(result.formatted); // Formatted CIN * console.log(result.metadata); // Additional formatting metadata * ``` */ format(cin) { var _a, _b, _c, _d, _e, _f, _g; try { // Validate and sanitize input const sanitized = (0, cin_1.sanitizeCIN)(cin); const validationResult = (0, cin_1.validateCIN)(sanitized); // Prepare result object const result = { formatted: null, originalInput: cin, isValid: validationResult.isValid, metadata: { region: (_a = validationResult.metadata) === null || _a === void 0 ? void 0 : _a.region, sequence: (_b = validationResult.metadata) === null || _b === void 0 ? void 0 : _b.sequence, issuingOffice: (_c = validationResult.metadata) === null || _c === void 0 ? void 0 : _c.issuingOffice, formattingApplied: [], originalFormat: String(cin), }, }; // If validation failed, return early with error information if (!validationResult.isValid) { result.error = (_d = validationResult.errors[0]) === null || _d === void 0 ? void 0 : _d.message; if (this.options.includeValidationStatus) { result.validation = this.getValidationDetails(validationResult); } return result; } // Extract parts from validation result const prefix = ((_e = validationResult.metadata) === null || _e === void 0 ? void 0 : _e.issuingOffice) || ''; const sequence = ((_f = validationResult.metadata) === null || _f === void 0 ? void 0 : _f.sequence) || ''; // Apply primary formatting result.formatted = this.applyFormatting(prefix, sequence, result); // Apply template if specified if (this.options.template) { result.formatted = this.applyTemplate(result.formatted, validationResult.metadata); result.metadata.formattingApplied.push('template'); } // Apply masking if enabled if ((_g = this.options.mask) === null || _g === void 0 ? void 0 : _g.enabled) { result.formatted = this.applyMasking(result.formatted); result.metadata.maskedDigits = this.calculateMaskedDigits(); result.metadata.formattingApplied.push('masking'); } // Add validation details if requested if (this.options.includeValidationStatus) { result.validation = this.getValidationDetails(validationResult); } return result; } catch (error) { return { formatted: null, originalInput: cin, isValid: false, error: error instanceof Error ? error.message : 'Unknown error occurred', metadata: { formattingApplied: [], }, }; } } /** * Apply formatting according to options */ applyFormatting(prefix, sequence, result) { var _a, _b; const formattedPrefix = this.formatLetterCase(prefix); const formattedSequence = this.formatSequence(sequence); let formatted = formattedPrefix; if (this.options.separator) { formatted += this.options.separator; result.metadata.formattingApplied.push('separator'); } if (this.options.addSpaces) { formatted += ' '; result.metadata.formattingApplied.push('spaces'); } formatted += formattedSequence; // Add additional information based on options if (this.options.includeRegion && ((_a = result.metadata) === null || _a === void 0 ? void 0 : _a.region)) { formatted += ' '; formatted += `(${result.metadata.region})`; result.metadata.formattingApplied.push('region'); } if (this.options.includeIssuingOffice && ((_b = result.metadata) === null || _b === void 0 ? void 0 : _b.issuingOffice)) { formatted += this.options.addSpaces ? ' ' : ''; formatted += `[Office: ${result.metadata.issuingOffice}]`; result.metadata.formattingApplied.push('issuingOffice'); } return formatted; } /** * Format letter case according to options */ formatLetterCase(text) { switch (this.options.letterCase) { case 'lower': return text.toLowerCase(); case 'upper': return text.toUpperCase(); default: return text; } } /** * Format sequence according to digit grouping options */ formatSequence(sequence) { if (!this.options.digitGrouping || this.options.digitGrouping.groupSize <= 0) { return sequence; } const { groupSize, separator } = this.options.digitGrouping; const regex = new RegExp(`(\\d{${groupSize}})(?=\\d)`, 'g'); return sequence.replace(regex, `$1${separator}`); } /** * Apply template formatting */ applyTemplate(formatted, metadata) { if (!this.options.template || !metadata) { return formatted; } const replacements = { '{prefix}': metadata.issuingOffice || '', '{sequence}': metadata.sequence || '', '{region}': this.options.includeRegion && metadata.region ? ` (${metadata.region})` : '', '{formatted}': formatted, }; let result = this.options.template; for (const [key, value] of Object.entries(replacements)) { result = result.replace(new RegExp(key, 'g'), value); } return result; } /** * Apply masking to sensitive digits */ applyMasking(formatted) { var _a; if (!((_a = this.options.mask) === null || _a === void 0 ? void 0 : _a.enabled)) { return formatted; } const maskChar = this.options.mask.character || '*'; const start = this.options.mask.start || 0; const length = this.options.mask.length || 4; const parts = formatted.split(''); const digitIndexes = this.findDigitIndexes(formatted); for (let i = start; i < start + length && i < digitIndexes.length; i++) { parts[digitIndexes[i]] = maskChar; } return parts.join(''); } /** * Find indexes of digits in formatted string */ findDigitIndexes(formatted) { return formatted.split('').reduce((indexes, char, index) => { if (/\d/.test(char)) { indexes.push(index); } return indexes; }, []); } /** * Calculate number of masked digits */ calculateMaskedDigits() { var _a; if (!((_a = this.options.mask) === null || _a === void 0 ? void 0 : _a.enabled)) { return 0; } return Math.min(this.options.mask.length || 4, 6 - (this.options.mask.start || 0)); } /** * Get detailed validation information */ getValidationDetails(validationResult) { const status = validationResult.isValid ? 'valid' : validationResult.errors.some(e => e.code === cin_2.CINErrorCode.PARTIALLY_VALID) ? 'partially_valid' : 'invalid'; return { status, checks: validationResult.errors.map(error => ({ check: error.code, passed: false, message: error.message, })), }; } } exports.CINFormatter = CINFormatter; CINFormatter.DEFAULT_OPTIONS = { addSpaces: false, letterCase: 'upper', includeRegion: false, separator: '', digitGrouping: { groupSize: 0, separator: '' }, includeIssuingOffice: false, dateFormat: 'YYYY', includeValidationStatus: false, template: '', mask: { enabled: false, character: '*', start: 0, length: 4, }, }; /** * Utility function to format a CIN with various display options. * Provides a simplified interface to the CINFormatter class. * * @param {unknown} cin - The CIN to format * @param {CINFormatOptions} options - Formatting options * @returns {CINFormatResult} Formatted result containing the formatted CIN and metadata * * @example * ```typescript * const result = formatCIN('ABC123456', { addSpaces: true }); * console.log(result.formatted); * ``` */ function formatCIN(cin, options = {}) { const formatter = new CINFormatter(options); return formatter.format(cin); } /** * Generates all possible standard format variations of a CIN. * Useful for displaying different formatting options or for comparison purposes. * * @param {unknown} cin - The CIN to format * @returns {Record<string, string> | null} Object containing different format variations, * or null if the input CIN is invalid * * @example * ```typescript * const formats = getAllCINFormats('ABC123456'); * if (formats) { * console.log(formats.standard); // Basic format * console.log(formats.withSpaces); // Format with spaces * console.log(formats.masked); // Masked format * } * ``` */ function getAllCINFormats(cin) { const validation = (0, cin_1.validateCIN)(cin); if (!validation.isValid) { return null; } return { standard: formatCIN(cin).formatted, withSpaces: formatCIN(cin, { addSpaces: true }).formatted, withRegion: formatCIN(cin, { includeRegion: true }).formatted, withSeparator: formatCIN(cin, { digitGrouping: { groupSize: 3, separator: '-' }, }).formatted, lowercase: formatCIN(cin, { letterCase: 'lower' }).formatted, withIssuingOffice: formatCIN(cin, { includeIssuingOffice: true }) .formatted, masked: formatCIN(cin, { mask: { enabled: true, character: '*', start: 2, length: 4 }, }).formatted, custom: formatCIN(cin, { template: '{prefix}-{sequence} ({region})', includeRegion: true, separator: '-', }).formatted, formatted: formatCIN(cin, { addSpaces: true, includeRegion: true, digitGrouping: { groupSize: 3, separator: '.' }, includeValidationStatus: true, }).formatted, }; }