@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
JavaScript
;
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);
}