@yext/phonenumber-util 0.1.0 → 0.2.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,17 @@
1
+ name: Run pull request checks
2
+
3
+ on:
4
+ pull_request:
5
+ branches: ["main"]
6
+
7
+ jobs:
8
+ test:
9
+ name: Test
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-node@v4
15
+ - run: npm ci
16
+ - run: npm run lint
17
+ - run: npm test
package/CODEOWNERS CHANGED
@@ -1,2 +1,2 @@
1
1
  # The following aliases have approval permissions in this repo:
2
- * @imbrianj @mi4l
2
+ * @imbrianj @mi4l @DerekWW
package/README.md CHANGED
@@ -47,7 +47,7 @@ Returns a boolean based on whether the passed number is presumed to be valid or
47
47
  This checks for region code, number length and validity of region code and area code (where applicable).
48
48
 
49
49
  ```javascript
50
- import { isValidPhoneNumber } from 'phonenumber-util';
50
+ import { isValidPhoneNumber } from '@yext/phonenumber-util';
51
51
  const validPhoneNumber = '3103496333';
52
52
  isValidPhoneNumber(validPhoneNumber); // Returns `true` - "310" is an area code for California
53
53
 
@@ -66,11 +66,11 @@ isValidPhoneNumber(invalidIntlNumber); // Returns `false` - "666" is not a valid
66
66
  Return an object of relevant phone number parts and information.
67
67
 
68
68
  ```javascript
69
- import { getPhoneParts } from 'phonenumber-util';
69
+ import { getPhoneParts } from '@yext/phonenumber-util';
70
70
  const validPhoneNumber = '3496333';
71
71
  getPhoneParts(validPhoneNumber); // Returns an object, assumed to be US / Canada, region code "1" but no area code can be reliably determined.
72
72
 
73
- const validPhoneNumber = '"+923331234567"';
73
+ const validPhoneNumber = '+923331234567';
74
74
  getPhoneParts(validPhoneNumber); // Returns an object, assumed to be Pakistan, region code "92".
75
75
  ```
76
76
 
@@ -109,7 +109,7 @@ Example for US with full area code provided ("310.349.9999"):
109
109
  ```javascript
