@yext/phonenumber-util 0.1.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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx lint-staged
5
+ npm run format
6
+ npx vitest run --coverage
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "printWidth": 80,
5
+ "tabWidth": 2
6
+ }
package/CODEOWNERS ADDED
@@ -0,0 +1,2 @@
1
+ # The following aliases have approval permissions in this repo:
2
+ * @imbrianj @mi4l
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Hearsay Systems
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ Utility for extracting and validating phone numbers. Extracts an array of phone numbers from an inputted string, validates that these numbers appear genuine and provide data about those phone numbers.
2
+
3
+ ### Scripts
4
+
5
+ #### Install dependencies
6
+
7
+ ```bash
8
+ npm i
9
+ ```
10
+
11
+ #### Formatting source code
12
+
13
+ ```bash
14
+ npm run format
15
+ ```
16
+
17
+ #### Linting
18
+
19
+ ```bash
20
+ npm run lint
21
+ ```
22
+
23
+ #### Testing
24
+
25
+ ```bash
26
+ npm test
27
+ ```
28
+
29
+ #### Code Coverage Report
30
+
31
+ ```bash
32
+ npx vitest run --coverage
33
+ ```
34
+
35
+ ### Warning
36
+
37
+ The returned object will include a `rawNumber` value. This value is the return of the exact value passed to the function. No sanitization occurs with this value. If you reference this number, ensure you sanitize it _BEFORE_ passing to this function.
38
+
39
+ ### Usage
40
+
41
+ There is additional functionality exposed as `export`, but the primary expected use case is:
42
+
43
+ #### isValidPhoneNumber
44
+
45
+ Returns a boolean based on whether the passed number is presumed to be valid or invalid.
46
+
47
+ This checks for region code, number length and validity of region code and area code (where applicable).
48
+
49
+ ```javascript
50
+ import { isValidPhoneNumber } from 'phonenumber-util';
51
+ const validPhoneNumber = '3103496333';
52
+ isValidPhoneNumber(validPhoneNumber); // Returns `true` - "310" is an area code for California
53
+
54
+ const invalidPhoneNumber = '3113496333';
55
+ isValidPhoneNumber(invalidPhoneNumber); // Returns `false` - "311" is not a valid area code
56
+
57
+ const intlNumber = '+380 97 123 4567';
58
+ isValidPhoneNumber(intlNumber); // Returns `true` - "380" is the region code for Ukraine
59
+
60
+ const invalidIntlNumber = '+666 97 123 4567';
61
+ isValidPhoneNumber(invalidIntlNumber); // Returns `false` - "666" is not a valid region code
62
+ ```
63
+
64
+ #### getPhoneParts
65
+
66
+ Return an object of relevant phone number parts and information.
67
+
68
+ ```javascript
69
+ import { getPhoneParts } from 'phonenumber-util';
70
+ const validPhoneNumber = '3496333';
71
+ getPhoneParts(validPhoneNumber); // Returns an object, assumed to be US / Canada, region code "1" but no area code can be reliably determined.
72
+
73
+ const validPhoneNumber = '"+923331234567"';
74
+ getPhoneParts(validPhoneNumber); // Returns an object, assumed to be Pakistan, region code "92".
75
+ ```
76
+
77
+ Example output for Pakistan ("+923331234567"):
78
+
79
+ ```javascript
80
+ {
81
+ areaCode: null,
82
+ e164: "+923331234567",
83
+ format: "+xx xxx xxx xxxx",
84
+ formattedNumber: "+92 333 123 4567",
85
+ href: "tel:+923331234567",
86
+ localNumber: "3331234567",
87
+ rawNumber: "+923331234567",
88
+ regionCode: "92"
89
+ }
90
+ ```
91
+
92
+ Example for US with 7 digits provided ("3496200") where no state can be determined. Note that e164 and formattedNumber also cannot be derived:
93
+
94
+ ```javascript
95
+ {
96
+ areaCode: null,
97
+ e164: null,
98
+ format: "(xxx) xxx-xxxx",
99
+ formattedNumber: null,
100
+ href: "tel:3496200",
101
+ localNumber: "3496200",
102
+ rawNumber: "349.6200",
103
+ regionCode: "1"
104
+ }
105
+ ```
106
+
107
+ Example for US with full area code provided ("310.349.9999"):
108
+
109
+ ```javascript
110
+ {
111
+ areaCode: "310",
112
+ e164: "+13103103499999",
113
+ format: "(xxx) xxx-xxxx",
114
+ formattedNumber: "(310) 349-9999",
115
+ href: "tel:+13103499999",
116
+ localNumber: "3103499999",
117
+ rawNumber: "310.349.9999",
118
+ regionCode: "1"
119
+ }
120
+ ```
121
+
122
+ #### findNumbersInString
123
+
124
+ Accepts a full string of text and returns an array of phone numbers extracted from within that text.
125
+
126
+ The objects within this array will be identical to the object provided in the `getPhoneParts` above with the only exception being the addition of a `index` and `lastIndex` integer noting the start and end of the instance of that specific number within the provided string.
127
+
128
+ Example for US with full area code provided ("Hey there, my number is 310.349.9999. Please give me a call!"):
129
+
130
+ ```javascript
131
+ [
132
+ {
133
+ index: 24,
134
+ lastIndex: 36,
135
+ areaCode: '310',
136
+ e164: '+13103103499999',
137
+ format: '(xxx) xxx-xxxx',
138
+ formattedNumber: '(310) 349-9999',
139
+ href: 'tel:+13103499999',
140
+ localNumber: '3103499999',
141
+ rawNumber: '310.349.9999',
142
+ regionCode: '1',
143
+ },
144
+ ];
145
+ ```
146
+
147
+ Numbers that lack area codes will NOT be returned via `findNumbersInString` since they cannot be reliably validated.
148
+
149
+ Example for US with no area code provided ("Hey there, my number is 349.9999. Please give me a call!"):
150
+
151
+ ```javascript
152
+ [];
153
+ ```
@@ -0,0 +1,5 @@
1
+ This file was generated with the generate-license-file npm package!
2
+ https://www.npmjs.com/package/generate-license-file
3
+
4
+ This file was generated with the generate-license-file npm package!
5
+ https://www.npmjs.com/package/generate-license-file
@@ -0,0 +1,7 @@
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+
4
+ export default [
5
+ {languageOptions: { globals: globals.browser }},
6
+ pluginJs.configs.recommended,
7
+ ];
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@yext/phonenumber-util",
3
+ "version": "0.1.0",
4
+ "author": "bajohnson@hearsaycorp.com",
5
+ "license": "BSD-3-Clause",
6
+ "description": "Utility for extracting and validating phone numbers",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "types": "src/index.d.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/hearsaycorp/phonenumber-util.git"
13
+ },
14
+ "scripts": {
15
+ "lint": "eslint **/*.js **/__tests__/*.js",
16
+ "format": "prettier --write **/*.js **/__tests__/*.js",
17
+ "test": "vitest",
18
+ "build": "node -e \"console.log('No build script for vanilla JS')\"",
19
+ "prepare": "husky"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.7.0",
23
+ "@vitest/coverage-v8": "^2.1.1",
24
+ "eslint": "^9.7.0",
25
+ "globals": "^15.8.0",
26
+ "husky": "^9.1.5",
27
+ "lint-staged": "^15.2.10",
28
+ "prettier": "^3.3.3",
29
+ "vitest": "^2.1.1"
30
+ },
31
+ "eslintConfig": {},
32
+ "lint-staged": {
33
+ "**/*.js": [
34
+ "npm run lint"
35
+ ],
36
+ "**/*.{js,json,md}": [
37
+ "npm run format"
38
+ ]
39
+ }
40
+ }
@@ -0,0 +1,239 @@
1
+ import {
2
+ formatPhoneNumberForE164,
3
+ formatPhoneNumberLink,
4
+ isValidPhoneNumber,
5
+ getPhoneParts,
6
+ sanitizeRawNumber,
7
+ findNumbersInString,
8
+ findPhoneFormat,
9
+ formatPhoneNumber,
10
+ } from '../index.js';
11
+ import { describe, it, expect } from 'vitest';
12
+
13
+ const testNumbers = {
14
+ 3103491234: '1',
15
+ 18333671100: '1',
16
+ '+86 755 8357 7777': '86',
17
+ '+91 987 654 3210': '91',
18
+ '+62 812 345 6789': '62',
19
+ '+1 212 456 7890': '1',
20
+ '+55 11 98765 4321': '55',
21
+ '+7 912 345 6789': '7',
22
+ '+92 333 123 4567': '92',
23
+ '+234 802 345 6789': '234',
24
+ '+880 1712 345 678': '880',
25
+ '+81 90 1234 5678': '81',
26
+ '+49 171 234 5678': '49',
27
+ '+49 30 97 88 88 88': '49',
28
+ '+63 917 123 4567': '63',
29
+ '+52 55 1234 5678': '52',
30
+ '+98 912 345 6789': '98',
31
+ '+20 10 123 4567': '20',
32
+ '+39 333 123 4567': '39',
33
+ '+44 791 112 3456': '44',
34
+ '+84 283 822 5555': '84',
35
+ '+90 532 123 4567': '90',
36
+ '+1 (310) 349-6543': '1',
37
+ '+33 7 56 78 90 12': '33',
38
+ '+66 92 345 6789': '66',
39
+ '+27 82 345 6789': '27',
40
+ '+57 321 123 45 67': '57',
41
+ '+380 97 123 4567': '380',
42
+ '+54 911 123 4567': '54',
43
+ };
44
+
45
+ describe('Region code mapping', () => {
46
+ it('should find the correct region based on inputted number', () => {
47
+ Object.keys(testNumbers).map(function (phoneNumber) {
48
+ const phoneParts = getPhoneParts(phoneNumber);
49
+
50
+ expect(phoneParts.regionCode).toBe(testNumbers[phoneNumber]);
51
+ expect(phoneParts.formattedNumber.indexOf('x')).toBe(-1);
52
+ });
53
+ });
54
+
55
+ it('handles bad data', () => {
56
+ expect(getPhoneParts().rawNumber).toBe(undefined);
57
+ });
58
+
59
+ it('handles a number that has an invalid area code', () => {
60
+ expect(getPhoneParts('+1 420 222 3333').formattedNumber).toBeNull();
61
+ });
62
+
63
+ it('handles a number is definitely not a phone number', () => {
64
+ expect(getPhoneParts('7/23/2025').formattedNumber).toBeNull();
65
+ });
66
+ });
67
+
68
+ describe('Sanitizing user inputted phone number values', () => {
69
+ it('should return safe strings', () => {
70
+ // These numbers are terribly malformed. We try and reasonably extract values, but safety of the inputs is the priority.
71
+ expect(sanitizeRawNumber("+1 <a>(3\\1%3C0) 3&49-'65`43")).toBe(
72
+ '+131303496543',
73
+ );
74
+ expect(sanitizeRawNumber("+1; (3<>1&0) 3`49\\-65'43")).toBe('+13103496543');
75
+ });
76
+ });
77
+
78
+ describe('Extracting numbers from a larger string of text', () => {
79
+ it('should find the correct number of phone numbers present in a large string', () => {
80
+ let rawString = '';
81
+ // First, build a string based on the names and values in testNumbers object
82
+ Object.keys(testNumbers).map(function (phoneNumber) {
83
+ rawString =
84
+ rawString +
85
+ ' This: ' +
86
+ phoneNumber +
87
+ ' is a phone number for ' +
88
+ testNumbers[phoneNumber] +
89
+ '\n';
90
+ });
91
+
92
+ const output = findNumbersInString(rawString);
93
+ expect(output.length).toBe(Object.keys(testNumbers).length);
94
+
95
+ // Make sure each number given resolves to a region. The lookup table will
96
+ // have undefined for any number that doesn't exactly match.
97
+ output.map(function (number) {
98
+ expect(testNumbers[number.rawNumber]).toBeTruthy();
99
+ });
100
+ });
101
+
102
+ it('should ingore numbers of the correct local length but lack area code', () => {
103
+ expect(findNumbersInString('Meet me on 9/14/2024 over here').length).toBe(
104
+ 0,
105
+ );
106
+ });
107
+ });
108
+
109
+ describe('Phone number formatting', () => {
110
+ it('should return undefined for a bad number', () => {
111
+ expect(getPhoneParts('4444444444').formattedNumber).toBe(null);
112
+ });
113
+
114
+ it('should return correct values for a good number', () => {
115
+ // Prevent malicious chars from being injected.
116
+ expect(getPhoneParts("+1 <a>(3\\1%3C0) 3&49-'65`43").e164).toBe(null);
117
+ expect(getPhoneParts("+1; (3<>1&0) 3`49\\-65'43").e164).toBe(
118
+ '+13103496543',
119
+ );
120
+ // US
121
+ expect(getPhoneParts('+1 (310) 349-6543').e164).toBe('+13103496543');
122
+ expect(getPhoneParts('+1 (310) 349-6543').formattedNumber).toBe(
123
+ '(310) 349-6543',
124
+ );
125
+ // US with a non-existent area code
126
+ expect(getPhoneParts('+1 (420) 349-6543').e164).toBeNull();
127
+ expect(getPhoneParts('+1 (420) 349-6543').formattedNumber).toBeNull();
128
+ // Intl number (France)
129
+ expect(getPhoneParts('+33 7 56 78 90 12').e164).toBe('+33756789012');
130
+ expect(getPhoneParts('+33 7 56 78 90 12').formattedNumber).toBe(
131
+ '+33 7 56 78 90 12',
132
+ );
133
+ });
134
+ });
135
+
136
+ describe('Creation of e164 numbers', () => {
137
+ it('should create a valid e164 number for numbers that have that info. Should return null for local numbers that exclude country code.', () => {
138
+ const testNumbers = {
139
+ usLocal: { regionCode: '1', localNumber: '3496200' },
140
+ usAreaCode: { regionCode: '1', areaCode: '310', localNumber: '3496200' },
141
+ intl: { regionCode: '49', localNumber: '1712345678' },
142
+ };
143
+
144
+ // US numbers without area codes don't offer enough info to provide a
145
+ // valid e164 number.
146
+ expect(formatPhoneNumberForE164(testNumbers.usLocal)).toBe(null);
147
+ expect(formatPhoneNumberForE164(testNumbers.usAreaCode)).toBe(
148
+ '+13103496200',
149
+ );
150
+ expect(formatPhoneNumberForE164(testNumbers.intl)).toBe('+491712345678');
151
+ });
152
+ });
153
+
154
+ describe('Creation of phone links for href', () => {
155
+ it('should create a valid href value for use in anchor tags', () => {
156
+ const testNumbers = {
157
+ nullCase: {
158
+ regionCode: null,
159
+ localNumber: null,
160
+ rawNumber: null,
161
+ },
162
+ usLocal: {
163
+ regionCode: '1',
164
+ localNumber: '3496200',
165
+ rawNumber: '3496200',
166
+ },
167
+ usAreaCode: {
168
+ regionCode: '1',
169
+ areaCode: '310',
170
+ localNumber: '3496200',
171
+ rawNumber: '(310) 349-6200',
172
+ },
173
+ intl: {
174
+ regionCode: '49',
175
+ localNumber: '1712345678',
176
+ rawNumber: '+49 171 234 5678',
177
+ },
178
+ };
179
+
180
+ expect(formatPhoneNumberLink(testNumbers.nullCase)).toBe(null);
181
+ expect(formatPhoneNumberLink(testNumbers.usLocal)).toBe('tel:3496200');
182
+ expect(formatPhoneNumberLink(testNumbers.usAreaCode)).toBe(
183
+ 'tel:+13103496200',
184
+ );
185
+ expect(formatPhoneNumberLink(testNumbers.intl)).toBe('tel:+491712345678');
186
+ });
187
+ });
188
+
189
+ describe('Phone number validation', () => {
190
+ it('should determine whether a phone number is presumed valid or not', () => {
191
+ expect(isValidPhoneNumber('13103496200')).toBe(true);
192
+ expect(isValidPhoneNumber('3103496200')).toBe(true);
193
+ expect(isValidPhoneNumber('+234 345 6789')).toBe(true);
194
+ expect(isValidPhoneNumber('349-6200')).toBe(false);
195
+ expect(isValidPhoneNumber('7/23/2025')).toBe(false);
196
+ expect(isValidPhoneNumber('7.23.2025')).toBe(false);
197
+ expect(isValidPhoneNumber('7-23-2025')).toBe(false);
198
+ expect(isValidPhoneNumber('$5055')).toBe(false);
199
+ expect(isValidPhoneNumber('310-496-32313')).toBe(false);
200
+ expect(isValidPhoneNumber('5553496200')).toBe(false);
201
+ expect(isValidPhoneNumber('4444444444')).toBe(false);
202
+ });
203
+ });
204
+
205
+ describe('Phone number pretty formatting', () => {
206
+ it('should create a pretty phone number for different regions', () => {
207
+ const testNumbers = {
208
+ nullCase: {
209
+ e164: null,
210
+ format: findPhoneFormat({}),
211
+ },
212
+ us: {
213
+ e164: '13103496200',
214
+ regionCode: '1',
215
+ format: findPhoneFormat({ regionCode: '1', e164: '13103496200' }),
216
+ },
217
+ colombia: {
218
+ e164: '+573211234567',
219
+ regionCode: '57',
220
+ format: findPhoneFormat({ regionCode: '57', e164: '+573211234567' }),
221
+ },
222
+ germany: {
223
+ e164: '+49 170 87654321',
224
+ regionCode: '49',
225
+ format: findPhoneFormat({ regionCode: '49', e164: '+4917087654321' }),
226
+ },
227
+ germanyAlt: {
228
+ e164: '+49 170 8765432',
229
+ regionCode: '49',
230
+ format: findPhoneFormat({ regionCode: '49', e164: '+491708765432' }),
231
+ },
232
+ };
233
+
234
+ expect(formatPhoneNumber(testNumbers.nullCase)).toBe(null);
235
+ expect(formatPhoneNumber(testNumbers.us)).toBe('(310) 349-6200');
236
+ expect(formatPhoneNumber(testNumbers.colombia)).toBe('+57 321 123 4567');
237
+ expect(formatPhoneNumber(testNumbers.germanyAlt)).toBe('+49 17 08765432');
238
+ });
239
+ });