Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/formatters/__tests__/PhoneNumberTransformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,45 @@ describe('PhoneNumberTransformer', () => {
});
});

// NANP data lists Canada's 7-digit "310-XXXX" service-number format ahead of
// the 10-digit format, so selecting a format by leading digits alone clamped
// every +1 310 number to seven digits.
describe('formats sharing a leading-digits prefix (NANP 310)', () => {
const intl = new PhoneNumberTransformer({ international: true });

it('formats a full 10-digit 310 number in international mode', () => {
expect(transform(intl, '+13102705123')?.value).toBe('+1 (310) 270-5123');
});

it('formats a full 10-digit 310 number in national mode', () => {
const us = new PhoneNumberTransformer({ country: 'US' });
expect(transform(us, '3102705123')?.value).toBe('+1 (310) 270-5123');
});

it('still formats a 7-digit 310 service number', () => {
const ca = new PhoneNumberTransformer({ country: 'CA' });
expect(transform(ca, '3102705')?.value).toBe('+1 310-2705');
expect(transform(intl, '+13102705')?.value).toBe('+1 310-2705');
});

it('switches from the 7-digit to the 10-digit format on the eighth digit', () => {
expect(transform(intl, '+13102705')?.value).toBe('+1 310-2705');
const result = transform(
intl,
'+1 310-27051',
{ start: 12, end: 12 },
'+1 310-2705',
{ start: 11, end: 11 },
);
expect(result?.value).toBe('+1 (310) 270-51');
expect(result?.selection).toEqual({ start: 15, end: 15 });
});

it('still limits a 310 number to 10 national digits', () => {
expect(transform(intl, '+131027051234')?.value).toBe('+1 (310) 270-5123');
});
});

describe('detectCountry', () => {
it('resolves the primary country from the calling code', () => {
expect(detectCountry('+1 415 555 2671')).toBe('US');
Expand Down
66 changes: 38 additions & 28 deletions src/formatters/phone-number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,34 +82,6 @@ const countDigitsBefore = (text: string, pos: number): number => {
return count;
};

// Select the best format for a given national number based on leading digits.
// Iterates formats and tests the leadingDigits regex against the start of digits.
// Returns the first match, or the last format as fallback.
const selectFormat = (
nationalDigits: string,
formats: PhoneFormat[],
): PhoneFormat | null => {
'worklet';
if (formats.length === 0) return null;
if (nationalDigits.length === 0) return formats[formats.length - 1]!;

for (const format of formats) {
if (!format.leadingDigits) {
// No leading digits constraint — matches everything
return format;
}
// Test leading digits regex against the national digits.
// The regex should match from the start of the digits.
const re = new RegExp('^(?:' + format.leadingDigits + ')');
if (re.test(nationalDigits)) {
return format;
}
}

// Fallback to last format
return formats[formats.length - 1]!;
};

// Count the max digits the format pattern can consume by counting \d occurrences
// in the capture groups.
const getFormatMaxDigits = (pattern: string): number => {
Expand Down Expand Up @@ -153,6 +125,44 @@ const getFormatMaxDigits = (pattern: string): number => {
return count;
};

// Select the best format for a given national number based on leading digits.
// Iterates formats and tests the leadingDigits regex against the start of digits.
// A format whose pattern can't hold every digit typed so far is passed over for
// the next match, so a short format that shares a prefix with a longer one
// (NANP lists the 7-digit "310-XXXX" service numbers ahead of the 10-digit
// "(310) XXX-XXXX") never truncates the longer number. Returns the first match
// that fits, else the widest match, else the last format as fallback.
const selectFormat = (
nationalDigits: string,
formats: PhoneFormat[],
): PhoneFormat | null => {
'worklet';
if (formats.length === 0) return null;
if (nationalDigits.length === 0) return formats[formats.length - 1]!;

let widest: PhoneFormat | null = null;
let widestMax = -1;
for (const format of formats) {
// Test leading digits regex against the national digits.
// The regex should match from the start of the digits.
if (format.leadingDigits) {
const re = new RegExp('^(?:' + format.leadingDigits + ')');
if (!re.test(nationalDigits)) continue;
}
const maxDigits = getFormatMaxDigits(format.pattern);
if (nationalDigits.length <= maxDigits) {
return format;
}
if (maxDigits > widestMax) {
widest = format;
widestMax = maxDigits;
}
}

// Every matching format overflowed: the widest one clamps the input.
return widest !== null ? widest : formats[formats.length - 1]!;
};

// Build a partial format for when we don't have enough digits to match the full pattern.
// We expand digit groups one at a time and fill what we can.
const buildPartialFormat = (digits: string, format: PhoneFormat): string => {
Expand Down
Loading