parse.js 12 KB

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