├── .c8rc.json ├── src ├── _internals │ ├── _hasPlaceholder.js │ ├── _uglifyFormats.js │ └── _curry2.js ├── index.js ├── uglify.js ├── normalize.js ├── findSeparators.js ├── validate.js ├── breakdown.js ├── isValidWithFormat.js ├── isValid.js ├── format.js └── breakdownWithFormat.js ├── .babelrc ├── codecov.yml ├── scripts ├── custom-tags.js └── create-export.js ├── .github ├── ISSUE_TEMPLATE │ ├── documentation.md │ ├── bug_report.md │ └── function_proposal.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── phone-fns.yml │ └── codeql-analysis.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── types ├── tsconfig.json ├── index.d.ts └── index.d.cts ├── tests ├── uglify.spec.js ├── main.spec.js ├── normalize.spec.js ├── findSeparators.spec.js ├── isValid.spec.js ├── isValidWithFormat.spec.js ├── breakdown.spec.js ├── breakdownWithFormat.spec.js ├── validate.spec.js └── format.spec.js ├── LICENSE ├── jsdoc.json ├── rollup.config.js ├── package.json ├── README.md ├── .gitignore ├── .npmignore └── changelog.md /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "src/_internals", 4 | "tests" 5 | ], 6 | "reporter": [ 7 | "lcov" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/_internals/_hasPlaceholder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | * @function 4 | * @param {String} str The string to check for placeholders 5 | * @returns {Boolean} Whether or not the string has a placeholder 6 | */ 7 | export default function _hasPlaceholder (str) { 8 | return str.includes('_') 9 | } 10 | -------------------------------------------------------------------------------- /src/_internals/_uglifyFormats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | * @function 4 | * @param {String} str The string to strip special characters 5 | * @returns {String} The newly created string with special characters stripped 6 | */ 7 | export default function _uglifyFormats (str) { 8 | return str.replace(/[^a-wyz]/gi, '') 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions", 9 | "ie >= 9" 10 | ] 11 | }, 12 | "modules": false 13 | } 14 | ] 15 | ], 16 | "exclude": "node_modules/**" 17 | } 18 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | after_n_builds: 1 5 | 6 | coverage: 7 | status: 8 | project: 9 | default: 10 | target: 80% 11 | patch: 12 | default: 13 | target: 80% 14 | 15 | comment: 16 | layout: "reach, diff, flags, files" 17 | behavior: default 18 | -------------------------------------------------------------------------------- /scripts/custom-tags.js: -------------------------------------------------------------------------------- 1 | exports.defineTags = function (dictionary) { 2 | dictionary.defineTag('category', { 3 | mustHaveValue: true, 4 | onTagged (doclet, tag) { 5 | doclet.category = tag.value 6 | } 7 | }) 8 | dictionary.defineTag('sig', { 9 | mustHaveValue: true, 10 | onTagged (doclet, tag) { 11 | doclet.sig = tag.value.split(/\r/g) 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Report an issue with the Docs or suggest an improvement 4 | 5 | --- 6 | 7 | **Function in Question** 8 | 9 | Which Function(s) (if any) is in question? 10 | 11 | **What it is Currently** 12 | 13 | What is the documentation you want to change currently? 14 | 15 | **What it is expected to be** 16 | 17 | Give some context about what you'd expect or want to see instead 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as breakdown } from './breakdown.js' 2 | export { default as breakdownWithFormat } from './breakdownWithFormat.js' 3 | export { default as findSeparators } from './findSeparators.js' 4 | export { default as format } from './format.js' 5 | export { default as isValid } from './isValid.js' 6 | export { default as isValidWithFormat } from './isValidWithFormat.js' 7 | export { default as normalize } from './normalize.js' 8 | export { default as uglify } from './uglify.js' 9 | export { default as validate } from './validate.js' 10 | -------------------------------------------------------------------------------- /src/_internals/_curry2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an optimized internal curry function for 2 param functions 3 | * @private 4 | * @category Function 5 | * @param {Function} fn The function to curry 6 | * @return {Function} The curried function 7 | */ 8 | function _curry2 (fn) { 9 | return function f2 (a, b) { 10 | if (!arguments.length) { 11 | return f2 12 | } 13 | 14 | if (arguments.length === 1) { 15 | return function (_b) { 16 | return fn(a, _b) 17 | } 18 | } 19 | 20 | return fn(a, b) 21 | } 22 | } 23 | 24 | export default _curry2 25 | -------------------------------------------------------------------------------- /src/uglify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name uglify 3 | * @since v0.1.0 4 | * @function 5 | * @category Function 6 | * @sig String -> String 7 | * @description Strips all of the special characters from the given string 8 | * @param {String} phone The phone number to trim and strip down 9 | * @return {String} Returns the newly created phone number string 10 | * 11 | * @example 12 | * uglify('555-444-3333') // => '5554443333' 13 | * uglify('5554443333') // => '5554443333' 14 | */ 15 | function uglify (phone) { 16 | return String(phone).replace(/[a-z]\w?|\W/gi, '') 17 | } 18 | 19 | export default uglify 20 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ 5 | "es6", 6 | "dom" 7 | ], 8 | "declaration": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": false, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": false, 13 | "baseUrl": "../", 14 | "typeRoots": [ 15 | "../" 16 | ], 17 | "types": [], 18 | "noEmit": true, 19 | "forceConsistentCasingInFileNames": true 20 | }, 21 | "include": ["../index.d.ts", "./*.ts"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /scripts/create-export.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import { globby } from 'globby' 4 | 5 | const globFiles = ['src/**/*.js', '!src/index.js', '!src/common.js', '!src/_internals'] 6 | const files = await globby(globFiles) 7 | 8 | const buildRes = () => 9 | files.sort().map(f => { 10 | const { base, name } = path.parse(f) 11 | 12 | return `export { default as ${name} } from './${base}'` 13 | }).join('\n') 14 | 15 | // Build Exports 16 | fs.writeFile('src/index.js', `${buildRes('standard')}\n`) 17 | .then(() => console.log('Finished Writing Exports... Wrapping up...')) 18 | .catch(console.error) 19 | -------------------------------------------------------------------------------- /tests/uglify.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import uglify from '../src/uglify.js' 3 | 4 | test('Test simple type', t => { 5 | const results = uglify('555-444-3333') 6 | 7 | t.ok(results, 'Results returned back ok') 8 | t.is(results, '5554443333', 'results came back valid') 9 | t.end() 10 | }) 11 | 12 | test('Handles when numbers are thrown at it', t => { 13 | t.same(uglify(5556667777), '5556667777') 14 | t.end() 15 | }) 16 | 17 | test('Handles when out of ordinary phone numbers are thrown at it', t => { 18 | t.same(uglify('046 123 456 789'), '046123456789') 19 | t.same(uglify('046 123 45 67 89'), '046123456789') 20 | t.end() 21 | }) 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Pass it '...' 14 | 3. See error 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Desktop (please complete the following information):** 20 | - OS: [e.g. iOS] 21 | - Browser [e.g. chrome, safari, node] 22 | 23 | **Screenshots** 24 | If applicable/needed, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. (If none needed feel free to remove this section) 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/function_proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Function Proposal 3 | about: Suggest a new function idea for the 4 | 5 | --- 6 | 7 | ## Function Name 8 | 9 | **Signature(s):** 10 | `If possible leave the signature type here too` 11 | 12 | **Type:** `What is the data type this function works with` 13 | 14 | **Leave a description of the purpose of your function here** 15 | 16 | ### Why 17 | 18 | **Why are you suggesting this new function?** 19 | 20 | ### Syntax 21 | 22 | **Parameters:** 23 | `a` - `type`: 24 | 25 | ```js 26 | // What does your function syntax look like 27 | ``` 28 | 29 | ### Usage 30 | 31 | ```js 32 | // Give some usage examples of your function 33 | ``` 34 | 35 | ### Additional Info (Optional) 36 | 37 | **Leave any extra info & Comments here if needed** 38 | -------------------------------------------------------------------------------- /src/normalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name normalize 3 | * @since v4.1.0 4 | * @function 5 | * @category Function 6 | * @sig String -> String 7 | * @description Strips all of the special characters from the given string but leaves extension and country code characters in place 8 | * @param {String} phone The phone number to trim and strip down 9 | * @return {String} Returns the newly created phone number string 10 | * 11 | * @example 12 | * import { normalize } from 'phone-fns' 13 | * 14 | * normalize('555-444-3333') // => '5554443333' 15 | * normalize('5554443333') // => '5554443333' 16 | * normalize('555.444.3333 x 123') // => '5554443333x123' 17 | */ 18 | export default function normalize (phone) { 19 | if (!phone) { 20 | return '' 21 | } 22 | 23 | return phone.replace(/[\s.\-()]/g, '').trim() 24 | } 25 | -------------------------------------------------------------------------------- /tests/main.spec.js: -------------------------------------------------------------------------------- 1 | import { breakdown, format, uglify, isValid } from '../src/index.js' 2 | import test from 'tape' 3 | 4 | test('Able to run breakdown without providing country code to instance', t => { 5 | const { areaCode, localCode, lineNumber } = breakdown('4445556666') 6 | 7 | t.is(areaCode, '444') 8 | t.is(localCode, '555') 9 | t.is(lineNumber, '6666') 10 | t.end() 11 | }) 12 | 13 | test('Able to run format without providing country code to instance', t => { 14 | t.is(format('NNN-NNN-NNNN', '4445556666'), '444-555-6666') 15 | t.end() 16 | }) 17 | 18 | test('Able to run isValid from instance', t => { 19 | t.true(isValid('4445556666')) 20 | t.false(isValid('334445555')) 21 | t.end() 22 | }) 23 | 24 | test('Able to run uglify from instance', t => { 25 | const results = uglify('(555) 444-3333') 26 | 27 | t.is(results, '5554443333') 28 | t.end() 29 | }) 30 | -------------------------------------------------------------------------------- /src/findSeparators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name findSeparators 3 | * @since v4.1.0 4 | * @function 5 | * @category Function 6 | * @sig String -> Array 7 | * @description 8 | * Finds a list of separators in a phone number string 9 | * @param {String} phone The phone number to breakdown 10 | * @return {Array} Returns an array of separators found in the phone number 11 | * @example 12 | * import { findSeparators } from 'phone-fns' 13 | * 14 | * findSeparators('123-456-7890') // => ['-'] 15 | * findSeparators('123.456.7890') // => ['.'] 16 | * findSeparators('123 456 7890') // => [' '] 17 | * findSeparators('1234567890') // => [] 18 | */ 19 | function findSeparators (phone) { 20 | const separators = ['-', '.', ' '] 21 | const foundSeparators = [] 22 | for (const separator of separators) { 23 | if (phone.includes(separator)) { 24 | foundSeparators.push(separator) 25 | } 26 | } 27 | return foundSeparators 28 | } 29 | 30 | export default findSeparators 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## :page_facing_up: Description 2 | 3 | > Short summary of the change 4 | 5 | ## :squid: Type of Change 6 | 7 | - [ ] Bug Fix 8 | - [ ] Chore 9 | - [ ] New Feature 10 | - [ ] Breaking Change 11 | 12 | ## :clipboard: PR Checklist 13 | [//]: # (Fill this out if you're adding a new feature if something isn't applicable, just check it) 14 | 15 | - [ ] This pull request has a descriptive title and information useful to a reviewer. 16 | - [ ] All tests are passing 17 | - [ ] Changelog Updated Properly 18 | - [ ] Types updated/added as needed 19 | - [ ] Has new/updated tests 20 | 21 | 22 | ## :memo: Changes 23 | 24 | [//]: # (Please delete any sections you do not use) 25 | ### :rotating_light: Breaking Changes 26 | 27 | - List of breaking changes if any 28 | 29 | ### :nail_care: New 30 | 31 | - List of new changes if any 32 | 33 | ### :confetti_ball: Enhanced 34 | 35 | - List of enhanced changes if any 36 | 37 | ### :wrench: Fixed 38 | 39 | - List of bug or functionality fixes if any 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dustin Hershman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/validate.js: -------------------------------------------------------------------------------- 1 | import normalize from './normalize.js' 2 | import uglify from './uglify.js' 3 | 4 | /** 5 | * @name validate 6 | * @since v4.1.0 7 | * @function 8 | * @category Function 9 | * @sig String -> Boolean 10 | * @description 11 | * Validates the base number, strips out special characters and spaces upon validation, can handle country code and extension in the phone number 12 | * @param {String} phone The phone number we want to validate 13 | * @return {Boolean} Returns a boolean if the number provided is valid or not 14 | * @example 15 | * import { validate } from 'phone-fns' 16 | * 17 | * validate('555-444-3333') // => true 18 | * validate('5555') // => false 19 | * validate('5554443333') // => true 20 | * validate('5554443333 x 123') // => true 21 | */ 22 | export default function validate (phone) { 23 | const normPhone = normalize(String(phone)) 24 | 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 25 | 26 | // Validate the length of the number without the ext or country code symbols inlcuded 27 | // (strips the + symbol and the ext/x symbols) 28 | if (uglify(normPhone).length > 15 || uglify(normPhone).length < 10) { 29 | return false 30 | } 31 | 32 | return phoneRegex.test(normPhone) 33 | } 34 | -------------------------------------------------------------------------------- /tests/normalize.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import normalize from '../src/normalize.js' 3 | 4 | test('normalize removes spaces, dots, dashes, and parentheses', (t) => { 5 | t.same(normalize('123 456 7890'), '1234567890', 'should remove spaces') 6 | t.same(normalize('123.456.7890'), '1234567890', 'should remove dots') 7 | t.same(normalize('123-456-7890'), '1234567890', 'should remove dashes') 8 | t.same(normalize('(123) 456-7890'), '1234567890', 'should remove parentheses') 9 | t.same(normalize('(123) 456-7890 x123'), '1234567890x123', 'should handle extension') 10 | t.same(normalize('(123) 456-7890 x 123'), '1234567890x123', 'should handle extension') 11 | t.end() 12 | }) 13 | 14 | test('normalize trims the input', (t) => { 15 | t.same(normalize(' 1234567890 '), '1234567890', 'should trim leading and trailing spaces') 16 | t.same(normalize('\t1234567890\t'), '1234567890', 'should trim leading and trailing tabs') 17 | t.end() 18 | }) 19 | 20 | test('normalize handles mixed characters', (t) => { 21 | t.same(normalize(' (123) 456-7890. '), '1234567890', 'should remove mixed characters and trim') 22 | t.end() 23 | }) 24 | 25 | test('normalize handles empty and null input', (t) => { 26 | t.same(normalize(''), '', 'should handle empty string') 27 | t.same(normalize(null), '', 'should handle null input') 28 | t.end() 29 | }) 30 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "source": { 6 | "includePattern": ".js$", 7 | "include": [ 8 | "src/", 9 | "package.json", 10 | "./README.md" 11 | ] 12 | }, 13 | "sourceType": "module", 14 | "plugins": [ 15 | "node_modules/pinet/tags.js" 16 | ], 17 | "opts": { 18 | "encoding": "utf8", 19 | "template": "node_modules/pinet", 20 | "destination": "./docs/", 21 | "recurse": true, 22 | "verbose": false 23 | }, 24 | "markdown": { 25 | "parser": "gfm", 26 | "hardwrap": true, 27 | "idInHeadings": true 28 | }, 29 | "templates": { 30 | "cleverLinks": false, 31 | "monospaceLinks": false, 32 | "default": { 33 | "outputSourceFiles": true, 34 | "includeDate": false 35 | } 36 | }, 37 | "pinet": { 38 | "title": "Phone-Fns - Documentation", 39 | "links": [ 40 | { 41 | "name": "Github", 42 | "link": "https://github.com/dhershman1/phone-fns" 43 | } 44 | ], 45 | "genSources": true, 46 | "changelog": "./CHANGELOG.md", 47 | "lang": "en", 48 | "customTags": [ 49 | "category", 50 | "signature" 51 | ], 52 | "meta": [ 53 | { 54 | "name": "viewport", 55 | "content": "width=device-width, initial-scale=1.0" 56 | }, 57 | { 58 | "charset": "UTF-8" 59 | } 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/findSeparators.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import findSeparators from '../src/findSeparators.js' // Adjust the path as necessary 3 | 4 | test('findSeparators - single separator', t => { 5 | t.same(findSeparators('123-456-7890'), ['-'], 'Should return ["-"] for "123-456-7890"') 6 | t.same(findSeparators('123.456.7890'), ['.'], 'Should return ["."] for "123.456.7890"') 7 | t.same(findSeparators('123 456 7890'), [' '], 'Should return [" "] for "123 456 7890"') 8 | t.end() 9 | }) 10 | 11 | test('findSeparators - multiple separators', t => { 12 | t.same(findSeparators('123-456 7890'), ['-', ' '], 'Should return ["-", " "] for "123-456 7890"') 13 | t.same(findSeparators('123.456 7890'), ['.', ' '], 'Should return [".", " "] for "123.456 7890"') 14 | t.same(findSeparators('123-456.7890'), ['-', '.'], 'Should return ["-", "."] for "123-456.7890"') 15 | t.end() 16 | }) 17 | 18 | test('findSeparators - no separators', t => { 19 | t.same(findSeparators('1234567890'), [], 'Should return [] for "1234567890"') 20 | t.end() 21 | }) 22 | 23 | test('findSeparators - all separators', t => { 24 | t.same(findSeparators('123-456.789 0'), ['-', '.', ' '], 'Should return ["-", ".", " "] for "123-456.789 0"') 25 | t.end() 26 | }) 27 | 28 | test('findSeparators - empty string', t => { 29 | t.same(findSeparators(''), [], 'Should return [] for an empty string') 30 | t.end() 31 | }) 32 | -------------------------------------------------------------------------------- /tests/isValid.spec.js: -------------------------------------------------------------------------------- 1 | import isValid from '../src/isValid.js' 2 | import test from 'tape' 3 | 4 | test('Test simple type', t => { 5 | t.ok(isValid('555-444-3333'), 'Results returned back ok') 6 | t.ok(isValid('4445556666')) 7 | t.ok(isValid(4445556666)) 8 | t.end() 9 | }) 10 | 11 | test('Test complex type', t => { 12 | t.ok(isValid('(555) 444 3333'), 'Results returned back ok') 13 | t.end() 14 | }) 15 | 16 | test('Test Country Code', t => { 17 | t.ok(isValid('1 (555) 444 3333'), 'Handles country code') 18 | t.ok(isValid('+1 (555) 444 3333'), 'Handles country code with extra symbols') 19 | t.end() 20 | }) 21 | 22 | test('Test extension', t => { 23 | t.ok(isValid('555-444-3333 ext 123'), 'Handles extension') 24 | t.ok(isValid('555-444-3333 x 123'), 'Handles extension with x') 25 | t.end() 26 | }) 27 | 28 | test('Test unordinary phone numbers', t => { 29 | t.ok(isValid('046 123 456 789'), 'Handles spaces') 30 | t.ok(isValid('046 123 45 67 89'), 'Handles spaces with less numbers') 31 | t.end() 32 | }) 33 | 34 | test('Test invalid type', t => { 35 | t.notOk(isValid('555444666'), 'Handles invalid length') 36 | t.notOk(isValid('(555)-444-666'), 'Handles invalid length') 37 | t.notOk(isValid('89965'), 'Results returned back false') 38 | t.notOk(isValid(''), 'Handles empty string') 39 | t.notOk(isValid('555-444-666a'), 'Handles invalid characters') 40 | t.end() 41 | }) 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import filesize from 'rollup-plugin-filesize' 3 | import terser from '@rollup/plugin-terser' 4 | import { nodeResolve } from '@rollup/plugin-node-resolve' 5 | 6 | export default [{ 7 | input: './src/index.js', 8 | plugins: [ 9 | babel({ babelHelpers: 'bundled', presets: [['@babel/preset-env', { targets: { ie: '11' } }]] }), 10 | terser(), 11 | filesize({ 12 | showMinifiedSize: false 13 | }) 14 | ], 15 | external: [ 16 | 'kyanite' 17 | ], 18 | output: { 19 | file: 'dist/phone-fns.min.js', 20 | format: 'es', 21 | name: 'phoneFns' 22 | } 23 | }, { 24 | input: './src/index.js', 25 | plugins: [ 26 | babel({ babelHelpers: 'bundled', presets: [['@babel/preset-env', { targets: { ie: '11' } }]] }), 27 | terser(), 28 | filesize({ 29 | showMinifiedSize: false 30 | }) 31 | ], 32 | external: [ 33 | 'kyanite' 34 | ], 35 | output: { 36 | file: 'dist/phone-fns.min.cjs', 37 | format: 'cjs', 38 | name: 'phoneFns' 39 | } 40 | }, { 41 | input: './src/index.js', 42 | plugins: [ 43 | babel({ babelHelpers: 'bundled', presets: [['@babel/preset-env', { targets: { ie: '11' } }]] }), 44 | terser(), 45 | nodeResolve({ browser: true }), 46 | filesize({ 47 | showMinifiedSize: false 48 | }) 49 | ], 50 | output: { 51 | file: 'dist/phone-fns.iife.min.js', 52 | format: 'iife', 53 | name: 'phoneFns' 54 | } 55 | }] 56 | -------------------------------------------------------------------------------- /src/breakdown.js: -------------------------------------------------------------------------------- 1 | import uglify from './uglify.js' 2 | 3 | /** 4 | * @name breakdown 5 | * @since v0.1.0 6 | * @function 7 | * @category Function 8 | * @sig String -> String -> Object 9 | * @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 10 | * @param {String} phone The phone number to breakdown 11 | * @return {Object} Returns an object of the broken down phone number 12 | * 13 | * @example 14 | * breakdown('111-222-3333'); 15 | * // => { areaCode: '111', localCode: '222', lineNumber: '3333', extension: '' } 16 | * 17 | * breakdown('5554441111'); 18 | * // => { areaCode: '555', localCode: '444', lineNumber: '1111', extension: '' } 19 | * 20 | * breakdown('555-444-3333 x 8989'); 21 | * // => { areaCode: '555', localCode: '444', lineNumber: '3333', extension: '8989' } 22 | * 23 | * // Works with placeholder syntax 24 | * breakdown('555-___-____') 25 | * // => { areaCode: '555', localCode: '___', lineNumber: '____', extension: '' } 26 | */ 27 | export default function breakdown (phone) { 28 | const [, areaCode, localCode, lineNumber, extension = ''] = uglify(phone) 29 | .match(/([_0-9]{3})?([_0-9]{3})([_0-9]{4})([_0-9]{1,})?/) 30 | 31 | return { 32 | areaCode, 33 | localCode, 34 | lineNumber, 35 | extension 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/isValidWithFormat.js: -------------------------------------------------------------------------------- 1 | import _curry2 from './_internals/_curry2.js' 2 | import validate from './validate.js' 3 | 4 | /** 5 | * @name isValidWithFormat 6 | * @since v4.1.0 7 | * @function 8 | * @category Function 9 | * @sig String -> String -> Boolean 10 | * @description 11 | * Validates a phone number based on a custom format provided 12 | * @param {String} format The format to validate against 13 | * @param {String} phone The phone number to validate 14 | * @return {Boolean} Returns a boolean if the number provided is valid or not 15 | * @example 16 | * import { isValidWithFormat } from 'phone-fns' 17 | * 18 | * isValidWithFormat('NNN-NNN-NNNN', '123-456-7890') // => true 19 | * isValidWithFormat('NNN-NNN-NNNN', '010-XYZ-1234') // => false 20 | * isValidWithFormat('NNN-NNN-NNNN', '1234567890') // => false 21 | * 22 | * // It's also curried 23 | * const fn = isValidWithFormat('NNN-NNN-NNNN') 24 | * 25 | * fn('123-456-7890') // => true 26 | * fn('010-XYZ-1234') // => false 27 | * fn('1234567890') // => false 28 | */ 29 | function isValidWithFormat (format, phone) { 30 | if (!format) { 31 | throw new Error('You must provide a format to validate') 32 | } 33 | 34 | if (phone.length !== format.length) { 35 | return false 36 | } 37 | 38 | for (let i = 0; i < format.length; i++) { 39 | if (format[i] === 'N' && isNaN(phone[i])) { 40 | return false 41 | } 42 | } 43 | 44 | return validate(phone) 45 | } 46 | 47 | export default _curry2(isValidWithFormat) 48 | -------------------------------------------------------------------------------- /src/isValid.js: -------------------------------------------------------------------------------- 1 | import breakdown from './breakdown.js' 2 | import uglify from './uglify.js' 3 | 4 | /** 5 | * @private 6 | * @function 7 | * @param {String} phone The ugly formatted phone number to test 8 | * @return {Boolean} Whether or not the phone passed validation 9 | */ 10 | function shortNumberTest (phone) { 11 | const { localCode, lineNumber } = breakdown(phone) 12 | const str = localCode + lineNumber 13 | 14 | return /^([0-9]{3})[-. ]?([0-9]{4})$/.test(str) 15 | } 16 | 17 | /** 18 | * @private 19 | * @function 20 | * @param {String} phone The ugly formatted phone number to test 21 | * @return {Boolean} Whether or not the phone passed validation 22 | */ 23 | function longNumberTest (phone) { 24 | const { areaCode, localCode, lineNumber } = breakdown(phone) 25 | const str = areaCode + localCode + lineNumber 26 | 27 | return /^\+?([0-9]{2})\)?[-. ]?([0-9]{4})[-. ]?([0-9]{4})$/.test(str) 28 | } 29 | 30 | /** 31 | * @name isValid 32 | * @deprecated Use isValidWithFormat instead 33 | * @since v0.1.0 34 | * @function 35 | * @category Function 36 | * @sig String -> Boolean 37 | * @description 38 | * Validates the base number, does not take the country code or extension into consideration for this validation. 39 | * Focuses more on NANP numbers and their format 40 | * @param {String} phone The phone number to breakdown 41 | * @return {Boolean} Returns a boolean if the number provided is valid or not 42 | * @example 43 | * isValid('555-444-3333') // => true 44 | * isValid('5555') // => false 45 | */ 46 | export default function isValid (phone) { 47 | const uglyPhone = uglify(phone) 48 | const len = uglyPhone.length 49 | 50 | if (len < 7) { 51 | return false 52 | } 53 | 54 | if (len === 7) { 55 | return shortNumberTest(uglyPhone) 56 | } 57 | 58 | return longNumberTest(uglyPhone) 59 | } 60 | -------------------------------------------------------------------------------- /tests/isValidWithFormat.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import isValidWithFormat from '../src/isValidWithFormat.js' 3 | 4 | test('isValidWithFormat', (t) => { 5 | t.same(isValidWithFormat('NNNNNNNNNN', '1234567890'), true, 'Valid phone number with correct format') 6 | t.same(isValidWithFormat('NNN-NNN-NNNN', '123-456-7890'), true, 'Valid phone number with dashes in correct format') 7 | t.same(isValidWithFormat('NNN-NNN-NNNN', '1234567890'), false, 'Invalid phone number without dashes for dashed format') 8 | t.same(isValidWithFormat('NNNNNNNNNN', '123-456-7890'), false, 'Invalid phone number with dashes for non-dashed format') 9 | t.same(isValidWithFormat('NNNNNNNNNN', '123456789'), false, 'Invalid phone number with incorrect length') 10 | t.same(isValidWithFormat('NNN-NNNN-NNNN', '010-1234--5678'), false, 'Invalid phone number with extra dashes') 11 | t.same(isValidWithFormat('NNNNNNNNNN', 'abcdefghij'), false, 'Invalid phone number with letters') 12 | t.same(isValidWithFormat('NNN-NNN-NNNN', '010-XYZ-1234'), false, 'Invalid phone number with letters') 13 | t.end() 14 | }) 15 | 16 | test('isValidWithFormat with country code and extension', (t) => { 17 | t.same(isValidWithFormat('+1 NNN-NNN-NNNN', '+1 234-567-1890'), true, 'Valid phone number with country code') 18 | t.same(isValidWithFormat('+1 NNN-NNN-NNNN x NNN', '+1 234-567-1890 x 123'), true, 'Valid phone number with country code and extension') 19 | t.end() 20 | }) 21 | 22 | test('isValidWithFormat curried', (t) => { 23 | const fn = isValidWithFormat('NNN-NNN-NNNN') 24 | t.same(fn('123-456-7890'), true, 'Valid phone number with curried function') 25 | t.same(fn('1234567890'), false, 'Invalid phone number with curried function') 26 | t.end() 27 | }) 28 | 29 | test('isValidWithFormat - missing format', (t) => { 30 | t.throws(() => isValidWithFormat(null, '123-456-7890'), /You must provide a format to validate/, 'Should throw an error if format is missing') 31 | t.end() 32 | }) 33 | -------------------------------------------------------------------------------- /tests/breakdown.spec.js: -------------------------------------------------------------------------------- 1 | import breakdown from '../src/breakdown.js' 2 | import test from 'tape' 3 | 4 | test('Test breakdown normal', t => { 5 | const results = breakdown('555-444-3333') 6 | 7 | t.ok(results, 'results came back') 8 | t.is(typeof results, 'object', 'results came back as an object') 9 | t.is(results.areaCode, '555', 'The area code in results was 555') 10 | t.is(results.localCode, '444', 'The local code in results was 444') 11 | t.is(results.lineNumber, '3333', 'The lineNumber in results was 3333') 12 | t.notOk(results.extension.length, 'There was no extension') 13 | t.end() 14 | }) 15 | 16 | test('Test breakdown with placeholder syntax', t => { 17 | const results = breakdown('555-___-____') 18 | 19 | t.is(results.areaCode, '555', 'The area code in results was 555') 20 | t.is(results.localCode, '___', 'The local code in results was ___') 21 | t.is(results.lineNumber, '____', 'The lineNumber in results was ____') 22 | t.end() 23 | }) 24 | 25 | test('Test breakdown extension', t => { 26 | const results = breakdown('555-444-3333 x 8989') 27 | 28 | t.ok(results, 'results came back') 29 | t.is(typeof results, 'object', 'results came back as an object') 30 | t.is(results.areaCode, '555', 'The area code in results was 555') 31 | t.is(results.localCode, '444', 'The local code in results was 444') 32 | t.is(results.lineNumber, '3333', 'The lineNumber in results was 3333') 33 | t.is(results.extension, '8989', 'The extension number was 8989') 34 | t.end() 35 | }) 36 | 37 | test('Test breakdown another normal', t => { 38 | const results = breakdown('5554441111') 39 | 40 | t.ok(results, 'results came back') 41 | t.is(typeof results, 'object', 'results came back as an object') 42 | t.is(results.areaCode, '555', 'The area code in results was 555') 43 | t.is(results.localCode, '444', 'The local code in results was 444') 44 | t.is(results.lineNumber, '1111', 'The lineNumber in results was 3333') 45 | t.notOk(results.extension.length, 'The extension number was empty') 46 | t.end() 47 | }) 48 | -------------------------------------------------------------------------------- /.github/workflows/phone-fns.yml: -------------------------------------------------------------------------------- 1 | name: Phone-Fns CI 2 | 3 | on: 4 | push: 5 | branches: [ master, development ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Check node_module Cache 24 | id: node-module-cache 25 | uses: actions/cache@v4 26 | with: 27 | path: | 28 | node_modules 29 | */*/node_modules 30 | key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }} 31 | - name: Install dependencies 32 | if: steps.node-module-cache.outputs.cache-hit != 'true' 33 | run: npm ci 34 | - name: Test 35 | run: npm run test:cov 36 | - name: Run Report 37 | run: npm run report 38 | - name: Check Coverage 39 | run: npm run coverage 40 | - name: Upload Coverage 41 | uses: codecov/codecov-action@v4 42 | env: 43 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 44 | lint: 45 | runs-on: ubuntu-latest 46 | strategy: 47 | matrix: 48 | node-version: [18.x, 20.x] 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Use Node.js ${{ matrix.node-version }} 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: ${{ matrix.node-version }} 56 | - name: Check node_module Cache 57 | id: node-module-cache 58 | uses: actions/cache@v4 59 | with: 60 | path: | 61 | node_modules 62 | */*/node_modules 63 | key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }} 64 | - name: Install dependencies 65 | if: steps.node-module-cache.outputs.cache-hit != 'true' 66 | run: npm ci 67 | - name: Lint 68 | run: npm run lint 69 | -------------------------------------------------------------------------------- /tests/breakdownWithFormat.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import breakdownWithFormat from '../src/breakdownWithFormat.js' 3 | 4 | test('breakdownWithFormat - valid input', (t) => { 5 | const format = '+C (AAA) LLL-NNNN xXXX' 6 | const phone = '+1 (555) 444-3333 x123' 7 | const expected = { 8 | countryCode: '1', 9 | areaCode: '555', 10 | localCode: '444', 11 | lineNumber: '3333', 12 | extension: '123' 13 | } 14 | 15 | const result = breakdownWithFormat(format, phone) 16 | t.same(result, expected, 'Should correctly breakdown the phone number with the given format') 17 | t.end() 18 | }) 19 | 20 | test('breakdownWithFormat - missing format', (t) => { 21 | const phone = '+1 (555) 444-3333 x123' 22 | 23 | t.throws(() => breakdownWithFormat(null, phone), /You must provide a format to breakdown/, 'Should throw an error if format is missing') 24 | t.end() 25 | }) 26 | 27 | test('breakdownWithFormat - invalid phone number', (t) => { 28 | const format = '+C (AAA) LLL-NNNN xXXX' 29 | const phone = 'invalid phone number' 30 | 31 | 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') 32 | t.end() 33 | }) 34 | 35 | test('breakdownWithFormat - different format', (t) => { 36 | const format = '+C-AAA-LLL-NNNN xXXX' 37 | const phone = '+1-555-444-3333 x123' 38 | const expected = { 39 | countryCode: '1', 40 | areaCode: '555', 41 | localCode: '444', 42 | lineNumber: '3333', 43 | extension: '123' 44 | } 45 | 46 | const result = breakdownWithFormat(format, phone) 47 | t.same(result, expected, 'Should correctly breakdown the phone number with a different format') 48 | t.end() 49 | }) 50 | 51 | test('breakdownWithFormat - no extension', (t) => { 52 | const format = '+C (AAA) LLL-NNNN' 53 | const phone = '+1 (555) 444-3333' 54 | const expected = { 55 | countryCode: '1', 56 | areaCode: '555', 57 | localCode: '444', 58 | lineNumber: '3333', 59 | extension: '' 60 | } 61 | 62 | const result = breakdownWithFormat(format, phone) 63 | t.same(result, expected, 'Should correctly breakdown the phone number without an extension') 64 | t.end() 65 | }) 66 | -------------------------------------------------------------------------------- /tests/validate.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import validate from '../src/validate.js' 3 | 4 | test('Test simple type', t => { 5 | t.ok(validate('555-444-3333'), 'Results returned back ok') 6 | t.ok(validate('4445556666')) 7 | t.ok(validate(4445556666)) 8 | t.end() 9 | }) 10 | 11 | test('Test complex type', t => { 12 | t.ok(validate('(555) 444 3333'), 'Results returned back ok') 13 | t.end() 14 | }) 15 | 16 | test('Test handles wide range of phone numbers', t => { 17 | const validPhoneNumbers = [ 18 | '010.1234.5678', // Number with dots 19 | '10 9876 5432', // South Korea number without country code 20 | '+82 2 3456 7890', // South Korea number with country code 21 | '+82 10 9876 5432', // South Korea mobile number with country code 22 | '+1 (555) 444-3333', // US number with country code 23 | '+44 20 7946 0958', // UK landline with country code 24 | '07911 123456', // UK mobile (domestic) 25 | '+49 30 12345678', // German number with country code 26 | '+1 800 123 4567 x123', // US toll-free number with extension 27 | '+61 2 1234 5678', // Australian number with country code, 28 | '+39 02 1234 5678', // Italian number with country code 29 | '+91 12345 67890' // Indian number with country code 30 | ] 31 | 32 | validPhoneNumbers.forEach(phone => { 33 | t.ok(validate(phone), `Valid phone number: ${phone}`) 34 | }) 35 | t.end() 36 | }) 37 | 38 | test('Handles Invalid range of phone numbers', t => { 39 | const invalidPhoneNumbers = [ 40 | '123', // Too short 41 | '010-1234', // Missing last part of mobile number 42 | '02-3456', // Incomplete landline number 43 | '02-3456-789', // Incomplete landline number (should be 7 digits) 44 | '+82 10', // Too short, incomplete mobile number 45 | '+82 02-1234', // Incomplete landline number 46 | '010-XYZ-1234', // Invalid characters (letters in the mobile number) 47 | '12345', // Too short 48 | 'abcd-efgh-ijkl', // Completely invalid characters 49 | '+44 (0)20 7946', // Incomplete UK number 50 | '+1-555-abc-1234', // Invalid characters 51 | '+12345678901234567890' // Too long 52 | ] 53 | 54 | invalidPhoneNumbers.forEach(phone => { 55 | t.notOk(validate(phone), `Invalid phone number: ${phone}`) 56 | }) 57 | t.end() 58 | }) 59 | -------------------------------------------------------------------------------- /src/format.js: -------------------------------------------------------------------------------- 1 | import _curry2 from './_internals/_curry2.js' 2 | import isValid from './isValid.js' 3 | import uglify from './uglify.js' 4 | import _uglifyFormats from './_internals/_uglifyFormats.js' 5 | import _hasPlaceholder from './_internals/_hasPlaceholder.js' 6 | 7 | /** 8 | * @private 9 | * @function 10 | * @param {String} layout The desired layout format 11 | * @param {String} phone The phone number to validate against 12 | */ 13 | function validFormat (layout, phone) { 14 | return phone.length === _uglifyFormats(layout).length 15 | } 16 | 17 | /** 18 | * @name format 19 | * @since v0.1.0 20 | * @function 21 | * @category Function 22 | * @sig String -> String -> String 23 | * @description 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 24 | * @param {String} layout The desired layout of the phone number 25 | * @param {String} phone The phone number to breakdown 26 | * @return {String} Returns a string which is the formatted phone number 27 | * 28 | * @example 29 | * format('(NNN) NNN.NNNN', '444-555-6666') // => '(444) 555.6666' 30 | * format('C + (NNN) NNN.NNNN', '1444-555-6666') // => '1 + (444) 555.6666' 31 | * format('CC + NNN.NNN.NNNN', '163334445555') // => '16 + 333.444.5555' 32 | * format('(NNN) NNN.NNNN x NNNN', '44455566668989') // => '(444) 555.6666 x 8989' 33 | * 34 | * // Format is case insensitive 35 | * format('(NNN) nnn-NNnn', '4445556666') // => (444) 555-6666 36 | * 37 | * // Format is also curried 38 | * const fn = format('NNN.NNN.NNNN') 39 | * 40 | * fn('4445556666') // => '444.555.6666' 41 | * fn('(333) 444-5555') // => '333.444.5555' 42 | */ 43 | function format (layout, phone) { 44 | const uglyPhone = uglify(phone) 45 | const cCount = (layout.match(/C/g) || []).length 46 | 47 | if (!validFormat(layout, uglyPhone)) { 48 | return phone 49 | } 50 | 51 | if (!_hasPlaceholder(uglyPhone)) { 52 | // We are skipping validation of the phone number if there are placeholders 53 | if (!isValid(phone)) { 54 | return phone 55 | } 56 | } 57 | 58 | return uglyPhone.split('').reduce((acc, d, i) => { 59 | if (cCount > i) { 60 | acc = acc.replace(/C/i, d) 61 | } else { 62 | acc = acc.replace(/N/i, d) 63 | } 64 | 65 | return acc 66 | }, layout) 67 | } 68 | 69 | export default _curry2(format) 70 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Phone-Fns 2 | // Project: Phone-Fns 3 | // Definitions by: Dustin Hershman 4 | declare let phoneFns: phoneFns.Static 5 | 6 | declare namespace phoneFns { 7 | interface Breakdown { 8 | countryCode?: string 9 | areaCode: string 10 | localCode: string 11 | lineNumber: string 12 | extension: string 13 | } 14 | 15 | interface Static { 16 | /** 17 | * 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 18 | */ 19 | format(layout: string, phone: string): string 20 | format(layout: string): (phone: string) => string 21 | 22 | /** 23 | * Takes a provided phone string and breaks it down into an object of codes 24 | */ 25 | breakdown(phone: string): Breakdown 26 | 27 | /** 28 | * Breaks down a phone number based on a custom format provided and returns an object with the parts of the phone number 29 | * C - Country Code A- Area Code L - Local Code N - Line Number X - Extension 30 | */ 31 | breakdownWithFormat(format: string, phone: string): Breakdown 32 | breakdownWithFormat(format: string): (phone: string) => Breakdown 33 | 34 | /** 35 | * Finds a list of separators in a phone number string 36 | */ 37 | findSeparators(phone: string): string[] 38 | 39 | /** 40 | * Validates the base number, does not take the country code or extension into consideration for this validation 41 | * @deprecated Use isValidWithFormat instead 42 | */ 43 | isValid(phone: string): boolean 44 | 45 | /** 46 | * Validates a phone number based on a custom format provided 47 | */ 48 | isValidWithFormat(format: string, phone: string): boolean 49 | isValidWithFormat(format: string): (phone: string) => boolean 50 | 51 | /** 52 | * Strips all of the special characters from the given string but leaves extension and country code characters in place 53 | */ 54 | normalize(phone: string): string 55 | 56 | /** 57 | * Strips all of the special characters from the given string 58 | */ 59 | uglify(phone: string | number): string 60 | 61 | /** 62 | * Validates the base number, strips out special characters and spaces upon validation, can handle country code and extension in the phone number 63 | */ 64 | validate(phone: string): boolean 65 | } 66 | } 67 | 68 | export = phoneFns 69 | export as namespace phoneFns 70 | -------------------------------------------------------------------------------- /src/breakdownWithFormat.js: -------------------------------------------------------------------------------- 1 | import _curry2 from './_internals/_curry2.js' 2 | import isValidWithFormat from './isValidWithFormat.js' 3 | 4 | /** 5 | * @name breakdownWithFormat 6 | * @since v4.1.0 7 | * @function 8 | * @category Function 9 | * @sig String -> String -> Object 10 | * @description 11 | * Breaks down a phone number based on a custom format provided and returns an object with the parts of the phone number 12 | * C - Country Code A- Area Code L - Local Code N - Line Number X - Extension 13 | * Does NOT work with placeholders 14 | * @param {String} format The format to validate against 15 | * @param {String} phone The phone number to breakdown 16 | * @return {Object} Returns an object with the parts of the phone number 17 | * @example 18 | * import { breakdownWithFormat } from 'phone-fns' 19 | * 20 | * breakdownWithFormat('+C (AAA) LLL-NNNN xXXX', '+1 (555) 444-3333 x123') // => { countryCode: '1', areaCode: '555', localCode: '444', lineNumber: '3333', extension: '123' } 21 | * breakdownWithFormat('AAA-LLL-NNNN', '010-XYZ-1234') // => Error: The phone number provided does not match the format provided or is an invalid phone number 22 | * 23 | * // it's also curried 24 | * const fn = breakdownWithFormat('+C (AAA) LLL-NNNN xXXX') 25 | * fn('+1 (555) 444-3333 x123') // => { countryCode: '1', areaCode: '123', localCode: '456', lineNumber: '7890', extension: '123' } 26 | */ 27 | function breakdownWithFormat (format, phone) { 28 | if (!format) { 29 | throw new Error('You must provide a format to breakdown') 30 | } 31 | 32 | if (!isValidWithFormat(format, phone)) { 33 | throw new Error('The phone number provided does not match the format provided or is an invalid phone number') 34 | } 35 | 36 | const results = { 37 | countryCode: '', 38 | areaCode: '', 39 | localCode: '', 40 | lineNumber: '', 41 | extension: '' 42 | } 43 | 44 | for (let i = 0; i < format.length; i++) { 45 | switch (format[i]) { 46 | case 'C': 47 | results.countryCode += phone[i] 48 | break 49 | case 'A': 50 | results.areaCode += phone[i] 51 | break 52 | case 'N': 53 | results.lineNumber += phone[i] 54 | break 55 | case 'L': 56 | results.localCode += phone[i] 57 | break 58 | case 'X': 59 | results.extension += phone[i] 60 | break 61 | } 62 | } 63 | 64 | return results 65 | } 66 | 67 | export default _curry2(breakdownWithFormat) 68 | -------------------------------------------------------------------------------- /types/index.d.cts: -------------------------------------------------------------------------------- 1 | // Type definitions for Phone-Fns 2 | // Project: Phone-Fns 3 | // Definitions by: Dustin Hershman 4 | 5 | declare let phoneFns: phoneFns.Static 6 | 7 | declare namespace phoneFns { 8 | interface Breakdown { 9 | countryCode?: string 10 | areaCode: string 11 | localCode: string 12 | lineNumber: string 13 | extension: string 14 | } 15 | 16 | interface Static { 17 | /** 18 | * 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 19 | */ 20 | format(layout: string, phone: string): string 21 | format(layout: string): (phone: string) => string 22 | 23 | /** 24 | * Takes a provided phone string and breaks it down into an object of codes 25 | */ 26 | breakdown(phone: string): Breakdown 27 | 28 | /** 29 | * Breaks down a phone number based on a custom format provided and returns an object with the parts of the phone number 30 | * C - Country Code A- Area Code L - Local Code N - Line Number X - Extension 31 | */ 32 | breakdownWithFormat(format: string, phone: string): Breakdown 33 | breakdownWithFormat(format: string): (phone: string) => Breakdown 34 | 35 | /** 36 | * Validates the base number, does not take the country code or extension into consideration for this validation 37 | * @deprecated Use isValidWithFormat instead 38 | */ 39 | isValid(phone: string): boolean 40 | 41 | /** 42 | * Validates a phone number based on a custom format provided 43 | */ 44 | isValidWithFormat(format: string, phone: string): boolean 45 | isValidWithFormat(format: string): (phone: string) => boolean 46 | 47 | /** 48 | * Finds a list of separators in a phone number string 49 | */ 50 | findSeparators(phone: string): string[] 51 | 52 | /** 53 | * Strips all of the special characters from the given string but leaves extension and country code characters in place 54 | */ 55 | normalize(phone: string): string 56 | 57 | /** 58 | * Strips all of the special characters from the given string 59 | */ 60 | uglify(phone: string | number): string 61 | 62 | /** 63 | * Validates the base number, strips out special characters and spaces upon validation, can handle country code and extension in the phone number 64 | */ 65 | validate(phone: string): boolean 66 | } 67 | } 68 | 69 | export = phoneFns 70 | export as namespace phoneFns 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phone-fns", 3 | "version": "4.1.2", 4 | "description": "A small, modern, and functional phone library for javascript", 5 | "main": "dist/phone-fns.min.js", 6 | "module": "src/index.js", 7 | "typings": "types/index.d.ts", 8 | "type": "module", 9 | "unpkg": "dist/phone-fns.iife.min.js", 10 | "jsdelivr": "dist/phone-fns.iife.min.js", 11 | "scripts": { 12 | "prepack": "npm-run-all --parallel docs scripts lint test --serial build", 13 | "scripts": "node scripts/create-export.js", 14 | "docs": "jsdoc -c jsdoc.json", 15 | "build": "rollup -c", 16 | "lint": "standard src/**/*.js", 17 | "test": "tape tests/*.spec.js | tap-on", 18 | "test:cov": "c8 tape tests/*.spec.js", 19 | "coverage": "c8 check-coverage --lines 90 --branches 90 --functions 90 --statements 90", 20 | "report": "c8 report --reporter=text-lcov > coverage.lcov" 21 | }, 22 | "exports": { 23 | ".": { 24 | "require": { 25 | "types": "./types/index.d.cts", 26 | "default": "./dist/phone-fns.min.cjs" 27 | }, 28 | "import": { 29 | "types": "./types/index.d.ts", 30 | "default": "./dist/phone-fns.min.js" 31 | }, 32 | "default": { 33 | "types": "./types/index.d.ts", 34 | "default": "./dist/phone-fns.iife.min.js" 35 | } 36 | } 37 | }, 38 | "standard": { 39 | "ignore": [ 40 | "docs/*", 41 | "dist/*", 42 | "types/*" 43 | ] 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/dhershman1/phone-fns" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/dhershman1/phone-fns/issues" 51 | }, 52 | "homepage": "https://phone-fns.dusty.codes/", 53 | "keywords": [ 54 | "format", 55 | "phone number", 56 | "functional", 57 | "phone libary", 58 | "phone", 59 | "phone-fns", 60 | "telephone", 61 | "tel", 62 | "functional library", 63 | "formatting", 64 | "modern" 65 | ], 66 | "author": "Dustin Hershman ", 67 | "license": "MIT", 68 | "private": false, 69 | "devDependencies": { 70 | "@babel/core": "7.25.7", 71 | "@babel/preset-env": "7.25.7", 72 | "@rollup/plugin-babel": "6.0.4", 73 | "@rollup/plugin-node-resolve": "^15.3.0", 74 | "@rollup/plugin-terser": "0.4.4", 75 | "c8": "^10.1.2", 76 | "globby": "13.2.2", 77 | "jsdoc": "4.0.3", 78 | "npm-run-all": "4.1.5", 79 | "pinet": "1.2.1", 80 | "rollup": "4.24.0", 81 | "rollup-plugin-filesize": "10.0.0", 82 | "standard": "17.1.2", 83 | "tap-on": "1.0.0", 84 | "tape": "5.9.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '33 17 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | I am open to, and grateful for, any contributions made to this project. 4 | 5 | Please abide by the linter setup which uses [Standardjs](http://standardjs.com) rules 6 | 7 | Please make sure all PRs are pointed at and processed through the main Development branch 8 | 9 | ## Commit Messages 10 | 11 | If able, I'd prefer small easy to digest commits rather than larger multiple change driven commits. 12 | 13 | I'd love it if your commit messages were in the format of `Type: summary` for example: `Updated: Changed var names to make more sense` 14 | 15 | The types I'd like to stick to are: 16 | 17 | - `Chore` -- A chore task like documentation/readme tweaks/dependency updates/etc 18 | - `Chore: Updated dependencies` 19 | - `Updated` -- Updating something minor 20 | - `Updated: Changed var names to make more sense` 21 | - `Fixed` -- Fixing bugs/typos etc 22 | - `Fixed: Typo in function name` 23 | - `Added` -- For changes that are adding new features/functionality/etc 24 | - `Added: New helper functions` 25 | - `Breaking` -- For changes that might be a breaking change 26 | - `Breaking: Changed function x return value` 27 | 28 | I know this format looks similar to something like [commitizen](https://www.npmjs.com/package/commitizen) but that changelog is too far gone to fully commit to it 29 | 30 | ## Testing 31 | 32 | All changes are expected to continue to pass the tests in place. 33 | 34 | If you are adding new functionality to the library you are expected to also unit test and create appropriate testing for your additions 35 | 36 | To run all tests use `npm t` 37 | 38 | If you want to test only your functionality feel free to change the test script to your `.js` file but **please** remember to change it back to `*.js` and re run `npm t` afterwards! 39 | 40 | ## Documentation 41 | 42 | For any new functionality additions please follow the format of other functionality in the form of jsdocs. The expected formatting should be: 43 | 44 | ```js 45 | /** 46 | * @name function name 47 | * @function 48 | * @since version it was created in 49 | * @category what the function applies to 50 | * @sig the signature type of the function 51 | * @description A description of the function 52 | * @param {Param Type} paramName Param Description for each parameter 53 | * @return {Type} What is the function returning 54 | * 55 | * @example 56 | * A use case example of the functionality 57 | */ 58 | ``` 59 | 60 | If you are editing functionality and it requires api changes please be sure to change the jsdocs accordingly for that function. 61 | 62 | ## Developing 63 | 64 | - The library is Functional so all new functions should follow this pattern 65 | - Run unit tests with `npm test` 66 | - Before opening a PR make sure you do `npm run scripts` to generate new documentation & build files 67 | 68 | ## Releasing 69 | 70 | Currently the CHANGELOG is updated manually before each release. You can document your changes here, or I will document them using your provided commit messages. 71 | 72 | Releases follow standard [semantic versioning](https://semver.org/). 73 | 74 | I will handle publishing to npm for now after your PR is merged in 75 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dustinh17@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | license:mit 7 | 8 | 9 | Netlify Status 10 | 11 | 12 | Npm Version 13 | 14 | 15 | Build Status 16 | 17 | 18 | codebeat badge 19 | 20 | 21 | 22 | 23 |

24 | 25 | # Phone Fns 26 | 27 | A small modern, and functional phone number library which gathers inspiration from the fun [date-fns](https://github.com/date-fns/date-fns) library 28 | 29 | 30 | - [Documentation](https://phone-fns.dusty.codes/) 31 | 32 | ## How-To 33 | 34 | ```cli 35 | npm i phone-fns 36 | ``` 37 | 38 | Standard module system 39 | 40 | ```js 41 | import * as phoneFns from 'phone-fns' 42 | 43 | phoneFns.uglify('555-444-3333') // => '5554443333' 44 | ``` 45 | 46 | Common JS 47 | 48 | ```js 49 | const phoneFns = require('phone-fns') 50 | 51 | phoneFns.uglify('555-444-3333') // => '5554443333' 52 | ``` 53 | 54 | Using Unpkg or jsdelivr CDN (As of v4.0.0+) 55 | 56 | ```html 57 | 58 | 61 | ``` 62 | 63 | 64 | Through the browser 65 | 66 | ```html 67 | 68 | 71 | ``` 72 | 73 | ## Usage 74 | 75 | ```javascript 76 | import * as phoneFns from 'phone-fns' 77 | 78 | phoneFns.breakdown('4443332222') 79 | // => { areaCode: '444', localCode: '333', lineNumber: '2222', extension: '' } 80 | 81 | phoneFns.format('(NNN) NNN-NNNN', '4443332222') 82 | // => '(444) 333-2222' 83 | ``` 84 | 85 | You can also destructure to only use the functions you want 86 | 87 | ```javascript 88 | import { breakdown, format } from 'phone-fns' 89 | 90 | breakdown('4443332222') 91 | // => { areaCode: '444', localCode: '333', lineNumber: '2222', extension: '' } 92 | 93 | format('(NNN) NNN-NNNN', '4443332222') 94 | // => '(444) 333-2222' 95 | ``` 96 | 97 | ## Placeholder Support 98 | 99 | Phone-Fns as of v3.2.0 now supports placeholder syntax using Underscores `_` 100 | 101 | ```js 102 | const fn = format('NNN-NNN-NNNN') 103 | 104 | fn('__________') // => '___-___-____' 105 | fn('444_______') // => '444-___-____' 106 | fn('444555____') // => '444-555-____' 107 | fn('4445556666') // => '444-555-6666' 108 | format('NNN-NNN-NNNN x NNNN', '5554443333____') // => '555-444-3333 x ____' 109 | ``` 110 | 111 | This will only work with underscores or other characters not picked up by the Regex `\W` type. 112 | 113 | Just call format as the phone number updates in order to get back the newly updated string. Useful for using it with a input mask setup 114 | -------------------------------------------------------------------------------- /tests/format.spec.js: -------------------------------------------------------------------------------- 1 | import format from '../src/format.js' 2 | import test from 'tape' 3 | 4 | test('Test custom format normal', t => { 5 | const result = format('(NNN) NNN.NNNN', '444-555-6666') 6 | 7 | t.ok(result, 'Result returned okay') 8 | t.is(result, '(444) 555.6666', 'Result formatted correctly on return') 9 | t.end() 10 | }) 11 | 12 | test('Test custom format with a number type provided', t => { 13 | const result = format('(NNN) NNN.NNNN', 4445556666) 14 | 15 | t.ok(result, 'Result returned okay') 16 | t.is(result, '(444) 555.6666', 'Result formatted correctly on return') 17 | t.end() 18 | }) 19 | 20 | test('Test an already formatted number to a new number', t => { 21 | t.same(format('NNN.NNN.NNNN', '(444) 555-6666'), '444.555.6666') 22 | t.end() 23 | }) 24 | 25 | test('Test custom format longDistance', t => { 26 | const result = format('C + (NNN) NNN.NNNN', '1444-555-6666') 27 | 28 | t.ok(result, 'Result returned okay') 29 | t.is(result, '1 + (444) 555.6666', 'Result formatted correctly on return') 30 | t.end() 31 | }) 32 | 33 | test('Test randomly placed Country Code', t => { 34 | t.same(format('(NNN) NNN-NNNN + C', '14445556666'), '(444) 555-6666 + 1') 35 | t.end() 36 | }) 37 | 38 | test('Test custom format extensions', t => { 39 | const result = format('(NNN) NNN.NNNN x NNNN', '444-555-66668989') 40 | 41 | t.ok(result, 'Result returned okay') 42 | t.is(result, '(444) 555.6666 x 8989', 'Result formatted correctly on return') 43 | t.end() 44 | }) 45 | 46 | test('Test custom format bad phone', t => { 47 | const result = format('(NNN).NNN.NNNN', '89965') 48 | 49 | t.ok(result, 'Results returned okay') 50 | t.is(result, '89965', 'Returned back the bad number') 51 | t.end() 52 | }) 53 | 54 | test('Test both longDistance and extensions', t => { 55 | const result = format('C + NNN.NNN.NNNN x NNNN', '155544433338989') 56 | 57 | t.ok(result, 'Results returned okay') 58 | t.is(result, '1 + 555.444.3333 x 8989', 'Results are formatted as expected') 59 | t.end() 60 | }) 61 | 62 | test('Is case insensitive', t => { 63 | const result = format('(nnn) nNn.NNnn', '4445556666') 64 | 65 | t.is(result, '(444) 555.6666') 66 | t.end() 67 | }) 68 | 69 | test('Is Curried', t => { 70 | const fn = format('NNN.NNN.NNNN') 71 | 72 | t.is(fn('4445556666'), '444.555.6666') 73 | t.end() 74 | }) 75 | 76 | test('Handle Djibouti numbers', t => { 77 | const result = format('+CCC NN NN NNNN', '25311223333') 78 | 79 | t.same(result, '+253 11 22 3333') 80 | t.end() 81 | }) 82 | 83 | test('Handle Morocco numbers', t => { 84 | const result = format('+CC NN NN NN NN', '0711223333') 85 | 86 | t.same(result, '+07 11 22 33 33') 87 | t.end() 88 | }) 89 | 90 | test('Handles numbers without area code', t => { 91 | const results = format('NNN.NNNN', '555-6666') 92 | 93 | t.same(results, '555.6666') 94 | t.end() 95 | }) 96 | 97 | test('Catches letters when passed in', t => { 98 | t.same(format('NNN.NNNN', 'abc1234'), 'abc1234') 99 | t.same(format('NNN.NNNN', '1234abc'), '1234abc') 100 | t.end() 101 | }) 102 | 103 | test('Handles more numbers than the format', t => { 104 | const results = format('NNN.NNNN', '123456789') 105 | 106 | t.same(results, '123456789') 107 | t.end() 108 | }) 109 | 110 | test('Handles less numbers than the format', t => { 111 | const results = format('NNN.NNNN', '1234') 112 | 113 | t.same(results, '1234') 114 | t.end() 115 | }) 116 | 117 | test('Handles non NANP formats', t => { 118 | const results = format('NNN NNN NN NN NN', '046123456789') 119 | 120 | t.same(results, '046 123 45 67 89') 121 | t.end() 122 | }) 123 | 124 | test('Supports Placeholder characters', t => { 125 | const fn = format('NNN-NNN-NNNN') 126 | 127 | t.same(fn('__________'), '___-___-____') 128 | t.same(fn('444_______'), '444-___-____') 129 | t.same(fn('444555____'), '444-555-____') 130 | t.same(fn('4445556666'), '444-555-6666') 131 | t.same(format('NNN-NNN-NNNN x NNNN', '5554443333____'), '555-444-3333 x ____') 132 | t.end() 133 | }) 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Custom 3 | tmp.js 4 | benching.js 5 | exp.js 6 | test.html 7 | dist/ 8 | TODO.md 9 | docs/ 10 | 11 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,node 12 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,node 13 | 14 | ### Linux ### 15 | *~ 16 | 17 | # temporary files which can be created if a process still has a handle open of a deleted file 18 | .fuse_hidden* 19 | 20 | # KDE directory preferences 21 | .directory 22 | 23 | # Linux trash folder which might appear on any partition or disk 24 | .Trash-* 25 | 26 | # .nfs files are created when an open file is removed but is still being accessed 27 | .nfs* 28 | 29 | ### macOS ### 30 | # General 31 | .DS_Store 32 | .AppleDouble 33 | .LSOverride 34 | 35 | # Icon must end with two \r 36 | Icon 37 | 38 | 39 | # Thumbnails 40 | ._* 41 | 42 | # Files that might appear in the root of a volume 43 | .DocumentRevisions-V100 44 | .fseventsd 45 | .Spotlight-V100 46 | .TemporaryItems 47 | .Trashes 48 | .VolumeIcon.icns 49 | .com.apple.timemachine.donotpresent 50 | 51 | # Directories potentially created on remote AFP share 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | 58 | ### Node ### 59 | # Logs 60 | logs 61 | *.log 62 | npm-debug.log* 63 | yarn-debug.log* 64 | yarn-error.log* 65 | lerna-debug.log* 66 | 67 | # Diagnostic reports (https://nodejs.org/api/report.html) 68 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 69 | 70 | # Runtime data 71 | pids 72 | *.pid 73 | *.seed 74 | *.pid.lock 75 | 76 | # Directory for instrumented libs generated by jscoverage/JSCover 77 | lib-cov 78 | 79 | # Coverage directory used by tools like istanbul 80 | coverage 81 | *.lcov 82 | 83 | # nyc test coverage 84 | .nyc_output 85 | 86 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 87 | .grunt 88 | 89 | # Bower dependency directory (https://bower.io/) 90 | bower_components 91 | 92 | # node-waf configuration 93 | .lock-wscript 94 | 95 | # Compiled binary addons (https://nodejs.org/api/addons.html) 96 | build/Release 97 | 98 | # Dependency directories 99 | node_modules/ 100 | jspm_packages/ 101 | 102 | # TypeScript v1 declaration files 103 | typings/ 104 | 105 | # TypeScript cache 106 | *.tsbuildinfo 107 | 108 | # Optional npm cache directory 109 | .npm 110 | 111 | # Optional eslint cache 112 | .eslintcache 113 | 114 | # Optional stylelint cache 115 | .stylelintcache 116 | 117 | # Microbundle cache 118 | .rpt2_cache/ 119 | .rts2_cache_cjs/ 120 | .rts2_cache_es/ 121 | .rts2_cache_umd/ 122 | 123 | # Optional REPL history 124 | .node_repl_history 125 | 126 | # Output of 'npm pack' 127 | *.tgz 128 | 129 | # Yarn Integrity file 130 | .yarn-integrity 131 | 132 | # dotenv environment variables file 133 | .env 134 | .env.test 135 | .env*.local 136 | 137 | # parcel-bundler cache (https://parceljs.org/) 138 | .cache 139 | .parcel-cache 140 | 141 | # Next.js build output 142 | .next 143 | 144 | # Nuxt.js build / generate output 145 | .nuxt 146 | dist 147 | 148 | # Storybook build outputs 149 | .out 150 | .storybook-out 151 | storybook-static 152 | 153 | # rollup.js default build output 154 | dist/ 155 | 156 | # Gatsby files 157 | .cache/ 158 | # Comment in the public line in if your project uses Gatsby and not Next.js 159 | # https://nextjs.org/blog/next-9-1#public-directory-support 160 | # public 161 | 162 | # vuepress build output 163 | .vuepress/dist 164 | 165 | # Serverless directories 166 | .serverless/ 167 | 168 | # FuseBox cache 169 | .fusebox/ 170 | 171 | # DynamoDB Local files 172 | .dynamodb/ 173 | 174 | # TernJS port file 175 | .tern-port 176 | 177 | # Stores VSCode versions used for testing VSCode extensions 178 | .vscode-test 179 | 180 | # Temporary folders 181 | tmp/ 182 | temp/ 183 | 184 | ### Windows ### 185 | # Windows thumbnail cache files 186 | Thumbs.db 187 | Thumbs.db:encryptable 188 | ehthumbs.db 189 | ehthumbs_vista.db 190 | 191 | # Dump file 192 | *.stackdump 193 | 194 | # Folder config file 195 | [Dd]esktop.ini 196 | 197 | # Recycle Bin used on file shares 198 | $RECYCLE.BIN/ 199 | 200 | # Windows Installer files 201 | *.cab 202 | *.msi 203 | *.msix 204 | *.msm 205 | *.msp 206 | 207 | # Windows shortcuts 208 | *.lnk 209 | 210 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,node 211 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | # Custom 3 | .travis.yml 4 | .circleci 5 | rollup.config.js 6 | rollup.split.js 7 | .eslintrc.js 8 | .eslintignore 9 | .gitignore 10 | scripts 11 | .nyc_output 12 | tests/ 13 | coverage 14 | .nyc_output 15 | .babelrc 16 | docs 17 | jsdoc.json 18 | .github 19 | benching.js 20 | tmp.js 21 | exp.js 22 | codecov.yml 23 | .c8rc.json 24 | 25 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,node 26 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,node 27 | 28 | ### Linux ### 29 | *~ 30 | 31 | # temporary files which can be created if a process still has a handle open of a deleted file 32 | .fuse_hidden* 33 | 34 | # KDE directory preferences 35 | .directory 36 | 37 | # Linux trash folder which might appear on any partition or disk 38 | .Trash-* 39 | 40 | # .nfs files are created when an open file is removed but is still being accessed 41 | .nfs* 42 | 43 | ### macOS ### 44 | # General 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | 49 | # Icon must end with two \r 50 | Icon 51 | 52 | 53 | # Thumbnails 54 | ._* 55 | 56 | # Files that might appear in the root of a volume 57 | .DocumentRevisions-V100 58 | .fseventsd 59 | .Spotlight-V100 60 | .TemporaryItems 61 | .Trashes 62 | .VolumeIcon.icns 63 | .com.apple.timemachine.donotpresent 64 | 65 | # Directories potentially created on remote AFP share 66 | .AppleDB 67 | .AppleDesktop 68 | Network Trash Folder 69 | Temporary Items 70 | .apdisk 71 | 72 | ### Node ### 73 | # Logs 74 | logs 75 | *.log 76 | npm-debug.log* 77 | yarn-debug.log* 78 | yarn-error.log* 79 | lerna-debug.log* 80 | 81 | # Diagnostic reports (https://nodejs.org/api/report.html) 82 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 83 | 84 | # Runtime data 85 | pids 86 | *.pid 87 | *.seed 88 | *.pid.lock 89 | 90 | # Directory for instrumented libs generated by jscoverage/JSCover 91 | lib-cov 92 | 93 | # Coverage directory used by tools like istanbul 94 | coverage 95 | *.lcov 96 | 97 | # nyc test coverage 98 | .nyc_output 99 | 100 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 101 | .grunt 102 | 103 | # Bower dependency directory (https://bower.io/) 104 | bower_components 105 | 106 | # node-waf configuration 107 | .lock-wscript 108 | 109 | # Compiled binary addons (https://nodejs.org/api/addons.html) 110 | build/Release 111 | 112 | # Dependency directories 113 | node_modules/ 114 | jspm_packages/ 115 | 116 | # TypeScript v1 declaration files 117 | typings/ 118 | 119 | # TypeScript cache 120 | *.tsbuildinfo 121 | 122 | # Optional npm cache directory 123 | .npm 124 | 125 | # Optional eslint cache 126 | .eslintcache 127 | 128 | # Optional stylelint cache 129 | .stylelintcache 130 | 131 | # Microbundle cache 132 | .rpt2_cache/ 133 | .rts2_cache_cjs/ 134 | .rts2_cache_es/ 135 | .rts2_cache_umd/ 136 | 137 | # Optional REPL history 138 | .node_repl_history 139 | 140 | # Output of 'npm pack' 141 | *.tgz 142 | 143 | # Yarn Integrity file 144 | .yarn-integrity 145 | 146 | # dotenv environment variables file 147 | .env 148 | .env.test 149 | .env*.local 150 | 151 | # parcel-bundler cache (https://parceljs.org/) 152 | .cache 153 | .parcel-cache 154 | 155 | # Next.js build output 156 | .next 157 | 158 | # Nuxt.js build / generate output 159 | .nuxt 160 | dist 161 | 162 | # Storybook build outputs 163 | .out 164 | .storybook-out 165 | storybook-static 166 | 167 | # rollup.js default build output 168 | dist/ 169 | 170 | # Gatsby files 171 | .cache/ 172 | # Comment in the public line in if your project uses Gatsby and not Next.js 173 | # https://nextjs.org/blog/next-9-1#public-directory-support 174 | # public 175 | 176 | # vuepress build output 177 | .vuepress/dist 178 | 179 | # Serverless directories 180 | .serverless/ 181 | 182 | # FuseBox cache 183 | .fusebox/ 184 | 185 | # DynamoDB Local files 186 | .dynamodb/ 187 | 188 | # TernJS port file 189 | .tern-port 190 | 191 | # Stores VSCode versions used for testing VSCode extensions 192 | .vscode-test 193 | 194 | # Temporary folders 195 | tmp/ 196 | temp/ 197 | 198 | ### Windows ### 199 | # Windows thumbnail cache files 200 | Thumbs.db 201 | Thumbs.db:encryptable 202 | ehthumbs.db 203 | ehthumbs_vista.db 204 | 205 | # Dump file 206 | *.stackdump 207 | 208 | # Folder config file 209 | [Dd]esktop.ini 210 | 211 | # Recycle Bin used on file shares 212 | $RECYCLE.BIN/ 213 | 214 | # Windows Installer files 215 | *.cab 216 | *.msi 217 | *.msix 218 | *.msm 219 | *.msp 220 | 221 | # Windows shortcuts 222 | *.lnk 223 | 224 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,node 225 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.1.2 4 | 5 | ### Fixed 6 | 7 | - Documentation examples for `breakdownWithFormat` function 8 | 9 | ### Changed 10 | 11 | - Updated `pinet` to v1.2.1 12 | 13 | ## v4.1.1 14 | 15 | ### New 16 | 17 | - Deprecated `isValid` function in favor of `isValidWithFormat` 18 | 19 | ### Changed 20 | 21 | - Reset codecov coverage report using `c8` package 22 | - Updated unit tests for some more coverage 23 | - Cleaned up some round about logic in the codebase 24 | 25 | ## v4.1.0 26 | 27 | ### New 28 | 29 | - Added `normalize` function 30 | - This function strips out special characters and trims the phone number, much like uglify but skips non-digit characters 31 | - Example: `normalize('555.444.3333 x 123') // => '5554443333x123'` vs `uglify('555.444.3333 x 123') // => 5554443333123` 32 | - Added `validate` function 33 | - This is a validation function, but works better for world wide phone numbers as well. Expects the full number 34 | - Example: `333-444-5555` comes back valid but `444-5555` is invalid to this function 35 | - Added `isValidWithFormat` function 36 | - This takes a string phone number and a format string and validates the phone using the format 37 | - It's also passed through the `validate` function for an extra step of validation 38 | - Added `findSeparators` function 39 | - A simple function that finds the separators in a phone number and returns them as an array 40 | - Added `breakdownWithFormat` function 41 | - Works a lot like `breakdown` but follows a strict format provided by the user to breakdown the number into an object 42 | - This allows for a wider range of phone number support for breakdown 43 | 44 | 45 | ### Changed 46 | 47 | - `Phone-fns` is no longer dependant on `Kyanite` and is dependency free! 48 | - `isValid` description to explain that it mostly focused on NANP numbers 49 | - `breakdown` description to better explain that it's main focus is NANP numbers and its gachas 50 | - We more than doubled our unit tests! Woo! 51 | 52 | ### Chore 53 | 54 | - Renamed test files to `*.spec.js` instead of just `*.js` 55 | 56 | ## v4.0.2 57 | 58 | ### Fixed 59 | 60 | - Typo for extension in types #14 [@JonBee](https://github.com/JonBee) 61 | 62 | ### Chore 63 | 64 | - Dependency Updates 65 | 66 | ## v4.0.1 67 | 68 | ### New 69 | 70 | - Added more unit tests for `isValid` and `breakdown` 71 | 72 | ### Fixed 73 | 74 | - Added warning into README that currently phone-fns is focused on US based phone number styles 75 | - Updated github action to LTS of node 76 | 77 | ### Chore 78 | 79 | - Updated dependencies 80 | 81 | ## v4.0.0 82 | 83 | ### Breaking Changes 84 | 85 | - Converted Phone-Fns to a standard ESM module 86 | - This means if you are using import statements in an ESM module for phone-fns this changes: 87 | - `import phoneFns from 'phone-fns'` --> `import * as phoneFns from 'phone-fns'` 88 | - You can also just destructure from here still like so: `import { isValid } from 'phone-fns'` 89 | - Phone fns should still support common js syntax out of the box as well 90 | - Upgraded to `Kyanite v2.0.1` 91 | 92 | ### New 93 | 94 | - CDN support sould be working for phone-fns again 95 | - Updated README to reflect these changes 96 | 97 | ### Improved 98 | 99 | - Made typings less confusing 100 | 101 | ## v3.2.5 102 | 103 | ### :confetti_ball: Enhanced 104 | 105 | - Added: New github flows and files 106 | 107 | ## v3.2.4 108 | 109 | ### Improved 110 | 111 | - Audited packaged 112 | - Replaced `tap-spec` with `tap-on` 113 | - Optimized ignore files 114 | - Removed dist from repo 115 | 116 | ## v3.2.3 117 | 118 | ### New 119 | 120 | - Switched over to [pinet](https://github.com/dhershman1/pinet) jsdoc templating 121 | - Changed documentation hosting 122 | 123 | ### Improved 124 | 125 | - All dependencies have been updated 126 | - Removed docs folder 127 | - Removed david-dm badge since it doesn't seem to be coming back 128 | - Documentation workflow now handled by circleci 129 | - `format` can now take in `Number` type phone numbers 130 | 131 | ## v3.2.2 132 | 133 | - Dev dependency fixes 134 | 135 | ## v3.2.1 136 | 137 | ### Improved 138 | 139 | - Dependency updates 140 | - Unit tests 141 | - Optimization of some code pieces 142 | - Some documentation 143 | 144 | ## v3.2.0 145 | 146 | ### New 147 | 148 | - Added new Placeholder support 149 | 150 | ## v3.1.0 151 | 152 | ### Improved 153 | 154 | - Switched to internal currying 155 | - Improved `format`s performance and broke it down to a more intelligent function 156 | 157 | ### Fixed 158 | 159 | - Typo in `format`s example 160 | - Remove unused code 161 | 162 | ## v3.0.0 163 | 164 | ### Breaking Changes 165 | 166 | - Removed `find` function since it was rather pointless 167 | - Removed `match` function since it was rather pointless 168 | - The main import is no longer a function but an object of functions 169 | - Re wrote how format works so it's easier to use and more light weight 170 | - Specify country code in the layout with C. Example: `C + (NNN) NNN-NNNN` 171 | - Usage example: `format('C + (NNN) NNN-NNNN', 14445556666) // => '1 + (444) 555-6666'` 172 | - `breakdown` no longer handles or accepts country codes 173 | - Technically the only function that cares about Country Codes is the `format` function this is to make functions easier to use 174 | - Scrapped modular functions 175 | - There is no need for this anymore, since Rollup & webpack v2+ treeshaking support curried functions, destructing works much better now 176 | 177 | ### New 178 | 179 | - Documentation was moved onto a more sustainable location for quicker and more up to date docs, should be hosted via github pages now too 180 | 181 | ### Improved 182 | 183 | - Using Kyanite to improve how format functions and better currying from the library 184 | - Replaced `uglify-js` with `terser` for performance gains 185 | - `isValid` optimizations using Kyanite 186 | - Overall documentation system should be built directly into its own page now 187 | - Added Typing declarations for TS and vscode IDE support 188 | 189 | ## v2.0.1 190 | 191 | ### Improved 192 | 193 | - A new script system in place for generating library documentation to make it easier for the main site to pull in the latest documentation 194 | 195 | ## v2.0.0 196 | 197 | ### BREAKING CHANGES 198 | 199 | - The API usage for `format` is now different, you only need to use the letter `N` in your layout now in the order you want the numbers to fill in 200 | - The letter `N` is case insensitive 201 | - Example: an old layout may have looked like: `(AAA) LLL-NNNN` now it is: `(NNN) NNN-NNNN` or `(nnn) nnn-nnNN` if you wanted to get creative 202 | 203 | ### New 204 | 205 | - `find` has been deprecated and may be removed in later versions, please transition to `breakdown` 206 | - Added a 2nd validation level to `format` it will now validate the phone number has enough digits to fill out the layout properly 207 | - You can now pass country code as a number or string 208 | 209 | ### Fixed 210 | 211 | - JSdocs for format were backwards 212 | - Able to properly handle numbers without an area code now 213 | 214 | ## v1.0.1 215 | 216 | ### New 217 | 218 | - Rebuilt the architecture of the main source (has no effect on usage) 219 | - Added in CDN info to documentation 220 | - Building using Rollup now instead of webpack decreasing the overall file size of our main file to 1.86kb (vs 3.83kb with webpack) and 970B gzipped 221 | - This also benefits each individual functions build so they're also much smaller 222 | - Converting testing to latest babel versions 223 | - Convert linter from `eslint` to `standardjs` 224 | - Created an uncompressed build along with the compressed one which you can use in dev for easier debugging 225 | 226 | ### Fixed 227 | 228 | - For some reason I didn't realize the package.json main was looking at a index.js file 229 | - Re built automated documentation script so it performs faster 230 | 231 | ## v1.0.0 232 | 233 | v1.0.0 is almost a total re write of how the library functions from v0.3.3 it is advised you migrate with caution 234 | 235 | ### BREAKING CHANGES 236 | 237 | - Removed `findLocal` function 238 | - Removed `getCode` function 239 | - Removed `getCountries` function 240 | - Removed `getCountryCode` function 241 | - Removed `callingCodes` 242 | - Changed `identical` to `match` 243 | - Re org of entire library 244 | - You can now create instances of the library with different country codes or none 245 | - This applies the country code to each method that needs it meaning you don't need to send it as a param 246 | - Changed the parameter of most of the functions 247 | - If using the individual functions you will have to provide a country code to those that need it 248 | - `find` parameters have been rotated 249 | 250 | ### New 251 | 252 | - Added the ability to call `phoneFns` as a function and provide a country code to create an instance around that country code, or not for only base phone number functionality 253 | - The `format` function is now curried 254 | - The `find` function is curried as well 255 | 256 | ### Improved 257 | 258 | - The `breakdown` function to be a little more lightweight 259 | - The `format` function to be a bit more lightweight and functional 260 | - The `isValid` function to do decently better phone number validation 261 | 262 | 263 | ## v0.3.3 264 | 265 | - Fix for automating docs file 266 | - Build files should appear again 267 | 268 | ## v0.3.2 269 | 270 | - Better(working) fix for webpack builds issue 271 | - Better Main JS generator 272 | 273 | ## v0.3.1 274 | 275 | - Fix for Webpack build issues when importing base module 276 | - README typo fixes 277 | 278 | ## v0.3.0 279 | 280 | > - Consistancy Tweaks in some files 281 | > - Changed `getCode` to `getCountryCode` 282 | > - Notice that this now means `getCode` is depricated 283 | > - This is not yet a breaking change but may be in the future (1.0.0 release probably) 284 | > - Changed `findLocal` to `getCountries` 285 | > - Notice that this now means `findLocal` is depricated 286 | > - This is not yet a breaking change but may be in the future (1.0.0 release probably) 287 | > - Introduced webpack as the builder to help build out the public module 288 | > - This shouldn't have any large effects, if you do see an issue please let me know asap! 289 | > - Updated all dependencies 290 | > - Text Tweaks in some error messages 291 | 292 | ## v0.2.0 293 | 294 | > - Tweaks to build process 295 | > - Added ability to call functional methods individually you can do so using the following: 296 | > - `phone-fns/format` 297 | > - `phone-fns/find` 298 | > - `phone-fns/breakdown` 299 | > - `phone-fns/identical` 300 | > - `phone-fns/isValid` 301 | > - `phone-fns/uglify` 302 | > - `phone-fns/getCode` 303 | > - `phone-fns/findLocal` 304 | > - `phone-fns/callingCodes` - Gives back the full list of country calling codes 305 | > - You can still use just `phone-fns` to get back all of the methods in an `object` 306 | > - Added a minified version `phone-fns.min.js` **the module points to this version by default** 307 | > - Renamed the file `phone-fns.umd.js` to just `phone-fns.js` 308 | > - Added Country Calling Codes from the [Countries Repo](https://github.com/mledoze/countries) 309 | > - Added `findLocal` function which returns the country names associated with the passed in code 310 | > - Added `getCode` function which returns the country calling code from the `json` 311 | > - Removed country code dashing, since it's not really a thing 312 | 313 | ## v0.1.0 314 | 315 | > - Initial Public Release 316 | --------------------------------------------------------------------------------