@sprucelabs/schema 32.1.63 → 32.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.
@@ -1,3 +1,3 @@
1
1
  export declare function isValidNumber(number: string): boolean;
2
- export default function formatPhoneNumber(val: string, shouldFailSilently?: boolean): string;
2
+ export default function formatPhoneNumber(value: string, shouldFailSilently?: boolean): string;
3
3
  export declare function isDummyNumber(phone: string): boolean;
@@ -1,69 +1,124 @@
1
- function formatNumberWithCode(phoneNumberString, code = '1') {
2
- if (phoneNumberString.match(/[a-zA-Z]/g)) {
3
- return null;
4
- }
5
- const cleaned = ('' + phoneNumberString).replace(/\D/g, '');
6
- // Dynamically insert the country code into the regex
7
- let regexPattern = new RegExp(`^(${code})?(\\d{3})(\\d{0,3})(\\d{0,4})$`);
8
- let match = cleaned.match(regexPattern);
9
- if (match) {
10
- let intlCode = match[1] ? `+${match[1]} ` : `+${code} `; // Use the matched code or the provided code
11
- const divider = code === '1' ? '-' : ' ';
12
- const number = [
13
- intlCode,
14
- '',
15
- match[2],
16
- divider,
17
- match[3],
18
- divider,
19
- match[4],
20
- ]
21
- .join('')
22
- .replace(/-+$/, '');
23
- return number;
24
- }
25
- return null;
26
- }
1
+ var _a;
2
+ const LETTER_PATTERN = /[a-zA-Z]/;
3
+ const DEFAULT_CODE_SEPARATOR = ' ';
4
+ const COUNTRY_FORMATS = [
5
+ {
6
+ code: '92',
7
+ groupSizes: [4, 7],
8
+ groupSeparator: ' ',
9
+ validDigits: [9, 10, 11],
10
+ detect: ({ digits, hasExplicitPlus }) => digits.startsWith('92') && (hasExplicitPlus || digits.length > 10),
11
+ },
12
+ {
13
+ code: '90',
14
+ groupSizes: [3, 3, 4],
15
+ groupSeparator: ' ',
16
+ validDigits: [10],
17
+ detect: ({ digits, hasExplicitPlus }) => digits.startsWith('90') && (hasExplicitPlus || digits.length > 10),
18
+ },
19
+ {
20
+ code: '49',
21
+ groupSizes: [3, 3, 4],
22
+ groupSeparator: ' ',
23
+ validDigits: [10],
24
+ detect: ({ digits, hasExplicitPlus }) => digits.startsWith('49') && (hasExplicitPlus || digits.length > 10),
25
+ },
26
+ {
27
+ code: '1',
28
+ groupSizes: [3, 3, 4],
29
+ groupSeparator: '-',
30
+ validDigits: [10],
31
+ },
32
+ ];
33
+ const DEFAULT_COUNTRY = (_a = COUNTRY_FORMATS.find((format) => format.code === '1')) !== null && _a !== void 0 ? _a : COUNTRY_FORMATS[0];
27
34
  export function isValidNumber(number) {
28
35
  var _a;
29
- const { code } = stripCode(number);
30
- const formatted = (_a = formatNumberWithCode(number, code)) === null || _a === void 0 ? void 0 : _a.replace(/[^0-9]/g, '');
31
- return (formatted === null || formatted === void 0 ? void 0 : formatted.length) === 11 || (formatted === null || formatted === void 0 ? void 0 : formatted.length) === 12;
32
- }
33
- function stripCode(number) {
34
- let code = `1`; // Default to North American country code
35
- const cleaned = number.replace(/(?!^\+)[^\d]/g, '');
36
- // Explicitly check for '+' sign to distinguish international codes
37
- if (cleaned.startsWith('+90') ||
38
- (cleaned.startsWith('90') && cleaned.length > 10)) {
39
- code = `90`;
36
+ if (LETTER_PATTERN.test(number)) {
37
+ return false;
40
38
  }
41
- else if (cleaned.startsWith('+49') ||
42
- (cleaned.startsWith('49') && cleaned.length > 10)) {
43
- code = `49`;
39
+ const parsed = parseInput(number);
40
+ const countryFormat = detectCountryFormat(parsed);
41
+ const localDigits = stripCountryCodeFromDigits(parsed.digits, countryFormat.code);
42
+ if (!localDigits.length) {
43
+ return false;
44
44
  }
45
- // Adjust the condition to ensure that '905...' numbers without a '+' are treated as North American
46
- else if (cleaned.startsWith('905') && cleaned.length === 10) {
47
- code = `1`;
45
+ if ((_a = countryFormat.validDigits) === null || _a === void 0 ? void 0 : _a.length) {
46
+ return countryFormat.validDigits.includes(localDigits.length);
48
47
  }
49
- return { code, phoneWithoutCode: cleaned.replace('+' + code, '') };
48
+ return true;
50
49
  }
51
- export default function formatPhoneNumber(val, shouldFailSilently = true) {
52
- const { code, phoneWithoutCode } = stripCode(val);
53
- const formatted = formatNumberWithCode(phoneWithoutCode, code);
54
- if (!formatted) {
50
+ export default function formatPhoneNumber(value, shouldFailSilently = true) {
51
+ var _a;
52
+ if (LETTER_PATTERN.test(value)) {
55
53
  if (!shouldFailSilently) {
56
54
  throw new Error('INVALID_PHONE_NUMBER');
57
55
  }
58
- else {
59
- return val;
56
+ return value;
57
+ }
58
+ const parsed = parseInput(value);
59
+ const countryFormat = detectCountryFormat(parsed);
60
+ const localDigits = stripCountryCodeFromDigits(parsed.digits, countryFormat.code);
61
+ if (!localDigits.length) {
62
+ if (!shouldFailSilently) {
63
+ throw new Error('INVALID_PHONE_NUMBER');
60
64
  }
65
+ return value;
61
66
  }
62
- // remove trailing spaces
63
- const cleaned = formatted.replace(/\s+$/, '');
64
- return cleaned;
67
+ const formattedLocal = formatLocalDigits(localDigits, countryFormat);
68
+ const codeSeparator = (_a = countryFormat.codeSeparator) !== null && _a !== void 0 ? _a : DEFAULT_CODE_SEPARATOR;
69
+ const formatted = formattedLocal
70
+ ? `+${countryFormat.code}${codeSeparator}${formattedLocal}`
71
+ : `+${countryFormat.code}`;
72
+ return formatted.trim();
65
73
  }
66
74
  export function isDummyNumber(phone) {
67
75
  const cleanedValue = phone.replace(/\D/g, '');
68
76
  return cleanedValue.startsWith('1555') || cleanedValue.startsWith('555');
69
77
  }
78
+ function parseInput(original) {
79
+ const digits = original.replace(/\D/g, '');
80
+ const hasExplicitPlus = original.trim().startsWith('+');
81
+ return {
82
+ original,
83
+ digits,
84
+ hasExplicitPlus,
85
+ };
86
+ }
87
+ function detectCountryFormat(input) {
88
+ if (!input.digits.length) {
89
+ return DEFAULT_COUNTRY;
90
+ }
91
+ if (input.hasExplicitPlus) {
92
+ const explicitMatch = COUNTRY_FORMATS.find((format) => input.digits.startsWith(format.code));
93
+ if (explicitMatch) {
94
+ return explicitMatch;
95
+ }
96
+ }
97
+ const detected = COUNTRY_FORMATS.find((format) => format.detect ? format.detect(input) : false);
98
+ return detected !== null && detected !== void 0 ? detected : DEFAULT_COUNTRY;
99
+ }
100
+ function stripCountryCodeFromDigits(digits, code) {
101
+ if (digits.startsWith(code)) {
102
+ return digits.slice(code.length);
103
+ }
104
+ return digits;
105
+ }
106
+ function formatLocalDigits(digits, format) {
107
+ if (!digits.length) {
108
+ return '';
109
+ }
110
+ const parts = [];
111
+ let index = 0;
112
+ for (const size of format.groupSizes) {
113
+ if (index >= digits.length) {
114
+ break;
115
+ }
116
+ const nextIndex = Math.min(index + size, digits.length);
117
+ parts.push(digits.slice(index, nextIndex));
118
+ index = nextIndex;
119
+ }
120
+ if (index < digits.length) {
121
+ parts.push(digits.slice(index));
122
+ }
123
+ return parts.join(format.groupSeparator);
124
+ }
@@ -1,3 +1,3 @@
1
1
  export declare function isValidNumber(number: string): boolean;
2
- export default function formatPhoneNumber(val: string, shouldFailSilently?: boolean): string;
2
+ export default function formatPhoneNumber(value: string, shouldFailSilently?: boolean): string;
3
3
  export declare function isDummyNumber(phone: string): boolean;
@@ -3,71 +3,124 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isValidNumber = isValidNumber;
4
4
  exports.default = formatPhoneNumber;
5
5
  exports.isDummyNumber = isDummyNumber;
6
- function formatNumberWithCode(phoneNumberString, code = '1') {
7
- if (phoneNumberString.match(/[a-zA-Z]/g)) {
8
- return null;
9
- }
10
- const cleaned = ('' + phoneNumberString).replace(/\D/g, '');
11
- // Dynamically insert the country code into the regex
12
- let regexPattern = new RegExp(`^(${code})?(\\d{3})(\\d{0,3})(\\d{0,4})$`);
13
- let match = cleaned.match(regexPattern);
14
- if (match) {
15
- let intlCode = match[1] ? `+${match[1]} ` : `+${code} `; // Use the matched code or the provided code
16
- const divider = code === '1' ? '-' : ' ';
17
- const number = [
18
- intlCode,
19
- '',
20
- match[2],
21
- divider,
22
- match[3],
23
- divider,
24
- match[4],
25
- ]
26
- .join('')
27
- .replace(/-+$/, '');
28
- return number;
29
- }
30
- return null;
31
- }
6
+ const LETTER_PATTERN = /[a-zA-Z]/;
7
+ const DEFAULT_CODE_SEPARATOR = ' ';
8
+ const COUNTRY_FORMATS = [
9
+ {
10
+ code: '92',
11
+ groupSizes: [4, 7],
12
+ groupSeparator: ' ',
13
+ validDigits: [9, 10, 11],
14
+ detect: ({ digits, hasExplicitPlus }) => digits.startsWith('92') && (hasExplicitPlus || digits.length > 10),
15
+ },
16
+ {
17
+ code: '90',
18
+ groupSizes: [3, 3, 4],
19
+ groupSeparator: ' ',
20
+ validDigits: [10],
21
+ detect: ({ digits, hasExplicitPlus }) => digits.startsWith('90') && (hasExplicitPlus || digits.length > 10),
22
+ },
23
+ {
24
+ code: '49',
25
+ groupSizes: [3, 3, 4],
26
+ groupSeparator: ' ',
27
+ validDigits: [10],
28
+ detect: ({ digits, hasExplicitPlus }) => digits.startsWith('49') && (hasExplicitPlus || digits.length > 10),
29
+ },
30
+ {
31
+ code: '1',
32
+ groupSizes: [3, 3, 4],
33
+ groupSeparator: '-',
34
+ validDigits: [10],
35
+ },
36
+ ];
37
+ const DEFAULT_COUNTRY = COUNTRY_FORMATS.find((format) => format.code === '1') ?? COUNTRY_FORMATS[0];
32
38
  function isValidNumber(number) {
33
- const { code } = stripCode(number);
34
- const formatted = formatNumberWithCode(number, code)?.replace(/[^0-9]/g, '');
35
- return formatted?.length === 11 || formatted?.length === 12;
36
- }
37
- function stripCode(number) {
38
- let code = `1`; // Default to North American country code
39
- const cleaned = number.replace(/(?!^\+)[^\d]/g, '');
40
- // Explicitly check for '+' sign to distinguish international codes
41
- if (cleaned.startsWith('+90') ||
42
- (cleaned.startsWith('90') && cleaned.length > 10)) {
43
- code = `90`;
39
+ if (LETTER_PATTERN.test(number)) {
40
+ return false;
44
41
  }
45
- else if (cleaned.startsWith('+49') ||
46
- (cleaned.startsWith('49') && cleaned.length > 10)) {
47
- code = `49`;
42
+ const parsed = parseInput(number);
43
+ const countryFormat = detectCountryFormat(parsed);
44
+ const localDigits = stripCountryCodeFromDigits(parsed.digits, countryFormat.code);
45
+ if (!localDigits.length) {
46
+ return false;
48
47
  }
49
- // Adjust the condition to ensure that '905...' numbers without a '+' are treated as North American
50
- else if (cleaned.startsWith('905') && cleaned.length === 10) {
51
- code = `1`;
48
+ if (countryFormat.validDigits?.length) {
49
+ return countryFormat.validDigits.includes(localDigits.length);
52
50
  }
53
- return { code, phoneWithoutCode: cleaned.replace('+' + code, '') };
51
+ return true;
54
52
  }
55
- function formatPhoneNumber(val, shouldFailSilently = true) {
56
- const { code, phoneWithoutCode } = stripCode(val);
57
- const formatted = formatNumberWithCode(phoneWithoutCode, code);
58
- if (!formatted) {
53
+ function formatPhoneNumber(value, shouldFailSilently = true) {
54
+ if (LETTER_PATTERN.test(value)) {
59
55
  if (!shouldFailSilently) {
60
56
  throw new Error('INVALID_PHONE_NUMBER');
61
57
  }
62
- else {
63
- return val;
58
+ return value;
59
+ }
60
+ const parsed = parseInput(value);
61
+ const countryFormat = detectCountryFormat(parsed);
62
+ const localDigits = stripCountryCodeFromDigits(parsed.digits, countryFormat.code);
63
+ if (!localDigits.length) {
64
+ if (!shouldFailSilently) {
65
+ throw new Error('INVALID_PHONE_NUMBER');
64
66
  }
67
+ return value;
65
68
  }
66
- // remove trailing spaces
67
- const cleaned = formatted.replace(/\s+$/, '');
68
- return cleaned;
69
+ const formattedLocal = formatLocalDigits(localDigits, countryFormat);
70
+ const codeSeparator = countryFormat.codeSeparator ?? DEFAULT_CODE_SEPARATOR;
71
+ const formatted = formattedLocal
72
+ ? `+${countryFormat.code}${codeSeparator}${formattedLocal}`
73
+ : `+${countryFormat.code}`;
74
+ return formatted.trim();
69
75
  }
70
76
  function isDummyNumber(phone) {
71
77
  const cleanedValue = phone.replace(/\D/g, '');
72
78
  return cleanedValue.startsWith('1555') || cleanedValue.startsWith('555');
73
79
  }
80
+ function parseInput(original) {
81
+ const digits = original.replace(/\D/g, '');
82
+ const hasExplicitPlus = original.trim().startsWith('+');
83
+ return {
84
+ original,
85
+ digits,
86
+ hasExplicitPlus,
87
+ };
88
+ }
89
+ function detectCountryFormat(input) {
90
+ if (!input.digits.length) {
91
+ return DEFAULT_COUNTRY;
92
+ }
93
+ if (input.hasExplicitPlus) {
94
+ const explicitMatch = COUNTRY_FORMATS.find((format) => input.digits.startsWith(format.code));
95
+ if (explicitMatch) {
96
+ return explicitMatch;
97
+ }
98
+ }
99
+ const detected = COUNTRY_FORMATS.find((format) => format.detect ? format.detect(input) : false);
100
+ return detected ?? DEFAULT_COUNTRY;
101
+ }
102
+ function stripCountryCodeFromDigits(digits, code) {
103
+ if (digits.startsWith(code)) {
104
+ return digits.slice(code.length);
105
+ }
106
+ return digits;
107
+ }
108
+ function formatLocalDigits(digits, format) {
109
+ if (!digits.length) {
110
+ return '';
111
+ }
112
+ const parts = [];
113
+ let index = 0;
114
+ for (const size of format.groupSizes) {
115
+ if (index >= digits.length) {
116
+ break;
117
+ }
118
+ const nextIndex = Math.min(index + size, digits.length);
119
+ parts.push(digits.slice(index, nextIndex));
120
+ index = nextIndex;
121
+ }
122
+ if (index < digits.length) {
123
+ parts.push(digits.slice(index));
124
+ }
125
+ return parts.join(format.groupSeparator);
126
+ }
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "!build/__tests__",
9
9
  "esm"
10
10
  ],
11
- "version": "32.1.63",
11
+ "version": "32.2.0",
12
12
  "main": "./build/index.js",
13
13
  "types": "./build/index.d.ts",
14
14
  "module": "./build/esm/index.js",
@@ -58,15 +58,15 @@
58
58
  "build.copy-files": "mkdir -p build && rsync -avzq --exclude='*.ts' ./src/ ./build/"
59
59
  },
60
60
  "dependencies": {
61
- "@sprucelabs/error": "^7.1.30",
62
- "@sprucelabs/test-utils": "^6.0.99",
61
+ "@sprucelabs/error": "^7.1.31",
62
+ "@sprucelabs/test-utils": "^6.0.100",
63
63
  "email-validator": "^2.0.4",
64
64
  "just-safe-get": "^4.2.0",
65
65
  "just-safe-set": "^4.2.1"
66
66
  },
67
67
  "devDependencies": {
68
- "@sprucelabs/esm-postbuild": "^7.0.42",
69
- "@sprucelabs/jest-json-reporter": "^9.0.63",
68
+ "@sprucelabs/esm-postbuild": "^7.0.43",
69
+ "@sprucelabs/jest-json-reporter": "^9.0.64",
70
70
  "@sprucelabs/resolve-path-aliases": "^3.0.28",
71
71
  "@sprucelabs/semantic-release": "^5.0.2",
72
72
  "@sprucelabs/test": "^10.0.24",