diff --git a/changelog.md b/changelog.md index c65da4a..3b11747 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,36 @@ # Changelog +## v4.1.0 + +### New + +- Added `normalize` function + - This function strips out special characters and trims the phone number, much like uglify but skips non-digit characters + - Example: `normalize('555.444.3333 x 123') // => '5554443333x123'` vs `uglify('555.444.3333 x 123') // => 5554443333123` +- Added `validate` function + - This is a validation function, but works better for world wide phone numbers as well. Expects the full number + - Example: `333-444-5555` comes back valid but `444-5555` is invalid to this function +- Added `isValidWithFormat` function + - This takes a string phone number and a format string and validates the phone using the format + - It's also passed through the `validate` function for an extra step of validation +- Added `findSeparators` function + - A simple function that finds the separators in a phone number and returns them as an array +- Added `breakdownWithFormat` function + - Works a lot like `breakdown` but follows a strict format provided by the user to breakdown the number into an object + - This allows for a wider range of phone number support for breakdown + + +### Changed + +- `Phone-fns` is no longer dependant on `Kyanite` and is dependency free! +- `isValid` description to explain that it mostly focused on NANP numbers +- `breakdown` description to better explain that it's main focus is NANP numbers and its gachas +- We more than doubled our unit tests! Woo! + +### Chore + +- Renamed test files to `*.spec.js` instead of just `*.js` + ## v4.0.2 ### Fixed diff --git a/package-lock.json b/package-lock.json index cfe9a0e..07d2947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,13 @@ { "name": "phone-fns", - "version": "4.0.2", + "version": "4.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "phone-fns", - "version": "4.0.2", + "version": "4.1.0", "license": "MIT", - "dependencies": { - "kyanite": "3.1.0" - }, "devDependencies": { "@babel/core": "7.25.7", "@babel/preset-env": "7.25.7", @@ -20,7 +17,7 @@ "globby": "13.2.2", "jsdoc": "4.0.3", "npm-run-all": "4.1.5", - "pinet": "1.1.5", + "pinet": "1.2.0", "rollup": "4.24.0", "rollup-plugin-filesize": "10.0.0", "standard": "17.1.2", @@ -5356,9 +5353,9 @@ } }, "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", "dev": true, "engines": { "node": ">=12.0.0" @@ -6161,7 +6158,8 @@ "node_modules/kyanite": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/kyanite/-/kyanite-3.1.0.tgz", - "integrity": "sha512-5jXNGnnXxcHNbWgHjTD5/paRTp0Em4C6hU+jHNSGDtY7seeDlLJey480jpLIxkU2LbiZNY0QwS22TdqRTSNY9A==" + "integrity": "sha512-5jXNGnnXxcHNbWgHjTD5/paRTp0Em4C6hU+jHNSGDtY7seeDlLJey480jpLIxkU2LbiZNY0QwS22TdqRTSNY9A==", + "dev": true }, "node_modules/levn": { "version": "0.4.1", @@ -7417,27 +7415,21 @@ } }, "node_modules/pinet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pinet/-/pinet-1.1.5.tgz", - "integrity": "sha512-MtKVa/EZalAWq6xSDCVfXsczaKmz1zrI7zl3u2FhiT+5SyuEyw+XCOjnWdKnFxl2MfpT4pW005Dadbri0P8Hgw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pinet/-/pinet-1.2.0.tgz", + "integrity": "sha512-DKBh1ozkqgUpC1hoHLkKSDOgkB+rLF5IQ9sFLszMXmGAvbmvpLULG/CDdqhhHAn8pxFOWSN9ql/7f6QsTVdGug==", "dev": true, "dependencies": { "fs-extra": "11.2.0", - "highlight.js": "11.9.0", - "kyanite": "2.0.1", - "marked": "11.2.0" + "highlight.js": "11.10.0", + "kyanite": "3.1.0", + "marked": "14.1.2" } }, - "node_modules/pinet/node_modules/kyanite": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kyanite/-/kyanite-2.0.1.tgz", - "integrity": "sha512-Tr3pRIVvIfQl6DPrkBPV9PMditHLKhpTEAJYwq1p+/W/YpHGQ9E/jAW+tvW+h+ekoV+5xOkicdL1eYjDje0NyQ==", - "dev": true - }, "node_modules/pinet/node_modules/marked": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", - "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.2.tgz", + "integrity": "sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==", "dev": true, "bin": { "marked": "bin/marked.js" @@ -13382,9 +13374,9 @@ } }, "highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", "dev": true }, "hosted-git-info": { @@ -13959,7 +13951,8 @@ "kyanite": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/kyanite/-/kyanite-3.1.0.tgz", - "integrity": "sha512-5jXNGnnXxcHNbWgHjTD5/paRTp0Em4C6hU+jHNSGDtY7seeDlLJey480jpLIxkU2LbiZNY0QwS22TdqRTSNY9A==" + "integrity": "sha512-5jXNGnnXxcHNbWgHjTD5/paRTp0Em4C6hU+jHNSGDtY7seeDlLJey480jpLIxkU2LbiZNY0QwS22TdqRTSNY9A==", + "dev": true }, "levn": { "version": "0.4.1", @@ -14926,27 +14919,21 @@ "dev": true }, "pinet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pinet/-/pinet-1.1.5.tgz", - "integrity": "sha512-MtKVa/EZalAWq6xSDCVfXsczaKmz1zrI7zl3u2FhiT+5SyuEyw+XCOjnWdKnFxl2MfpT4pW005Dadbri0P8Hgw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pinet/-/pinet-1.2.0.tgz", + "integrity": "sha512-DKBh1ozkqgUpC1hoHLkKSDOgkB+rLF5IQ9sFLszMXmGAvbmvpLULG/CDdqhhHAn8pxFOWSN9ql/7f6QsTVdGug==", "dev": true, "requires": { "fs-extra": "11.2.0", - "highlight.js": "11.9.0", - "kyanite": "2.0.1", - "marked": "11.2.0" + "highlight.js": "11.10.0", + "kyanite": "3.1.0", + "marked": "14.1.2" }, "dependencies": { - "kyanite": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kyanite/-/kyanite-2.0.1.tgz", - "integrity": "sha512-Tr3pRIVvIfQl6DPrkBPV9PMditHLKhpTEAJYwq1p+/W/YpHGQ9E/jAW+tvW+h+ekoV+5xOkicdL1eYjDje0NyQ==", - "dev": true - }, "marked": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", - "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.2.tgz", + "integrity": "sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==", "dev": true } } diff --git a/package.json b/package.json index 9881b14..661b495 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phone-fns", - "version": "4.0.2", + "version": "4.1.0", "description": "A small, modern, and functional phone library for javascript", "main": "dist/phone-fns.min.js", "module": "src/index.js", @@ -11,10 +11,10 @@ "scripts": { "prepack": "npm-run-all --parallel docs scripts lint test --serial build", "scripts": "node scripts/create-export.js", - "docs": "node_modules/.bin/jsdoc -c jsdoc.json", + "docs": "jsdoc -c jsdoc.json", "build": "rollup -c", "lint": "standard src/**/*.js", - "test": "tape tests/*.js | tap-on" + "test": "tape tests/*.spec.js | tap-on" }, "exports": { ".": { @@ -35,7 +35,8 @@ "standard": { "ignore": [ "docs/*", - "dist/*" + "dist/*", + "types/*" ] }, "repository": { @@ -71,14 +72,11 @@ "globby": "13.2.2", "jsdoc": "4.0.3", "npm-run-all": "4.1.5", - "pinet": "1.1.5", + "pinet": "1.2.0", "rollup": "4.24.0", "rollup-plugin-filesize": "10.0.0", "standard": "17.1.2", "tap-on": "1.0.0", "tape": "5.9.0" - }, - "dependencies": { - "kyanite": "3.1.0" } } diff --git a/src/_internals/_hasPlaceholder.js b/src/_internals/_hasPlaceholder.js new file mode 100644 index 0000000..6f991b6 --- /dev/null +++ b/src/_internals/_hasPlaceholder.js @@ -0,0 +1,9 @@ +/** + * @private + * @function + * @param {String} str The string to check for placeholders + * @returns {Boolean} Whether or not the string has a placeholder + */ +export default function _hasPlaceholder (str) { + return str.includes('_') +} diff --git a/src/_internals/_uglifyFormats.js b/src/_internals/_uglifyFormats.js new file mode 100644 index 0000000..eb8fef1 --- /dev/null +++ b/src/_internals/_uglifyFormats.js @@ -0,0 +1,9 @@ +/** + * @private + * @function + * @param {String} str The string to strip special characters + * @returns {String} The newly created string with special characters stripped + */ +export default function _uglifyFormats (str) { + return str.replace(/[^a-wyz]/gi, '') +} diff --git a/src/breakdown.js b/src/breakdown.js index 850023d..e990551 100644 --- a/src/breakdown.js +++ b/src/breakdown.js @@ -6,7 +6,7 @@ import uglify from './uglify.js' * @function * @category Function * @sig String -> String -> Object - * @description Takes a provided phone string and breaks it down into an object of codes + * @description Takes a provided phone string and breaks it down into an object of codes only works loosely for NANP numbers. The gatcha here is that NANP numbers take the form of NXX NXX XXXX where N is a digit from 2-9 and X is a digit from 0-9, but in order to support placeholders we use a [_0-9]{3} check * @param {String} phone The phone number to breakdown * @return {Object} Returns an object of the broken down phone number * diff --git a/src/breakdownWithFormat.js b/src/breakdownWithFormat.js new file mode 100644 index 0000000..dccade8 --- /dev/null +++ b/src/breakdownWithFormat.js @@ -0,0 +1,67 @@ +import _curry2 from './_internals/_curry2.js' +import isValidWithFormat from './isValidWithFormat.js' + +/** + * @name breakdownWithFormat + * @since v4.1.0 + * @function + * @category Function + * @sig String -> String -> Object + * @description + * Breaks down a phone number based on a custom format provided and returns an object with the parts of the phone number + * C - Country Code A- Area Code L - Local Code N - Line Number X - Extension + * Does NOT work with placeholders + * @param {String} format The format to validate against + * @param {String} phone The phone number to breakdown + * @return {Object} Returns an object with the parts of the phone number + * @example + * import { breakdownWithFormat } from 'phone-fns' + * + * breakdownWithFormat('+C (AAA) LLL-NNNN xXXX', '+1-555-444-3333 x123') // => { countryCode: '1', areaCode: '555', localCode: '444', lineNumber: '3333', extension: '123' } + * breakdownWithFormat('AAA-LLL-NNNN', '010-XYZ-1234') // => Error: The phone number provided does not match the format provided or is an invalid phone number + * + * // it's also curried + * const fn = breakdownWithFormat('+C (AAA) LLL-NNNN xXXX') + * fn('+1-555-444-3333 x123') // => { countryCode: '', areaCode: '123', localCode: '456', lineNumber: '7890', extension: '' } + */ +function breakdownWithFormat (format, phone) { + if (!format) { + throw new Error('You must provide a format to breakdown') + } + + if (!isValidWithFormat(format, phone)) { + throw new Error('The phone number provided does not match the format provided or is an invalid phone number') + } + + const results = { + countryCode: '', + areaCode: '', + localCode: '', + lineNumber: '', + extension: '' + } + + for (let i = 0; i < format.length; i++) { + switch (format[i]) { + case 'C': + results.countryCode += phone[i] + break + case 'A': + results.areaCode += phone[i] + break + case 'N': + results.lineNumber += phone[i] + break + case 'L': + results.localCode += phone[i] + break + case 'X': + results.extension += phone[i] + break + } + } + + return results +} + +export default _curry2(breakdownWithFormat) diff --git a/src/findSeparators.js b/src/findSeparators.js new file mode 100644 index 0000000..f9c671b --- /dev/null +++ b/src/findSeparators.js @@ -0,0 +1,30 @@ +/** + * @name findSeparators + * @since v4.1.0 + * @function + * @category Function + * @sig String -> Array + * @description + * Finds a list of separators in a phone number string + * @param {String} phone The phone number to breakdown + * @return {Array} Returns an array of separators found in the phone number + * @example + * import { findSeparators } from 'phone-fns' + * + * findSeparators('123-456-7890') // => ['-'] + * findSeparators('123.456.7890') // => ['.'] + * findSeparators('123 456 7890') // => [' '] + * findSeparators('1234567890') // => [] + */ +function findSeparators (phone) { + const separators = ['-', '.', ' '] + const foundSeparators = [] + for (const separator of separators) { + if (phone.includes(separator)) { + foundSeparators.push(separator) + } + } + return foundSeparators +} + +export default findSeparators diff --git a/src/format.js b/src/format.js index 6c5b760..8fc598a 100644 --- a/src/format.js +++ b/src/format.js @@ -1,25 +1,8 @@ -import { - add, - addIndex, - both, - branch, - complement, - compose, - countBy, - eq, - gt, - identity, - includes, - length, - pipe, - reduce, - replace, - split, - toUpper -} from 'kyanite' import _curry2 from './_internals/_curry2.js' import isValid from './isValid.js' import uglify from './uglify.js' +import _uglifyFormats from './_internals/_uglifyFormats.js' +import _hasPlaceholder from './_internals/_hasPlaceholder.js' /** * @private @@ -27,16 +10,8 @@ import uglify from './uglify.js' * @param {String} layout The desired layout format * @param {String} phone The phone number to validate against */ -function validFormat (layout) { - return phone => { - const { N, C = 0 } = compose(countBy(toUpper), split(''), layout) - - return pipe([ - uglify, - length, - eq(add(N, C)) - ], phone) - } +function validFormat (layout, phone) { + return phone.length === _uglifyFormats(layout).length } /** @@ -66,22 +41,33 @@ function validFormat (layout) { * fn('(333) 444-5555') // => '333.444.5555' */ function format (layout, phone) { - const cCount = includes('C', layout) ? length(layout.match(/C/g)) : 0 - const _reduce = addIndex(reduce) + let cCount = 0 + const uglyPhone = uglify(phone) + + if (layout.includes('C')) { + cCount = (layout.match(/C/g) || []).length + } + + if (!validFormat(layout, uglyPhone)) { + return phone + } + + if (!_hasPlaceholder(uglyPhone)) { + // We are skipping validation of the phone number if there are placeholders + if (!isValid(phone)) { + return phone + } + } + + return uglyPhone.split('').reduce((acc, d, i) => { + if (cCount > i) { + acc = acc.replace(/C/i, d) + } else { + acc = acc.replace(/N/i, d) + } - return branch( - both( - complement(isValid), - complement(validFormat(layout)) - ), - identity, - pipe([ - uglify, - split(''), - _reduce((d, acc, i) => gt(i, cCount) ? replace(/C/i, d, acc) : replace(/N/i, d, acc), layout) - ]), - phone - ) + return acc + }, layout) } export default _curry2(format) diff --git a/src/isValid.js b/src/isValid.js index b76001c..97dfb39 100644 --- a/src/isValid.js +++ b/src/isValid.js @@ -1,8 +1,16 @@ -import { compose, when, F, reduced, eq, isEmpty, length, lt, pipe, test } from 'kyanite' - import breakdown from './breakdown.js' import uglify from './uglify.js' +/** + * @private + * @function + * @param {String} x The value to check if it is empty + * @returns {Boolean} Whether or not the value is empty + */ +function isEmpty (x) { + return x === '' +} + /** * @private * @function @@ -10,11 +18,10 @@ import uglify from './uglify.js' * @return {Boolean} Whether or not the phone passed validation */ function shortNumberTest (phone) { - return () => { - const { localCode, lineNumber } = breakdown(phone) + const { localCode, lineNumber } = breakdown(phone) + const str = localCode + lineNumber - return test(/^([0-9]{3})[-. ]?([0-9]{4})$/, localCode + lineNumber) - } + return /^([0-9]{3})[-. ]?([0-9]{4})$/.test(str) } /** @@ -24,11 +31,10 @@ function shortNumberTest (phone) { * @return {Boolean} Whether or not the phone passed validation */ function longNumberTest (phone) { - return () => { - const { areaCode, localCode, lineNumber } = breakdown(phone) + const { areaCode, localCode, lineNumber } = breakdown(phone) + const str = areaCode + localCode + lineNumber - return test(/^\+?([0-9]{2})\)?[-. ]?([0-9]{4})[-. ]?([0-9]{4})$/, areaCode + localCode + lineNumber) - } + return /^\+?([0-9]{2})\)?[-. ]?([0-9]{4})[-. ]?([0-9]{4})$/.test(str) } /** @@ -38,7 +44,8 @@ function longNumberTest (phone) { * @category Function * @sig String -> Boolean * @description - * Validates the base number, does not take the country code or extension into consideration for this validation + * Validates the base number, does not take the country code or extension into consideration for this validation. + * Focuses more on NANP numbers and their format * @param {String} phone The phone number to breakdown * @return {Boolean} Returns a boolean if the number provided is valid or not * @example @@ -47,13 +54,15 @@ function longNumberTest (phone) { */ export default function isValid (phone) { const uglyPhone = uglify(phone) - const done = compose(reduced) - - return pipe([ - when(isEmpty, done(F)), - length, - when(lt(7), done(F)), - when(eq(7), shortNumberTest(uglyPhone)), - longNumberTest(uglyPhone) - ], uglyPhone) + const len = uglyPhone.length + + if (isEmpty(uglyPhone) || len < 7) { + return false + } + + if (len === 7) { + return shortNumberTest(uglyPhone) + } + + return longNumberTest(uglyPhone) } diff --git a/src/isValidWithFormat.js b/src/isValidWithFormat.js new file mode 100644 index 0000000..9fe495d --- /dev/null +++ b/src/isValidWithFormat.js @@ -0,0 +1,44 @@ +import _curry2 from './_internals/_curry2.js' +import validate from './validate.js' + +/** + * @name isValidWithFormat + * @since v4.1.0 + * @function + * @category Function + * @sig String -> String -> Boolean + * @description + * Validates a phone number based on a custom format provided + * @param {String} format The format to validate against + * @param {String} phone The phone number to validate + * @return {Boolean} Returns a boolean if the number provided is valid or not + * @example + * import { isValidWithFormat } from 'phone-fns' + * + * isValidWithFormat('NNN-NNN-NNNN', '123-456-7890') // => true + * isValidWithFormat('NNN-NNN-NNNN', '010-XYZ-1234') // => false + * + * // It's also curried + * const fn = isValidWithFormat('NNN-NNN-NNNN') + * fn('123-456-7890') // => true + * fn('010-XYZ-1234') // => false + */ +function isValidWithFormat (format, phone) { + if (!format) { + throw new Error('You must provide a format to validate') + } + + if (phone.length !== format.length) { + return false + } + + for (let i = 0; i < format.length; i++) { + if (format[i] === 'N' && isNaN(phone[i])) { + return false + } + } + + return validate(phone) +} + +export default _curry2(isValidWithFormat) diff --git a/src/normalize.js b/src/normalize.js new file mode 100644 index 0000000..5b49cb0 --- /dev/null +++ b/src/normalize.js @@ -0,0 +1,24 @@ +/** + * @name normalize + * @since v4.1.0 + * @function + * @category Function + * @sig String -> String + * @description Strips all of the special characters from the given string but leaves extension and country code characters in place + * @param {String} phone The phone number to trim and strip down + * @return {String} Returns the newly created phone number string + * + * @example + * import { normalize } from 'phone-fns' + * + * normalize('555-444-3333') // => '5554443333' + * normalize('5554443333') // => '5554443333' + * normalize('555.444.3333 x 123') // => '5554443333x123' + */ +export default function normalize (phone) { + if (!phone) { + return '' + } + + return phone.replace(/[\s.\-()]/g, '').trim() +} diff --git a/src/uglify.js b/src/uglify.js index f1f2297..5956f1b 100644 --- a/src/uglify.js +++ b/src/uglify.js @@ -1,5 +1,3 @@ -import { replace, compose } from 'kyanite' - /** * @name uglify * @since v0.1.0 @@ -14,6 +12,8 @@ import { replace, compose } from 'kyanite' * uglify('555-444-3333') // => '5554443333' * uglify('5554443333') // => '5554443333' */ -const uglify = compose(replace(/[a-z]\w?|\W/gi, ''), String) +function uglify (phone) { + return String(phone).replace(/[a-z]\w?|\W/gi, '') +} export default uglify diff --git a/src/validate.js b/src/validate.js new file mode 100644 index 0000000..b4f68e0 --- /dev/null +++ b/src/validate.js @@ -0,0 +1,33 @@ +import normalize from './normalize.js' +import uglify from './uglify.js' + +/** + * @name validate + * @since v4.1.0 + * @function + * @category Function + * @sig String -> Boolean + * @description + * Validates the base number, strips out special characters and spaces upon validation, can handle country code and extension in the phone number + * @param {String} phone The phone number we want to validate + * @return {Boolean} Returns a boolean if the number provided is valid or not + * @example + * import { validate } from 'phone-fns' + * + * validate('555-444-3333') // => true + * validate('5555') // => false + * validate('5554443333') // => true + * validate('5554443333 x 123') // => true + */ +export default function validate (phone) { + const normPhone = normalize(String(phone)) + const phoneRegex = /^(\+?\d{1,4})?[\s\-.]?\(?\d{1,4}\)?[\s\-.]?\d{1,4}[\s\-.]?\d{1,4}[\s\-.]?\d{1,9}(?:[\s\-.]?(?:x|ext)?\d{1,5})?$/i + + // Validate the length of the number without the ext or country code symbols inlcuded + // (strips the + symbol and the ext/x symbols) + if (uglify(normPhone).length > 15 || uglify(normPhone).length < 10) { + return false + } + + return phoneRegex.test(normPhone) +} diff --git a/tests/breakdown.js b/tests/breakdown.spec.js similarity index 100% rename from tests/breakdown.js rename to tests/breakdown.spec.js diff --git a/tests/breakdownWithFormat.spec.js b/tests/breakdownWithFormat.spec.js new file mode 100644 index 0000000..9c6f1b3 --- /dev/null +++ b/tests/breakdownWithFormat.spec.js @@ -0,0 +1,65 @@ +import test from 'tape' +import breakdownWithFormat from '../src/breakdownWithFormat.js' + +test('breakdownWithFormat - valid input', (t) => { + const format = '+C (AAA) LLL-NNNN xXXX' + const phone = '+1 (555) 444-3333 x123' + const expected = { + countryCode: '1', + areaCode: '555', + localCode: '444', + lineNumber: '3333', + extension: '123' + } + + const result = breakdownWithFormat(format, phone) + t.same(result, expected, 'Should correctly breakdown the phone number with the given format') + t.end() +}) + +test('breakdownWithFormat - missing format', (t) => { + const phone = '+1 (555) 444-3333 x123' + + t.throws(() => breakdownWithFormat(null, phone), /You must provide a format to breakdown/, 'Should throw an error if format is missing') + t.end() +}) + +test('breakdownWithFormat - invalid phone number', (t) => { + const format = '+C (AAA) LLL-NNNN xXXX' + const phone = 'invalid phone number' + + t.throws(() => breakdownWithFormat(format, phone), /The phone number provided does not match the format provided or is an invalid phone number/, 'Should throw an error if phone number does not match the format') + t.end() +}) + +test('breakdownWithFormat - different format', (t) => { + const format = '+C-AAA-LLL-NNNN xXXX' + const phone = '+1-555-444-3333 x123' + const expected = { + countryCode: '1', + areaCode: '555', + localCode: '444', + lineNumber: '3333', + extension: '123' + } + + const result = breakdownWithFormat(format, phone) + t.same(result, expected, 'Should correctly breakdown the phone number with a different format') + t.end() +}) + +test('breakdownWithFormat - no extension', (t) => { + const format = '+C (AAA) LLL-NNNN' + const phone = '+1 (555) 444-3333' + const expected = { + countryCode: '1', + areaCode: '555', + localCode: '444', + lineNumber: '3333', + extension: '' + } + + const result = breakdownWithFormat(format, phone) + t.same(result, expected, 'Should correctly breakdown the phone number without an extension') + t.end() +}) diff --git a/tests/findSeparators.spec.js b/tests/findSeparators.spec.js new file mode 100644 index 0000000..391155b --- /dev/null +++ b/tests/findSeparators.spec.js @@ -0,0 +1,31 @@ +import test from 'tape' +import findSeparators from '../src/findSeparators.js' // Adjust the path as necessary + +test('findSeparators - single separator', t => { + t.same(findSeparators('123-456-7890'), ['-'], 'Should return ["-"] for "123-456-7890"') + t.same(findSeparators('123.456.7890'), ['.'], 'Should return ["."] for "123.456.7890"') + t.same(findSeparators('123 456 7890'), [' '], 'Should return [" "] for "123 456 7890"') + t.end() +}) + +test('findSeparators - multiple separators', t => { + t.same(findSeparators('123-456 7890'), ['-', ' '], 'Should return ["-", " "] for "123-456 7890"') + t.same(findSeparators('123.456 7890'), ['.', ' '], 'Should return [".", " "] for "123.456 7890"') + t.same(findSeparators('123-456.7890'), ['-', '.'], 'Should return ["-", "."] for "123-456.7890"') + t.end() +}) + +test('findSeparators - no separators', t => { + t.same(findSeparators('1234567890'), [], 'Should return [] for "1234567890"') + t.end() +}) + +test('findSeparators - all separators', t => { + t.same(findSeparators('123-456.789 0'), ['-', '.', ' '], 'Should return ["-", ".", " "] for "123-456.789 0"') + t.end() +}) + +test('findSeparators - empty string', t => { + t.same(findSeparators(''), [], 'Should return [] for an empty string') + t.end() +}) diff --git a/tests/format.js b/tests/format.spec.js similarity index 91% rename from tests/format.js rename to tests/format.spec.js index 7ed0d0a..21a2cdc 100644 --- a/tests/format.js +++ b/tests/format.spec.js @@ -100,6 +100,13 @@ test('Catches letters when passed in', t => { t.end() }) +test('Handles non NANP formats', t => { + const results = format('NNN NNN NN NN NN', '046123456789') + + t.same(results, '046 123 45 67 89') + t.end() +}) + test('Supports Placeholder characters', t => { const fn = format('NNN-NNN-NNNN') diff --git a/tests/isValid.js b/tests/isValid.spec.js similarity index 66% rename from tests/isValid.js rename to tests/isValid.spec.js index dde53b8..ec8a267 100644 --- a/tests/isValid.js +++ b/tests/isValid.spec.js @@ -19,6 +19,18 @@ test('Test Country Code', t => { t.end() }) +test('Test extension', t => { + t.ok(isValid('555-444-3333 ext 123'), 'Handles extension') + t.ok(isValid('555-444-3333 x 123'), 'Handles extension with x') + t.end() +}) + +test('Test unordinary phone numbers', t => { + t.ok(isValid('046 123 456 789'), 'Handles spaces') + t.ok(isValid('046 123 45 67 89'), 'Handles spaces with less numbers') + t.end() +}) + test('Test invalid type', t => { t.notOk(isValid('555444666'), 'Handles invalid length') t.notOk(isValid('(555)-444-666'), 'Handles invalid length') diff --git a/tests/isValidWithFormat.spec.js b/tests/isValidWithFormat.spec.js new file mode 100644 index 0000000..0e7d652 --- /dev/null +++ b/tests/isValidWithFormat.spec.js @@ -0,0 +1,32 @@ +import test from 'tape' +import isValidWithFormat from '../src/isValidWithFormat.js' + +test('isValidWithFormat', (t) => { + t.same(isValidWithFormat('NNNNNNNNNN', '1234567890'), true, 'Valid phone number with correct format') + t.same(isValidWithFormat('NNN-NNN-NNNN', '123-456-7890'), true, 'Valid phone number with dashes in correct format') + t.same(isValidWithFormat('NNN-NNN-NNNN', '1234567890'), false, 'Invalid phone number without dashes for dashed format') + t.same(isValidWithFormat('NNNNNNNNNN', '123-456-7890'), false, 'Invalid phone number with dashes for non-dashed format') + t.same(isValidWithFormat('NNNNNNNNNN', '123456789'), false, 'Invalid phone number with incorrect length') + t.same(isValidWithFormat('NNN-NNNN-NNNN', '010-1234--5678'), false, 'Invalid phone number with extra dashes') + t.same(isValidWithFormat('NNNNNNNNNN', 'abcdefghij'), false, 'Invalid phone number with letters') + t.same(isValidWithFormat('NNN-NNN-NNNN', '010-XYZ-1234'), false, 'Invalid phone number with letters') + t.end() +}) + +test('isValidWithFormat with country code and extension', (t) => { + t.same(isValidWithFormat('+1 NNN-NNN-NNNN', '+1 234-567-1890'), true, 'Valid phone number with country code') + t.same(isValidWithFormat('+1 NNN-NNN-NNNN x NNN', '+1 234-567-1890 x 123'), true, 'Valid phone number with country code and extension') + t.end() +}) + +test('isValidWithFormat curried', (t) => { + const fn = isValidWithFormat('NNN-NNN-NNNN') + t.same(fn('123-456-7890'), true, 'Valid phone number with curried function') + t.same(fn('1234567890'), false, 'Invalid phone number with curried function') + t.end() +}) + +test('isValidWithFormat - missing format', (t) => { + t.throws(() => isValidWithFormat(null, '123-456-7890'), /You must provide a format to validate/, 'Should throw an error if format is missing') + t.end() +}) diff --git a/tests/main.js b/tests/main.spec.js similarity index 100% rename from tests/main.js rename to tests/main.spec.js diff --git a/tests/normalize.spec.js b/tests/normalize.spec.js new file mode 100644 index 0000000..e0cfca3 --- /dev/null +++ b/tests/normalize.spec.js @@ -0,0 +1,29 @@ +import test from 'tape' +import normalize from '../src/normalize.js' + +test('normalize removes spaces, dots, dashes, and parentheses', (t) => { + t.same(normalize('123 456 7890'), '1234567890', 'should remove spaces') + t.same(normalize('123.456.7890'), '1234567890', 'should remove dots') + t.same(normalize('123-456-7890'), '1234567890', 'should remove dashes') + t.same(normalize('(123) 456-7890'), '1234567890', 'should remove parentheses') + t.same(normalize('(123) 456-7890 x123'), '1234567890x123', 'should handle extension') + t.same(normalize('(123) 456-7890 x 123'), '1234567890x123', 'should handle extension') + t.end() +}) + +test('normalize trims the input', (t) => { + t.same(normalize(' 1234567890 '), '1234567890', 'should trim leading and trailing spaces') + t.same(normalize('\t1234567890\t'), '1234567890', 'should trim leading and trailing tabs') + t.end() +}) + +test('normalize handles mixed characters', (t) => { + t.same(normalize(' (123) 456-7890. '), '1234567890', 'should remove mixed characters and trim') + t.end() +}) + +test('normalize handles empty and null input', (t) => { + t.same(normalize(''), '', 'should handle empty string') + t.same(normalize(null), '', 'should handle null input') + t.end() +}) diff --git a/tests/uglify.js b/tests/uglify.spec.js similarity index 62% rename from tests/uglify.js rename to tests/uglify.spec.js index 0c4fae7..bc29b3c 100644 --- a/tests/uglify.js +++ b/tests/uglify.spec.js @@ -14,8 +14,8 @@ test('Handles when numbers are thrown at it', t => { t.end() }) -// test('Uglify placeholders', t => { -// console.log(uglify('__________')) -// t.same(uglify('__________'), '') -// t.end() -// }) +test('Handles when out of ordinary phone numbers are thrown at it', t => { + t.same(uglify('046 123 456 789'), '046123456789') + t.same(uglify('046 123 45 67 89'), '046123456789') + t.end() +}) diff --git a/tests/validate.spec.js b/tests/validate.spec.js new file mode 100644 index 0000000..0440715 --- /dev/null +++ b/tests/validate.spec.js @@ -0,0 +1,58 @@ +import test from 'tape' +import validate from '../src/validate.js' + +test('Test simple type', t => { + t.ok(validate('555-444-3333'), 'Results returned back ok') + t.ok(validate('4445556666')) + t.ok(validate(4445556666)) + t.end() +}) + +test('Test complex type', t => { + t.ok(validate('(555) 444 3333'), 'Results returned back ok') + t.end() +}) + +test('Test handles wide range of phone numbers', t => { + const validPhoneNumbers = [ + '010.1234.5678', // Number with dots + '10 9876 5432', // South Korea number without country code + '+82 2 3456 7890', // South Korea number with country code + '+82 10 9876 5432', // South Korea mobile number with country code + '+1 (555) 444-3333', // US number with country code + '+44 20 7946 0958', // UK landline with country code + '07911 123456', // UK mobile (domestic) + '+49 30 12345678', // German number with country code + '+1 800 123 4567 x123', // US toll-free number with extension + '+61 2 1234 5678', // Australian number with country code, + '+39 02 1234 5678', // Italian number with country code + '+91 12345 67890' // Indian number with country code + ] + + validPhoneNumbers.forEach(phone => { + t.ok(validate(phone), `Valid phone number: ${phone}`) + }) + t.end() +}) + +test('Handles Invalid range of phone numbers', t => { + const invalidPhoneNumbers = [ + '123', // Too short + '010-1234', // Missing last part of mobile number + '02-3456', // Incomplete landline number + '02-3456-789', // Incomplete landline number (should be 7 digits) + '+82 10', // Too short, incomplete mobile number + '+82 02-1234', // Incomplete landline number + '010-XYZ-1234', // Invalid characters (letters in the mobile number) + '12345', // Too short + 'abcd-efgh-ijkl', // Completely invalid characters + '+44 (0)20 7946', // Incomplete UK number + '+1-555-abc-1234', // Invalid characters + '+12345678901234567890' // Too long + ] + + invalidPhoneNumbers.forEach(phone => { + t.notOk(validate(phone), `Invalid phone number: ${phone}`) + }) + t.end() +}) diff --git a/types/index.d.cts b/types/index.d.cts index 0c43194..f8c0536 100644 --- a/types/index.d.cts +++ b/types/index.d.cts @@ -2,39 +2,68 @@ // Project: Phone-Fns // Definitions by: Dustin Hershman -declare let phoneFns: phoneFns.Static; +declare let phoneFns: phoneFns.Static declare namespace phoneFns { interface Breakdown { - areaCode: string; - localCode: string; - lineNumber: string; - extension: string; + countryCode?: string + areaCode: string + localCode: string + lineNumber: string + extension: string } interface Static { /** * Allows you to format phone numbers however you desire using N as number placeholders and C as country code placeholders these placeholders are case insensitive */ - format(layout: string, phone: string): string; - format(layout: string): (phone: string) => string; + format(layout: string, phone: string): string + format(layout: string): (phone: string) => string /** * Takes a provided phone string and breaks it down into an object of codes */ - breakdown(phone: string): Breakdown; + breakdown(phone: string): Breakdown + + /** + * Breaks down a phone number based on a custom format provided and returns an object with the parts of the phone number + * C - Country Code A- Area Code L - Local Code N - Line Number X - Extension + */ + breakdownWithFormat(format: string, phone: string): Breakdown + breakdownWithFormat(format: string): (phone: string) => Breakdown /** * Validates the base number, does not take the country code or extension into consideration for this validation */ - isValid(phone: string): boolean; + isValid(phone: string): boolean + + /** + * Validates a phone number based on a custom format provided + */ + isValidWithFormat(format: string, phone: string): boolean + isValidWithFormat(format: string): (phone: string) => boolean + + /** + * Finds a list of separators in a phone number string + */ + findSeparators(phone: string): string[] + + /** + * Strips all of the special characters from the given string but leaves extension and country code characters in place + */ + normalize(phone: string): string /** * Strips all of the special characters from the given string */ - uglify(phone: string | number): string; + uglify(phone: string | number): string + + /** + * Validates the base number, strips out special characters and spaces upon validation, can handle country code and extension in the phone number + */ + validate(phone: string): boolean } } -export = phoneFns; -export as namespace phoneFns; +export = phoneFns +export as namespace phoneFns diff --git a/types/index.d.ts b/types/index.d.ts index 0c43194..7d66433 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,40 +1,68 @@ // Type definitions for Phone-Fns // Project: Phone-Fns // Definitions by: Dustin Hershman - -declare let phoneFns: phoneFns.Static; +declare let phoneFns: phoneFns.Static declare namespace phoneFns { interface Breakdown { - areaCode: string; - localCode: string; - lineNumber: string; - extension: string; + countryCode?: string + areaCode: string + localCode: string + lineNumber: string + extension: string } interface Static { /** * Allows you to format phone numbers however you desire using N as number placeholders and C as country code placeholders these placeholders are case insensitive */ - format(layout: string, phone: string): string; - format(layout: string): (phone: string) => string; + format(layout: string, phone: string): string + format(layout: string): (phone: string) => string /** * Takes a provided phone string and breaks it down into an object of codes */ - breakdown(phone: string): Breakdown; + breakdown(phone: string): Breakdown + + /** + * Breaks down a phone number based on a custom format provided and returns an object with the parts of the phone number + * C - Country Code A- Area Code L - Local Code N - Line Number X - Extension + */ + breakdownWithFormat(format: string, phone: string): Breakdown + breakdownWithFormat(format: string): (phone: string) => Breakdown + + /** + * Finds a list of separators in a phone number string + */ + findSeparators(phone: string): string[] /** * Validates the base number, does not take the country code or extension into consideration for this validation */ - isValid(phone: string): boolean; + isValid(phone: string): boolean + + /** + * Validates a phone number based on a custom format provided + */ + isValidWithFormat(format: string, phone: string): boolean + isValidWithFormat(format: string): (phone: string) => boolean + + /** + * Strips all of the special characters from the given string but leaves extension and country code characters in place + */ + normalize(phone: string): string /** * Strips all of the special characters from the given string */ - uglify(phone: string | number): string; + uglify(phone: string | number): string + + /** + * Validates the base number, strips out special characters and spaces upon validation, can handle country code and extension in the phone number + */ + validate(phone: string): boolean } } -export = phoneFns; -export as namespace phoneFns; +export = phoneFns +export as namespace phoneFns