parse.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. // This is a port of Google Android `libphonenumber`'s
  2. // `phonenumberutil.js` of December 31th, 2018.
  3. //
  4. // https://github.com/googlei18n/libphonenumber/commits/master/javascript/i18n/phonenumbers/phonenumberutil.js
  5. import {
  6. VALID_DIGITS,
  7. PLUS_CHARS,
  8. MIN_LENGTH_FOR_NSN,
  9. MAX_LENGTH_FOR_NSN
  10. } from './constants.js'
  11. import ParseError from './ParseError.js'
  12. import Metadata from './metadata.js'
  13. import isViablePhoneNumber, { isViablePhoneNumberStart } from './helpers/isViablePhoneNumber.js'
  14. import extractExtension from './helpers/extension/extractExtension.js'
  15. import parseIncompletePhoneNumber from './parseIncompletePhoneNumber.js'
  16. import getCountryCallingCode from './getCountryCallingCode.js'
  17. import { isPossibleNumber } from './isPossible.js'
  18. // import { parseRFC3966 } from './helpers/RFC3966.js'
  19. import PhoneNumber from './PhoneNumber.js'
  20. import matchesEntirely from './helpers/matchesEntirely.js'
  21. import extractCountryCallingCode from './helpers/extractCountryCallingCode.js'
  22. import extractNationalNumber from './helpers/extractNationalNumber.js'
  23. import stripIddPrefix from './helpers/stripIddPrefix.js'
  24. import getCountryByCallingCode from './helpers/getCountryByCallingCode.js'
  25. import extractFormattedPhoneNumberFromPossibleRfc3966NumberUri from './helpers/extractFormattedPhoneNumberFromPossibleRfc3966NumberUri.js'
  26. // We don't allow input strings for parsing to be longer than 250 chars.
  27. // This prevents malicious input from consuming CPU.
  28. const MAX_INPUT_STRING_LENGTH = 250
  29. // This consists of the plus symbol, digits, and arabic-indic digits.
  30. const PHONE_NUMBER_START_PATTERN = new RegExp('[' + PLUS_CHARS + VALID_DIGITS + ']')
  31. // Regular expression of trailing characters that we want to remove.
  32. // A trailing `#` is sometimes used when writing phone numbers with extensions in US.
  33. // Example: "+1 (645) 123 1234-910#" number has extension "910".
  34. const AFTER_PHONE_NUMBER_END_PATTERN = new RegExp('[^' + VALID_DIGITS + '#' + ']+$')
  35. const USE_NON_GEOGRAPHIC_COUNTRY_CODE = false
  36. // Examples:
  37. //
  38. // ```js
  39. // parse('8 (800) 555-35-35', 'RU')
  40. // parse('8 (800) 555-35-35', 'RU', metadata)
  41. // parse('8 (800) 555-35-35', { country: { default: 'RU' } })
  42. // parse('8 (800) 555-35-35', { country: { default: 'RU' } }, metadata)
  43. // parse('+7 800 555 35 35')
  44. // parse('+7 800 555 35 35', metadata)
  45. // ```
  46. //
  47. /**
  48. * Parses a phone number.
  49. *
  50. * parse('123456789', { defaultCountry: 'RU', v2: true }, metadata)
  51. * parse('123456789', { defaultCountry: 'RU' }, metadata)
  52. * parse('123456789', undefined, metadata)
  53. *
  54. * @param {string} input
  55. * @param {object} [options]
  56. * @param {object} metadata
  57. * @return {object|PhoneNumber?} If `options.v2: true` flag is passed, it returns a `PhoneNumber?` instance. Otherwise, returns an object of shape `{ phone: '...', country: '...' }` (or just `{}` if no phone number was parsed).
  58. */
  59. export default function parse(text, options, metadata) {
  60. // If assigning the `{}` default value is moved to the arguments above,
  61. // code coverage would decrease for some weird reason.
  62. options = options || {}
  63. metadata = new Metadata(metadata)
  64. // Validate `defaultCountry`.
  65. if (options.defaultCountry && !metadata.hasCountry(options.defaultCountry)) {
  66. if (options.v2) {
  67. throw new ParseError('INVALID_COUNTRY')
  68. }
  69. throw new Error(`Unknown country: ${options.defaultCountry}`)
  70. }
  71. // Parse the phone number.
  72. const { number: formattedPhoneNumber, ext, error } = parseInput(text, options.v2, options.extract)
  73. // If the phone number is not viable then return nothing.
  74. if (!formattedPhoneNumber) {
  75. if (options.v2) {
  76. if (error === 'TOO_SHORT') {
  77. throw new ParseError('TOO_SHORT')
  78. }
  79. throw new ParseError('NOT_A_NUMBER')
  80. }
  81. return {}
  82. }
  83. const {
  84. country,
  85. nationalNumber,
  86. countryCallingCode,
  87. countryCallingCodeSource,
  88. carrierCode
  89. } = parsePhoneNumber(
  90. formattedPhoneNumber,
  91. options.defaultCountry,
  92. options.defaultCallingCode,
  93. metadata
  94. )
  95. if (!metadata.hasSelectedNumberingPlan()) {
  96. if (options.v2) {
  97. throw new ParseError('INVALID_COUNTRY')
  98. }
  99. return {}
  100. }
  101. // Validate national (significant) number length.
  102. if (!nationalNumber || nationalNumber.length < MIN_LENGTH_FOR_NSN) {
  103. // Won't throw here because the regexp already demands length > 1.
  104. /* istanbul ignore if */
  105. if (options.v2) {
  106. throw new ParseError('TOO_SHORT')
  107. }
  108. // Google's demo just throws an error in this case.
  109. return {}
  110. }
  111. // Validate national (significant) number length.
  112. //
  113. // A sidenote:
  114. //
  115. // They say that sometimes national (significant) numbers
  116. // can be longer than `MAX_LENGTH_FOR_NSN` (e.g. in Germany).
  117. // https://github.com/googlei18n/libphonenumber/blob/7e1748645552da39c4e1ba731e47969d97bdb539/resources/phonenumber.proto#L36
  118. // Such numbers will just be discarded.
  119. //
  120. if (nationalNumber.length > MAX_LENGTH_FOR_NSN) {
  121. if (options.v2) {
  122. throw new ParseError('TOO_LONG')
  123. }
  124. // Google's demo just throws an error in this case.
  125. return {}
  126. }
  127. if (options.v2) {
  128. const phoneNumber = new PhoneNumber(
  129. countryCallingCode,
  130. nationalNumber,
  131. metadata.metadata
  132. )
  133. if (country) {
  134. phoneNumber.country = country
  135. }
  136. if (carrierCode) {
  137. phoneNumber.carrierCode = carrierCode
  138. }
  139. if (ext) {
  140. phoneNumber.ext = ext
  141. }
  142. phoneNumber.__countryCallingCodeSource = countryCallingCodeSource
  143. return phoneNumber
  144. }
  145. // Check if national phone number pattern matches the number.
  146. // National number pattern is different for each country,
  147. // even for those ones which are part of the "NANPA" group.
  148. const valid = (options.extended ? metadata.hasSelectedNumberingPlan() : country) ?
  149. matchesEntirely(nationalNumber, metadata.nationalNumberPattern()) :
  150. false
  151. if (!options.extended) {
  152. return valid ? result(country, nationalNumber, ext) : {}
  153. }
  154. // isInternational: countryCallingCode !== undefined
  155. return {
  156. country,
  157. countryCallingCode,
  158. carrierCode,
  159. valid,
  160. possible: valid ? true : (
  161. options.extended === true &&
  162. metadata.possibleLengths() &&
  163. isPossibleNumber(nationalNumber, metadata) ? true : false
  164. ),
  165. phone: nationalNumber,
  166. ext
  167. }
  168. }
  169. /**
  170. * Extracts a formatted phone number from text.
  171. * Doesn't guarantee that the extracted phone number
  172. * is a valid phone number (for example, doesn't validate its length).
  173. * @param {string} text
  174. * @param {boolean} [extract] — If `false`, then will parse the entire `text` as a phone number.
  175. * @param {boolean} [throwOnError] — By default, it won't throw if the text is too long.
  176. * @return {string}
  177. * @example
  178. * // Returns "(213) 373-4253".
  179. * extractFormattedPhoneNumber("Call (213) 373-4253 for assistance.")
  180. */
  181. function extractFormattedPhoneNumber(text, extract, throwOnError) {
  182. if (!text) {
  183. return
  184. }
  185. if (text.length > MAX_INPUT_STRING_LENGTH) {
  186. if (throwOnError) {
  187. throw new ParseError('TOO_LONG')
  188. }
  189. return
  190. }
  191. if (extract === false) {
  192. return text
  193. }
  194. // Attempt to extract a possible number from the string passed in
  195. const startsAt = text.search(PHONE_NUMBER_START_PATTERN)
  196. if (startsAt < 0) {
  197. return
  198. }
  199. return text
  200. // Trim everything to the left of the phone number
  201. .slice(startsAt)
  202. // Remove trailing non-numerical characters
  203. .replace(AFTER_PHONE_NUMBER_END_PATTERN, '')
  204. }
  205. /**
  206. * @param {string} text - Input.
  207. * @param {boolean} v2 - Legacy API functions don't pass `v2: true` flag.
  208. * @param {boolean} [extract] - Whether to extract a phone number from `text`, or attempt to parse the entire text as a phone number.
  209. * @return {object} `{ ?number, ?ext }`.
  210. */
  211. function parseInput(text, v2, extract) {
  212. // // Parse RFC 3966 phone number URI.
  213. // if (text && text.indexOf('tel:') === 0) {
  214. // return parseRFC3966(text)
  215. // }
  216. // let number = extractFormattedPhoneNumber(text, extract, v2)
  217. let number = extractFormattedPhoneNumberFromPossibleRfc3966NumberUri(text, {
  218. extractFormattedPhoneNumber: (text) => extractFormattedPhoneNumber(text, extract, v2)
  219. })
  220. // If the phone number is not viable, then abort.
  221. if (!number) {
  222. return {}
  223. }
  224. if (!isViablePhoneNumber(number)) {
  225. if (isViablePhoneNumberStart(number)) {
  226. return { error: 'TOO_SHORT' }
  227. }
  228. return {}
  229. }
  230. // Attempt to parse extension first, since it doesn't require region-specific
  231. // data and we want to have the non-normalised number here.
  232. const withExtensionStripped = extractExtension(number)
  233. if (withExtensionStripped.ext) {
  234. return withExtensionStripped
  235. }
  236. return { number }
  237. }
  238. /**
  239. * Creates `parse()` result object.
  240. */
  241. function result(country, nationalNumber, ext) {
  242. const result = {
  243. country,
  244. phone: nationalNumber
  245. }
  246. if (ext) {
  247. result.ext = ext
  248. }
  249. return result
  250. }
  251. /**
  252. * Parses a viable phone number.
  253. * @param {string} formattedPhoneNumber — Example: "(213) 373-4253".
  254. * @param {string} [defaultCountry]
  255. * @param {string} [defaultCallingCode]
  256. * @param {Metadata} metadata
  257. * @return {object} Returns `{ country: string?, countryCallingCode: string?, nationalNumber: string? }`.
  258. */
  259. function parsePhoneNumber(
  260. formattedPhoneNumber,
  261. defaultCountry,
  262. defaultCallingCode,
  263. metadata
  264. ) {
  265. // Extract calling code from phone number.
  266. let { countryCallingCodeSource, countryCallingCode, number } = extractCountryCallingCode(
  267. parseIncompletePhoneNumber(formattedPhoneNumber),
  268. defaultCountry,
  269. defaultCallingCode,
  270. metadata.metadata
  271. )
  272. // Choose a country by `countryCallingCode`.
  273. let country
  274. if (countryCallingCode) {
  275. metadata.selectNumberingPlan(countryCallingCode)
  276. }
  277. // If `formattedPhoneNumber` is passed in "national" format
  278. // then `number` is defined and `countryCallingCode` is `undefined`.
  279. else if (number && (defaultCountry || defaultCallingCode)) {
  280. metadata.selectNumberingPlan(defaultCountry, defaultCallingCode)
  281. if (defaultCountry) {
  282. country = defaultCountry
  283. } else {
  284. /* istanbul ignore if */
  285. if (USE_NON_GEOGRAPHIC_COUNTRY_CODE) {
  286. if (metadata.isNonGeographicCallingCode(defaultCallingCode)) {
  287. country = '001'
  288. }
  289. }
  290. }
  291. countryCallingCode = defaultCallingCode || getCountryCallingCode(defaultCountry, metadata.metadata)
  292. }
  293. else return {}
  294. if (!number) {
  295. return {
  296. countryCallingCodeSource,
  297. countryCallingCode
  298. }
  299. }
  300. const {
  301. nationalNumber,
  302. carrierCode
  303. } = extractNationalNumber(
  304. parseIncompletePhoneNumber(number),
  305. metadata
  306. )
  307. // Sometimes there are several countries
  308. // corresponding to the same country phone code
  309. // (e.g. NANPA countries all having `1` country phone code).
  310. // Therefore, to reliably determine the exact country,
  311. // national (significant) number should have been parsed first.
  312. //
  313. // When `metadata.json` is generated, all "ambiguous" country phone codes
  314. // get their countries populated with the full set of
  315. // "phone number type" regular expressions.
  316. //
  317. const exactCountry = getCountryByCallingCode(countryCallingCode, {
  318. nationalNumber,
  319. defaultCountry,
  320. metadata
  321. })
  322. if (exactCountry) {
  323. country = exactCountry
  324. /* istanbul ignore if */
  325. if (exactCountry === '001') {
  326. // Can't happen with `USE_NON_GEOGRAPHIC_COUNTRY_CODE` being `false`.
  327. // If `USE_NON_GEOGRAPHIC_COUNTRY_CODE` is set to `true` for some reason,
  328. // then remove the "istanbul ignore if".
  329. } else {
  330. metadata.country(country)
  331. }
  332. }
  333. return {
  334. country,
  335. countryCallingCode,
  336. countryCallingCodeSource,
  337. nationalNumber,
  338. carrierCode
  339. }
  340. }