metadata.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import compare from './tools/semver-compare.js'
  2. import isObject from './helpers/isObject.js'
  3. // Added "possibleLengths" and renamed
  4. // "country_phone_code_to_countries" to "country_calling_codes".
  5. const V2 = '1.0.18'
  6. // Added "idd_prefix" and "default_idd_prefix".
  7. const V3 = '1.2.0'
  8. // Moved `001` country code to "nonGeographic" section of metadata.
  9. const V4 = '1.7.35'
  10. const DEFAULT_EXT_PREFIX = ' ext. '
  11. const CALLING_CODE_REG_EXP = /^\d+$/
  12. /**
  13. * See: https://gitlab.com/catamphetamine/libphonenumber-js/blob/master/METADATA.md
  14. */
  15. export default class Metadata {
  16. constructor(metadata) {
  17. validateMetadata(metadata)
  18. this.metadata = metadata
  19. setVersion.call(this, metadata)
  20. }
  21. getCountries() {
  22. return Object.keys(this.metadata.countries).filter(_ => _ !== '001')
  23. }
  24. getCountryMetadata(countryCode) {
  25. return this.metadata.countries[countryCode]
  26. }
  27. nonGeographic() {
  28. if (this.v1 || this.v2 || this.v3) return
  29. // `nonGeographical` was a typo.
  30. // It's present in metadata generated from `1.7.35` to `1.7.37`.
  31. // The test case could be found by searching for "nonGeographical".
  32. return this.metadata.nonGeographic || this.metadata.nonGeographical
  33. }
  34. hasCountry(country) {
  35. return this.getCountryMetadata(country) !== undefined
  36. }
  37. hasCallingCode(callingCode) {
  38. if (this.getCountryCodesForCallingCode(callingCode)) {
  39. return true
  40. }
  41. if (this.nonGeographic()) {
  42. if (this.nonGeographic()[callingCode]) {
  43. return true
  44. }
  45. } else {
  46. // A hacky workaround for old custom metadata (generated before V4).
  47. const countryCodes = this.countryCallingCodes()[callingCode]
  48. if (countryCodes && countryCodes.length === 1 && countryCodes[0] === '001') {
  49. return true
  50. }
  51. }
  52. }
  53. isNonGeographicCallingCode(callingCode) {
  54. if (this.nonGeographic()) {
  55. return this.nonGeographic()[callingCode] ? true : false
  56. } else {
  57. return this.getCountryCodesForCallingCode(callingCode) ? false : true
  58. }
  59. }
  60. // Deprecated.
  61. country(countryCode) {
  62. return this.selectNumberingPlan(countryCode)
  63. }
  64. selectNumberingPlan(countryCode, callingCode) {
  65. // Supports just passing `callingCode` as the first argument.
  66. if (countryCode && CALLING_CODE_REG_EXP.test(countryCode)) {
  67. callingCode = countryCode
  68. countryCode = null
  69. }
  70. if (countryCode && countryCode !== '001') {
  71. if (!this.hasCountry(countryCode)) {
  72. throw new Error(`Unknown country: ${countryCode}`)
  73. }
  74. this.numberingPlan = new NumberingPlan(this.getCountryMetadata(countryCode), this)
  75. } else if (callingCode) {
  76. if (!this.hasCallingCode(callingCode)) {
  77. throw new Error(`Unknown calling code: ${callingCode}`)
  78. }
  79. this.numberingPlan = new NumberingPlan(this.getNumberingPlanMetadata(callingCode), this)
  80. } else {
  81. this.numberingPlan = undefined
  82. }
  83. return this
  84. }
  85. getCountryCodesForCallingCode(callingCode) {
  86. const countryCodes = this.countryCallingCodes()[callingCode]
  87. if (countryCodes) {
  88. // Metadata before V4 included "non-geographic entity" calling codes
  89. // inside `country_calling_codes` (for example, `"881":["001"]`).
  90. // Now the semantics of `country_calling_codes` has changed:
  91. // it's specifically for "countries" now.
  92. // Older versions of custom metadata will simply skip parsing
  93. // "non-geographic entity" phone numbers with new versions
  94. // of this library: it's not considered a bug,
  95. // because such numbers are extremely rare,
  96. // and developers extremely rarely use custom metadata.
  97. if (countryCodes.length === 1 && countryCodes[0].length === 3) {
  98. return
  99. }
  100. return countryCodes
  101. }
  102. }
  103. getCountryCodeForCallingCode(callingCode) {
  104. const countryCodes = this.getCountryCodesForCallingCode(callingCode)
  105. if (countryCodes) {
  106. return countryCodes[0]
  107. }
  108. }
  109. getNumberingPlanMetadata(callingCode) {
  110. const countryCode = this.getCountryCodeForCallingCode(callingCode)
  111. if (countryCode) {
  112. return this.getCountryMetadata(countryCode)
  113. }
  114. if (this.nonGeographic()) {
  115. const metadata = this.nonGeographic()[callingCode]
  116. if (metadata) {
  117. return metadata
  118. }
  119. } else {
  120. // A hacky workaround for old custom metadata (generated before V4).
  121. // In that metadata, there was no concept of "non-geographic" metadata
  122. // so metadata for `001` country code was stored along with other countries.
  123. // The test case can be found by searching for:
  124. // "should work around `nonGeographic` metadata not existing".
  125. const countryCodes = this.countryCallingCodes()[callingCode]
  126. if (countryCodes && countryCodes.length === 1 && countryCodes[0] === '001') {
  127. return this.metadata.countries['001']
  128. }
  129. }
  130. }
  131. // Deprecated.
  132. countryCallingCode() {
  133. return this.numberingPlan.callingCode()
  134. }
  135. // Deprecated.
  136. IDDPrefix() {
  137. return this.numberingPlan.IDDPrefix()
  138. }
  139. // Deprecated.
  140. defaultIDDPrefix() {
  141. return this.numberingPlan.defaultIDDPrefix()
  142. }
  143. // Deprecated.
  144. nationalNumberPattern() {
  145. return this.numberingPlan.nationalNumberPattern()
  146. }
  147. // Deprecated.
  148. possibleLengths() {
  149. return this.numberingPlan.possibleLengths()
  150. }
  151. // Deprecated.
  152. formats() {
  153. return this.numberingPlan.formats()
  154. }
  155. // Deprecated.
  156. nationalPrefixForParsing() {
  157. return this.numberingPlan.nationalPrefixForParsing()
  158. }
  159. // Deprecated.
  160. nationalPrefixTransformRule() {
  161. return this.numberingPlan.nationalPrefixTransformRule()
  162. }
  163. // Deprecated.
  164. leadingDigits() {
  165. return this.numberingPlan.leadingDigits()
  166. }
  167. // Deprecated.
  168. hasTypes() {
  169. return this.numberingPlan.hasTypes()
  170. }
  171. // Deprecated.
  172. type(type) {
  173. return this.numberingPlan.type(type)
  174. }
  175. // Deprecated.
  176. ext() {
  177. return this.numberingPlan.ext()
  178. }
  179. countryCallingCodes() {
  180. if (this.v1) return this.metadata.country_phone_code_to_countries
  181. return this.metadata.country_calling_codes
  182. }
  183. // Deprecated.
  184. chooseCountryByCountryCallingCode(callingCode) {
  185. return this.selectNumberingPlan(callingCode)
  186. }
  187. hasSelectedNumberingPlan() {
  188. return this.numberingPlan !== undefined
  189. }
  190. }
  191. class NumberingPlan {
  192. constructor(metadata, globalMetadataObject) {
  193. this.globalMetadataObject = globalMetadataObject
  194. this.metadata = metadata
  195. setVersion.call(this, globalMetadataObject.metadata)
  196. }
  197. callingCode() {
  198. return this.metadata[0]
  199. }
  200. // Formatting information for regions which share
  201. // a country calling code is contained by only one region
  202. // for performance reasons. For example, for NANPA region
  203. // ("North American Numbering Plan Administration",
  204. // which includes USA, Canada, Cayman Islands, Bahamas, etc)
  205. // it will be contained in the metadata for `US`.
  206. getDefaultCountryMetadataForRegion() {
  207. return this.globalMetadataObject.getNumberingPlanMetadata(this.callingCode())
  208. }
  209. // Is always present.
  210. IDDPrefix() {
  211. if (this.v1 || this.v2) return
  212. return this.metadata[1]
  213. }
  214. // Is only present when a country supports multiple IDD prefixes.
  215. defaultIDDPrefix() {
  216. if (this.v1 || this.v2) return
  217. return this.metadata[12]
  218. }
  219. nationalNumberPattern() {
  220. if (this.v1 || this.v2) return this.metadata[1]
  221. return this.metadata[2]
  222. }
  223. // "possible length" data is always present in Google's metadata.
  224. possibleLengths() {
  225. if (this.v1) return
  226. return this.metadata[this.v2 ? 2 : 3]
  227. }
  228. _getFormats(metadata) {
  229. return metadata[this.v1 ? 2 : this.v2 ? 3 : 4]
  230. }
  231. // For countries of the same region (e.g. NANPA)
  232. // formats are all stored in the "main" country for that region.
  233. // E.g. "RU" and "KZ", "US" and "CA".
  234. formats() {
  235. const formats = this._getFormats(this.metadata) || this._getFormats(this.getDefaultCountryMetadataForRegion()) || []
  236. return formats.map(_ => new Format(_, this))
  237. }
  238. nationalPrefix() {
  239. return this.metadata[this.v1 ? 3 : this.v2 ? 4 : 5]
  240. }
  241. _getNationalPrefixFormattingRule(metadata) {
  242. return metadata[this.v1 ? 4 : this.v2 ? 5 : 6]
  243. }
  244. // For countries of the same region (e.g. NANPA)
  245. // national prefix formatting rule is stored in the "main" country for that region.
  246. // E.g. "RU" and "KZ", "US" and "CA".
  247. nationalPrefixFormattingRule() {
  248. return this._getNationalPrefixFormattingRule(this.metadata) || this._getNationalPrefixFormattingRule(this.getDefaultCountryMetadataForRegion())
  249. }
  250. _nationalPrefixForParsing() {
  251. return this.metadata[this.v1 ? 5 : this.v2 ? 6 : 7]
  252. }
  253. nationalPrefixForParsing() {
  254. // If `national_prefix_for_parsing` is not set explicitly,
  255. // then infer it from `national_prefix` (if any)
  256. return this._nationalPrefixForParsing() || this.nationalPrefix()
  257. }
  258. nationalPrefixTransformRule() {
  259. return this.metadata[this.v1 ? 6 : this.v2 ? 7 : 8]
  260. }
  261. _getNationalPrefixIsOptionalWhenFormatting() {
  262. return !!this.metadata[this.v1 ? 7 : this.v2 ? 8 : 9]
  263. }
  264. // For countries of the same region (e.g. NANPA)
  265. // "national prefix is optional when formatting" flag is
  266. // stored in the "main" country for that region.
  267. // E.g. "RU" and "KZ", "US" and "CA".
  268. nationalPrefixIsOptionalWhenFormattingInNationalFormat() {
  269. return this._getNationalPrefixIsOptionalWhenFormatting(this.metadata) ||
  270. this._getNationalPrefixIsOptionalWhenFormatting(this.getDefaultCountryMetadataForRegion())
  271. }
  272. leadingDigits() {
  273. return this.metadata[this.v1 ? 8 : this.v2 ? 9 : 10]
  274. }
  275. types() {
  276. return this.metadata[this.v1 ? 9 : this.v2 ? 10 : 11]
  277. }
  278. hasTypes() {
  279. // Versions 1.2.0 - 1.2.4: can be `[]`.
  280. /* istanbul ignore next */
  281. if (this.types() && this.types().length === 0) {
  282. return false
  283. }
  284. // Versions <= 1.2.4: can be `undefined`.
  285. // Version >= 1.2.5: can be `0`.
  286. return !!this.types()
  287. }
  288. type(type) {
  289. if (this.hasTypes() && getType(this.types(), type)) {
  290. return new Type(getType(this.types(), type), this)
  291. }
  292. }
  293. ext() {
  294. if (this.v1 || this.v2) return DEFAULT_EXT_PREFIX
  295. return this.metadata[13] || DEFAULT_EXT_PREFIX
  296. }
  297. }
  298. class Format {
  299. constructor(format, metadata) {
  300. this._format = format
  301. this.metadata = metadata
  302. }
  303. pattern() {
  304. return this._format[0]
  305. }
  306. format() {
  307. return this._format[1]
  308. }
  309. leadingDigitsPatterns() {
  310. return this._format[2] || []
  311. }
  312. nationalPrefixFormattingRule() {
  313. return this._format[3] || this.metadata.nationalPrefixFormattingRule()
  314. }
  315. nationalPrefixIsOptionalWhenFormattingInNationalFormat() {
  316. return !!this._format[4] || this.metadata.nationalPrefixIsOptionalWhenFormattingInNationalFormat()
  317. }
  318. nationalPrefixIsMandatoryWhenFormattingInNationalFormat() {
  319. // National prefix is omitted if there's no national prefix formatting rule
  320. // set for this country, or when the national prefix formatting rule
  321. // contains no national prefix itself, or when this rule is set but
  322. // national prefix is optional for this phone number format
  323. // (and it is not enforced explicitly)
  324. return this.usesNationalPrefix() && !this.nationalPrefixIsOptionalWhenFormattingInNationalFormat()
  325. }
  326. // Checks whether national prefix formatting rule contains national prefix.
  327. usesNationalPrefix() {
  328. return this.nationalPrefixFormattingRule() &&
  329. // Check that national prefix formatting rule is not a "dummy" one.
  330. !FIRST_GROUP_ONLY_PREFIX_PATTERN.test(this.nationalPrefixFormattingRule())
  331. // In compressed metadata, `this.nationalPrefixFormattingRule()` is `0`
  332. // when `national_prefix_formatting_rule` is not present.
  333. // So, `true` or `false` are returned explicitly here, so that
  334. // `0` number isn't returned.
  335. ? true
  336. : false
  337. }
  338. internationalFormat() {
  339. return this._format[5] || this.format()
  340. }
  341. }
  342. /**
  343. * A pattern that is used to determine if the national prefix formatting rule
  344. * has the first group only, i.e., does not start with the national prefix.
  345. * Note that the pattern explicitly allows for unbalanced parentheses.
  346. */
  347. const FIRST_GROUP_ONLY_PREFIX_PATTERN = /^\(?\$1\)?$/
  348. class Type {
  349. constructor(type, metadata) {
  350. this.type = type
  351. this.metadata = metadata
  352. }
  353. pattern() {
  354. if (this.metadata.v1) return this.type
  355. return this.type[0]
  356. }
  357. possibleLengths() {
  358. if (this.metadata.v1) return
  359. return this.type[1] || this.metadata.possibleLengths()
  360. }
  361. }
  362. function getType(types, type) {
  363. switch (type) {
  364. case 'FIXED_LINE':
  365. return types[0]
  366. case 'MOBILE':
  367. return types[1]
  368. case 'TOLL_FREE':
  369. return types[2]
  370. case 'PREMIUM_RATE':
  371. return types[3]
  372. case 'PERSONAL_NUMBER':
  373. return types[4]
  374. case 'VOICEMAIL':
  375. return types[5]
  376. case 'UAN':
  377. return types[6]
  378. case 'PAGER':
  379. return types[7]
  380. case 'VOIP':
  381. return types[8]
  382. case 'SHARED_COST':
  383. return types[9]
  384. }
  385. }
  386. export function validateMetadata(metadata) {
  387. if (!metadata) {
  388. throw new Error('[libphonenumber-js] `metadata` argument not passed. Check your arguments.')
  389. }
  390. // `country_phone_code_to_countries` was renamed to
  391. // `country_calling_codes` in `1.0.18`.
  392. if (!isObject(metadata) || !isObject(metadata.countries)) {
  393. throw new Error(`[libphonenumber-js] \`metadata\` argument was passed but it's not a valid metadata. Must be an object having \`.countries\` child object property. Got ${isObject(metadata) ? 'an object of shape: { ' + Object.keys(metadata).join(', ') + ' }' : 'a ' + typeOf(metadata) + ': ' + metadata}.`)
  394. }
  395. }
  396. // Babel transforms `typeof` into some "branches"
  397. // so istanbul will show this as "branch not covered".
  398. /* istanbul ignore next */
  399. const typeOf = _ => typeof _
  400. /**
  401. * Returns extension prefix for a country.
  402. * @param {string} country
  403. * @param {object} metadata
  404. * @return {string?}
  405. * @example
  406. * // Returns " ext. "
  407. * getExtPrefix("US")
  408. */
  409. export function getExtPrefix(country, metadata) {
  410. metadata = new Metadata(metadata)
  411. if (metadata.hasCountry(country)) {
  412. return metadata.country(country).ext()
  413. }
  414. return DEFAULT_EXT_PREFIX
  415. }
  416. /**
  417. * Returns "country calling code" for a country.
  418. * Throws an error if the country doesn't exist or isn't supported by this library.
  419. * @param {string} country
  420. * @param {object} metadata
  421. * @return {string}
  422. * @example
  423. * // Returns "44"
  424. * getCountryCallingCode("GB")
  425. */
  426. export function getCountryCallingCode(country, metadata) {
  427. metadata = new Metadata(metadata)
  428. if (metadata.hasCountry(country)) {
  429. return metadata.country(country).countryCallingCode()
  430. }
  431. throw new Error(`Unknown country: ${country}`)
  432. }
  433. export function isSupportedCountry(country, metadata) {
  434. // metadata = new Metadata(metadata)
  435. // return metadata.hasCountry(country)
  436. return metadata.countries.hasOwnProperty(country)
  437. }
  438. function setVersion(metadata) {
  439. const { version } = metadata
  440. if (typeof version === 'number') {
  441. this.v1 = version === 1
  442. this.v2 = version === 2
  443. this.v3 = version === 3
  444. this.v4 = version === 4
  445. } else {
  446. if (!version) {
  447. this.v1 = true
  448. } else if (compare(version, V3) === -1) {
  449. this.v2 = true
  450. } else if (compare(version, V4) === -1) {
  451. this.v3 = true
  452. } else {
  453. this.v4 = true
  454. }
  455. }
  456. }
  457. // const ISO_COUNTRY_CODE = /^[A-Z]{2}$/
  458. // function isCountryCode(countryCode) {
  459. // return ISO_COUNTRY_CODE.test(countryCodeOrCountryCallingCode)
  460. // }