UNPKG

@sprucelabs/schema

Version:

Static and dynamic binding plus runtime validation and transformation to ensure your app is sound. 🤓

149 lines (148 loc) • 4.88 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isValidNumber = isValidNumber; exports.default = formatPhoneNumber; exports.isDummyNumber = isDummyNumber; const LETTER_PATTERN = /[a-zA-Z]/; const DEFAULT_CODE_SEPARATOR = ' '; const COUNTRY_FORMATS = [ { code: '92', groupSizes: [4, 7], groupSeparator: ' ', validDigits: [9, 10, 11], detect: ({ digits, hasExplicitPlus }) => digits.startsWith('92') && (hasExplicitPlus || digits.length > 10), }, { code: '90', groupSizes: [3, 3, 4], groupSeparator: ' ', validDigits: [10], detect: ({ digits, hasExplicitPlus }) => digits.startsWith('90') && (hasExplicitPlus || digits.length > 10), }, { code: '49', groupSizes: [3, 3, 4], groupSeparator: ' ', validDigits: [10], detect: ({ digits, hasExplicitPlus }) => digits.startsWith('49') && (hasExplicitPlus || digits.length > 10), }, { code: '1', groupSizes: [3, 3, 4], groupSeparator: '-', validDigits: [10], }, ]; const DEFAULT_COUNTRY = COUNTRY_FORMATS.find((format) => format.code === '1') ?? COUNTRY_FORMATS[0]; const createAdHocFormat = (code) => ({ code, groupSizes: [], groupSeparator: DEFAULT_CODE_SEPARATOR, }); function isValidNumber(number) { if (LETTER_PATTERN.test(number)) { return false; } const parsed = parseInput(number); const countryFormat = detectCountryFormat(parsed); const localDigits = stripCountryCodeFromDigits(parsed.digits, countryFormat.code); if (!localDigits.length) { return false; } if (countryFormat.validDigits?.length) { return countryFormat.validDigits.includes(localDigits.length); } return true; } function formatPhoneNumber(value, shouldFailSilently = true) { if (LETTER_PATTERN.test(value)) { if (!shouldFailSilently) { throw new Error('INVALID_PHONE_NUMBER'); } return value; } const parsed = parseInput(value); const countryFormat = detectCountryFormat(parsed); const localDigits = stripCountryCodeFromDigits(parsed.digits, countryFormat.code); const isCodeOnly = parsed.digits.length > 0 && parsed.digits === countryFormat.code; if (!localDigits.length && !isCodeOnly) { if (!shouldFailSilently) { throw new Error('INVALID_PHONE_NUMBER'); } return value; } const formattedLocal = formatLocalDigits(localDigits, countryFormat); const codeSeparator = countryFormat.codeSeparator ?? DEFAULT_CODE_SEPARATOR; let formatted = `+${countryFormat.code}`; if (formattedLocal) { formatted += `${codeSeparator}${formattedLocal}`; } else if (parsed.hasTrailingSpace && isCodeOnly) { formatted += codeSeparator; } return formatted; } function isDummyNumber(phone) { const cleanedValue = phone.replace(/\D/g, ''); return cleanedValue.startsWith('1555') || cleanedValue.startsWith('555'); } function parseInput(original) { const digits = original.replace(/\D/g, ''); const hasExplicitPlus = original.trim().startsWith('+'); const hasTrailingSpace = /\s$/.test(original); return { original, digits, hasExplicitPlus, hasTrailingSpace, }; } function detectCountryFormat(input) { if (!input.digits.length) { return DEFAULT_COUNTRY; } if (input.hasExplicitPlus) { const explicitMatch = COUNTRY_FORMATS.find((format) => input.digits.startsWith(format.code)); if (explicitMatch) { return explicitMatch; } return createAdHocFormat(input.digits); } if (input.hasTrailingSpace && input.digits.length > 0 && input.digits.length <= 2) { const exactMatch = COUNTRY_FORMATS.find((format) => format.code === input.digits); if (exactMatch) { return exactMatch; } return createAdHocFormat(input.digits); } const detected = COUNTRY_FORMATS.find((format) => format.detect ? format.detect(input) : false); return detected ?? DEFAULT_COUNTRY; } function stripCountryCodeFromDigits(digits, code) { if (digits.startsWith(code)) { return digits.slice(code.length); } return digits; } function formatLocalDigits(digits, format) { if (!digits.length) { return ''; } const parts = []; let index = 0; for (const size of format.groupSizes) { if (index >= digits.length) { break; } const nextIndex = Math.min(index + size, digits.length); parts.push(digits.slice(index, nextIndex)); index = nextIndex; } if (index < digits.length) { parts.push(digits.slice(index)); } return parts.join(format.groupSeparator); }