@yext/phonenumber-util 0.2.12 → 0.3.1
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/.github/workflows/pull-request.yml +3 -0
- package/README.md +27 -2
- package/package.json +8 -8
- package/src/__tests__/base.test.js +190 -0
- package/src/__tests__/geo.test.js +72 -0
- package/src/areaCodeList.js +10 -0
- package/src/base.d.ts +8 -1
- package/src/base.js +78 -31
- package/src/daylightSavings.js +2 -0
- package/src/phoneCodes.js +12 -0
- package/src/phoneFormats.js +0 -1
- package/src/timezones.js +7 -0
- 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.1",
|
|
4
4
|
"author": "bajohnson@hearsaycorp.com",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"description": "Utility for extracting and validating phone numbers",
|
|
@@ -25,16 +25,16 @@
|
|
|
25
25
|
"make-badges": "istanbul-badges-readme"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@eslint/js": "^9.
|
|
29
|
-
"@vitest/coverage-v8": "^3.2.
|
|
30
|
-
"eslint": "^9.
|
|
28
|
+
"@eslint/js": "^9.31.0",
|
|
29
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
30
|
+
"eslint": "^9.31.0",
|
|
31
31
|
"generate-license-file": "4.0.0",
|
|
32
|
-
"globals": "^16.
|
|
32
|
+
"globals": "^16.3.0",
|
|
33
33
|
"husky": "^9.1.7",
|
|
34
34
|
"istanbul-badges-readme": "^1.9.0",
|
|
35
|
-
"lint-staged": "^16.1.
|
|
36
|
-
"prettier": "^3.
|
|
37
|
-
"vitest": "^3.2.
|
|
35
|
+
"lint-staged": "^16.1.2",
|
|
36
|
+
"prettier": "^3.6.2",
|
|
37
|
+
"vitest": "^3.2.4"
|
|
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);
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
findTimeFromAreaCode,
|
|
7
7
|
findRegionFromRegionCode,
|
|
8
8
|
} from '../geo.js';
|
|
9
|
+
import { AREA_CODE_LIST } from '../areaCodeList.js';
|
|
10
|
+
import { AREA_CODES, REGION_CODES } from '../phoneCodes.js';
|
|
11
|
+
import { PHONE_FORMATS } from '../phoneFormats.js';
|
|
12
|
+
|
|
9
13
|
import { describe, it, expect } from 'vitest';
|
|
10
14
|
|
|
11
15
|
const invalidPhone = {
|
|
@@ -184,6 +188,74 @@ const canadianPhone = {
|
|
|
184
188
|
},
|
|
185
189
|
};
|
|
186
190
|
|
|
191
|
+
describe('Validate that every allow-list area code has matching geo and time info', () => {
|
|
192
|
+
it('should ensure every area code in AREA_CODE_LIST has a matching region code in AREA_CODES', () => {
|
|
193
|
+
AREA_CODE_LIST.forEach((areaCode) => {
|
|
194
|
+
const areaCodeInfo = AREA_CODES[areaCode];
|
|
195
|
+
|
|
196
|
+
if (!areaCodeInfo) {
|
|
197
|
+
console.warn(
|
|
198
|
+
`Area code ${areaCode} does not have a matching AREA_CODES entry.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
expect(areaCodeInfo).toBeDefined();
|
|
203
|
+
expect(areaCodeInfo).not.toBeNull();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
Object.keys(AREA_CODES).forEach((areaCode) => {
|
|
207
|
+
const exists = AREA_CODE_LIST.includes(areaCode);
|
|
208
|
+
|
|
209
|
+
if (!exists) {
|
|
210
|
+
console.warn(
|
|
211
|
+
`Area code ${areaCode} does not have a matching AREA_CODE_LIST entry.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(exists).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should ensure every area code in AREA_CODE_LIST has a matching timezone', () => {
|
|
220
|
+
AREA_CODE_LIST.forEach((areaCode) => {
|
|
221
|
+
const timezone = findTimeFromAreaCode(
|
|
222
|
+
areaCode,
|
|
223
|
+
new Date('2024-07-15T08:00:00'),
|
|
224
|
+
);
|
|
225
|
+
expect(timezone).toBeDefined();
|
|
226
|
+
expect(timezone).not.toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should ensure every region code in REGION_CODES has a matching phone format', () => {
|
|
231
|
+
Object.keys(REGION_CODES).forEach((regionCode) => {
|
|
232
|
+
const phoneFormat = PHONE_FORMATS[regionCode];
|
|
233
|
+
|
|
234
|
+
if (!phoneFormat) {
|
|
235
|
+
console.warn(
|
|
236
|
+
`Region code ${regionCode} does not have a matching PHONE_FORMATS entry.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expect(phoneFormat).toBeDefined();
|
|
241
|
+
expect(phoneFormat).not.toBeNull();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
Object.keys(PHONE_FORMATS).forEach((regionCode) => {
|
|
245
|
+
const regionInfo = REGION_CODES[regionCode];
|
|
246
|
+
|
|
247
|
+
if (!regionInfo) {
|
|
248
|
+
console.warn(
|
|
249
|
+
`Region code ${regionCode} does not have a matching REGION_CODES entry.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
expect(regionInfo).toBeDefined();
|
|
254
|
+
expect(regionInfo).not.toBeNull();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
187
259
|
describe('Daylight Savings', () => {
|
|
188
260
|
it('should correctly be determined if the time given is or is not within daylight savings time', () => {
|
|
189
261
|
const daylightSavings = new Date('2024-07-15T12:00:00');
|
package/src/areaCodeList.js
CHANGED
|
@@ -38,6 +38,7 @@ export const AREA_CODE_LIST = [
|
|
|
38
38
|
'242',
|
|
39
39
|
'246',
|
|
40
40
|
'248',
|
|
41
|
+
'249',
|
|
41
42
|
'250',
|
|
42
43
|
'251',
|
|
43
44
|
'252',
|
|
@@ -46,6 +47,7 @@ export const AREA_CODE_LIST = [
|
|
|
46
47
|
'256',
|
|
47
48
|
'260',
|
|
48
49
|
'262',
|
|
50
|
+
'263',
|
|
49
51
|
'264',
|
|
50
52
|
'267',
|
|
51
53
|
'268',
|
|
@@ -98,15 +100,18 @@ export const AREA_CODE_LIST = [
|
|
|
98
100
|
'350',
|
|
99
101
|
'351',
|
|
100
102
|
'352',
|
|
103
|
+
'354',
|
|
101
104
|
'357',
|
|
102
105
|
'360',
|
|
103
106
|
'361',
|
|
104
107
|
'363',
|
|
105
108
|
'364',
|
|
106
109
|
'365',
|
|
110
|
+
'367',
|
|
107
111
|
'368',
|
|
108
112
|
'369',
|
|
109
113
|
'380',
|
|
114
|
+
'382',
|
|
110
115
|
'385',
|
|
111
116
|
'386',
|
|
112
117
|
'387',
|
|
@@ -150,11 +155,13 @@ export const AREA_CODE_LIST = [
|
|
|
150
155
|
'458',
|
|
151
156
|
'463',
|
|
152
157
|
'464',
|
|
158
|
+
'468',
|
|
153
159
|
'469',
|
|
154
160
|
'470',
|
|
155
161
|
'471',
|
|
156
162
|
'472',
|
|
157
163
|
'473',
|
|
164
|
+
'474',
|
|
158
165
|
'475',
|
|
159
166
|
'478',
|
|
160
167
|
'479',
|
|
@@ -236,6 +243,7 @@ export const AREA_CODE_LIST = [
|
|
|
236
243
|
'580',
|
|
237
244
|
'581',
|
|
238
245
|
'582',
|
|
246
|
+
'584',
|
|
239
247
|
'585',
|
|
240
248
|
'586',
|
|
241
249
|
'587',
|
|
@@ -301,6 +309,7 @@ export const AREA_CODE_LIST = [
|
|
|
301
309
|
'680',
|
|
302
310
|
'681',
|
|
303
311
|
'682',
|
|
312
|
+
'683',
|
|
304
313
|
'684',
|
|
305
314
|
'686',
|
|
306
315
|
'688',
|
|
@@ -343,6 +352,7 @@ export const AREA_CODE_LIST = [
|
|
|
343
352
|
'743',
|
|
344
353
|
'747',
|
|
345
354
|
'748',
|
|
355
|
+
'753',
|
|
346
356
|
'754',
|
|
347
357
|
'757',
|
|
348
358
|
'758',
|
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
|
|
|
@@ -275,12 +322,12 @@ export const findNumbersInString = (text) => {
|
|
|
275
322
|
if (
|
|
276
323
|
number.replace(new RegExp(`[${VALID_PUNCTUATION}]`, 'g'), '').length >= 6
|
|
277
324
|
) {
|
|
278
|
-
const index =
|
|
279
|
-
const lastIndex =
|
|
325
|
+
const index = match.index;
|
|
326
|
+
const lastIndex = regex.lastIndex;
|
|
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/src/daylightSavings.js
CHANGED
package/src/phoneCodes.js
CHANGED
|
@@ -568,6 +568,7 @@ export const AREA_CODES = {
|
|
|
568
568
|
778: { name: 'British Columbia', code: 'BC', region: CANADA },
|
|
569
569
|
204: { name: 'Manitoba', code: 'MB', region: CANADA },
|
|
570
570
|
431: { name: 'Manitoba', code: 'MB', region: CANADA },
|
|
571
|
+
584: { name: 'Manitoba', code: 'MB', region: CANADA },
|
|
571
572
|
506: { name: 'New Brunswick', code: 'NB', region: CANADA },
|
|
572
573
|
709: { name: 'Newfoundland and Labrador', code: 'NL', region: CANADA },
|
|
573
574
|
782: {
|
|
@@ -581,30 +582,39 @@ export const AREA_CODES = {
|
|
|
581
582
|
region: CANADA,
|
|
582
583
|
},
|
|
583
584
|
226: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
585
|
+
249: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
584
586
|
289: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
585
587
|
343: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
586
588
|
365: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
589
|
+
382: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
587
590
|
416: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
588
591
|
437: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
589
592
|
519: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
590
593
|
548: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
591
594
|
613: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
592
595
|
647: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
596
|
+
683: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
593
597
|
705: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
598
|
+
753: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
594
599
|
742: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
595
600
|
807: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
596
601
|
942: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
597
602
|
905: { name: 'Ontario', code: 'ON', region: CANADA },
|
|
603
|
+
263: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
604
|
+
354: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
605
|
+
367: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
598
606
|
387: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
599
607
|
418: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
600
608
|
438: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
601
609
|
450: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
610
|
+
468: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
602
611
|
514: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
603
612
|
579: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
604
613
|
581: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
605
614
|
819: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
606
615
|
873: { name: 'Quebec', code: 'QC', region: CANADA },
|
|
607
616
|
306: { name: 'Saskatchewan', code: 'SK', region: CANADA },
|
|
617
|
+
474: { name: 'Saskatchewan', code: 'SK', region: CANADA },
|
|
608
618
|
639: { name: 'Saskatchewan', code: 'SK', region: CANADA },
|
|
609
619
|
867: {
|
|
610
620
|
name: 'Yukon, Northwest Territories, and Nunavut',
|
|
@@ -708,6 +718,7 @@ export const REGION_CODES = {
|
|
|
708
718
|
679: { name: 'Fiji', code: 'FJ', flag: '🇫🇯' },
|
|
709
719
|
358: { name: 'Finland', code: 'FI', flag: '🇫🇮' },
|
|
710
720
|
33: { name: 'France', code: 'FR', flag: '🇫🇷' },
|
|
721
|
+
594: { name: 'French Guiana', code: 'GF', flag: '🇬🇫' },
|
|
711
722
|
689: { name: 'French Polynesia', code: 'PF', flag: '🇵🇫' },
|
|
712
723
|
241: { name: 'Gabon', code: 'GA', flag: '🇬🇦' },
|
|
713
724
|
220: { name: 'Gambia', code: 'GM', flag: '🇬🇲' },
|
|
@@ -761,6 +772,7 @@ export const REGION_CODES = {
|
|
|
761
772
|
223: { name: 'Mali', code: 'ML', flag: '🇲🇱' },
|
|
762
773
|
356: { name: 'Malta', code: 'MT', flag: '🇲🇹' },
|
|
763
774
|
692: { name: 'Marshall Islands', code: 'MH', flag: '🇲🇭' },
|
|
775
|
+
596: { name: 'Martinique', code: 'MQ', flag: '🇲🇶' },
|
|
764
776
|
222: { name: 'Mauritania', code: 'MR', flag: '🇲🇷' },
|
|
765
777
|
230: { name: 'Mauritius', code: 'MU', flag: '🇲🇺' },
|
|
766
778
|
262: { name: 'Mayotte, Reunion', code: 'YT/RE', flag: '🇾🇹/🇷🇪' },
|
package/src/phoneFormats.js
CHANGED
|
@@ -145,7 +145,6 @@ export const PHONE_FORMATS = {
|
|
|
145
145
|
269: '+xxx xxx xx xx', // Comoros
|
|
146
146
|
290: '+xxx xxxx', // Saint Helena
|
|
147
147
|
291: '+xxx x xxx xxx', // Eritrea
|
|
148
|
-
295: '+xxx xxx xxxx', // San Marino
|
|
149
148
|
297: '+xxx xxx xxxx', // Aruba
|
|
150
149
|
298: '+xxx xxx xxx', // Faroe Islands
|
|
151
150
|
299: '+xxx xx xx xx', // Greenland
|
package/src/timezones.js
CHANGED
|
@@ -118,6 +118,11 @@ export const STATES_WITH_MULTIPLE_TIMEZONES = {
|
|
|
118
118
|
458: ['-08:00', '-07:00'],
|
|
119
119
|
541: ['-08:00', '-07:00'],
|
|
120
120
|
},
|
|
121
|
+
Saskatchewan: {
|
|
122
|
+
306: ['-07:00', '-06:00', '-05:00'],
|
|
123
|
+
474: ['-07:00', '-06:00', '-05:00'],
|
|
124
|
+
639: ['-07:00', '-06:00', '-05:00'],
|
|
125
|
+
},
|
|
121
126
|
'South Dakota': {
|
|
122
127
|
605: ['-06:00', '-07:00'],
|
|
123
128
|
},
|
|
@@ -142,7 +147,9 @@ export const STATES_WITH_MULTIPLE_TIMEZONES = {
|
|
|
142
147
|
807: ['-05:00', '-06:00'],
|
|
143
148
|
},
|
|
144
149
|
Quebec: {
|
|
150
|
+
367: ['-04:00'],
|
|
145
151
|
418: ['-04:00'],
|
|
152
|
+
581: ['-04:00'],
|
|
146
153
|
},
|
|
147
154
|
'Newfoundland and Labrador': {
|
|
148
155
|
709: ['-03:30', '-04:00'],
|
package/vitest.config.js
CHANGED