@yext/phonenumber-util 0.2.12 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/package.json +3 -3
- package/src/__tests__/base.test.js +190 -0
- package/src/base.d.ts +8 -1
- package/src/base.js +76 -29
- package/vitest.config.js +3 -1
package/README.md
CHANGED
|
@@ -70,8 +70,33 @@ isValidPhoneNumber(invalidPhoneNumber); // Returns `false` - "311" is not a vali
|
|
|
70
70
|
const intlNumber = '+380 97 123 4567';
|
|
71
71
|
isValidPhoneNumber(intlNumber); // Returns `true` - "380" is the region code for Ukraine
|
|
72
72
|
|
|
73
|
-
const invalidIntlNumber = '+
|
|
74
|
-
isValidPhoneNumber(invalidIntlNumber); // Returns `false` - "
|
|
73
|
+
const invalidIntlNumber = '+000 97 123 4567';
|
|
74
|
+
isValidPhoneNumber(invalidIntlNumber); // Returns `false` - "000" is not a valid region code
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### isValidPhoneNumberWithDescription
|
|
78
|
+
|
|
79
|
+
Returns an object that will contain a boolean of `isValid` which will behave exactly the same as `isValidPhoneNumber` (`true` or `false` based on whether the passed number is presumed to be valid or invalid) in addition to a string `description` that will contain one of the following values:
|
|
80
|
+
|
|
81
|
+
- `NOT_A_NUMBER` - The passed value is falsey (null, empty string, undefined or 0) or the passed value is not the expected `string` format.
|
|
82
|
+
- `UNKNOWN_NUMBER` - The portion of the phone number including the region code and/or local number is unrecognized, unexpected or invalid.
|
|
83
|
+
- `UNKNOWN_AREA_CODE` - The NANP number includes an area code of the correct length but cannot be validated. If you believe this to be in error, please file a [Github Issue](https://github.com/hearsaycorp/phonenumber-util/issues/new?title=Area+Code+Missing).
|
|
84
|
+
- `VALID_NUMBER` - When `isValid` is `true`, this `description` will always read `VALID_NUMBER`.
|
|
85
|
+
- `UNKNOWN_FORMAT` - There were issues parsing the provided number and does not appear to be a valid phone number structure.
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
import { isValidPhoneNumberWithDescription } from '@yext/phonenumber-util';
|
|
89
|
+
const validPhoneNumber = '3103496333';
|
|
90
|
+
isValidPhoneNumberWithDescription(validPhoneNumber); // Returns { description: 'VALID_NUMBER', isValid: true }
|
|
91
|
+
|
|
92
|
+
const invalidPhoneNumber = '3113496333';
|
|
93
|
+
isValidPhoneNumberWithDescription(invalidPhoneNumber); // Returns { description: 'UNKNOWN_NUMBER', isValid: false }
|
|
94
|
+
|
|
95
|
+
const intlNumber = '+380 97 123 4567';
|
|
96
|
+
isValidPhoneNumberWithDescription(intlNumber); // Returns { description: 'VALID_NUMBER', isValid: true }
|
|
97
|
+
|
|
98
|
+
const invalidIntlNumber = '+000 97 123 4567';
|
|
99
|
+
isValidPhoneNumberWithDescription(invalidIntlNumber); // Returns { description: 'UNKNOWN_NUMBER', isValid: false }
|
|
75
100
|
```
|
|
76
101
|
|
|
77
102
|
#### getPhoneParts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yext/phonenumber-util",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"author": "bajohnson@hearsaycorp.com",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"description": "Utility for extracting and validating phone numbers",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@eslint/js": "^9.28.0",
|
|
29
|
-
"@vitest/coverage-v8": "^3.2.
|
|
29
|
+
"@vitest/coverage-v8": "^3.2.3",
|
|
30
30
|
"eslint": "^9.28.0",
|
|
31
31
|
"generate-license-file": "4.0.0",
|
|
32
32
|
"globals": "^16.2.0",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"istanbul-badges-readme": "^1.9.0",
|
|
35
35
|
"lint-staged": "^16.1.0",
|
|
36
36
|
"prettier": "^3.5.3",
|
|
37
|
-
"vitest": "^3.2.
|
|
37
|
+
"vitest": "^3.2.3"
|
|
38
38
|
},
|
|
39
39
|
"eslintConfig": {},
|
|
40
40
|
"lint-staged": {
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
formatPhoneNumberForE164,
|
|
3
3
|
formatPhoneNumberLink,
|
|
4
4
|
isValidPhoneNumber,
|
|
5
|
+
isValidPhoneNumberWithDescription,
|
|
5
6
|
getPhoneParts,
|
|
6
7
|
sanitizeRawNumber,
|
|
7
8
|
findNumbersInString,
|
|
@@ -73,6 +74,22 @@ describe('Sanitizing user inputted phone number values', () => {
|
|
|
73
74
|
);
|
|
74
75
|
expect(sanitizeRawNumber("+1; (3<>1&0) 3`49\\-65'43")).toBe('+13103496543');
|
|
75
76
|
});
|
|
77
|
+
|
|
78
|
+
it('should return empty string for null or undefined input', () => {
|
|
79
|
+
expect(sanitizeRawNumber(null)).toBe('');
|
|
80
|
+
expect(sanitizeRawNumber(undefined)).toBe('');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return empty string for non-string input', () => {
|
|
84
|
+
expect(sanitizeRawNumber(123)).toBe('');
|
|
85
|
+
expect(sanitizeRawNumber({})).toBe('');
|
|
86
|
+
expect(sanitizeRawNumber([])).toBe('');
|
|
87
|
+
expect(sanitizeRawNumber(true)).toBe('');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return empty string for empty string input', () => {
|
|
91
|
+
expect(sanitizeRawNumber('')).toBe('');
|
|
92
|
+
});
|
|
76
93
|
});
|
|
77
94
|
|
|
78
95
|
describe('Extracting numbers from a larger string of text', () => {
|
|
@@ -104,6 +121,22 @@ describe('Extracting numbers from a larger string of text', () => {
|
|
|
104
121
|
0,
|
|
105
122
|
);
|
|
106
123
|
});
|
|
124
|
+
|
|
125
|
+
it('should return empty array for null or undefined input', () => {
|
|
126
|
+
expect(findNumbersInString(null)).toEqual([]);
|
|
127
|
+
expect(findNumbersInString(undefined)).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return empty array for non-string input', () => {
|
|
131
|
+
expect(findNumbersInString(123)).toEqual([]);
|
|
132
|
+
expect(findNumbersInString({})).toEqual([]);
|
|
133
|
+
expect(findNumbersInString([])).toEqual([]);
|
|
134
|
+
expect(findNumbersInString(true)).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return empty array for empty string input', () => {
|
|
138
|
+
expect(findNumbersInString('')).toEqual([]);
|
|
139
|
+
});
|
|
107
140
|
});
|
|
108
141
|
|
|
109
142
|
describe('Phone number formatting', () => {
|
|
@@ -195,6 +228,163 @@ describe('Creation of phone links for href', () => {
|
|
|
195
228
|
});
|
|
196
229
|
});
|
|
197
230
|
|
|
231
|
+
describe('Phone number validation with description', () => {
|
|
232
|
+
describe('NOT_A_NUMBER cases', () => {
|
|
233
|
+
it('should return NOT_A_NUMBER for null input', () => {
|
|
234
|
+
const result = isValidPhoneNumberWithDescription(null);
|
|
235
|
+
expect(result.description).toBe('NOT_A_NUMBER');
|
|
236
|
+
expect(result.isValid).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return NOT_A_NUMBER for undefined input', () => {
|
|
240
|
+
const result = isValidPhoneNumberWithDescription(undefined);
|
|
241
|
+
expect(result.description).toBe('NOT_A_NUMBER');
|
|
242
|
+
expect(result.isValid).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should return NOT_A_NUMBER for empty string', () => {
|
|
246
|
+
const result = isValidPhoneNumberWithDescription('');
|
|
247
|
+
expect(result.description).toBe('NOT_A_NUMBER');
|
|
248
|
+
expect(result.isValid).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should return NOT_A_NUMBER for non-string input', () => {
|
|
252
|
+
const result = isValidPhoneNumberWithDescription(123);
|
|
253
|
+
expect(result.description).toBe('NOT_A_NUMBER');
|
|
254
|
+
expect(result.isValid).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should return NOT_A_NUMBER for object input', () => {
|
|
258
|
+
const result = isValidPhoneNumberWithDescription({});
|
|
259
|
+
expect(result.description).toBe('NOT_A_NUMBER');
|
|
260
|
+
expect(result.isValid).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('UNKNOWN_FORMAT cases', () => {
|
|
265
|
+
it('should return UNKNOWN_FORMAT for strings that dont match regex', () => {
|
|
266
|
+
const result = isValidPhoneNumberWithDescription('hello world');
|
|
267
|
+
expect(result.description).toBe('UNKNOWN_FORMAT');
|
|
268
|
+
expect(result.isValid).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should return UNKNOWN_AREA_CODE for date-like strings that match regex but fail parsing', () => {
|
|
272
|
+
const result = isValidPhoneNumberWithDescription('7/23/2025');
|
|
273
|
+
expect(result.description).toBe('UNKNOWN_AREA_CODE');
|
|
274
|
+
expect(result.isValid).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should return UNKNOWN_NUMBER for currency strings that match regex', () => {
|
|
278
|
+
const result = isValidPhoneNumberWithDescription('$5055');
|
|
279
|
+
expect(result.description).toBe('UNKNOWN_NUMBER');
|
|
280
|
+
expect(result.isValid).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should return UNKNOWN_NUMBER for short number sequences that match regex', () => {
|
|
284
|
+
const result = isValidPhoneNumberWithDescription('123');
|
|
285
|
+
expect(result.description).toBe('UNKNOWN_NUMBER');
|
|
286
|
+
expect(result.isValid).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('UNKNOWN_NUMBER cases', () => {
|
|
291
|
+
it('should return UNKNOWN_NUMBER for numbers too short to extract local number', () => {
|
|
292
|
+
// This creates a scenario where regex matches but getPhoneParts can't extract localNumber
|
|
293
|
+
const result = isValidPhoneNumberWithDescription('123456');
|
|
294
|
+
expect(result.description).toBe('UNKNOWN_NUMBER');
|
|
295
|
+
expect(result.isValid).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('UNKNOWN_AREA_CODE cases', () => {
|
|
300
|
+
it('should return UNKNOWN_AREA_CODE for US numbers missing area code', () => {
|
|
301
|
+
// US number (region code 1) without area code
|
|
302
|
+
const result = isValidPhoneNumberWithDescription('3496200');
|
|
303
|
+
expect(result.description).toBe('UNKNOWN_AREA_CODE');
|
|
304
|
+
expect(result.isValid).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should return UNKNOWN_AREA_CODE for US numbers with invalid area code', () => {
|
|
308
|
+
// US number with invalid area code (420 doesn't exist)
|
|
309
|
+
const result = isValidPhoneNumberWithDescription('+1 420 222 3333');
|
|
310
|
+
expect(result.description).toBe('UNKNOWN_AREA_CODE');
|
|
311
|
+
expect(result.isValid).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('VALID_NUMBER cases', () => {
|
|
316
|
+
it('should return VALID_NUMBER for valid US numbers', () => {
|
|
317
|
+
const result = isValidPhoneNumberWithDescription('+1 (310) 349-6543');
|
|
318
|
+
expect(result.description).toBe('VALID_NUMBER');
|
|
319
|
+
expect(result.isValid).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should return VALID_NUMBER for valid US numbers without formatting', () => {
|
|
323
|
+
const result = isValidPhoneNumberWithDescription('3103496543');
|
|
324
|
+
expect(result.description).toBe('VALID_NUMBER');
|
|
325
|
+
expect(result.isValid).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should return VALID_NUMBER for valid international numbers', () => {
|
|
329
|
+
const result = isValidPhoneNumberWithDescription('+49 171 234 5678');
|
|
330
|
+
expect(result.description).toBe('VALID_NUMBER');
|
|
331
|
+
expect(result.isValid).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should return VALID_NUMBER for valid international numbers with different formats', () => {
|
|
335
|
+
const result = isValidPhoneNumberWithDescription('+33 7 56 78 90 12');
|
|
336
|
+
expect(result.description).toBe('VALID_NUMBER');
|
|
337
|
+
expect(result.isValid).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should return VALID_NUMBER for 11-digit US numbers', () => {
|
|
341
|
+
const result = isValidPhoneNumberWithDescription('13103496543');
|
|
342
|
+
expect(result.description).toBe('VALID_NUMBER');
|
|
343
|
+
expect(result.isValid).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('Edge cases and comprehensive coverage', () => {
|
|
348
|
+
it('should handle malformed but parseable numbers', () => {
|
|
349
|
+
const result = isValidPhoneNumberWithDescription(
|
|
350
|
+
"+1; (3<>1&0) 3`49\\-65'43",
|
|
351
|
+
);
|
|
352
|
+
expect(result.description).toBe('VALID_NUMBER');
|
|
353
|
+
expect(result.isValid).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should reject numbers with too many characters as UNKNOWN_NUMBER', () => {
|
|
357
|
+
const result = isValidPhoneNumberWithDescription('310-496-32313');
|
|
358
|
+
expect(result.description).toBe('UNKNOWN_NUMBER');
|
|
359
|
+
expect(result.isValid).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should reject repeated digits that form invalid area codes as UNKNOWN_NUMBER', () => {
|
|
363
|
+
const result = isValidPhoneNumberWithDescription('4444444444');
|
|
364
|
+
expect(result.description).toBe('UNKNOWN_NUMBER');
|
|
365
|
+
expect(result.isValid).toBe(false);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should reject 555 prefix numbers as UNKNOWN_NUMBER', () => {
|
|
369
|
+
const result = isValidPhoneNumberWithDescription('5553496200');
|
|
370
|
+
expect(result.description).toBe('UNKNOWN_NUMBER');
|
|
371
|
+
expect(result.isValid).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should return UNKNOWN_REGION_CODE for edge case where phoneParts has localNumber but no regionCode', () => {
|
|
375
|
+
// This tests the specific code path where localNumber exists but regionCode doesn't
|
|
376
|
+
// While this is rare in practice, we want to ensure 100% code coverage
|
|
377
|
+
// We can test this by creating a number that has localNumber extracted but no regionCode set
|
|
378
|
+
// International numbers with unrecognized region codes could hit this.
|
|
379
|
+
|
|
380
|
+
// For now, let's test a case that we know behaves as expected
|
|
381
|
+
const result = isValidPhoneNumberWithDescription('12345678'); // 8 digits, no pattern match
|
|
382
|
+
expect(result.description).toBe('UNKNOWN_NUMBER');
|
|
383
|
+
expect(result.isValid).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
198
388
|
describe('Phone number validation', () => {
|
|
199
389
|
it('should determine whether a phone number is presumed valid or not', () => {
|
|
200
390
|
expect(isValidPhoneNumber('13103496200')).toBe(true);
|
package/src/base.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export interface PhoneParts {
|
|
|
9
9
|
regionCode: string | null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface PhoneValidationResult {
|
|
13
|
+
description: 'NOT_A_NUMBER' | 'VALID_NUMBER' | 'UNKNOWN_NUMBER' | 'UNKNOWN_AREA_CODE' | 'UNKNOWN_FORMAT';
|
|
14
|
+
isValid: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
export function formatPhoneNumberForE164(
|
|
13
18
|
phoneParts: Pick<PhoneParts, 'regionCode' | 'areaCode' | 'localNumber'>
|
|
14
19
|
): string | null;
|
|
@@ -19,7 +24,9 @@ export function formatPhoneNumberLink(
|
|
|
19
24
|
|
|
20
25
|
export function isValidPhoneNumber(phoneNumber: string): boolean;
|
|
21
26
|
|
|
22
|
-
export function
|
|
27
|
+
export function isValidPhoneNumberWithDescription(phoneNumber: string): PhoneValidationResult;
|
|
28
|
+
|
|
29
|
+
export function getPhoneParts(phoneNumber?: string | null): PhoneParts;
|
|
23
30
|
|
|
24
31
|
export function sanitizeRawNumber(phoneNumber: string): string;
|
|
25
32
|
|
package/src/base.js
CHANGED
|
@@ -10,6 +10,14 @@ const TILDES = '~\u2053\u223C\uFF5E';
|
|
|
10
10
|
const VALID_DIGITS = '0-9';
|
|
11
11
|
const PLUS_CHARS = '\\+';
|
|
12
12
|
|
|
13
|
+
// Phone number length constants
|
|
14
|
+
const MIN_PHONE_LENGTH = 7;
|
|
15
|
+
const MAX_PHONE_LENGTH = 15;
|
|
16
|
+
const US_PHONE_LENGTH = 10;
|
|
17
|
+
const US_PHONE_WITH_COUNTRY_LENGTH = 11;
|
|
18
|
+
const INTL_PHONE_WITH_PLUS_LENGTH = 12;
|
|
19
|
+
const LOCAL_PHONE_LENGTH = 7;
|
|
20
|
+
|
|
13
21
|
const VALID_PUNCTUATION = `${DASHES}${SLASHES}${DOTS}${WHITESPACE}${BRACKETS}${TILDES}`;
|
|
14
22
|
|
|
15
23
|
const VALID_PHONE_NUMBER = new RegExp(
|
|
@@ -24,6 +32,7 @@ const VALID_PHONE_NUMBER = new RegExp(
|
|
|
24
32
|
*
|
|
25
33
|
* This function takes the components of a phone number and formats it into the
|
|
26
34
|
* E.164 standard, which includes the region code and the local number. The format
|
|
35
|
+
* is +[country code][area code][local number] for US numbers or +[country code][local number] for international numbers.
|
|
27
36
|
*
|
|
28
37
|
* @param {Object} phoneParts - An object containing parts of the phone number.
|
|
29
38
|
* @param {string} phoneParts.areaCode - The area code of the phone number.
|
|
@@ -89,26 +98,54 @@ export const formatPhoneNumberLink = ({
|
|
|
89
98
|
* of the phone number and further confirms its validity based on the presence of these parts.
|
|
90
99
|
*
|
|
91
100
|
* @param {string} phoneNumber - The phone number to validate.
|
|
92
|
-
* @returns {
|
|
101
|
+
* @returns {Object} Returns an object with validation details.
|
|
102
|
+
* @returns {string} returns.description - Description of the validation result. Possible values: 'NOT_A_NUMBER', 'VALID_NUMBER', 'UNKNOWN_NUMBER', 'UNKNOWN_AREA_CODE', 'UNKNOWN_FORMAT'.
|
|
103
|
+
* @returns {boolean} returns.isValid - True if the phone number is valid, otherwise false.
|
|
93
104
|
*/
|
|
94
|
-
export const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
export const isValidPhoneNumberWithDescription = (phoneNumber) => {
|
|
106
|
+
let isValid = { description: '', isValid: false };
|
|
107
|
+
|
|
108
|
+
// Input validation
|
|
109
|
+
if (!phoneNumber || typeof phoneNumber !== 'string') {
|
|
110
|
+
isValid.description = 'NOT_A_NUMBER';
|
|
111
|
+
return isValid;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check the big chunky regex for phone validity (reuse existing regex)
|
|
115
|
+
if (VALID_PHONE_NUMBER.test(phoneNumber)) {
|
|
98
116
|
const phoneParts = getPhoneParts(phoneNumber);
|
|
99
117
|
|
|
100
|
-
//
|
|
118
|
+
// Check for phone validity by ability to extract useful parts.
|
|
101
119
|
// This will also confirm area codes / region codes are valid.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
) {
|
|
108
|
-
|
|
120
|
+
//
|
|
121
|
+
// The localNumber value is just used here to validate correct parsing of the larger number.
|
|
122
|
+
// This can commonly be null if the regionCode is undefined.
|
|
123
|
+
if (!phoneParts.localNumber) {
|
|
124
|
+
isValid.description = 'UNKNOWN_NUMBER';
|
|
125
|
+
} else if (phoneParts.regionCode === '1' && !phoneParts.areaCode) {
|
|
126
|
+
isValid.description = 'UNKNOWN_AREA_CODE';
|
|
127
|
+
} else {
|
|
128
|
+
isValid.description = 'VALID_NUMBER';
|
|
129
|
+
isValid.isValid = true;
|
|
109
130
|
}
|
|
131
|
+
return isValid;
|
|
110
132
|
}
|
|
111
|
-
|
|
133
|
+
|
|
134
|
+
isValid.description = 'UNKNOWN_FORMAT';
|
|
135
|
+
|
|
136
|
+
return isValid;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validates a phone number based on a regex pattern and the ability to extract useful parts.
|
|
141
|
+
*
|
|
142
|
+
* This function uses the `isValidPhoneNumberWithDescription` function to check the phone number's validity and simply returns the boolean without any description.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} phoneNumber - The phone number to validate.
|
|
145
|
+
* @returns {boolean} Returns true if the phone number is valid, otherwise false.
|
|
146
|
+
*/
|
|
147
|
+
export const isValidPhoneNumber = (phoneNumber) => {
|
|
148
|
+
return isValidPhoneNumberWithDescription(phoneNumber).isValid;
|
|
112
149
|
};
|
|
113
150
|
|
|
114
151
|
/**
|
|
@@ -122,9 +159,11 @@ export const isValidPhoneNumber = (phoneNumber) => {
|
|
|
122
159
|
* @returns {Object} An object containing relevant phone number information.
|
|
123
160
|
* @property {string|null} areaCode - The area code of the phone number.
|
|
124
161
|
* @property {string|null} e164 - The E.164 formatted version of the phone number.
|
|
162
|
+
* @property {string|null} format - The format template for the phone number (e.g., "(xxx) xxx-xxxx").
|
|
163
|
+
* @property {string|null} formattedNumber - The formatted phone number according to the region's format.
|
|
125
164
|
* @property {string|null} href - A formatted phone number link.
|
|
126
165
|
* @property {string|null} localNumber - The local part of the phone number.
|
|
127
|
-
* @property {string} rawNumber - The original raw phone number.
|
|
166
|
+
* @property {string} rawNumber - The original raw phone number. Unsanitized.
|
|
128
167
|
* @property {string|null} regionCode - The region code of the phone number.
|
|
129
168
|
*/
|
|
130
169
|
export const getPhoneParts = (phoneNumber) => {
|
|
@@ -150,15 +189,15 @@ export const getPhoneParts = (phoneNumber) => {
|
|
|
150
189
|
// The shortest length for a phone number (that we care about) is 7 digits.
|
|
151
190
|
// The longest phone number is 15 digits.
|
|
152
191
|
if (
|
|
153
|
-
strippedPhoneNumber.replace(/\D/g, '').length >=
|
|
154
|
-
strippedPhoneNumber.replace(/\D/g, '').length <=
|
|
192
|
+
strippedPhoneNumber.replace(/\D/g, '').length >= MIN_PHONE_LENGTH &&
|
|
193
|
+
strippedPhoneNumber.replace(/\D/g, '').length <= MAX_PHONE_LENGTH
|
|
155
194
|
) {
|
|
156
195
|
// Extract the region code if not explicitly provided and it is part of the
|
|
157
196
|
// phone number
|
|
158
197
|
if (strippedPhoneNumber.startsWith('+')) {
|
|
159
198
|
// US number formatted with +12065551234
|
|
160
199
|
if (
|
|
161
|
-
strippedPhoneNumber.length ===
|
|
200
|
+
strippedPhoneNumber.length === INTL_PHONE_WITH_PLUS_LENGTH &&
|
|
162
201
|
strippedPhoneNumber.startsWith('+1')
|
|
163
202
|
) {
|
|
164
203
|
phoneParts.regionCode = '1';
|
|
@@ -185,7 +224,7 @@ export const getPhoneParts = (phoneNumber) => {
|
|
|
185
224
|
}
|
|
186
225
|
// If no region code is provided, assume US with the format 3109309000 after being stripped of non-numeric values.
|
|
187
226
|
// We'll try and derive the area code by looking it up against the known area codes.
|
|
188
|
-
else if (strippedPhoneNumber.length ===
|
|
227
|
+
else if (strippedPhoneNumber.length === US_PHONE_LENGTH) {
|
|
189
228
|
if (AREA_CODE_LIST.indexOf(strippedPhoneNumber.substring(0, 3)) !== -1) {
|
|
190
229
|
phoneParts.regionCode = '1';
|
|
191
230
|
phoneParts.areaCode = strippedPhoneNumber.substring(0, 3);
|
|
@@ -194,14 +233,14 @@ export const getPhoneParts = (phoneNumber) => {
|
|
|
194
233
|
}
|
|
195
234
|
// If no region code is provided, assume US with the format 9309000 after being stripped of non-numeric values.
|
|
196
235
|
// This is not able to be validated or formatted since it lacks an area code.
|
|
197
|
-
else if (strippedPhoneNumber.length ===
|
|
236
|
+
else if (strippedPhoneNumber.length === LOCAL_PHONE_LENGTH) {
|
|
198
237
|
phoneParts.regionCode = '1';
|
|
199
238
|
phoneParts.localNumber = strippedPhoneNumber;
|
|
200
239
|
}
|
|
201
240
|
|
|
202
241
|
// Default to region code 1 for US numbers if none is provided
|
|
203
242
|
if (
|
|
204
|
-
strippedPhoneNumber.length ===
|
|
243
|
+
strippedPhoneNumber.length === US_PHONE_WITH_COUNTRY_LENGTH &&
|
|
205
244
|
strippedPhoneNumber.startsWith('1')
|
|
206
245
|
) {
|
|
207
246
|
phoneParts.regionCode = '1';
|
|
@@ -212,7 +251,7 @@ export const getPhoneParts = (phoneNumber) => {
|
|
|
212
251
|
// US likes a format that isn't as common
|
|
213
252
|
if (phoneParts.localNumber && phoneParts.regionCode === '1') {
|
|
214
253
|
// Specific format for US numbers with areaCode (206-930-9000).
|
|
215
|
-
if (phoneParts.localNumber.length ===
|
|
254
|
+
if (phoneParts.localNumber.length === US_PHONE_LENGTH) {
|
|
216
255
|
phoneParts.areaCode = phoneParts.localNumber.slice(0, 3);
|
|
217
256
|
}
|
|
218
257
|
} else if (phoneParts.localNumber) {
|
|
@@ -221,10 +260,11 @@ export const getPhoneParts = (phoneNumber) => {
|
|
|
221
260
|
}
|
|
222
261
|
}
|
|
223
262
|
|
|
224
|
-
// Unset any known invalid area codes.
|
|
263
|
+
// Unset any known invalid area codes. We only care about region 1 (USA).
|
|
225
264
|
if (
|
|
226
|
-
|
|
227
|
-
phoneParts.
|
|
265
|
+
phoneParts.regionCode === '1' &&
|
|
266
|
+
phoneParts.areaCode &&
|
|
267
|
+
AREA_CODE_LIST.indexOf(phoneParts.areaCode) === -1
|
|
228
268
|
) {
|
|
229
269
|
phoneParts.areaCode = null;
|
|
230
270
|
}
|
|
@@ -244,9 +284,12 @@ export const getPhoneParts = (phoneNumber) => {
|
|
|
244
284
|
* except for the leading plus sign (+) if it exists.
|
|
245
285
|
*
|
|
246
286
|
* @param {string} phoneNumber - The raw phone number input.
|
|
247
|
-
* @returns {string}
|
|
287
|
+
* @returns {string} The sanitized phone number containing only digits and an optional leading plus sign.
|
|
248
288
|
*/
|
|
249
289
|
export const sanitizeRawNumber = (phoneNumber) => {
|
|
290
|
+
if (!phoneNumber || typeof phoneNumber !== 'string') {
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
250
293
|
return phoneNumber.replace(/(?!^\+)\D/g, '');
|
|
251
294
|
};
|
|
252
295
|
|
|
@@ -263,11 +306,15 @@ export const sanitizeRawNumber = (phoneNumber) => {
|
|
|
263
306
|
* @property {Object} phoneParts - The parts of the phone number, as returned by the getPhoneParts function.
|
|
264
307
|
*/
|
|
265
308
|
export const findNumbersInString = (text) => {
|
|
309
|
+
if (!text || typeof text !== 'string') {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
266
313
|
const regex = new RegExp(VALID_PHONE_NUMBER, 'g');
|
|
267
314
|
let matches = [];
|
|
268
315
|
let match;
|
|
269
316
|
|
|
270
|
-
// Regex finds possible matches.
|
|
317
|
+
// Regex finds possible matches. Go through each of them to further validate and extract relevant info.
|
|
271
318
|
while ((match = regex.exec(text)) !== null) {
|
|
272
319
|
let number = match[0].trim(); // Access the captured group
|
|
273
320
|
|
|
@@ -280,7 +327,7 @@ export const findNumbersInString = (text) => {
|
|
|
280
327
|
const phoneParts = getPhoneParts(number);
|
|
281
328
|
|
|
282
329
|
// Presumed phone numbers may be invalidated by omission of formattedNumber from getPhoneParts.
|
|
283
|
-
// This will prevent short-numbers that
|
|
330
|
+
// This will prevent short-numbers that omit area code from being fetched from a larger string since it may be unreliable.
|
|
284
331
|
if (phoneParts.formattedNumber) {
|
|
285
332
|
matches.push({
|
|
286
333
|
index,
|
|
@@ -302,7 +349,7 @@ export const findNumbersInString = (text) => {
|
|
|
302
349
|
* it defaults to the format for region code '1' (US).
|
|
303
350
|
*
|
|
304
351
|
* @param {Object} params - The parameters for formatting the phone number.
|
|
305
|
-
* @param {string} regionCode - The region code to look up the phone number format.
|
|
352
|
+
* @param {string} params.regionCode - The region code to look up the phone number format.
|
|
306
353
|
* @param {string} params.e164 - The E.164 formatted phone number to format. Example: `+12065551234`.
|
|
307
354
|
* @returns {string} The phone number format for the given region code in the format of "(xxx) xxx-xxxx".
|
|
308
355
|
*/
|
package/vitest.config.js
CHANGED