110
110
  {
111
111
  areaCode: "310",
112
- e164: "+13103103499999",
112
+ e164: "+13103499999",
113
113
  format: "(xxx) xxx-xxxx",
114
114
  formattedNumber: "(310) 349-9999",
115
115
  href: "tel:+13103499999",
@@ -133,7 +133,7 @@ Example for US with full area code provided ("Hey there, my number is 310.349.99
133
133
  index: 24,
134
134
  lastIndex: 36,
135
135
  areaCode: '310',
136
- e164: '+13103103499999',
136
+ e164: '+13103499999',
137
137
  format: '(xxx) xxx-xxxx',
138
138
  formattedNumber: '(310) 349-9999',
139
139
  href: 'tel:+13103499999',
@@ -151,3 +151,94 @@ Example for US with no area code provided ("Hey there, my number is 349.9999. Pl
151
151
  ```javascript
152
152
  [];
153
153
  ```
154
+
155
+ ## Geography / Time Functionality
156
+
157
+ In addition to the above methods, the following methods are also available via a different export.
158
+
159
+ There is additional functionality exposed as `export`, but the primary expected use case is:
160
+
161
+ #### findTimeFromAreaCode
162
+
163
+ Returns an object with geographic and time related information for a given region.
164
+
165
+ _NOTE:_ This is only applicable for United States and Canada.
166
+
167
+ ```javascript
168
+ import { findTimeFromAreaCode } from '@yext/phonenumber-util/geo';
169
+ findTimeFromAreaCode('928');
170
+ ```
171
+
172
+ Example output for Arizona:
173
+
174
+ ```javascript
175
+ {
176
+ areaCodeHasMultipleTimezones: false,
177
+ daylightSavings: true,
178
+ estimatedTime: true,
179
+ isQuietHours: false,
180
+ isTCPAQuietHours: false,
181
+ localTime24Hour: "15:00:00",
182
+ localTimeReadable: "3:00:00 PM",
183
+ region: {
184
+ code: "US",
185
+ flag: "🇺🇸",
186
+ name: "United States"
187
+ },
188
+ state: {
189
+ code: "AZ",
190
+ name: "Arizona"
191
+ },
192
+ stateHasMultipleTimezones: false,
193
+ timezoneOffset: "-07:00"
194
+ }
195
+ ```
196
+
197
+ ```javascript
198
+ import { findTimeFromAreaCode } from '@yext/phonenumber-util/geo';
199
+ findTimeFromAreaCode('250');
200
+ ```
201
+
202
+ Example output for British Columbia:
203
+
204
+ ```javascript
205
+ {
206
+ areaCodeHasMultipleTimezones: true,
207
+ daylightSavings: true,
208
+ estimatedTime: true,
209
+ isCRTCQuietHours: false,
210
+ isQuietHours: false,
211
+ localTime24Hour: "17:00:00",
212
+ localTimeReadable: "5:00:00 PM",
213
+ region: {
214
+ code: "CA",
215
+ flag: "🇨🇦",
216
+ name: "Canada"
217
+ },
218
+ state: {
219
+ code: "BC",
220
+ name: "British Columbia"
221
+ },
222
+ stateHasMultipleTimezones: true,
223
+ timezoneOffset: "-07:00"
224
+ }
225
+ ```
226
+
227
+ #### findRegionFromRegionCode
228
+
229
+ ```javascript
230
+ import { findRegionFromRegionCode } from '@yext/phonenumber-util/geo';
231
+ findRegionFromRegionCode('47');
232
+ ```
233
+
234
+ Returns the string name of a given region name based on region code provided.
235
+
236
+ _NOTE:_ This string is provided in English only (example: "Norway" and not the region-specific name "Norge").
237
+
238
+ ```javascript
239
+ {
240
+ code: "NO",
241
+ flag: "🇳🇴",
242
+ name: "Norway"
243
+ }
244
+ ```
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@yext/phonenumber-util",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "author": "bajohnson@hearsaycorp.com",
5
5
  "license": "BSD-3-Clause",
6
6
  "description": "Utility for extracting and validating phone numbers",
7
7
  "type": "module",
8
- "main": "src/index.js",
8
+ "main": "src",
9
+ "exports": {
10
+ ".": "./src/base.js",
11
+ "./geo": "./src/geo.js"
12
+ },
9
13
  "types": "src/index.d.ts",
14
+ "sideEffects": false,
10
15
  "repository": {
11
16
  "type": "git",
12
17
  "url": "git+https://github.com/hearsaycorp/phonenumber-util.git"
@@ -14,7 +19,7 @@
14
19
  "scripts": {
15
20
  "lint": "eslint **/*.js **/__tests__/*.js",
16
21
  "format": "prettier --write **/*.js **/__tests__/*.js",
17
- "test": "vitest",
22
+ "test": "TZ=America/Los_Angeles vitest",
18
23
  "build": "node -e \"console.log('No build script for vanilla JS')\"",
19
24
  "prepare": "husky"
20
25
  },
@@ -7,7 +7,7 @@ import {
7
7
  findNumbersInString,
8
8
  findPhoneFormat,
9
9
  formatPhoneNumber,
10
- } from '../index.js';
10
+ } from '../base.js';
11
11
  import { describe, it, expect } from 'vitest';
12
12
 
13
13
  const testNumbers = {
@@ -0,0 +1,286 @@
1
+ import {
2
+ isDaylightSavingTime,
3
+ formatTimeOffset,
4
+ offsetTieBreaker,
5
+ findTimeDetails,
6
+ findTimeFromAreaCode,
7
+ findRegionFromRegionCode,
8
+ } from '../geo.js';
9
+ import { describe, it, expect } from 'vitest';
10
+
11
+ const invalidPhone = {
12
+ timezoneOffset: null,
13
+ daylightSavings: null,
14
+ stateHasMultipleTimezones: null,
15
+ areaCodeHasMultipleTimezones: null,
16
+ estimatedTime: false,
17
+ };
18
+
19
+ const seattlePhone = {
20
+ areaCodeHasMultipleTimezones: false,
21
+ daylightSavings: true,
22
+ estimatedTime: false,
23
+ isQuietHours: false,
24
+ isTCPAQuietHours: false,
25
+ localTime24Hour: '08:00:00',
26
+ localTimeReadable: '8:00:00 AM',
27
+ stateHasMultipleTimezones: false,
28
+ timezoneOffset: '-07:00',
29
+ state: {
30
+ name: 'Washington',
31
+ code: 'WA',
32
+ },
33
+ region: {
34
+ name: 'United States',
35
+ code: 'US',
36
+ flag: '🇺🇸',
37
+ },
38
+ };
39
+
40
+ const arizonaPhoneJul = {
41
+ areaCodeHasMultipleTimezones: false,
42
+ daylightSavings: true,
43
+ estimatedTime: true,
44
+ isQuietHours: false,
45
+ isTCPAQuietHours: false,
46
+ localTime24Hour: '09:00:00',
47
+ localTimeReadable: '9:00:00 AM',
48
+ stateHasMultipleTimezones: false,
49
+ timezoneOffset: '-06:00',
50
+ state: {
51
+ name: 'Arizona',
52
+ code: 'AZ',
53
+ },
54
+ region: {
55
+ name: 'United States',
56
+ code: 'US',
57
+ flag: '🇺🇸',
58
+ },
59
+ };
60
+
61
+ const arizonaPhoneDec = {
62
+ areaCodeHasMultipleTimezones: false,
63
+ daylightSavings: false,
64
+ estimatedTime: false,
65
+ isQuietHours: false,
66
+ isTCPAQuietHours: false,
67
+ localTime24Hour: '09:00:00',
68
+ localTimeReadable: '9:00:00 AM',
69
+ stateHasMultipleTimezones: false,
70
+ timezoneOffset: '-07:00',
71
+ state: {
72
+ name: 'Arizona',
73
+ code: 'AZ',
74
+ },
75
+ region: {
76
+ name: 'United States',
77
+ code: 'US',
78
+ flag: '🇺🇸',
79
+ },
80
+ };
81
+
82
+ const texasPhone = {
83
+ areaCodeHasMultipleTimezones: true,
84
+ daylightSavings: false,
85
+ estimatedTime: true,
86
+ isQuietHours: false,
87
+ isTCPAQuietHours: false,
88
+ localTime24Hour: '10:00:00',
89
+ localTimeReadable: '10:00:00 AM',
90
+ stateHasMultipleTimezones: true,
91
+ timezoneOffset: '-06:00',
92
+ state: {
93
+ name: 'Texas',
94
+ code: 'TX',
95
+ },
96
+ region: {
97
+ name: 'United States',
98
+ code: 'US',
99
+ flag: '🇺🇸',
100
+ },
101
+ };
102
+
103
+ const floridaPhone = {
104
+ areaCodeHasMultipleTimezones: false,
105
+ daylightSavings: false,
106
+ estimatedTime: false,
107
+ isQuietHours: false,
108
+ isTCPAQuietHours: false,
109
+ localTime24Hour: '10:00:00',
110
+ localTimeReadable: '10:00:00 AM',
111
+ stateHasMultipleTimezones: true,
112
+ timezoneOffset: '-06:00',
113
+ state: {
114
+ name: 'Florida',
115
+ code: 'FL',
116
+ },
117
+ region: {
118
+ name: 'United States',
119
+ code: 'US',
120
+ flag: '🇺🇸',
121
+ },
122
+ };
123
+
124
+ const hawaiiPhone = {
125
+ areaCodeHasMultipleTimezones: false,
126
+ daylightSavings: false,
127
+ estimatedTime: false,
128
+ isQuietHours: true,
129
+ isTCPAQuietHours: true,
130
+ localTime24Hour: '06:00:00',
131
+ localTimeReadable: '6:00:00 AM',
132
+ stateHasMultipleTimezones: false,
133
+ timezoneOffset: '-10:00',
134
+ state: {
135
+ name: 'Hawaii',
136
+ code: 'HI',
137
+ },
138
+ region: {
139
+ name: 'United States',
140
+ code: 'US',
141
+ flag: '🇺🇸',
142
+ },
143
+ };
144
+
145
+ const canadianPhone = {
146
+ areaCodeHasMultipleTimezones: true,
147
+ daylightSavings: true,
148
+ estimatedTime: true,
149
+ isQuietHours: false,
150
+ isCRTCQuietHours: false,
151
+ localTime24Hour: '10:00:00',
152
+ localTimeReadable: '10:00:00 AM',
153
+ stateHasMultipleTimezones: true,
154
+ timezoneOffset: '-05:00',
155
+ state: {
156
+ name: 'British Columbia',
157
+ code: 'BC',
158
+ },
159
+ region: {
160
+ name: 'Canada',
161
+ code: 'CA',
162
+ flag: '🇨🇦',
163
+ },
164
+ };
165
+
166
+ describe('Daylight Savings', () => {
167
+ it('should correctly be determined if the time given is or is not within daylight savings time', () => {
168
+ const daylightSavings = new Date('2024-07-15T12:00:00');
169
+ const notDaylightSavings = new Date('2024-12-15T12:00:00');
170
+
171
+ expect(isDaylightSavingTime(daylightSavings)).toBe(true);
172
+ expect(isDaylightSavingTime(notDaylightSavings)).toBe(false);
173
+ });
174
+ });
175
+
176
+ describe('Formatting time offset', () => {
177
+ it('return a proper ISO 8601 formatted date string, regardless of hour length', () => {
178
+ expect(formatTimeOffset('-8:00')).toBe('-08:00');
179
+ expect(formatTimeOffset('-12:00')).toBe('-12:00');
180
+ expect(formatTimeOffset('+7:00')).toBe('+07:00');
181
+ expect(formatTimeOffset('-3:30')).toBe('-03:30');
182
+ });
183
+ });
184
+
185
+ describe('Errs on the side of caution, minimizing the given time options to the more narrow', () => {
186
+ it('Late at night, the available options for morning should be later', () => {
187
+ expect(
188
+ offsetTieBreaker(['-8:00', '-7:00'], new Date('2024-07-15T23:00:00')),
189
+ ).toBe('-7:00');
190
+ });
191
+
192
+ it('Early in day, the available options for morning should be earlier', () => {
193
+ expect(
194
+ offsetTieBreaker(['-8:00', '-7:00'], new Date('2024-07-15T08:00:00')),
195
+ ).toBe('-8:00');
196
+ });
197
+ });
198
+
199
+ describe('Provides compliance quiet hours for any given region', () => {
200
+ it('Returns quiet hours booleans for standard US area', () => {
201
+ // US regions should have TCPA quiet hours but not CRTC quiet hours.
202
+ expect(
203
+ findTimeDetails('-08:00', new Date('2024-07-15T21:00:00'), 'California')
204
+ .isTCPAQuietHours,
205
+ ).toEqual(false);
206
+ expect(
207
+ findTimeDetails('-08:00', new Date('2024-07-15T22:00:00'), 'California')
208
+ .isTCPAQuietHours,
209
+ ).toEqual(true);
210
+ expect(
211
+ findTimeDetails('-08:00', new Date('2024-07-15T08:00:00'), 'California')
212
+ .isTCPAQuietHours,
213
+ ).toEqual(true);
214
+ expect(
215
+ findTimeDetails('-08:00', new Date('2024-07-15T08:00:00'), 'California')
216
+ .isCRTCQuietHours,
217
+ ).toEqual(undefined);
218
+ expect(
219
+ findTimeDetails('-08:00', new Date('2024-07-15T09:00:00'), 'California')
220
+ .isTCPAQuietHours,
221
+ ).toEqual(false);
222
+
223
+ // Alberta should have CRTC quiet hours but not TCPA quiet hours.
224
+ expect(
225
+ findTimeDetails('-07:00', new Date('2024-07-15T09:00:00'), 'Alberta')
226
+ .isTCPAQuietHours,
227
+ ).toEqual(undefined);
228
+ expect(
229
+ findTimeDetails('-07:00', new Date('2024-07-20T07:00:00'), 'Alberta')
230
+ .isCRTCQuietHours,
231
+ ).toEqual(true);
232
+
233
+ // Both should have an abstracted general quiet hours value.
234
+ expect(
235
+ findTimeDetails('-08:00', new Date('2024-07-15T08:00:00'), 'California')
236
+ .isQuietHours,
237
+ ).toEqual(true);
238
+ expect(
239
+ findTimeDetails('-07:00', new Date('2024-07-15T7:00:00'), 'Alberta')
240
+ .isQuietHours,
241
+ ).toEqual(true);
242
+ });
243
+ });
244
+
245
+ describe('Provides general time information for the given phone number (US and Canada only)', () => {
246
+ it('Returns general time information for a phone number region', () => {
247
+ expect(findTimeFromAreaCode(null, new Date('2024-07-15T08:00:00'))).toEqual(
248
+ invalidPhone,
249
+ );
250
+ expect(
251
+ findTimeFromAreaCode('206', new Date('2024-07-15T08:00:00')),
252
+ ).toEqual(seattlePhone);
253
+ expect(
254
+ findTimeFromAreaCode('928', new Date('2024-07-15T08:00:00')),
255
+ ).toEqual(arizonaPhoneJul);
256
+ expect(
257
+ findTimeFromAreaCode('928', new Date('2024-12-15T08:00:00')),
258
+ ).toEqual(arizonaPhoneDec);
259
+ expect(
260
+ findTimeFromAreaCode('432', new Date('2024-12-15T08:00:00')),
261
+ ).toEqual(texasPhone);
262
+ expect(
263
+ findTimeFromAreaCode('850', new Date('2024-12-15T08:00:00')),
264
+ ).toEqual(floridaPhone);
265
+ expect(
266
+ findTimeFromAreaCode('808', new Date('2024-12-15T08:00:00')),
267
+ ).toEqual(hawaiiPhone);
268
+ expect(
269
+ findTimeFromAreaCode('236', new Date('2024-07-20T08:00:00')),
270
+ ).toEqual(canadianPhone);
271
+ });
272
+ });
273
+
274
+ describe('Provides region name for a given region code', () => {
275
+ it('Returns region name', () => {
276
+ expect(findRegionFromRegionCode(1).name).toEqual('United States, Canada');
277
+ expect(findRegionFromRegionCode(1, 206).name).toEqual('United States');
278
+ expect(findRegionFromRegionCode(7).name).toEqual('Russia, Kazakhstan');
279
+ expect(findRegionFromRegionCode(20).name).toEqual('Egypt');
280
+ expect(findRegionFromRegionCode(27).name).toEqual('South Africa');
281
+ expect(findRegionFromRegionCode(30).name).toEqual('Greece');
282
+ expect(findRegionFromRegionCode(31).name).toEqual('Netherlands');
283
+ expect(findRegionFromRegionCode(32).name).toEqual('Belgium');
284
+ expect(findRegionFromRegionCode(33).name).toEqual('France');
285
+ });
286
+ });
@@ -21,6 +21,7 @@ export const AREA_CODE_LIST = [
21
21
  '223',
22
22
  '224',
23
23
  '225',
24
+ '257',
24
25
  '228',
25
26
  '229',
26
27
  '231',
@@ -89,6 +90,7 @@ export const AREA_CODE_LIST = [
89
90
  '350',
90
91
  '351',
91
92
  '352',
93
+ '357',
92
94
  '360',
93
95
  '361',
94
96
  '363',
@@ -134,17 +136,20 @@ export const AREA_CODE_LIST = [
134
136
  '447',
135
137
  '448',
136
138
  '450',
139
+ '457',
137
140
  '458',
138
141
  '463',
139
142
  '464',
140
143
  '469',
141
144
  '470',
145
+ '471',
142
146
  '472',
143
147
  '473',
144
148
  '475',
145
149
  '478',
146
150
  '479',
147
151
  '480',
152
+ '483',
148
153
  '484',
149
154
  '501',
150
155
  '502',
@@ -212,6 +217,7 @@ export const AREA_CODE_LIST = [
212
217
  '618',
213
218
  '619',
214
219
  '620',
220
+ '621',
215
221
  '623',
216
222
  '626',
217
223
  '628',
@@ -241,6 +247,7 @@ export const AREA_CODE_LIST = [
241
247
  '671',
242
248
  '672',
243
249
  '678',
250
+ '679',
244
251
  '680',
245
252
  '681',
246
253
  '682',
@@ -269,14 +276,18 @@ export const AREA_CODE_LIST = [
269
276
  '725',
270
277
  '726',
271
278
  '727',
279
+ '728',
280
+ '729',
272
281
  '731',
273
282
  '732',
274
283
  '734',
275
284
  '737',
285
+ '738',
276
286
  '740',
277
287
  '742',
278
288
  '743',
279
289
  '747',
290
+ '748',
280
291
  '754',
281
292
  '757',
282
293
  '758',
@@ -330,6 +341,7 @@ export const AREA_CODE_LIST = [
330
341
  '832',
331
342
  '833',
332
343
  '835',
344
+ '837',
333
345
  '838',
334
346
  '839',
335
347
  '840',
@@ -392,6 +404,7 @@ export const AREA_CODE_LIST = [
392
404
  '939',
393
405
  '940',
394
406
  '941',
407
+ '942',
395
408
  '943',
396
409
  '945',
397
410
  '947',
@@ -0,0 +1,31 @@
1
+ export const TCPA_QUIET_HOURS = {
2
+ start: 8,
3
+ end: 21,
4
+ };
5
+
6
+ export const CRTC_QUIET_HOURS = {
7
+ weekdays: {
8
+ start: 9,
9
+ end: 21.5,
10
+ },
11
+ weekends: {
12
+ start: 10,
13
+ end: 18,
14
+ },
15
+ };
16
+
17
+ export const CRTC_STATES = [
18
+ 'Alberta',
19
+ 'British Columbia',
20
+ 'Manitoba',
21
+ 'New Brunswick',
22
+ 'Newfoundland and Labrador',
23
+ 'Northwest Territories',
24
+ 'Nova Scotia',
25
+ 'Nunavut',
26
+ 'Ontario',
27
+ 'Nova Scotia and Prince Edward Island',
28
+ 'Quebec',
29
+ 'Saskatchewan',
30
+ 'Yukon',
31
+ ];
@@ -0,0 +1,21 @@
1
+ export const STATES_THAT_DONT_HAVE_DAYLIGHT_SAVINGS = [
2
+ 'Arizona',
3
+ 'Hawaii',
4
+ 'British Columbia',
5
+ 'Puerto Rico',
6
+ 'Saskatchewan',
7
+ 'Virgin Islands',
8
+ 'Yukon, Northwest Territories, and Nunavut',
9
+ ];
10
+
11
+ // Arizona does not follow Daylight Savings, but the Navajo Nation does. The
12
+ // 928 area code spans both Arizone (no DST) and Navajo Nation lands (DST).
13
+ export const AREA_CODES_WITH_MULTIPLE_DAYLIGHT_SAVINGS = {
14
+ 928: 'Arizona',
15
+ 236: 'British Columbia',
16
+ 250: 'British Columbia',
17
+ 257: 'British Columbia',
18
+ 672: 'British Columbia',
19
+ 778: 'British Columbia',
20
+ 306: 'Saskatchewan',
21
+ };
package/src/geo.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export function isDaylightSavingTime(date?: Date): boolean;
2
+
3
+ export function formatTimeOffset(offset: string): string;
4
+
5
+ export function offsetTieBreaker(timezones: string[], date: Date): string;
6
+
7
+ export function findTimeDetails(
8
+ offset: string,
9
+ date: Date,
10
+ state: string
11
+ ): {
12
+ localTimeReadable: string;
13
+ localTime24Hour: string;
14
+ isTCPAQuietHours?: boolean;
15
+ isCRTCQuietHours?: boolean;
16
+ isQuietHours: boolean;
17
+ };
18
+
19
+ export function findTimeFromAreaCode(
20
+ areaCode: string,
21
+ date?: Date
22
+ ): {
23
+ timezoneOffset: string | null;
24
+ daylightSavings: boolean | null;
25
+ stateHasMultipleTimezones: boolean | null;
26
+ state: string | null;
27
+ areaCodeHasMultipleTimezones: boolean | null;
28
+ estimatedTime: boolean;
29
+ };
30
+
31
+ export function findRegionFromRegionCode(regionCode: string): string | undefined;