backend v4 half
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
import isViablePhoneNumber from './isViablePhoneNumber.js'
|
||||
|
||||
// https://www.ietf.org/rfc/rfc3966.txt
|
||||
|
||||
/**
|
||||
* @param {string} text - Phone URI (RFC 3966).
|
||||
* @return {object} `{ ?number, ?ext }`.
|
||||
*/
|
||||
export function parseRFC3966(text) {
|
||||
let number
|
||||
let ext
|
||||
|
||||
// Replace "tel:" with "tel=" for parsing convenience.
|
||||
text = text.replace(/^tel:/, 'tel=')
|
||||
|
||||
for (const part of text.split(';')) {
|
||||
const [name, value] = part.split('=')
|
||||
switch (name) {
|
||||
case 'tel':
|
||||
number = value
|
||||
break
|
||||
case 'ext':
|
||||
ext = value
|
||||
break
|
||||
case 'phone-context':
|
||||
// Only "country contexts" are supported.
|
||||
// "Domain contexts" are ignored.
|
||||
if (value[0] === '+') {
|
||||
number = value + number
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the phone number is not viable, then abort.
|
||||
if (!isViablePhoneNumber(number)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const result = { number }
|
||||
if (ext) {
|
||||
result.ext = ext
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} - `{ ?number, ?extension }`.
|
||||
* @return {string} Phone URI (RFC 3966).
|
||||
*/
|
||||
export function formatRFC3966({ number, ext }) {
|
||||
if (!number) {
|
||||
return ''
|
||||
}
|
||||
if (number[0] !== '+') {
|
||||
throw new Error(`"formatRFC3966()" expects "number" to be in E.164 format.`)
|
||||
}
|
||||
return `tel:${number}${ext ? ';ext=' + ext : ''}`
|
||||
}
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { parseRFC3966, formatRFC3966 } from './RFC3966.js'
|
||||
|
||||
describe('RFC3966', () => {
|
||||
it('should format', () => {
|
||||
expect(() => formatRFC3966({ number: '123' })).to.throw('expects "number" to be in E.164 format')
|
||||
formatRFC3966({}).should.equal('')
|
||||
formatRFC3966({ number: '+78005553535' }).should.equal('tel:+78005553535')
|
||||
formatRFC3966({ number: '+78005553535', ext: '123' }).should.equal('tel:+78005553535;ext=123')
|
||||
})
|
||||
|
||||
it('should parse', () => {
|
||||
parseRFC3966('tel:+78005553535').should.deep.equal({
|
||||
number : '+78005553535'
|
||||
})
|
||||
|
||||
parseRFC3966('tel:+78005553535;ext=123').should.deep.equal({
|
||||
number : '+78005553535',
|
||||
ext : '123'
|
||||
})
|
||||
|
||||
// With `phone-context`
|
||||
parseRFC3966('tel:8005553535;ext=123;phone-context=+7').should.deep.equal({
|
||||
number : '+78005553535',
|
||||
ext : '123'
|
||||
})
|
||||
|
||||
// "Domain contexts" are ignored
|
||||
parseRFC3966('tel:8005553535;ext=123;phone-context=www.leningrad.spb.ru').should.deep.equal({
|
||||
number : '8005553535',
|
||||
ext : '123'
|
||||
})
|
||||
|
||||
// Not a viable phone number.
|
||||
parseRFC3966('tel:3').should.deep.equal({})
|
||||
})
|
||||
})
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
import { VALID_PUNCTUATION } from '../constants.js'
|
||||
|
||||
// Removes brackets and replaces dashes with spaces.
|
||||
//
|
||||
// E.g. "(999) 111-22-33" -> "999 111 22 33"
|
||||
//
|
||||
// For some reason Google's metadata contains `<intlFormat/>`s with brackets and dashes.
|
||||
// Meanwhile, there's no single opinion about using punctuation in international phone numbers.
|
||||
//
|
||||
// For example, Google's `<intlFormat/>` for USA is `+1 213-373-4253`.
|
||||
// And here's a quote from WikiPedia's "North American Numbering Plan" page:
|
||||
// https://en.wikipedia.org/wiki/North_American_Numbering_Plan
|
||||
//
|
||||
// "The country calling code for all countries participating in the NANP is 1.
|
||||
// In international format, an NANP number should be listed as +1 301 555 01 00,
|
||||
// where 301 is an area code (Maryland)."
|
||||
//
|
||||
// I personally prefer the international format without any punctuation.
|
||||
// For example, brackets are remnants of the old age, meaning that the
|
||||
// phone number part in brackets (so called "area code") can be omitted
|
||||
// if dialing within the same "area".
|
||||
// And hyphens were clearly introduced for splitting local numbers into memorizable groups.
|
||||
// For example, remembering "5553535" is difficult but "555-35-35" is much simpler.
|
||||
// Imagine a man taking a bus from home to work and seeing an ad with a phone number.
|
||||
// He has a couple of seconds to memorize that number until it passes by.
|
||||
// If it were spaces instead of hyphens the man wouldn't necessarily get it,
|
||||
// but with hyphens instead of spaces the grouping is more explicit.
|
||||
// I personally think that hyphens introduce visual clutter,
|
||||
// so I prefer replacing them with spaces in international numbers.
|
||||
// In the modern age all output is done on displays where spaces are clearly distinguishable
|
||||
// so hyphens can be safely replaced with spaces without losing any legibility.
|
||||
//
|
||||
export default function applyInternationalSeparatorStyle(formattedNumber) {
|
||||
return formattedNumber.replace(new RegExp(`[${VALID_PUNCTUATION}]+`, 'g'), ' ').trim()
|
||||
}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import applyInternationalSeparatorStyle from './applyInternationalSeparatorStyle.js'
|
||||
|
||||
describe('applyInternationalSeparatorStyle', () => {
|
||||
it('should change Google\'s international format style', () => {
|
||||
applyInternationalSeparatorStyle('(xxx) xxx-xx-xx').should.equal('xxx xxx xx xx')
|
||||
applyInternationalSeparatorStyle('(xxx)xxx').should.equal('xxx xxx')
|
||||
})
|
||||
})
|
||||
Generated
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
import mergeArrays from './mergeArrays.js'
|
||||
|
||||
export default function checkNumberLength(nationalNumber, metadata) {
|
||||
return checkNumberLengthForType(nationalNumber, undefined, metadata)
|
||||
}
|
||||
|
||||
// Checks whether a number is possible for the country based on its length.
|
||||
// Should only be called for the "new" metadata which has "possible lengths".
|
||||
export function checkNumberLengthForType(nationalNumber, type, metadata) {
|
||||
const type_info = metadata.type(type)
|
||||
|
||||
// There should always be "<possiblePengths/>" set for every type element.
|
||||
// This is declared in the XML schema.
|
||||
// For size efficiency, where a sub-description (e.g. fixed-line)
|
||||
// has the same "<possiblePengths/>" as the "general description", this is missing,
|
||||
// so we fall back to the "general description". Where no numbers of the type
|
||||
// exist at all, there is one possible length (-1) which is guaranteed
|
||||
// not to match the length of any real phone number.
|
||||
let possible_lengths = type_info && type_info.possibleLengths() || metadata.possibleLengths()
|
||||
// let local_lengths = type_info && type.possibleLengthsLocal() || metadata.possibleLengthsLocal()
|
||||
|
||||
// Metadata before version `1.0.18` didn't contain `possible_lengths`.
|
||||
if (!possible_lengths) {
|
||||
return 'IS_POSSIBLE'
|
||||
}
|
||||
|
||||
if (type === 'FIXED_LINE_OR_MOBILE') {
|
||||
// No such country in metadata.
|
||||
/* istanbul ignore next */
|
||||
if (!metadata.type('FIXED_LINE')) {
|
||||
// The rare case has been encountered where no fixedLine data is available
|
||||
// (true for some non-geographic entities), so we just check mobile.
|
||||
return checkNumberLengthForType(nationalNumber, 'MOBILE', metadata)
|
||||
}
|
||||
|
||||
const mobile_type = metadata.type('MOBILE')
|
||||
if (mobile_type) {
|
||||
// Merge the mobile data in if there was any. "Concat" creates a new
|
||||
// array, it doesn't edit possible_lengths in place, so we don't need a copy.
|
||||
// Note that when adding the possible lengths from mobile, we have
|
||||
// to again check they aren't empty since if they are this indicates
|
||||
// they are the same as the general desc and should be obtained from there.
|
||||
possible_lengths = mergeArrays(possible_lengths, mobile_type.possibleLengths())
|
||||
// The current list is sorted; we need to merge in the new list and
|
||||
// re-sort (duplicates are okay). Sorting isn't so expensive because
|
||||
// the lists are very small.
|
||||
|
||||
// if (local_lengths) {
|
||||
// local_lengths = mergeArrays(local_lengths, mobile_type.possibleLengthsLocal())
|
||||
// } else {
|
||||
// local_lengths = mobile_type.possibleLengthsLocal()
|
||||
// }
|
||||
}
|
||||
}
|
||||
// If the type doesn't exist then return 'INVALID_LENGTH'.
|
||||
else if (type && !type_info) {
|
||||
return 'INVALID_LENGTH'
|
||||
}
|
||||
|
||||
const actual_length = nationalNumber.length
|
||||
|
||||
// In `libphonenumber-js` all "local-only" formats are dropped for simplicity.
|
||||
// // This is safe because there is never an overlap beween the possible lengths
|
||||
// // and the local-only lengths; this is checked at build time.
|
||||
// if (local_lengths && local_lengths.indexOf(nationalNumber.length) >= 0)
|
||||
// {
|
||||
// return 'IS_POSSIBLE_LOCAL_ONLY'
|
||||
// }
|
||||
|
||||
const minimum_length = possible_lengths[0]
|
||||
|
||||
if (minimum_length === actual_length) {
|
||||
return 'IS_POSSIBLE'
|
||||
}
|
||||
|
||||
if (minimum_length > actual_length) {
|
||||
return 'TOO_SHORT'
|
||||
}
|
||||
|
||||
if (possible_lengths[possible_lengths.length - 1] < actual_length) {
|
||||
return 'TOO_LONG'
|
||||
}
|
||||
|
||||
// We skip the first element since we've already checked it.
|
||||
return possible_lengths.indexOf(actual_length, 1) >= 0 ? 'IS_POSSIBLE' : 'INVALID_LENGTH'
|
||||
}
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
import Metadata from '../metadata.js'
|
||||
import metadata from '../../metadata.max.json' assert { type: 'json' }
|
||||
import oldMetadata from '../../test/metadata/1.0.0/metadata.min.json' assert { type: 'json' }
|
||||
|
||||
import { checkNumberLengthForType } from './checkNumberLength.js'
|
||||
|
||||
describe('checkNumberLength', () => {
|
||||
it('should check phone number length', () => {
|
||||
// Too short.
|
||||
checkNumberLength('800555353', 'FIXED_LINE', 'RU').should.equal('TOO_SHORT')
|
||||
// Normal.
|
||||
checkNumberLength('8005553535', 'FIXED_LINE', 'RU').should.equal('IS_POSSIBLE')
|
||||
// Too long.
|
||||
checkNumberLength('80055535355', 'FIXED_LINE', 'RU').should.equal('TOO_LONG')
|
||||
|
||||
// No such type.
|
||||
checkNumberLength('169454850', 'VOIP', 'AC').should.equal('INVALID_LENGTH')
|
||||
// No such possible length.
|
||||
checkNumberLength('1694548', undefined, 'AD').should.equal('INVALID_LENGTH')
|
||||
|
||||
// FIXED_LINE_OR_MOBILE
|
||||
checkNumberLength('1694548', 'FIXED_LINE_OR_MOBILE', 'AD').should.equal('INVALID_LENGTH')
|
||||
// No mobile phones.
|
||||
checkNumberLength('8123', 'FIXED_LINE_OR_MOBILE', 'TA').should.equal('IS_POSSIBLE')
|
||||
// No "possible lengths" for "mobile".
|
||||
checkNumberLength('81234567', 'FIXED_LINE_OR_MOBILE', 'SZ').should.equal('IS_POSSIBLE')
|
||||
})
|
||||
|
||||
it('should work for old metadata', function() {
|
||||
const _oldMetadata = new Metadata(oldMetadata)
|
||||
_oldMetadata.country('RU')
|
||||
checkNumberLengthForType('8005553535', 'FIXED_LINE', _oldMetadata).should.equal('IS_POSSIBLE')
|
||||
})
|
||||
})
|
||||
|
||||
function checkNumberLength(number, type, country) {
|
||||
const _metadata = new Metadata(metadata)
|
||||
_metadata.country(country)
|
||||
return checkNumberLengthForType(number, type, _metadata)
|
||||
}
|
||||
Generated
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
import { VALID_DIGITS } from '../../constants.js'
|
||||
|
||||
// The RFC 3966 format for extensions.
|
||||
const RFC3966_EXTN_PREFIX = ';ext='
|
||||
|
||||
/**
|
||||
* Helper method for constructing regular expressions for parsing. Creates
|
||||
* an expression that captures up to max_length digits.
|
||||
* @return {string} RegEx pattern to capture extension digits.
|
||||
*/
|
||||
const getExtensionDigitsPattern = (maxLength) => `([${VALID_DIGITS}]{1,${maxLength}})`
|
||||
|
||||
/**
|
||||
* Helper initialiser method to create the regular-expression pattern to match
|
||||
* extensions.
|
||||
* Copy-pasted from Google's `libphonenumber`:
|
||||
* https://github.com/google/libphonenumber/blob/55b2646ec9393f4d3d6661b9c82ef9e258e8b829/javascript/i18n/phonenumbers/phonenumberutil.js#L759-L766
|
||||
* @return {string} RegEx pattern to capture extensions.
|
||||
*/
|
||||
export default function createExtensionPattern(purpose) {
|
||||
// We cap the maximum length of an extension based on the ambiguity of the way
|
||||
// the extension is prefixed. As per ITU, the officially allowed length for
|
||||
// extensions is actually 40, but we don't support this since we haven't seen real
|
||||
// examples and this introduces many false interpretations as the extension labels
|
||||
// are not standardized.
|
||||
/** @type {string} */
|
||||
var extLimitAfterExplicitLabel = '20';
|
||||
/** @type {string} */
|
||||
var extLimitAfterLikelyLabel = '15';
|
||||
/** @type {string} */
|
||||
var extLimitAfterAmbiguousChar = '9';
|
||||
/** @type {string} */
|
||||
var extLimitWhenNotSure = '6';
|
||||
|
||||
/** @type {string} */
|
||||
var possibleSeparatorsBetweenNumberAndExtLabel = "[ \u00A0\\t,]*";
|
||||
// Optional full stop (.) or colon, followed by zero or more spaces/tabs/commas.
|
||||
/** @type {string} */
|
||||
var possibleCharsAfterExtLabel = "[:\\.\uFF0E]?[ \u00A0\\t,-]*";
|
||||
/** @type {string} */
|
||||
var optionalExtnSuffix = "#?";
|
||||
|
||||
// Here the extension is called out in more explicit way, i.e mentioning it obvious
|
||||
// patterns like "ext.".
|
||||
/** @type {string} */
|
||||
var explicitExtLabels =
|
||||
"(?:e?xt(?:ensi(?:o\u0301?|\u00F3))?n?|\uFF45?\uFF58\uFF54\uFF4E?|\u0434\u043E\u0431|anexo)";
|
||||
// One-character symbols that can be used to indicate an extension, and less
|
||||
// commonly used or more ambiguous extension labels.
|
||||
/** @type {string} */
|
||||
var ambiguousExtLabels = "(?:[x\uFF58#\uFF03~\uFF5E]|int|\uFF49\uFF4E\uFF54)";
|
||||
// When extension is not separated clearly.
|
||||
/** @type {string} */
|
||||
var ambiguousSeparator = "[- ]+";
|
||||
// This is the same as possibleSeparatorsBetweenNumberAndExtLabel, but not matching
|
||||
// comma as extension label may have it.
|
||||
/** @type {string} */
|
||||
var possibleSeparatorsNumberExtLabelNoComma = "[ \u00A0\\t]*";
|
||||
// ",," is commonly used for auto dialling the extension when connected. First
|
||||
// comma is matched through possibleSeparatorsBetweenNumberAndExtLabel, so we do
|
||||
// not repeat it here. Semi-colon works in Iphone and Android also to pop up a
|
||||
// button with the extension number following.
|
||||
/** @type {string} */
|
||||
var autoDiallingAndExtLabelsFound = "(?:,{2}|;)";
|
||||
|
||||
/** @type {string} */
|
||||
var rfcExtn = RFC3966_EXTN_PREFIX
|
||||
+ getExtensionDigitsPattern(extLimitAfterExplicitLabel);
|
||||
/** @type {string} */
|
||||
var explicitExtn = possibleSeparatorsBetweenNumberAndExtLabel + explicitExtLabels
|
||||
+ possibleCharsAfterExtLabel
|
||||
+ getExtensionDigitsPattern(extLimitAfterExplicitLabel)
|
||||
+ optionalExtnSuffix;
|
||||
/** @type {string} */
|
||||
var ambiguousExtn = possibleSeparatorsBetweenNumberAndExtLabel + ambiguousExtLabels
|
||||
+ possibleCharsAfterExtLabel
|
||||
+ getExtensionDigitsPattern(extLimitAfterAmbiguousChar)
|
||||
+ optionalExtnSuffix;
|
||||
/** @type {string} */
|
||||
var americanStyleExtnWithSuffix = ambiguousSeparator
|
||||
+ getExtensionDigitsPattern(extLimitWhenNotSure) + "#";
|
||||
|
||||
/** @type {string} */
|
||||
var autoDiallingExtn = possibleSeparatorsNumberExtLabelNoComma
|
||||
+ autoDiallingAndExtLabelsFound + possibleCharsAfterExtLabel
|
||||
+ getExtensionDigitsPattern(extLimitAfterLikelyLabel)
|
||||
+ optionalExtnSuffix;
|
||||
/** @type {string} */
|
||||
var onlyCommasExtn = possibleSeparatorsNumberExtLabelNoComma
|
||||
+ "(?:,)+" + possibleCharsAfterExtLabel
|
||||
+ getExtensionDigitsPattern(extLimitAfterAmbiguousChar)
|
||||
+ optionalExtnSuffix;
|
||||
|
||||
// The first regular expression covers RFC 3966 format, where the extension is added
|
||||
// using ";ext=". The second more generic where extension is mentioned with explicit
|
||||
// labels like "ext:". In both the above cases we allow more numbers in extension than
|
||||
// any other extension labels. The third one captures when single character extension
|
||||
// labels or less commonly used labels are used. In such cases we capture fewer
|
||||
// extension digits in order to reduce the chance of falsely interpreting two
|
||||
// numbers beside each other as a number + extension. The fourth one covers the
|
||||
// special case of American numbers where the extension is written with a hash
|
||||
// at the end, such as "- 503#". The fifth one is exclusively for extension
|
||||
// autodialling formats which are used when dialling and in this case we accept longer
|
||||
// extensions. The last one is more liberal on the number of commas that acts as
|
||||
// extension labels, so we have a strict cap on the number of digits in such extensions.
|
||||
return rfcExtn + "|"
|
||||
+ explicitExtn + "|"
|
||||
+ ambiguousExtn + "|"
|
||||
+ americanStyleExtnWithSuffix + "|"
|
||||
+ autoDiallingExtn + "|"
|
||||
+ onlyCommasExtn;
|
||||
}
|
||||
Generated
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
import createExtensionPattern from './createExtensionPattern.js'
|
||||
|
||||
// Regexp of all known extension prefixes used by different regions followed by
|
||||
// 1 or more valid digits, for use when parsing.
|
||||
const EXTN_PATTERN = new RegExp('(?:' + createExtensionPattern() + ')$', 'i')
|
||||
|
||||
// Strips any extension (as in, the part of the number dialled after the call is
|
||||
// connected, usually indicated with extn, ext, x or similar) from the end of
|
||||
// the number, and returns it.
|
||||
export default function extractExtension(number) {
|
||||
const start = number.search(EXTN_PATTERN)
|
||||
if (start < 0) {
|
||||
return {}
|
||||
}
|
||||
// If we find a potential extension, and the number preceding this is a viable
|
||||
// number, we assume it is an extension.
|
||||
const numberWithoutExtension = number.slice(0, start)
|
||||
const matches = number.match(EXTN_PATTERN)
|
||||
let i = 1
|
||||
while (i < matches.length) {
|
||||
if (matches[i]) {
|
||||
return {
|
||||
number: numberWithoutExtension,
|
||||
ext: matches[i]
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+151
@@ -0,0 +1,151 @@
|
||||
import stripIddPrefix from './stripIddPrefix.js'
|
||||
import extractCountryCallingCodeFromInternationalNumberWithoutPlusSign from './extractCountryCallingCodeFromInternationalNumberWithoutPlusSign.js'
|
||||
import Metadata from '../metadata.js'
|
||||
import { MAX_LENGTH_COUNTRY_CODE } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Converts a phone number digits (possibly with a `+`)
|
||||
* into a calling code and the rest phone number digits.
|
||||
* The "rest phone number digits" could include
|
||||
* a national prefix, carrier code, and national
|
||||
* (significant) number.
|
||||
* @param {string} number — Phone number digits (possibly with a `+`).
|
||||
* @param {string} [country] — Default country.
|
||||
* @param {string} [callingCode] — Default calling code (some phone numbering plans are non-geographic).
|
||||
* @param {object} metadata
|
||||
* @return {object} `{ countryCallingCodeSource: string?, countryCallingCode: string?, number: string }`
|
||||
* @example
|
||||
* // Returns `{ countryCallingCode: "1", number: "2133734253" }`.
|
||||
* extractCountryCallingCode('2133734253', 'US', null, metadata)
|
||||
* extractCountryCallingCode('2133734253', null, '1', metadata)
|
||||
* extractCountryCallingCode('+12133734253', null, null, metadata)
|
||||
* extractCountryCallingCode('+12133734253', 'RU', null, metadata)
|
||||
*/
|
||||
export default function extractCountryCallingCode(
|
||||
number,
|
||||
country,
|
||||
callingCode,
|
||||
metadata
|
||||
) {
|
||||
if (!number) {
|
||||
return {}
|
||||
}
|
||||
|
||||
let isNumberWithIddPrefix
|
||||
|
||||
// If this is not an international phone number,
|
||||
// then either extract an "IDD" prefix, or extract a
|
||||
// country calling code from a number by autocorrecting it
|
||||
// by prepending a leading `+` in cases when it starts
|
||||
// with the country calling code.
|
||||
// https://wikitravel.org/en/International_dialling_prefix
|
||||
// https://github.com/catamphetamine/libphonenumber-js/issues/376
|
||||
if (number[0] !== '+') {
|
||||
// Convert an "out-of-country" dialing phone number
|
||||
// to a proper international phone number.
|
||||
const numberWithoutIDD = stripIddPrefix(number, country, callingCode, metadata)
|
||||
// If an IDD prefix was stripped then
|
||||
// convert the number to international one
|
||||
// for subsequent parsing.
|
||||
if (numberWithoutIDD && numberWithoutIDD !== number) {
|
||||
isNumberWithIddPrefix = true
|
||||
number = '+' + numberWithoutIDD
|
||||
} else {
|
||||
// Check to see if the number starts with the country calling code
|
||||
// for the default country. If so, we remove the country calling code,
|
||||
// and do some checks on the validity of the number before and after.
|
||||
// https://github.com/catamphetamine/libphonenumber-js/issues/376
|
||||
if (country || callingCode) {
|
||||
const {
|
||||
countryCallingCode,
|
||||
number: shorterNumber
|
||||
} = extractCountryCallingCodeFromInternationalNumberWithoutPlusSign(
|
||||
number,
|
||||
country,
|
||||
callingCode,
|
||||
metadata
|
||||
)
|
||||
if (countryCallingCode) {
|
||||
return {
|
||||
countryCallingCodeSource: 'FROM_NUMBER_WITHOUT_PLUS_SIGN',
|
||||
countryCallingCode,
|
||||
number: shorterNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
// No need to set it to `UNSPECIFIED`. It can be just `undefined`.
|
||||
// countryCallingCodeSource: 'UNSPECIFIED',
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fast abortion: country codes do not begin with a '0'
|
||||
if (number[1] === '0') {
|
||||
return {}
|
||||
}
|
||||
|
||||
metadata = new Metadata(metadata)
|
||||
|
||||
// The thing with country phone codes
|
||||
// is that they are orthogonal to each other
|
||||
// i.e. there's no such country phone code A
|
||||
// for which country phone code B exists
|
||||
// where B starts with A.
|
||||
// Therefore, while scanning digits,
|
||||
// if a valid country code is found,
|
||||
// that means that it is the country code.
|
||||
//
|
||||
let i = 2
|
||||
while (i - 1 <= MAX_LENGTH_COUNTRY_CODE && i <= number.length) {
|
||||
const countryCallingCode = number.slice(1, i)
|
||||
if (metadata.hasCallingCode(countryCallingCode)) {
|
||||
metadata.selectNumberingPlan(countryCallingCode)
|
||||
return {
|
||||
countryCallingCodeSource: isNumberWithIddPrefix ? 'FROM_NUMBER_WITH_IDD' : 'FROM_NUMBER_WITH_PLUS_SIGN',
|
||||
countryCallingCode,
|
||||
number: number.slice(i)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
// The possible values for the returned `countryCallingCodeSource` are:
|
||||
//
|
||||
// Copy-pasted from:
|
||||
// https://github.com/google/libphonenumber/blob/master/resources/phonenumber.proto
|
||||
//
|
||||
// // The source from which the country_code is derived. This is not set in the
|
||||
// // general parsing method, but in the method that parses and keeps raw_input.
|
||||
// // New fields could be added upon request.
|
||||
// enum CountryCodeSource {
|
||||
// // Default value returned if this is not set, because the phone number was
|
||||
// // created using parse, not parseAndKeepRawInput. hasCountryCodeSource will
|
||||
// // return false if this is the case.
|
||||
// UNSPECIFIED = 0;
|
||||
//
|
||||
// // The country_code is derived based on a phone number with a leading "+",
|
||||
// // e.g. the French number "+33 1 42 68 53 00".
|
||||
// FROM_NUMBER_WITH_PLUS_SIGN = 1;
|
||||
//
|
||||
// // The country_code is derived based on a phone number with a leading IDD,
|
||||
// // e.g. the French number "011 33 1 42 68 53 00", as it is dialled from US.
|
||||
// FROM_NUMBER_WITH_IDD = 5;
|
||||
//
|
||||
// // The country_code is derived based on a phone number without a leading
|
||||
// // "+", e.g. the French number "33 1 42 68 53 00" when defaultCountry is
|
||||
// // supplied as France.
|
||||
// FROM_NUMBER_WITHOUT_PLUS_SIGN = 10;
|
||||
//
|
||||
// // The country_code is derived NOT based on the phone number itself, but
|
||||
// // from the defaultCountry parameter provided in the parsing function by the
|
||||
// // clients. This happens mostly for numbers written in the national format
|
||||
// // (without country code). For example, this would be set when parsing the
|
||||
// // French number "01 42 68 53 00", when defaultCountry is supplied as
|
||||
// // France.
|
||||
// FROM_DEFAULT_COUNTRY = 20;
|
||||
// }
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
import extractCountryCallingCode from './extractCountryCallingCode.js'
|
||||
import metadata from '../../metadata.min.json' assert { type: 'json' }
|
||||
|
||||
describe('extractCountryCallingCode', () => {
|
||||
it('should extract country calling code from a number', () => {
|
||||
extractCountryCallingCode('+78005553535', null, null, metadata).should.deep.equal({
|
||||
countryCallingCodeSource: 'FROM_NUMBER_WITH_PLUS_SIGN',
|
||||
countryCallingCode: '7',
|
||||
number: '8005553535'
|
||||
})
|
||||
|
||||
extractCountryCallingCode('+7800', null, null, metadata).should.deep.equal({
|
||||
countryCallingCodeSource: 'FROM_NUMBER_WITH_PLUS_SIGN',
|
||||
countryCallingCode: '7',
|
||||
number: '800'
|
||||
})
|
||||
|
||||
extractCountryCallingCode('', null, null, metadata).should.deep.equal({})
|
||||
})
|
||||
})
|
||||
Generated
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
import Metadata from '../metadata.js'
|
||||
import matchesEntirely from './matchesEntirely.js'
|
||||
import extractNationalNumber from './extractNationalNumber.js'
|
||||
import checkNumberLength from './checkNumberLength.js'
|
||||
import getCountryCallingCode from '../getCountryCallingCode.js'
|
||||
|
||||
/**
|
||||
* Sometimes some people incorrectly input international phone numbers
|
||||
* without the leading `+`. This function corrects such input.
|
||||
* @param {string} number — Phone number digits.
|
||||
* @param {string?} country
|
||||
* @param {string?} callingCode
|
||||
* @param {object} metadata
|
||||
* @return {object} `{ countryCallingCode: string?, number: string }`.
|
||||
*/
|
||||
export default function extractCountryCallingCodeFromInternationalNumberWithoutPlusSign(
|
||||
number,
|
||||
country,
|
||||
callingCode,
|
||||
metadata
|
||||
) {
|
||||
const countryCallingCode = country ? getCountryCallingCode(country, metadata) : callingCode
|
||||
if (number.indexOf(countryCallingCode) === 0) {
|
||||
metadata = new Metadata(metadata)
|
||||
metadata.selectNumberingPlan(country, callingCode)
|
||||
const possibleShorterNumber = number.slice(countryCallingCode.length)
|
||||
const {
|
||||
nationalNumber: possibleShorterNationalNumber,
|
||||
} = extractNationalNumber(
|
||||
possibleShorterNumber,
|
||||
metadata
|
||||
)
|
||||
const {
|
||||
nationalNumber
|
||||
} = extractNationalNumber(
|
||||
number,
|
||||
metadata
|
||||
)
|
||||
// If the number was not valid before but is valid now,
|
||||
// or if it was too long before, we consider the number
|
||||
// with the country calling code stripped to be a better result
|
||||
// and keep that instead.
|
||||
// For example, in Germany (+49), `49` is a valid area code,
|
||||
// so if a number starts with `49`, it could be both a valid
|
||||
// national German number or an international number without
|
||||
// a leading `+`.
|
||||
if (
|
||||
(
|
||||
!matchesEntirely(nationalNumber, metadata.nationalNumberPattern())
|
||||
&&
|
||||
matchesEntirely(possibleShorterNationalNumber, metadata.nationalNumberPattern())
|
||||
)
|
||||
||
|
||||
checkNumberLength(nationalNumber, metadata) === 'TOO_LONG'
|
||||
) {
|
||||
return {
|
||||
countryCallingCode,
|
||||
number: possibleShorterNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
return { number }
|
||||
}
|
||||
Generated
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
import extractPhoneContext, {
|
||||
isPhoneContextValid,
|
||||
PLUS_SIGN,
|
||||
RFC3966_PREFIX_,
|
||||
RFC3966_PHONE_CONTEXT_,
|
||||
RFC3966_ISDN_SUBADDRESS_
|
||||
} from './extractPhoneContext.js'
|
||||
|
||||
import ParseError from '../ParseError.js'
|
||||
|
||||
/**
|
||||
* @param {string} numberToParse
|
||||
* @param {string} nationalNumber
|
||||
* @return {}
|
||||
*/
|
||||
export default function extractFormattedPhoneNumberFromPossibleRfc3966NumberUri(numberToParse, {
|
||||
extractFormattedPhoneNumber
|
||||
}) {
|
||||
const phoneContext = extractPhoneContext(numberToParse)
|
||||
if (!isPhoneContextValid(phoneContext)) {
|
||||
throw new ParseError('NOT_A_NUMBER')
|
||||
}
|
||||
|
||||
let phoneNumberString
|
||||
|
||||
if (phoneContext === null) {
|
||||
// Extract a possible number from the string passed in.
|
||||
// (this strips leading characters that could not be the start of a phone number)
|
||||
phoneNumberString = extractFormattedPhoneNumber(numberToParse) || ''
|
||||
} else {
|
||||
phoneNumberString = ''
|
||||
|
||||
// If the phone context contains a phone number prefix, we need to capture
|
||||
// it, whereas domains will be ignored.
|
||||
if (phoneContext.charAt(0) === PLUS_SIGN) {
|
||||
phoneNumberString += phoneContext
|
||||
}
|
||||
|
||||
// Now append everything between the "tel:" prefix and the phone-context.
|
||||
// This should include the national number, an optional extension or
|
||||
// isdn-subaddress component. Note we also handle the case when "tel:" is
|
||||
// missing, as we have seen in some of the phone number inputs.
|
||||
// In that case, we append everything from the beginning.
|
||||
const indexOfRfc3966Prefix = numberToParse.indexOf(RFC3966_PREFIX_)
|
||||
let indexOfNationalNumber
|
||||
// RFC 3966 "tel:" prefix is preset at this stage because
|
||||
// `isPhoneContextValid()` requires it to be present.
|
||||
/* istanbul ignore else */
|
||||
if (indexOfRfc3966Prefix >= 0) {
|
||||
indexOfNationalNumber = indexOfRfc3966Prefix + RFC3966_PREFIX_.length
|
||||
} else {
|
||||
indexOfNationalNumber = 0
|
||||
}
|
||||
const indexOfPhoneContext = numberToParse.indexOf(RFC3966_PHONE_CONTEXT_)
|
||||
phoneNumberString += numberToParse.substring(indexOfNationalNumber, indexOfPhoneContext)
|
||||
}
|
||||
|
||||
// Delete the isdn-subaddress and everything after it if it is present.
|
||||
// Note extension won't appear at the same time with isdn-subaddress
|
||||
// according to paragraph 5.3 of the RFC3966 spec.
|
||||
const indexOfIsdn = phoneNumberString.indexOf(RFC3966_ISDN_SUBADDRESS_)
|
||||
if (indexOfIsdn > 0) {
|
||||
phoneNumberString = phoneNumberString.substring(0, indexOfIsdn)
|
||||
}
|
||||
// If both phone context and isdn-subaddress are absent but other
|
||||
// parameters are present, the parameters are left in nationalNumber.
|
||||
// This is because we are concerned about deleting content from a potential
|
||||
// number string when there is no strong evidence that the number is
|
||||
// actually written in RFC3966.
|
||||
|
||||
if (phoneNumberString !== '') {
|
||||
return phoneNumberString
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+106
@@ -0,0 +1,106 @@
|
||||
import extractNationalNumberFromPossiblyIncompleteNumber from './extractNationalNumberFromPossiblyIncompleteNumber.js'
|
||||
import matchesEntirely from './matchesEntirely.js'
|
||||
import checkNumberLength from './checkNumberLength.js'
|
||||
|
||||
/**
|
||||
* Strips national prefix and carrier code from a complete phone number.
|
||||
* The difference from the non-"FromCompleteNumber" function is that
|
||||
* it won't extract national prefix if the resultant number is too short
|
||||
* to be a complete number for the selected phone numbering plan.
|
||||
* @param {string} number — Complete phone number digits.
|
||||
* @param {Metadata} metadata — Metadata with a phone numbering plan selected.
|
||||
* @return {object} `{ nationalNumber: string, carrierCode: string? }`.
|
||||
*/
|
||||
export default function extractNationalNumber(number, metadata) {
|
||||
// Parsing national prefixes and carrier codes
|
||||
// is only required for local phone numbers
|
||||
// but some people don't understand that
|
||||
// and sometimes write international phone numbers
|
||||
// with national prefixes (or maybe even carrier codes).
|
||||
// http://ucken.blogspot.ru/2016/03/trunk-prefixes-in-skype4b.html
|
||||
// Google's original library forgives such mistakes
|
||||
// and so does this library, because it has been requested:
|
||||
// https://github.com/catamphetamine/libphonenumber-js/issues/127
|
||||
const {
|
||||
carrierCode,
|
||||
nationalNumber
|
||||
} = extractNationalNumberFromPossiblyIncompleteNumber(
|
||||
number,
|
||||
metadata
|
||||
)
|
||||
|
||||
if (nationalNumber !== number) {
|
||||
if (!shouldHaveExtractedNationalPrefix(number, nationalNumber, metadata)) {
|
||||
// Don't strip the national prefix.
|
||||
return { nationalNumber: number }
|
||||
}
|
||||
// Check the national (significant) number length after extracting national prefix and carrier code.
|
||||
// Legacy generated metadata (before `1.0.18`) didn't support the "possible lengths" feature.
|
||||
if (metadata.possibleLengths()) {
|
||||
// The number remaining after stripping the national prefix and carrier code
|
||||
// should be long enough to have a possible length for the country.
|
||||
// Otherwise, don't strip the national prefix and carrier code,
|
||||
// since the original number could be a valid number.
|
||||
// This check has been copy-pasted "as is" from Google's original library:
|
||||
// https://github.com/google/libphonenumber/blob/876268eb1ad6cdc1b7b5bef17fc5e43052702d57/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L3236-L3250
|
||||
// It doesn't check for the "possibility" of the original `number`.
|
||||
// I guess it's fine not checking that one. It works as is anyway.
|
||||
if (!isPossibleIncompleteNationalNumber(nationalNumber, metadata)) {
|
||||
// Don't strip the national prefix.
|
||||
return { nationalNumber: number }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { nationalNumber, carrierCode }
|
||||
}
|
||||
|
||||
// In some countries, the same digit could be a national prefix
|
||||
// or a leading digit of a valid phone number.
|
||||
// For example, in Russia, national prefix is `8`,
|
||||
// and also `800 555 35 35` is a valid number
|
||||
// in which `8` is not a national prefix, but the first digit
|
||||
// of a national (significant) number.
|
||||
// Same's with Belarus:
|
||||
// `82004910060` is a valid national (significant) number,
|
||||
// but `2004910060` is not.
|
||||
// To support such cases (to prevent the code from always stripping
|
||||
// national prefix), a condition is imposed: a national prefix
|
||||
// is not extracted when the original number is "viable" and the
|
||||
// resultant number is not, a "viable" national number being the one
|
||||
// that matches `national_number_pattern`.
|
||||
function shouldHaveExtractedNationalPrefix(nationalNumberBefore, nationalNumberAfter, metadata) {
|
||||
// The equivalent in Google's code is:
|
||||
// https://github.com/google/libphonenumber/blob/e326fa1fc4283bb05eb35cb3c15c18f98a31af33/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L2969-L3004
|
||||
if (matchesEntirely(nationalNumberBefore, metadata.nationalNumberPattern()) &&
|
||||
!matchesEntirely(nationalNumberAfter, metadata.nationalNumberPattern())) {
|
||||
return false
|
||||
}
|
||||
// This "is possible" national number (length) check has been commented out
|
||||
// because it's superceded by the (effectively) same check done in the
|
||||
// `extractNationalNumber()` function after it calls `shouldHaveExtractedNationalPrefix()`.
|
||||
// In other words, why run the same check twice if it could only be run once.
|
||||
// // Check the national (significant) number length after extracting national prefix and carrier code.
|
||||
// // Fixes a minor "weird behavior" bug: https://gitlab.com/catamphetamine/libphonenumber-js/-/issues/57
|
||||
// // (Legacy generated metadata (before `1.0.18`) didn't support the "possible lengths" feature).
|
||||
// if (metadata.possibleLengths()) {
|
||||
// if (isPossibleIncompleteNationalNumber(nationalNumberBefore, metadata) &&
|
||||
// !isPossibleIncompleteNationalNumber(nationalNumberAfter, metadata)) {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
return true
|
||||
}
|
||||
|
||||
function isPossibleIncompleteNationalNumber(nationalNumber, metadata) {
|
||||
switch (checkNumberLength(nationalNumber, metadata)) {
|
||||
case 'TOO_SHORT':
|
||||
case 'INVALID_LENGTH':
|
||||
// This library ignores "local-only" phone numbers (for simplicity).
|
||||
// See the readme for more info on what are "local-only" phone numbers.
|
||||
// case 'IS_POSSIBLE_LOCAL_ONLY':
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import extractNationalNumber from './extractNationalNumber.js'
|
||||
|
||||
import Metadata from '../metadata.js'
|
||||
import oldMetadata from '../../test/metadata/1.0.0/metadata.min.json' assert { type: 'json' }
|
||||
|
||||
describe('extractNationalNumber', function() {
|
||||
it('should extract a national number when using old metadata', function() {
|
||||
const _oldMetadata = new Metadata(oldMetadata)
|
||||
_oldMetadata.selectNumberingPlan('RU')
|
||||
extractNationalNumber('88005553535', _oldMetadata).should.deep.equal({
|
||||
nationalNumber: '8005553535',
|
||||
carrierCode: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
Generated
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Strips any national prefix (such as 0, 1) present in a
|
||||
* (possibly incomplete) number provided.
|
||||
* "Carrier codes" are only used in Colombia and Brazil,
|
||||
* and only when dialing within those countries from a mobile phone to a fixed line number.
|
||||
* Sometimes it won't actually strip national prefix
|
||||
* and will instead prepend some digits to the `number`:
|
||||
* for example, when number `2345678` is passed with `VI` country selected,
|
||||
* it will return `{ number: "3402345678" }`, because `340` area code is prepended.
|
||||
* @param {string} number — National number digits.
|
||||
* @param {object} metadata — Metadata with country selected.
|
||||
* @return {object} `{ nationalNumber: string, nationalPrefix: string? carrierCode: string? }`. Even if a national prefix was extracted, it's not necessarily present in the returned object, so don't rely on its presence in the returned object in order to find out whether a national prefix has been extracted or not.
|
||||
*/
|
||||
export default function extractNationalNumberFromPossiblyIncompleteNumber(number, metadata) {
|
||||
if (number && metadata.numberingPlan.nationalPrefixForParsing()) {
|
||||
// See METADATA.md for the description of
|
||||
// `national_prefix_for_parsing` and `national_prefix_transform_rule`.
|
||||
// Attempt to parse the first digits as a national prefix.
|
||||
const prefixPattern = new RegExp('^(?:' + metadata.numberingPlan.nationalPrefixForParsing() + ')')
|
||||
const prefixMatch = prefixPattern.exec(number)
|
||||
if (prefixMatch) {
|
||||
let nationalNumber
|
||||
let carrierCode
|
||||
// https://gitlab.com/catamphetamine/libphonenumber-js/-/blob/master/METADATA.md#national_prefix_for_parsing--national_prefix_transform_rule
|
||||
// If a `national_prefix_for_parsing` has any "capturing groups"
|
||||
// then it means that the national (significant) number is equal to
|
||||
// those "capturing groups" transformed via `national_prefix_transform_rule`,
|
||||
// and nothing could be said about the actual national prefix:
|
||||
// what is it and was it even there.
|
||||
// If a `national_prefix_for_parsing` doesn't have any "capturing groups",
|
||||
// then everything it matches is a national prefix.
|
||||
// To determine whether `national_prefix_for_parsing` matched any
|
||||
// "capturing groups", the value of the result of calling `.exec()`
|
||||
// is looked at, and if it has non-undefined values where there're
|
||||
// "capturing groups" in the regular expression, then it means
|
||||
// that "capturing groups" have been matched.
|
||||
// It's not possible to tell whether there'll be any "capturing gropus"
|
||||
// before the matching process, because a `national_prefix_for_parsing`
|
||||
// could exhibit both behaviors.
|
||||
const capturedGroupsCount = prefixMatch.length - 1
|
||||
const hasCapturedGroups = capturedGroupsCount > 0 && prefixMatch[capturedGroupsCount]
|
||||
if (metadata.nationalPrefixTransformRule() && hasCapturedGroups) {
|
||||
nationalNumber = number.replace(
|
||||
prefixPattern,
|
||||
metadata.nationalPrefixTransformRule()
|
||||
)
|
||||
// If there's more than one captured group,
|
||||
// then carrier code is the second one.
|
||||
if (capturedGroupsCount > 1) {
|
||||
carrierCode = prefixMatch[1]
|
||||
}
|
||||
}
|
||||
// If there're no "capturing groups",
|
||||
// or if there're "capturing groups" but no
|
||||
// `national_prefix_transform_rule`,
|
||||
// then just strip the national prefix from the number,
|
||||
// and possibly a carrier code.
|
||||
// Seems like there could be more.
|
||||
else {
|
||||
// `prefixBeforeNationalNumber` is the whole substring matched by
|
||||
// the `national_prefix_for_parsing` regular expression.
|
||||
// There seem to be no guarantees that it's just a national prefix.
|
||||
// For example, if there's a carrier code, it's gonna be a
|
||||
// part of `prefixBeforeNationalNumber` too.
|
||||
const prefixBeforeNationalNumber = prefixMatch[0]
|
||||
nationalNumber = number.slice(prefixBeforeNationalNumber.length)
|
||||
// If there's at least one captured group,
|
||||
// then carrier code is the first one.
|
||||
if (hasCapturedGroups) {
|
||||
carrierCode = prefixMatch[1]
|
||||
}
|
||||
}
|
||||
// Tries to guess whether a national prefix was present in the input.
|
||||
// This is not something copy-pasted from Google's library:
|
||||
// they don't seem to have an equivalent for that.
|
||||
// So this isn't an "officially approved" way of doing something like that.
|
||||
// But since there seems no other existing method, this library uses it.
|
||||
let nationalPrefix
|
||||
if (hasCapturedGroups) {
|
||||
const possiblePositionOfTheFirstCapturedGroup = number.indexOf(prefixMatch[1])
|
||||
const possibleNationalPrefix = number.slice(0, possiblePositionOfTheFirstCapturedGroup)
|
||||
// Example: an Argentinian (AR) phone number `0111523456789`.
|
||||
// `prefixMatch[0]` is `01115`, and `$1` is `11`,
|
||||
// and the rest of the phone number is `23456789`.
|
||||
// The national number is transformed via `9$1` to `91123456789`.
|
||||
// National prefix `0` is detected being present at the start.
|
||||
// if (possibleNationalPrefix.indexOf(metadata.numberingPlan.nationalPrefix()) === 0) {
|
||||
if (possibleNationalPrefix === metadata.numberingPlan.nationalPrefix()) {
|
||||
nationalPrefix = metadata.numberingPlan.nationalPrefix()
|
||||
}
|
||||
} else {
|
||||
nationalPrefix = prefixMatch[0]
|
||||
}
|
||||
return {
|
||||
nationalNumber,
|
||||
nationalPrefix,
|
||||
carrierCode
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
nationalNumber: number
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import Metadata from '../metadata.js'
|
||||
import metadata from '../../metadata.min.json' assert { type: 'json' }
|
||||
import extractNationalNumberFromPossiblyIncompleteNumber from './extractNationalNumberFromPossiblyIncompleteNumber.js'
|
||||
|
||||
describe('extractNationalNumberFromPossiblyIncompleteNumber', () => {
|
||||
it('should parse a carrier code when there is no national prefix transform rule', () => {
|
||||
const meta = new Metadata(metadata)
|
||||
meta.country('AU')
|
||||
extractNationalNumberFromPossiblyIncompleteNumber('18311800123', meta).should.deep.equal({
|
||||
nationalPrefix: undefined,
|
||||
carrierCode: '1831',
|
||||
nationalNumber: '1800123'
|
||||
})
|
||||
})
|
||||
})
|
||||
Generated
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
// When phone numbers are written in `RFC3966` format — `"tel:+12133734253"` —
|
||||
// they can have their "calling code" part written separately in a `phone-context` parameter.
|
||||
// Example: `"tel:12133734253;phone-context=+1"`.
|
||||
// This function parses the full phone number from the local number and the `phone-context`
|
||||
// when the `phone-context` contains a `+` sign.
|
||||
|
||||
import {
|
||||
VALID_DIGITS,
|
||||
// PLUS_CHARS
|
||||
} from '../constants.js'
|
||||
|
||||
export const PLUS_SIGN = '+'
|
||||
|
||||
const RFC3966_VISUAL_SEPARATOR_ = '[\\-\\.\\(\\)]?'
|
||||
|
||||
const RFC3966_PHONE_DIGIT_ = '(' + '[' + VALID_DIGITS + ']' + '|' + RFC3966_VISUAL_SEPARATOR_ + ')'
|
||||
|
||||
const RFC3966_GLOBAL_NUMBER_DIGITS_ =
|
||||
'^' +
|
||||
'\\' +
|
||||
PLUS_SIGN +
|
||||
RFC3966_PHONE_DIGIT_ +
|
||||
'*' +
|
||||
'[' + VALID_DIGITS + ']' +
|
||||
RFC3966_PHONE_DIGIT_ +
|
||||
'*' +
|
||||
'$'
|
||||
|
||||
/**
|
||||
* Regular expression of valid global-number-digits for the phone-context
|
||||
* parameter, following the syntax defined in RFC3966.
|
||||
*/
|
||||
const RFC3966_GLOBAL_NUMBER_DIGITS_PATTERN_ = new RegExp(RFC3966_GLOBAL_NUMBER_DIGITS_, 'g')
|
||||
|
||||
// In this port of Google's library, we don't accept alpha characters in phone numbers.
|
||||
// const ALPHANUM_ = VALID_ALPHA_ + VALID_DIGITS
|
||||
const ALPHANUM_ = VALID_DIGITS
|
||||
|
||||
const RFC3966_DOMAINLABEL_ = '[' + ALPHANUM_ + ']+((\\-)*[' + ALPHANUM_ + '])*'
|
||||
|
||||
const VALID_ALPHA_ = 'a-zA-Z'
|
||||
const RFC3966_TOPLABEL_ = '[' + VALID_ALPHA_ + ']+((\\-)*[' + ALPHANUM_ + '])*'
|
||||
|
||||
const RFC3966_DOMAINNAME_ = '^(' + RFC3966_DOMAINLABEL_ + '\\.)*' + RFC3966_TOPLABEL_ + '\\.?$'
|
||||
|
||||
/**
|
||||
* Regular expression of valid domainname for the phone-context parameter,
|
||||
* following the syntax defined in RFC3966.
|
||||
*/
|
||||
const RFC3966_DOMAINNAME_PATTERN_ = new RegExp(RFC3966_DOMAINNAME_, 'g')
|
||||
|
||||
export const RFC3966_PREFIX_ = 'tel:'
|
||||
export const RFC3966_PHONE_CONTEXT_ = ';phone-context='
|
||||
export const RFC3966_ISDN_SUBADDRESS_ = ';isub='
|
||||
|
||||
/**
|
||||
* Extracts the value of the phone-context parameter of `numberToExtractFrom`,
|
||||
* following the syntax defined in RFC3966.
|
||||
*
|
||||
* @param {string} numberToExtractFrom
|
||||
* @return {string|null} the extracted string (possibly empty), or `null` if no phone-context parameter is found.
|
||||
*/
|
||||
export default function extractPhoneContext(numberToExtractFrom) {
|
||||
const indexOfPhoneContext = numberToExtractFrom.indexOf(RFC3966_PHONE_CONTEXT_)
|
||||
// If no phone-context parameter is present
|
||||
if (indexOfPhoneContext < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const phoneContextStart = indexOfPhoneContext + RFC3966_PHONE_CONTEXT_.length
|
||||
// If phone-context parameter is empty
|
||||
if (phoneContextStart >= numberToExtractFrom.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const phoneContextEnd = numberToExtractFrom.indexOf(';', phoneContextStart)
|
||||
// If phone-context is not the last parameter
|
||||
if (phoneContextEnd >= 0) {
|
||||
return numberToExtractFrom.substring(phoneContextStart, phoneContextEnd)
|
||||
} else {
|
||||
return numberToExtractFrom.substring(phoneContextStart)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the value of phoneContext follows the syntax defined in RFC3966.
|
||||
*
|
||||
* @param {string|null} phoneContext
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isPhoneContextValid(phoneContext) {
|
||||
if (phoneContext === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (phoneContext.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Does phone-context value match pattern of global-number-digits or domainname.
|
||||
return RFC3966_GLOBAL_NUMBER_DIGITS_PATTERN_.test(phoneContext) ||
|
||||
RFC3966_DOMAINNAME_PATTERN_.test(phoneContext)
|
||||
}
|
||||
Generated
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
import parsePhoneNumber_ from '../parsePhoneNumber.js'
|
||||
import PhoneNumber from '../PhoneNumber.js'
|
||||
import metadata from '../../metadata.min.json' assert { type: 'json' }
|
||||
|
||||
function parsePhoneNumber(...parameters) {
|
||||
parameters.push(metadata)
|
||||
return parsePhoneNumber_.apply(this, parameters)
|
||||
}
|
||||
|
||||
describe('extractPhoneContext', function() {
|
||||
it('should parse RFC 3966 phone number URIs', function() {
|
||||
// context = ";phone-context=" descriptor
|
||||
// descriptor = domainname / global-number-digits
|
||||
|
||||
const NZ_NUMBER = new PhoneNumber('64', '33316005', metadata)
|
||||
|
||||
// Valid global-phone-digits
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=+64'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=+64;{this isn\'t part of phone-context anymore!}'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
|
||||
const nzFromPhoneContext = new PhoneNumber('64', '3033316005', metadata)
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=+64-3'),
|
||||
nzFromPhoneContext
|
||||
)
|
||||
|
||||
const brFromPhoneContext = new PhoneNumber('55', '5033316005', metadata)
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=+(555)'),
|
||||
brFromPhoneContext
|
||||
)
|
||||
|
||||
const usFromPhoneContext = new PhoneNumber('1', '23033316005', metadata)
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=+-1-2.3()'),
|
||||
usFromPhoneContext
|
||||
)
|
||||
|
||||
// Valid domainname.
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=abc.nz', 'NZ'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=www.PHONE-numb3r.com', 'NZ'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=a', 'NZ'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=3phone.J.', 'NZ'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;phone-context=a--z', 'NZ'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
|
||||
// Should strip ISDN subaddress.
|
||||
expectPhoneNumbersToBeEqual(
|
||||
parsePhoneNumber('tel:033316005;isub=/@;phone-context=+64', 'NZ'),
|
||||
NZ_NUMBER
|
||||
)
|
||||
|
||||
// // Should support incorrectly-written RFC 3966 phone numbers:
|
||||
// // the ones written without a `tel:` prefix.
|
||||
// expectPhoneNumbersToBeEqual(
|
||||
// parsePhoneNumber('033316005;phone-context=+64', 'NZ'),
|
||||
// NZ_NUMBER
|
||||
// )
|
||||
|
||||
// Invalid descriptor.
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=+')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=64')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=++64')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=+abc')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=.')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=3phone')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=a-.nz')
|
||||
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=a{b}c')
|
||||
})
|
||||
})
|
||||
|
||||
function expectToThrowForInvalidPhoneContext(string) {
|
||||
expect(parsePhoneNumber(string)).to.be.undefined
|
||||
}
|
||||
|
||||
function expectPhoneNumbersToBeEqual(phoneNumber1, phoneNumber2) {
|
||||
if (!phoneNumber1 || !phoneNumber2) {
|
||||
return false
|
||||
}
|
||||
return phoneNumber1.number === phoneNumber2.number &&
|
||||
phoneNumber1.ext === phoneNumber2.ext
|
||||
}
|
||||
SerpentRace_Backend/node_modules/libphonenumber-js/source/helpers/formatNationalNumberUsingFormat.js
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
import applyInternationalSeparatorStyle from './applyInternationalSeparatorStyle.js'
|
||||
|
||||
// This was originally set to $1 but there are some countries for which the
|
||||
// first group is not used in the national pattern (e.g. Argentina) so the $1
|
||||
// group does not match correctly. Therefore, we use `\d`, so that the first
|
||||
// group actually used in the pattern will be matched.
|
||||
export const FIRST_GROUP_PATTERN = /(\$\d)/
|
||||
|
||||
export default function formatNationalNumberUsingFormat(
|
||||
number,
|
||||
format,
|
||||
{
|
||||
useInternationalFormat,
|
||||
withNationalPrefix,
|
||||
carrierCode,
|
||||
metadata
|
||||
}
|
||||
) {
|
||||
const formattedNumber = number.replace(
|
||||
new RegExp(format.pattern()),
|
||||
useInternationalFormat
|
||||
? format.internationalFormat()
|
||||
: (
|
||||
// This library doesn't use `domestic_carrier_code_formatting_rule`,
|
||||
// because that one is only used when formatting phone numbers
|
||||
// for dialing from a mobile phone, and this is not a dialing library.
|
||||
// carrierCode && format.domesticCarrierCodeFormattingRule()
|
||||
// // First, replace the $CC in the formatting rule with the desired carrier code.
|
||||
// // Then, replace the $FG in the formatting rule with the first group
|
||||
// // and the carrier code combined in the appropriate way.
|
||||
// ? format.format().replace(FIRST_GROUP_PATTERN, format.domesticCarrierCodeFormattingRule().replace('$CC', carrierCode))
|
||||
// : (
|
||||
// withNationalPrefix && format.nationalPrefixFormattingRule()
|
||||
// ? format.format().replace(FIRST_GROUP_PATTERN, format.nationalPrefixFormattingRule())
|
||||
// : format.format()
|
||||
// )
|
||||
withNationalPrefix && format.nationalPrefixFormattingRule()
|
||||
? format.format().replace(FIRST_GROUP_PATTERN, format.nationalPrefixFormattingRule())
|
||||
: format.format()
|
||||
)
|
||||
)
|
||||
if (useInternationalFormat) {
|
||||
return applyInternationalSeparatorStyle(formattedNumber)
|
||||
}
|
||||
return formattedNumber
|
||||
}
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
import getCountryByNationalNumber from './getCountryByNationalNumber.js'
|
||||
|
||||
const USE_NON_GEOGRAPHIC_COUNTRY_CODE = false
|
||||
|
||||
export default function getCountryByCallingCode(callingCode, {
|
||||
nationalNumber: nationalPhoneNumber,
|
||||
defaultCountry,
|
||||
metadata
|
||||
}) {
|
||||
/* istanbul ignore if */
|
||||
if (USE_NON_GEOGRAPHIC_COUNTRY_CODE) {
|
||||
if (metadata.isNonGeographicCallingCode(callingCode)) {
|
||||
return '001'
|
||||
}
|
||||
}
|
||||
const possibleCountries = metadata.getCountryCodesForCallingCode(callingCode)
|
||||
if (!possibleCountries) {
|
||||
return
|
||||
}
|
||||
// If there's just one country corresponding to the country code,
|
||||
// then just return it, without further phone number digits validation.
|
||||
if (possibleCountries.length === 1) {
|
||||
return possibleCountries[0]
|
||||
}
|
||||
return getCountryByNationalNumber(nationalPhoneNumber, {
|
||||
countries: possibleCountries,
|
||||
defaultCountry,
|
||||
metadata: metadata.metadata
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import Metadata from '../metadata.js'
|
||||
import getNumberType from './getNumberType.js'
|
||||
|
||||
export default function getCountryByNationalNumber(nationalPhoneNumber, {
|
||||
countries,
|
||||
defaultCountry,
|
||||
metadata
|
||||
}) {
|
||||
// Re-create `metadata` because it will be selecting a `country`.
|
||||
metadata = new Metadata(metadata)
|
||||
|
||||
// const matchingCountries = []
|
||||
|
||||
for (const country of countries) {
|
||||
metadata.country(country)
|
||||
// "Leading digits" patterns are only defined for about 20% of all countries.
|
||||
// By definition, matching "leading digits" is a sufficient but not a necessary
|
||||
// condition for a phone number to belong to a country.
|
||||
// The point of "leading digits" check is that it's the fastest one to get a match.
|
||||
// https://gitlab.com/catamphetamine/libphonenumber-js/blob/master/METADATA.md#leading_digits
|
||||
// I'd suppose that "leading digits" patterns are mutually exclusive for different countries
|
||||
// because of the intended use of that feature.
|
||||
if (metadata.leadingDigits()) {
|
||||
if (nationalPhoneNumber &&
|
||||
nationalPhoneNumber.search(metadata.leadingDigits()) === 0) {
|
||||
return country
|
||||
}
|
||||
}
|
||||
// Else perform full validation with all of those
|
||||
// fixed-line/mobile/etc regular expressions.
|
||||
else if (getNumberType({ phone: nationalPhoneNumber, country }, undefined, metadata.metadata)) {
|
||||
// If both the `defaultCountry` and the "main" one match the phone number,
|
||||
// don't prefer the `defaultCountry` over the "main" one.
|
||||
// https://gitlab.com/catamphetamine/libphonenumber-js/-/issues/154
|
||||
return country
|
||||
// // If the `defaultCountry` is among the `matchingCountries` then return it.
|
||||
// if (defaultCountry) {
|
||||
// if (country === defaultCountry) {
|
||||
// return country
|
||||
// }
|
||||
// matchingCountries.push(country)
|
||||
// } else {
|
||||
// return country
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// // Return the first ("main") one of the `matchingCountries`.
|
||||
// if (matchingCountries.length > 0) {
|
||||
// return matchingCountries[0]
|
||||
// }
|
||||
}
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
import Metadata from '../metadata.js'
|
||||
|
||||
/**
|
||||
* Pattern that makes it easy to distinguish whether a region has a single
|
||||
* international dialing prefix or not. If a region has a single international
|
||||
* prefix (e.g. 011 in USA), it will be represented as a string that contains
|
||||
* a sequence of ASCII digits, and possibly a tilde, which signals waiting for
|
||||
* the tone. If there are multiple available international prefixes in a
|
||||
* region, they will be represented as a regex string that always contains one
|
||||
* or more characters that are not ASCII digits or a tilde.
|
||||
*/
|
||||
const SINGLE_IDD_PREFIX_REG_EXP = /^[\d]+(?:[~\u2053\u223C\uFF5E][\d]+)?$/
|
||||
|
||||
// For regions that have multiple IDD prefixes
|
||||
// a preferred IDD prefix is returned.
|
||||
export default function getIddPrefix(country, callingCode, metadata) {
|
||||
const countryMetadata = new Metadata(metadata)
|
||||
countryMetadata.selectNumberingPlan(country, callingCode)
|
||||
if (countryMetadata.defaultIDDPrefix()) {
|
||||
return countryMetadata.defaultIDDPrefix()
|
||||
}
|
||||
if (SINGLE_IDD_PREFIX_REG_EXP.test(countryMetadata.IDDPrefix())) {
|
||||
return countryMetadata.IDDPrefix()
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+98
@@ -0,0 +1,98 @@
|
||||
import Metadata from '../metadata.js'
|
||||
import matchesEntirely from './matchesEntirely.js'
|
||||
|
||||
const NON_FIXED_LINE_PHONE_TYPES = [
|
||||
'MOBILE',
|
||||
'PREMIUM_RATE',
|
||||
'TOLL_FREE',
|
||||
'SHARED_COST',
|
||||
'VOIP',
|
||||
'PERSONAL_NUMBER',
|
||||
'PAGER',
|
||||
'UAN',
|
||||
'VOICEMAIL'
|
||||
]
|
||||
|
||||
// Finds out national phone number type (fixed line, mobile, etc)
|
||||
export default function getNumberType(input, options, metadata)
|
||||
{
|
||||
// If assigning the `{}` default value is moved to the arguments above,
|
||||
// code coverage would decrease for some weird reason.
|
||||
options = options || {}
|
||||
|
||||
// When `parse()` returns an empty object — `{}` —
|
||||
// that means that the phone number is malformed,
|
||||
// so it can't possibly be valid.
|
||||
if (!input.country && !input.countryCallingCode) {
|
||||
return
|
||||
}
|
||||
|
||||
metadata = new Metadata(metadata)
|
||||
|
||||
metadata.selectNumberingPlan(input.country, input.countryCallingCode)
|
||||
|
||||
const nationalNumber = options.v2 ? input.nationalNumber : input.phone
|
||||
|
||||
// The following is copy-pasted from the original function:
|
||||
// https://github.com/googlei18n/libphonenumber/blob/3ea547d4fbaa2d0b67588904dfa5d3f2557c27ff/javascript/i18n/phonenumbers/phonenumberutil.js#L2835
|
||||
|
||||
// Is this national number even valid for this country
|
||||
if (!matchesEntirely(nationalNumber, metadata.nationalNumberPattern())) {
|
||||
return
|
||||
}
|
||||
|
||||
// Is it fixed line number
|
||||
if (isNumberTypeEqualTo(nationalNumber, 'FIXED_LINE', metadata)) {
|
||||
// Because duplicate regular expressions are removed
|
||||
// to reduce metadata size, if "mobile" pattern is ""
|
||||
// then it means it was removed due to being a duplicate of the fixed-line pattern.
|
||||
//
|
||||
if (metadata.type('MOBILE') && metadata.type('MOBILE').pattern() === '') {
|
||||
return 'FIXED_LINE_OR_MOBILE'
|
||||
}
|
||||
|
||||
// `MOBILE` type pattern isn't included if it matched `FIXED_LINE` one.
|
||||
// For example, for "US" country.
|
||||
// Old metadata (< `1.0.18`) had a specific "types" data structure
|
||||
// that happened to be `undefined` for `MOBILE` in that case.
|
||||
// Newer metadata (>= `1.0.18`) has another data structure that is
|
||||
// not `undefined` for `MOBILE` in that case (it's just an empty array).
|
||||
// So this `if` is just for backwards compatibility with old metadata.
|
||||
if (!metadata.type('MOBILE')) {
|
||||
return 'FIXED_LINE_OR_MOBILE'
|
||||
}
|
||||
|
||||
// Check if the number happens to qualify as both fixed line and mobile.
|
||||
// (no such country in the minimal metadata set)
|
||||
/* istanbul ignore if */
|
||||
if (isNumberTypeEqualTo(nationalNumber, 'MOBILE', metadata)) {
|
||||
return 'FIXED_LINE_OR_MOBILE'
|
||||
}
|
||||
|
||||
return 'FIXED_LINE'
|
||||
}
|
||||
|
||||
for (const type of NON_FIXED_LINE_PHONE_TYPES) {
|
||||
if (isNumberTypeEqualTo(nationalNumber, type, metadata)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isNumberTypeEqualTo(nationalNumber, type, metadata) {
|
||||
type = metadata.type(type)
|
||||
if (!type || !type.pattern()) {
|
||||
return false
|
||||
}
|
||||
// Check if any possible number lengths are present;
|
||||
// if so, we use them to avoid checking
|
||||
// the validation pattern if they don't match.
|
||||
// If they are absent, this means they match
|
||||
// the general description, which we have
|
||||
// already checked before a specific number type.
|
||||
if (type.possibleLengths() &&
|
||||
type.possibleLengths().indexOf(nationalNumber.length) < 0) {
|
||||
return false
|
||||
}
|
||||
return matchesEntirely(nationalNumber, type.pattern())
|
||||
}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
import getNumberType from './getNumberType.js'
|
||||
|
||||
import oldMetadata from '../../test/metadata/1.0.0/metadata.min.json' assert { type: 'json' }
|
||||
|
||||
import Metadata from '../metadata.js'
|
||||
|
||||
describe('getNumberType', function() {
|
||||
it('should get number type when using old metadata', function() {
|
||||
getNumberType(
|
||||
{
|
||||
nationalNumber: '2133734253',
|
||||
country: 'US'
|
||||
},
|
||||
{ v2: true },
|
||||
oldMetadata
|
||||
).should.equal('FIXED_LINE_OR_MOBILE')
|
||||
})
|
||||
|
||||
it('should return `undefined` when the phone number is a malformed one', function() {
|
||||
expect(getNumberType(
|
||||
{},
|
||||
{ v2: true },
|
||||
oldMetadata
|
||||
)).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
Generated
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import Metadata from '../metadata.js'
|
||||
|
||||
/**
|
||||
* Returns a list of countries that the phone number could potentially belong to.
|
||||
* @param {string} callingCode — Calling code.
|
||||
* @param {string} nationalNumber — National (significant) number.
|
||||
* @param {object} metadata — Metadata.
|
||||
* @return {string[]} A list of possible countries.
|
||||
*/
|
||||
export default function getPossibleCountriesForNumber(callingCode, nationalNumber, metadata) {
|
||||
const _metadata = new Metadata(metadata)
|
||||
let possibleCountries = _metadata.getCountryCodesForCallingCode(callingCode)
|
||||
if (!possibleCountries) {
|
||||
return []
|
||||
}
|
||||
return possibleCountries.filter((country) => {
|
||||
return couldNationalNumberBelongToCountry(nationalNumber, country, metadata)
|
||||
})
|
||||
}
|
||||
|
||||
function couldNationalNumberBelongToCountry(nationalNumber, country, metadata) {
|
||||
const _metadata = new Metadata(metadata)
|
||||
_metadata.selectNumberingPlan(country)
|
||||
if (_metadata.numberingPlan.possibleLengths().indexOf(nationalNumber.length) >= 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
const objectConstructor = {}.constructor;
|
||||
|
||||
export default function isObject(object) {
|
||||
return object !== undefined && object !== null && object.constructor === objectConstructor;
|
||||
}
|
||||
Generated
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
MIN_LENGTH_FOR_NSN,
|
||||
VALID_DIGITS,
|
||||
VALID_PUNCTUATION,
|
||||
PLUS_CHARS
|
||||
} from '../constants.js'
|
||||
|
||||
import createExtensionPattern from './extension/createExtensionPattern.js'
|
||||
|
||||
// Regular expression of viable phone numbers. This is location independent.
|
||||
// Checks we have at least three leading digits, and only valid punctuation,
|
||||
// alpha characters and digits in the phone number. Does not include extension
|
||||
// data. The symbol 'x' is allowed here as valid punctuation since it is often
|
||||
// used as a placeholder for carrier codes, for example in Brazilian phone
|
||||
// numbers. We also allow multiple '+' characters at the start.
|
||||
//
|
||||
// Corresponds to the following:
|
||||
// [digits]{minLengthNsn}|
|
||||
// plus_sign*
|
||||
// (([punctuation]|[star])*[digits]){3,}([punctuation]|[star]|[digits]|[alpha])*
|
||||
//
|
||||
// The first reg-ex is to allow short numbers (two digits long) to be parsed if
|
||||
// they are entered as "15" etc, but only if there is no punctuation in them.
|
||||
// The second expression restricts the number of digits to three or more, but
|
||||
// then allows them to be in international form, and to have alpha-characters
|
||||
// and punctuation. We split up the two reg-exes here and combine them when
|
||||
// creating the reg-ex VALID_PHONE_NUMBER_PATTERN itself so we can prefix it
|
||||
// with ^ and append $ to each branch.
|
||||
//
|
||||
// "Note VALID_PUNCTUATION starts with a -,
|
||||
// so must be the first in the range" (c) Google devs.
|
||||
// (wtf did they mean by saying that; probably nothing)
|
||||
//
|
||||
const MIN_LENGTH_PHONE_NUMBER_PATTERN = '[' + VALID_DIGITS + ']{' + MIN_LENGTH_FOR_NSN + '}'
|
||||
//
|
||||
// And this is the second reg-exp:
|
||||
// (see MIN_LENGTH_PHONE_NUMBER_PATTERN for a full description of this reg-exp)
|
||||
//
|
||||
export const VALID_PHONE_NUMBER =
|
||||
'[' + PLUS_CHARS + ']{0,1}' +
|
||||
'(?:' +
|
||||
'[' + VALID_PUNCTUATION + ']*' +
|
||||
'[' + VALID_DIGITS + ']' +
|
||||
'){3,}' +
|
||||
'[' +
|
||||
VALID_PUNCTUATION +
|
||||
VALID_DIGITS +
|
||||
']*'
|
||||
|
||||
// This regular expression isn't present in Google's `libphonenumber`
|
||||
// and is only used to determine whether the phone number being input
|
||||
// is too short for it to even consider it a "valid" number.
|
||||
// This is just a way to differentiate between a really invalid phone
|
||||
// number like "abcde" and a valid phone number that a user has just
|
||||
// started inputting, like "+1" or "1": both these cases would be
|
||||
// considered `NOT_A_NUMBER` by Google's `libphonenumber`, but this
|
||||
// library can provide a more detailed error message — whether it's
|
||||
// really "not a number", or is it just a start of a valid phone number.
|
||||
const VALID_PHONE_NUMBER_START_REG_EXP = new RegExp(
|
||||
'^' +
|
||||
'[' + PLUS_CHARS + ']{0,1}' +
|
||||
'(?:' +
|
||||
'[' + VALID_PUNCTUATION + ']*' +
|
||||
'[' + VALID_DIGITS + ']' +
|
||||
'){1,2}' +
|
||||
'$'
|
||||
, 'i')
|
||||
|
||||
export const VALID_PHONE_NUMBER_WITH_EXTENSION =
|
||||
VALID_PHONE_NUMBER +
|
||||
// Phone number extensions
|
||||
'(?:' + createExtensionPattern() + ')?'
|
||||
|
||||
// The combined regular expression for valid phone numbers:
|
||||
//
|
||||
const VALID_PHONE_NUMBER_PATTERN = new RegExp(
|
||||
// Either a short two-digit-only phone number
|
||||
'^' +
|
||||
MIN_LENGTH_PHONE_NUMBER_PATTERN +
|
||||
'$' +
|
||||
'|' +
|
||||
// Or a longer fully parsed phone number (min 3 characters)
|
||||
'^' +
|
||||
VALID_PHONE_NUMBER_WITH_EXTENSION +
|
||||
'$'
|
||||
, 'i')
|
||||
|
||||
// Checks to see if the string of characters could possibly be a phone number at
|
||||
// all. At the moment, checks to see that the string begins with at least 2
|
||||
// digits, ignoring any punctuation commonly found in phone numbers. This method
|
||||
// does not require the number to be normalized in advance - but does assume
|
||||
// that leading non-number symbols have been removed, such as by the method
|
||||
// `extract_possible_number`.
|
||||
//
|
||||
export default function isViablePhoneNumber(number) {
|
||||
return number.length >= MIN_LENGTH_FOR_NSN &&
|
||||
VALID_PHONE_NUMBER_PATTERN.test(number)
|
||||
}
|
||||
|
||||
// This is just a way to differentiate between a really invalid phone
|
||||
// number like "abcde" and a valid phone number that a user has just
|
||||
// started inputting, like "+1" or "1": both these cases would be
|
||||
// considered `NOT_A_NUMBER` by Google's `libphonenumber`, but this
|
||||
// library can provide a more detailed error message — whether it's
|
||||
// really "not a number", or is it just a start of a valid phone number.
|
||||
export function isViablePhoneNumberStart(number) {
|
||||
return VALID_PHONE_NUMBER_START_REG_EXP.test(number)
|
||||
}
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Checks whether the entire input sequence can be matched
|
||||
* against the regular expression.
|
||||
* @return {boolean}
|
||||
*/
|
||||
export default function matchesEntirely(text, regular_expression) {
|
||||
// If assigning the `''` default value is moved to the arguments above,
|
||||
// code coverage would decrease for some weird reason.
|
||||
text = text || ''
|
||||
return new RegExp('^(?:' + regular_expression + ')$').test(text)
|
||||
}
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import matchesEntirely from './matchesEntirely.js'
|
||||
|
||||
describe('matchesEntirely', () => {
|
||||
it('should work in edge cases', () => {
|
||||
// No text.
|
||||
matchesEntirely(undefined, '').should.equal(true)
|
||||
|
||||
// "OR" in regexp.
|
||||
matchesEntirely('911231231', '4\d{8}|[1-9]\d{7}').should.equal(false)
|
||||
})
|
||||
})
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Merges two arrays.
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @return {*}
|
||||
*/
|
||||
export default function mergeArrays(a, b) {
|
||||
const merged = a.slice()
|
||||
|
||||
for (const element of b) {
|
||||
if (a.indexOf(element) < 0) {
|
||||
merged.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
return merged.sort((a, b) => a - b)
|
||||
|
||||
// ES6 version, requires Set polyfill.
|
||||
// let merged = new Set(a)
|
||||
// for (const element of b) {
|
||||
// merged.add(i)
|
||||
// }
|
||||
// return Array.from(merged).sort((a, b) => a - b)
|
||||
}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import mergeArrays from './mergeArrays.js'
|
||||
|
||||
describe('mergeArrays', () => {
|
||||
it('should merge arrays', () => {
|
||||
mergeArrays([1, 2], [2, 3]).should.deep.equal([1, 2, 3])
|
||||
})
|
||||
})
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// These mappings map a character (key) to a specific digit that should
|
||||
// replace it for normalization purposes. Non-European digits that
|
||||
// may be used in phone numbers are mapped to a European equivalent.
|
||||
//
|
||||
// E.g. in Iraq they don't write `+442323234` but rather `+٤٤٢٣٢٣٢٣٤`.
|
||||
//
|
||||
export const DIGITS = {
|
||||
'0': '0',
|
||||
'1': '1',
|
||||
'2': '2',
|
||||
'3': '3',
|
||||
'4': '4',
|
||||
'5': '5',
|
||||
'6': '6',
|
||||
'7': '7',
|
||||
'8': '8',
|
||||
'9': '9',
|
||||
'\uFF10': '0', // Fullwidth digit 0
|
||||
'\uFF11': '1', // Fullwidth digit 1
|
||||
'\uFF12': '2', // Fullwidth digit 2
|
||||
'\uFF13': '3', // Fullwidth digit 3
|
||||
'\uFF14': '4', // Fullwidth digit 4
|
||||
'\uFF15': '5', // Fullwidth digit 5
|
||||
'\uFF16': '6', // Fullwidth digit 6
|
||||
'\uFF17': '7', // Fullwidth digit 7
|
||||
'\uFF18': '8', // Fullwidth digit 8
|
||||
'\uFF19': '9', // Fullwidth digit 9
|
||||
'\u0660': '0', // Arabic-indic digit 0
|
||||
'\u0661': '1', // Arabic-indic digit 1
|
||||
'\u0662': '2', // Arabic-indic digit 2
|
||||
'\u0663': '3', // Arabic-indic digit 3
|
||||
'\u0664': '4', // Arabic-indic digit 4
|
||||
'\u0665': '5', // Arabic-indic digit 5
|
||||
'\u0666': '6', // Arabic-indic digit 6
|
||||
'\u0667': '7', // Arabic-indic digit 7
|
||||
'\u0668': '8', // Arabic-indic digit 8
|
||||
'\u0669': '9', // Arabic-indic digit 9
|
||||
'\u06F0': '0', // Eastern-Arabic digit 0
|
||||
'\u06F1': '1', // Eastern-Arabic digit 1
|
||||
'\u06F2': '2', // Eastern-Arabic digit 2
|
||||
'\u06F3': '3', // Eastern-Arabic digit 3
|
||||
'\u06F4': '4', // Eastern-Arabic digit 4
|
||||
'\u06F5': '5', // Eastern-Arabic digit 5
|
||||
'\u06F6': '6', // Eastern-Arabic digit 6
|
||||
'\u06F7': '7', // Eastern-Arabic digit 7
|
||||
'\u06F8': '8', // Eastern-Arabic digit 8
|
||||
'\u06F9': '9' // Eastern-Arabic digit 9
|
||||
}
|
||||
|
||||
export function parseDigit(character) {
|
||||
return DIGITS[character]
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses phone number digits from a string.
|
||||
* Drops all punctuation leaving only digits.
|
||||
* Also converts wide-ascii and arabic-indic numerals to conventional numerals.
|
||||
* E.g. in Iraq they don't write `+442323234` but rather `+٤٤٢٣٢٣٢٣٤`.
|
||||
* @param {string} string
|
||||
* @return {string}
|
||||
* @example
|
||||
* ```js
|
||||
* parseDigits('8 (800) 555')
|
||||
* // Outputs '8800555'.
|
||||
* ```
|
||||
*/
|
||||
export default function parseDigits(string) {
|
||||
let result = ''
|
||||
// Using `.split('')` here instead of normal `for ... of`
|
||||
// because the importing application doesn't neccessarily include an ES6 polyfill.
|
||||
// The `.split('')` approach discards "exotic" UTF-8 characters
|
||||
// (the ones consisting of four bytes) but digits
|
||||
// (including non-European ones) don't fall into that range
|
||||
// so such "exotic" characters would be discarded anyway.
|
||||
for (const character of string.split('')) {
|
||||
const digit = parseDigit(character)
|
||||
if (digit) {
|
||||
result += digit
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import parseDigits from './parseDigits.js'
|
||||
|
||||
describe('parseDigits', () => {
|
||||
it('should parse digits', () => {
|
||||
parseDigits('+٤٤٢٣٢٣٢٣٤').should.equal('442323234')
|
||||
})
|
||||
})
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
import Metadata from '../metadata.js'
|
||||
import { VALID_DIGITS } from '../constants.js'
|
||||
|
||||
const CAPTURING_DIGIT_PATTERN = new RegExp('([' + VALID_DIGITS + '])')
|
||||
|
||||
export default function stripIddPrefix(number, country, callingCode, metadata) {
|
||||
if (!country) {
|
||||
return
|
||||
}
|
||||
// Check if the number is IDD-prefixed.
|
||||
const countryMetadata = new Metadata(metadata)
|
||||
countryMetadata.selectNumberingPlan(country, callingCode)
|
||||
const IDDPrefixPattern = new RegExp(countryMetadata.IDDPrefix())
|
||||
if (number.search(IDDPrefixPattern) !== 0) {
|
||||
return
|
||||
}
|
||||
// Strip IDD prefix.
|
||||
number = number.slice(number.match(IDDPrefixPattern)[0].length)
|
||||
// If there're any digits after an IDD prefix,
|
||||
// then those digits are a country calling code.
|
||||
// Since no country code starts with a `0`,
|
||||
// the code below validates that the next digit (if present) is not `0`.
|
||||
const matchedGroups = number.match(CAPTURING_DIGIT_PATTERN)
|
||||
if (matchedGroups && matchedGroups[1] != null && matchedGroups[1].length > 0) {
|
||||
if (matchedGroups[1] === '0') {
|
||||
return
|
||||
}
|
||||
}
|
||||
return number
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
import stripIddPrefix from './stripIddPrefix.js'
|
||||
|
||||
import metadata from '../../metadata.min.json' assert { type: 'json' }
|
||||
|
||||
describe('stripIddPrefix', () => {
|
||||
it('should strip a valid IDD prefix', () => {
|
||||
stripIddPrefix('01178005553535', 'US', '1', metadata).should.equal('78005553535')
|
||||
})
|
||||
|
||||
it('should strip a valid IDD prefix (no country calling code)', () => {
|
||||
stripIddPrefix('011', 'US', '1', metadata).should.equal('')
|
||||
})
|
||||
|
||||
it('should strip a valid IDD prefix (valid country calling code)', () => {
|
||||
stripIddPrefix('0117', 'US', '1', metadata).should.equal('7')
|
||||
})
|
||||
|
||||
it('should strip a valid IDD prefix (not a valid country calling code)', () => {
|
||||
expect(stripIddPrefix('0110', 'US', '1', metadata)).to.be.undefined
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user