format.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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 matchesEntirely from './helpers/matchesEntirely.js'
  6. import formatNationalNumberUsingFormat from './helpers/formatNationalNumberUsingFormat.js'
  7. import Metadata, { getCountryCallingCode } from './metadata.js'
  8. import getIddPrefix from './helpers/getIddPrefix.js'
  9. import { formatRFC3966 } from './helpers/RFC3966.js'
  10. const DEFAULT_OPTIONS = {
  11. formatExtension: (formattedNumber, extension, metadata) => `${formattedNumber}${metadata.ext()}${extension}`
  12. }
  13. /**
  14. * Formats a phone number.
  15. *
  16. * format(phoneNumberInstance, 'INTERNATIONAL', { ..., v2: true }, metadata)
  17. * format(phoneNumberInstance, 'NATIONAL', { ..., v2: true }, metadata)
  18. *
  19. * format({ phone: '8005553535', country: 'RU' }, 'INTERNATIONAL', { ... }, metadata)
  20. * format({ phone: '8005553535', country: 'RU' }, 'NATIONAL', undefined, metadata)
  21. *
  22. * @param {object|PhoneNumber} input — If `options.v2: true` flag is passed, the `input` should be a `PhoneNumber` instance. Otherwise, it should be an object of shape `{ phone: '...', country: '...' }`.
  23. * @param {string} format
  24. * @param {object} [options]
  25. * @param {object} metadata
  26. * @return {string}
  27. */
  28. export default function formatNumber(input, format, options, metadata) {
  29. // Apply default options.
  30. if (options) {
  31. options = { ...DEFAULT_OPTIONS, ...options }
  32. } else {
  33. options = DEFAULT_OPTIONS
  34. }
  35. metadata = new Metadata(metadata)
  36. if (input.country && input.country !== '001') {
  37. // Validate `input.country`.
  38. if (!metadata.hasCountry(input.country)) {
  39. throw new Error(`Unknown country: ${input.country}`)
  40. }
  41. metadata.country(input.country)
  42. }
  43. else if (input.countryCallingCode) {
  44. metadata.selectNumberingPlan(input.countryCallingCode)
  45. }
  46. else return input.phone || ''
  47. const countryCallingCode = metadata.countryCallingCode()
  48. const nationalNumber = options.v2 ? input.nationalNumber : input.phone
  49. // This variable should have been declared inside `case`s
  50. // but Babel has a bug and it says "duplicate variable declaration".
  51. let number
  52. switch (format) {
  53. case 'NATIONAL':
  54. // Legacy argument support.
  55. // (`{ country: ..., phone: '' }`)
  56. if (!nationalNumber) {
  57. return ''
  58. }
  59. number = formatNationalNumber(nationalNumber, input.carrierCode, 'NATIONAL', metadata, options)
  60. return addExtension(number, input.ext, metadata, options.formatExtension)
  61. case 'INTERNATIONAL':
  62. // Legacy argument support.
  63. // (`{ country: ..., phone: '' }`)
  64. if (!nationalNumber) {
  65. return `+${countryCallingCode}`
  66. }
  67. number = formatNationalNumber(nationalNumber, null, 'INTERNATIONAL', metadata, options)
  68. number = `+${countryCallingCode} ${number}`
  69. return addExtension(number, input.ext, metadata, options.formatExtension)
  70. case 'E.164':
  71. // `E.164` doesn't define "phone number extensions".
  72. return `+${countryCallingCode}${nationalNumber}`
  73. case 'RFC3966':
  74. return formatRFC3966({
  75. number: `+${countryCallingCode}${nationalNumber}`,
  76. ext: input.ext
  77. })
  78. // For reference, here's Google's IDD formatter:
  79. // https://github.com/google/libphonenumber/blob/32719cf74e68796788d1ca45abc85dcdc63ba5b9/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L1546
  80. // Not saying that this IDD formatter replicates it 1:1, but it seems to work.
  81. // Who would even need to format phone numbers in IDD format anyway?
  82. case 'IDD':
  83. if (!options.fromCountry) {
  84. return
  85. // throw new Error('`fromCountry` option not passed for IDD-prefixed formatting.')
  86. }
  87. const formattedNumber = formatIDD(
  88. nationalNumber,
  89. input.carrierCode,
  90. countryCallingCode,
  91. options.fromCountry,
  92. metadata
  93. )
  94. return addExtension(formattedNumber, input.ext, metadata, options.formatExtension)
  95. default:
  96. throw new Error(`Unknown "format" argument passed to "formatNumber()": "${format}"`)
  97. }
  98. }
  99. function formatNationalNumber(number, carrierCode, formatAs, metadata, options) {
  100. const format = chooseFormatForNumber(metadata.formats(), number)
  101. if (!format) {
  102. return number
  103. }
  104. return formatNationalNumberUsingFormat(
  105. number,
  106. format,
  107. {
  108. useInternationalFormat: formatAs === 'INTERNATIONAL',
  109. withNationalPrefix: format.nationalPrefixIsOptionalWhenFormattingInNationalFormat() && (options && options.nationalPrefix === false) ? false : true,
  110. carrierCode,
  111. metadata
  112. }
  113. )
  114. }
  115. export function chooseFormatForNumber(availableFormats, nationalNnumber) {
  116. for (const format of availableFormats) {
  117. // Validate leading digits.
  118. // The test case for "else path" could be found by searching for
  119. // "format.leadingDigitsPatterns().length === 0".
  120. if (format.leadingDigitsPatterns().length > 0) {
  121. // The last leading_digits_pattern is used here, as it is the most detailed
  122. const lastLeadingDigitsPattern = format.leadingDigitsPatterns()[format.leadingDigitsPatterns().length - 1]
  123. // If leading digits don't match then move on to the next phone number format
  124. if (nationalNnumber.search(lastLeadingDigitsPattern) !== 0) {
  125. continue
  126. }
  127. }
  128. // Check that the national number matches the phone number format regular expression
  129. if (matchesEntirely(nationalNnumber, format.pattern())) {
  130. return format
  131. }
  132. }
  133. }
  134. function addExtension(formattedNumber, ext, metadata, formatExtension) {
  135. return ext ? formatExtension(formattedNumber, ext, metadata) : formattedNumber
  136. }
  137. function formatIDD(
  138. nationalNumber,
  139. carrierCode,
  140. countryCallingCode,
  141. fromCountry,
  142. metadata
  143. ) {
  144. const fromCountryCallingCode = getCountryCallingCode(fromCountry, metadata.metadata)
  145. // When calling within the same country calling code.
  146. if (fromCountryCallingCode === countryCallingCode) {
  147. const formattedNumber = formatNationalNumber(nationalNumber, carrierCode, 'NATIONAL', metadata)
  148. // For NANPA regions, return the national format for these regions
  149. // but prefix it with the country calling code.
  150. if (countryCallingCode === '1') {
  151. return countryCallingCode + ' ' + formattedNumber
  152. }
  153. // If regions share a country calling code, the country calling code need
  154. // not be dialled. This also applies when dialling within a region, so this
  155. // if clause covers both these cases. Technically this is the case for
  156. // dialling from La Reunion to other overseas departments of France (French
  157. // Guiana, Martinique, Guadeloupe), but not vice versa - so we don't cover
  158. // this edge case for now and for those cases return the version including
  159. // country calling code. Details here:
  160. // http://www.petitfute.com/voyage/225-info-pratiques-reunion
  161. //
  162. return formattedNumber
  163. }
  164. const iddPrefix = getIddPrefix(fromCountry, undefined, metadata.metadata)
  165. if (iddPrefix) {
  166. return `${iddPrefix} ${countryCallingCode} ${formatNationalNumber(nationalNumber, null, 'INTERNATIONAL', metadata)}`
  167. }
  168. }