You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					184 lines
				
				6.7 KiB
			
		
		
			
		
	
	
					184 lines
				
				6.7 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								// This is a port of Google Android `libphonenumber`'s
							 | 
						||
| 
								 | 
							
								// `phonenumberutil.js` of December 31th, 2018.
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// https://github.com/googlei18n/libphonenumber/commits/master/javascript/i18n/phonenumbers/phonenumberutil.js
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import matchesEntirely from './helpers/matchesEntirely.js'
							 | 
						||
| 
								 | 
							
								import formatNationalNumberUsingFormat from './helpers/formatNationalNumberUsingFormat.js'
							 | 
						||
| 
								 | 
							
								import Metadata, { getCountryCallingCode } from './metadata.js'
							 | 
						||
| 
								 | 
							
								import getIddPrefix from './helpers/getIddPrefix.js'
							 | 
						||
| 
								 | 
							
								import { formatRFC3966 } from './helpers/RFC3966.js'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const DEFAULT_OPTIONS = {
							 | 
						||
| 
								 | 
							
									formatExtension: (formattedNumber, extension, metadata) => `${formattedNumber}${metadata.ext()}${extension}`
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Formats a phone number
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// Example use cases:
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// ```js
							 | 
						||
| 
								 | 
							
								// formatNumber('8005553535', 'RU', 'INTERNATIONAL')
							 | 
						||
| 
								 | 
							
								// formatNumber('8005553535', 'RU', 'INTERNATIONAL', metadata)
							 | 
						||
| 
								 | 
							
								// formatNumber({ phone: '8005553535', country: 'RU' }, 'INTERNATIONAL')
							 | 
						||
| 
								 | 
							
								// formatNumber({ phone: '8005553535', country: 'RU' }, 'INTERNATIONAL', metadata)
							 | 
						||
| 
								 | 
							
								// formatNumber('+78005553535', 'NATIONAL')
							 | 
						||
| 
								 | 
							
								// formatNumber('+78005553535', 'NATIONAL', metadata)
							 | 
						||
| 
								 | 
							
								// ```
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								export default function formatNumber(input, format, options, metadata) {
							 | 
						||
| 
								 | 
							
									// Apply default options.
							 | 
						||
| 
								 | 
							
									if (options) {
							 | 
						||
| 
								 | 
							
										options = { ...DEFAULT_OPTIONS, ...options }
							 | 
						||
| 
								 | 
							
									} else {
							 | 
						||
| 
								 | 
							
										options = DEFAULT_OPTIONS
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									metadata = new Metadata(metadata)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if (input.country && input.country !== '001') {
							 | 
						||
| 
								 | 
							
										// Validate `input.country`.
							 | 
						||
| 
								 | 
							
										if (!metadata.hasCountry(input.country)) {
							 | 
						||
| 
								 | 
							
											throw new Error(`Unknown country: ${input.country}`)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
										metadata.country(input.country)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									else if (input.countryCallingCode) {
							 | 
						||
| 
								 | 
							
										metadata.selectNumberingPlan(input.countryCallingCode)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									else return input.phone || ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									const countryCallingCode = metadata.countryCallingCode()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									const nationalNumber = options.v2 ? input.nationalNumber : input.phone
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// This variable should have been declared inside `case`s
							 | 
						||
| 
								 | 
							
									// but Babel has a bug and it says "duplicate variable declaration".
							 | 
						||
| 
								 | 
							
									let number
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									switch (format) {
							 | 
						||
| 
								 | 
							
										case 'NATIONAL':
							 | 
						||
| 
								 | 
							
											// Legacy argument support.
							 | 
						||
| 
								 | 
							
											// (`{ country: ..., phone: '' }`)
							 | 
						||
| 
								 | 
							
											if (!nationalNumber) {
							 | 
						||
| 
								 | 
							
												return ''
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
											number = formatNationalNumber(nationalNumber, input.carrierCode, 'NATIONAL', metadata, options)
							 | 
						||
| 
								 | 
							
											return addExtension(number, input.ext, metadata, options.formatExtension)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										case 'INTERNATIONAL':
							 | 
						||
| 
								 | 
							
											// Legacy argument support.
							 | 
						||
| 
								 | 
							
											// (`{ country: ..., phone: '' }`)
							 | 
						||
| 
								 | 
							
											if (!nationalNumber) {
							 | 
						||
| 
								 | 
							
												return `+${countryCallingCode}`
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
											number = formatNationalNumber(nationalNumber, null, 'INTERNATIONAL', metadata, options)
							 | 
						||
| 
								 | 
							
											number = `+${countryCallingCode} ${number}`
							 | 
						||
| 
								 | 
							
											return addExtension(number, input.ext, metadata, options.formatExtension)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										case 'E.164':
							 | 
						||
| 
								 | 
							
											// `E.164` doesn't define "phone number extensions".
							 | 
						||
| 
								 | 
							
											return `+${countryCallingCode}${nationalNumber}`
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										case 'RFC3966':
							 | 
						||
| 
								 | 
							
											return formatRFC3966({
							 | 
						||
| 
								 | 
							
												number: `+${countryCallingCode}${nationalNumber}`,
							 | 
						||
| 
								 | 
							
												ext: input.ext
							 | 
						||
| 
								 | 
							
											})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// For reference, here's Google's IDD formatter:
							 | 
						||
| 
								 | 
							
										// https://github.com/google/libphonenumber/blob/32719cf74e68796788d1ca45abc85dcdc63ba5b9/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L1546
							 | 
						||
| 
								 | 
							
										// Not saying that this IDD formatter replicates it 1:1, but it seems to work.
							 | 
						||
| 
								 | 
							
										// Who would even need to format phone numbers in IDD format anyway?
							 | 
						||
| 
								 | 
							
										case 'IDD':
							 | 
						||
| 
								 | 
							
											if (!options.fromCountry) {
							 | 
						||
| 
								 | 
							
												return
							 | 
						||
| 
								 | 
							
												// throw new Error('`fromCountry` option not passed for IDD-prefixed formatting.')
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
											const formattedNumber = formatIDD(
							 | 
						||
| 
								 | 
							
												nationalNumber,
							 | 
						||
| 
								 | 
							
												input.carrierCode,
							 | 
						||
| 
								 | 
							
												countryCallingCode,
							 | 
						||
| 
								 | 
							
												options.fromCountry,
							 | 
						||
| 
								 | 
							
												metadata
							 | 
						||
| 
								 | 
							
											)
							 | 
						||
| 
								 | 
							
											return addExtension(formattedNumber, input.ext, metadata, options.formatExtension)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										default:
							 | 
						||
| 
								 | 
							
											throw new Error(`Unknown "format" argument passed to "formatNumber()": "${format}"`)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function formatNationalNumber(number, carrierCode, formatAs, metadata, options) {
							 | 
						||
| 
								 | 
							
									const format = chooseFormatForNumber(metadata.formats(), number)
							 | 
						||
| 
								 | 
							
									if (!format) {
							 | 
						||
| 
								 | 
							
										return number
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									return formatNationalNumberUsingFormat(
							 | 
						||
| 
								 | 
							
										number,
							 | 
						||
| 
								 | 
							
										format,
							 | 
						||
| 
								 | 
							
										{
							 | 
						||
| 
								 | 
							
											useInternationalFormat: formatAs === 'INTERNATIONAL',
							 | 
						||
| 
								 | 
							
											withNationalPrefix: format.nationalPrefixIsOptionalWhenFormattingInNationalFormat() && (options && options.nationalPrefix === false) ? false : true,
							 | 
						||
| 
								 | 
							
											carrierCode,
							 | 
						||
| 
								 | 
							
											metadata
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
									)
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function chooseFormatForNumber(availableFormats, nationalNnumber) {
							 | 
						||
| 
								 | 
							
									for (const format of availableFormats) {
							 | 
						||
| 
								 | 
							
										// Validate leading digits.
							 | 
						||
| 
								 | 
							
										// The test case for "else path" could be found by searching for
							 | 
						||
| 
								 | 
							
										// "format.leadingDigitsPatterns().length === 0".
							 | 
						||
| 
								 | 
							
										if (format.leadingDigitsPatterns().length > 0) {
							 | 
						||
| 
								 | 
							
											// The last leading_digits_pattern is used here, as it is the most detailed
							 | 
						||
| 
								 | 
							
											const lastLeadingDigitsPattern = format.leadingDigitsPatterns()[format.leadingDigitsPatterns().length - 1]
							 | 
						||
| 
								 | 
							
											// If leading digits don't match then move on to the next phone number format
							 | 
						||
| 
								 | 
							
											if (nationalNnumber.search(lastLeadingDigitsPattern) !== 0) {
							 | 
						||
| 
								 | 
							
												continue
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
										// Check that the national number matches the phone number format regular expression
							 | 
						||
| 
								 | 
							
										if (matchesEntirely(nationalNnumber, format.pattern())) {
							 | 
						||
| 
								 | 
							
											return format
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function addExtension(formattedNumber, ext, metadata, formatExtension) {
							 | 
						||
| 
								 | 
							
									return ext ? formatExtension(formattedNumber, ext, metadata) : formattedNumber
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function formatIDD(
							 | 
						||
| 
								 | 
							
									nationalNumber,
							 | 
						||
| 
								 | 
							
									carrierCode,
							 | 
						||
| 
								 | 
							
									countryCallingCode,
							 | 
						||
| 
								 | 
							
									fromCountry,
							 | 
						||
| 
								 | 
							
									metadata
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
									const fromCountryCallingCode = getCountryCallingCode(fromCountry, metadata.metadata)
							 | 
						||
| 
								 | 
							
									// When calling within the same country calling code.
							 | 
						||
| 
								 | 
							
									if (fromCountryCallingCode === countryCallingCode) {
							 | 
						||
| 
								 | 
							
										const formattedNumber = formatNationalNumber(nationalNumber, carrierCode, 'NATIONAL', metadata)
							 | 
						||
| 
								 | 
							
										// For NANPA regions, return the national format for these regions
							 | 
						||
| 
								 | 
							
										// but prefix it with the country calling code.
							 | 
						||
| 
								 | 
							
										if (countryCallingCode === '1') {
							 | 
						||
| 
								 | 
							
											return countryCallingCode + ' ' + formattedNumber
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
										// If regions share a country calling code, the country calling code need
							 | 
						||
| 
								 | 
							
										// not be dialled. This also applies when dialling within a region, so this
							 | 
						||
| 
								 | 
							
										// if clause covers both these cases. Technically this is the case for
							 | 
						||
| 
								 | 
							
										// dialling from La Reunion to other overseas departments of France (French
							 | 
						||
| 
								 | 
							
										// Guiana, Martinique, Guadeloupe), but not vice versa - so we don't cover
							 | 
						||
| 
								 | 
							
										// this edge case for now and for those cases return the version including
							 | 
						||
| 
								 | 
							
										// country calling code. Details here:
							 | 
						||
| 
								 | 
							
										// http://www.petitfute.com/voyage/225-info-pratiques-reunion
							 | 
						||
| 
								 | 
							
										//
							 | 
						||
| 
								 | 
							
										return formattedNumber
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									const iddPrefix = getIddPrefix(fromCountry, undefined, metadata.metadata)
							 | 
						||
| 
								 | 
							
									if (iddPrefix) {
							 | 
						||
| 
								 | 
							
										return `${iddPrefix} ${countryCallingCode} ${formatNationalNumber(nationalNumber, null, 'INTERNATIONAL', metadata)}`
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								}
							 |