UNPKG

17.5 kBJavaScriptView Raw
1import extractCountryCallingCode from './helpers/extractCountryCallingCode'
2import extractCountryCallingCodeFromInternationalNumberWithoutPlusSign from './helpers/extractCountryCallingCodeFromInternationalNumberWithoutPlusSign'
3import extractNationalNumberFromPossiblyIncompleteNumber from './helpers/extractNationalNumberFromPossiblyIncompleteNumber'
4import stripIddPrefix from './helpers/stripIddPrefix'
5import parseDigits from './helpers/parseDigits'
6
7import {
8 VALID_DIGITS,
9 VALID_PUNCTUATION,
10 PLUS_CHARS
11} from './constants'
12
13const VALID_FORMATTED_PHONE_NUMBER_PART =
14 '[' +
15 VALID_PUNCTUATION +
16 VALID_DIGITS +
17 ']+'
18
19const VALID_FORMATTED_PHONE_NUMBER_PART_PATTERN = new RegExp('^' + VALID_FORMATTED_PHONE_NUMBER_PART + '$', 'i')
20
21const VALID_PHONE_NUMBER =
22 '(?:' +
23 '[' + PLUS_CHARS + ']' +
24 '[' +
25 VALID_PUNCTUATION +
26 VALID_DIGITS +
27 ']*' +
28 '|' +
29 '[' +
30 VALID_PUNCTUATION +
31 VALID_DIGITS +
32 ']+' +
33 ')'
34
35const AFTER_PHONE_NUMBER_DIGITS_END_PATTERN = new RegExp(
36 '[^' +
37 VALID_PUNCTUATION +
38 VALID_DIGITS +
39 ']+' +
40 '.*' +
41 '$'
42)
43
44// Tests whether `national_prefix_for_parsing` could match
45// different national prefixes.
46// Matches anything that's not a digit or a square bracket.
47const COMPLEX_NATIONAL_PREFIX = /[^\d\[\]]/
48
49export default class AsYouTypeParser {
50 constructor({
51 defaultCountry,
52 defaultCallingCode,
53 metadata,
54 onNationalSignificantNumberChange
55 }) {
56 this.defaultCountry = defaultCountry
57 this.defaultCallingCode = defaultCallingCode
58 this.metadata = metadata
59 this.onNationalSignificantNumberChange = onNationalSignificantNumberChange
60 }
61
62 input(text, state) {
63 const [formattedDigits, hasPlus] = extractFormattedDigitsAndPlus(text)
64 const digits = parseDigits(formattedDigits)
65 // Checks for a special case: just a leading `+` has been entered.
66 let justLeadingPlus
67 if (hasPlus) {
68 if (!state.digits) {
69 state.startInternationalNumber()
70 if (!digits) {
71 justLeadingPlus = true
72 }
73 }
74 }
75 if (digits) {
76 this.inputDigits(digits, state)
77 }
78 return {
79 digits,
80 justLeadingPlus
81 }
82 }
83
84 /**
85 * Inputs "next" phone number digits.
86 * @param {string} digits
87 * @return {string} [formattedNumber] Formatted national phone number (if it can be formatted at this stage). Returning `undefined` means "don't format the national phone number at this stage".
88 */
89 inputDigits(nextDigits, state) {
90 const { digits } = state
91 const hasReceivedThreeLeadingDigits = digits.length < 3 && digits.length + nextDigits.length >= 3
92
93 // Append phone number digits.
94 state.appendDigits(nextDigits)
95
96 // Attempt to extract IDD prefix:
97 // Some users input their phone number in international format,
98 // but in an "out-of-country" dialing format instead of using the leading `+`.
99 // https://github.com/catamphetamine/libphonenumber-js/issues/185
100 // Detect such numbers as soon as there're at least 3 digits.
101 // Google's library attempts to extract IDD prefix at 3 digits,
102 // so this library just copies that behavior.
103 // I guess that's because the most commot IDD prefixes are
104 // `00` (Europe) and `011` (US).
105 // There exist really long IDD prefixes too:
106 // for example, in Australia the default IDD prefix is `0011`,
107 // and it could even be as long as `14880011`.
108 // An IDD prefix is extracted here, and then every time when
109 // there's a new digit and the number couldn't be formatted.
110 if (hasReceivedThreeLeadingDigits) {
111 this.extractIddPrefix(state)
112 }
113
114 if (this.isWaitingForCountryCallingCode(state)) {
115 if (!this.extractCountryCallingCode(state)) {
116 return
117 }
118 } else {
119 state.appendNationalSignificantNumberDigits(nextDigits)
120 }
121
122 // If a phone number is being input in international format,
123 // then it's not valid for it to have a national prefix.
124 // Still, some people incorrectly input such numbers with a national prefix.
125 // In such cases, only attempt to strip a national prefix if the number becomes too long.
126 // (but that is done later, not here)
127 if (!state.international) {
128 if (!this.hasExtractedNationalSignificantNumber) {
129 this.extractNationalSignificantNumber(state.getNationalDigits(), state.update)
130 }
131 }
132 }
133
134 isWaitingForCountryCallingCode({ international, callingCode }) {
135 return international && !callingCode
136 }
137
138 // Extracts a country calling code from a number
139 // being entered in internatonal format.
140 extractCountryCallingCode(state) {
141 const { countryCallingCode, number } = extractCountryCallingCode(
142 '+' + state.getDigitsWithoutInternationalPrefix(),
143 this.defaultCountry,
144 this.defaultCallingCode,
145 this.metadata.metadata
146 )
147 if (countryCallingCode) {
148 state.setCallingCode(countryCallingCode)
149 state.update({
150 nationalSignificantNumber: number
151 })
152 return true
153 }
154 }
155
156 reset(numberingPlan) {
157 if (numberingPlan) {
158 this.hasSelectedNumberingPlan = true
159 const nationalPrefixForParsing = numberingPlan._nationalPrefixForParsing()
160 this.couldPossiblyExtractAnotherNationalSignificantNumber = nationalPrefixForParsing && COMPLEX_NATIONAL_PREFIX.test(nationalPrefixForParsing)
161 } else {
162 this.hasSelectedNumberingPlan = undefined
163 this.couldPossiblyExtractAnotherNationalSignificantNumber = undefined
164 }
165 }
166
167 /**
168 * Extracts a national (significant) number from user input.
169 * Google's library is different in that it only applies `national_prefix_for_parsing`
170 * and doesn't apply `national_prefix_transform_rule` after that.
171 * https://github.com/google/libphonenumber/blob/a3d70b0487875475e6ad659af404943211d26456/java/libphonenumber/src/com/google/i18n/phonenumbers/AsYouTypeFormatter.java#L539
172 * @return {boolean} [extracted]
173 */
174 extractNationalSignificantNumber(nationalDigits, setState) {
175 if (!this.hasSelectedNumberingPlan) {
176 return
177 }
178 const {
179 nationalPrefix,
180 nationalNumber,
181 carrierCode
182 } = extractNationalNumberFromPossiblyIncompleteNumber(
183 nationalDigits,
184 this.metadata
185 )
186 if (nationalNumber === nationalDigits) {
187 return
188 }
189 this.onExtractedNationalNumber(
190 nationalPrefix,
191 carrierCode,
192 nationalNumber,
193 nationalDigits,
194 setState
195 )
196 return true
197 }
198
199 /**
200 * In Google's code this function is called "attempt to extract longer NDD".
201 * "Some national prefixes are a substring of others", they say.
202 * @return {boolean} [result] — Returns `true` if extracting a national prefix produced different results from what they were.
203 */
204 extractAnotherNationalSignificantNumber(nationalDigits, prevNationalSignificantNumber, setState) {
205 if (!this.hasExtractedNationalSignificantNumber) {
206 return this.extractNationalSignificantNumber(nationalDigits, setState)
207 }
208 if (!this.couldPossiblyExtractAnotherNationalSignificantNumber) {
209 return
210 }
211 const {
212 nationalPrefix,
213 nationalNumber,
214 carrierCode
215 } = extractNationalNumberFromPossiblyIncompleteNumber(
216 nationalDigits,
217 this.metadata
218 )
219 // If a national prefix has been extracted previously,
220 // then it's always extracted as additional digits are added.
221 // That's assuming `extractNationalNumberFromPossiblyIncompleteNumber()`
222 // doesn't do anything different from what it currently does.
223 // So, just in case, here's this check, though it doesn't occur.
224 /* istanbul ignore if */
225 if (nationalNumber === prevNationalSignificantNumber) {
226 return
227 }
228 this.onExtractedNationalNumber(
229 nationalPrefix,
230 carrierCode,
231 nationalNumber,
232 nationalDigits,
233 setState
234 )
235 return true
236 }
237
238 onExtractedNationalNumber(
239 nationalPrefix,
240 carrierCode,
241 nationalSignificantNumber,
242 nationalDigits,
243 setState
244 ) {
245 let complexPrefixBeforeNationalSignificantNumber
246 let nationalSignificantNumberMatchesInput
247 // This check also works with empty `this.nationalSignificantNumber`.
248 const nationalSignificantNumberIndex = nationalDigits.lastIndexOf(nationalSignificantNumber)
249 // If the extracted national (significant) number is the
250 // last substring of the `digits`, then it means that it hasn't been altered:
251 // no digits have been removed from the national (significant) number
252 // while applying `national_prefix_transform_rule`.
253 // https://gitlab.com/catamphetamine/libphonenumber-js/-/blob/master/METADATA.md#national_prefix_for_parsing--national_prefix_transform_rule
254 if (nationalSignificantNumberIndex >= 0 &&
255 nationalSignificantNumberIndex === nationalDigits.length - nationalSignificantNumber.length) {
256 nationalSignificantNumberMatchesInput = true
257 // If a prefix of a national (significant) number is not as simple
258 // as just a basic national prefix, then such prefix is stored in
259 // `this.complexPrefixBeforeNationalSignificantNumber` property and will be
260 // prepended "as is" to the national (significant) number to produce
261 // a formatted result.
262 const prefixBeforeNationalNumber = nationalDigits.slice(0, nationalSignificantNumberIndex)
263 // `prefixBeforeNationalNumber` is always non-empty,
264 // because `onExtractedNationalNumber()` isn't called
265 // when a national (significant) number hasn't been actually "extracted":
266 // when a national (significant) number is equal to the national part of `digits`,
267 // then `onExtractedNationalNumber()` doesn't get called.
268 if (prefixBeforeNationalNumber !== nationalPrefix) {
269 complexPrefixBeforeNationalSignificantNumber = prefixBeforeNationalNumber
270 }
271 }
272 setState({
273 nationalPrefix,
274 carrierCode,
275 nationalSignificantNumber,
276 nationalSignificantNumberMatchesInput,
277 complexPrefixBeforeNationalSignificantNumber
278 })
279 // `onExtractedNationalNumber()` is only called when
280 // the national (significant) number actually did change.
281 this.hasExtractedNationalSignificantNumber = true
282 this.onNationalSignificantNumberChange()
283 }
284
285 reExtractNationalSignificantNumber(state) {
286 // Attempt to extract a national prefix.
287 //
288 // Some people incorrectly input national prefix
289 // in an international phone number.
290 // For example, some people write British phone numbers as `+44(0)...`.
291 //
292 // Also, in some rare cases, it is valid for a national prefix
293 // to be a part of an international phone number.
294 // For example, mobile phone numbers in Mexico are supposed to be
295 // dialled internationally using a `1` national prefix,
296 // so the national prefix will be part of an international number.
297 //
298 // Quote from:
299 // https://www.mexperience.com/dialing-cell-phones-in-mexico/
300 //
301 // "Dialing a Mexican cell phone from abroad
302 // When you are calling a cell phone number in Mexico from outside Mexico,
303 // it’s necessary to dial an additional “1” after Mexico’s country code
304 // (which is “52”) and before the area code.
305 // You also ignore the 045, and simply dial the area code and the
306 // cell phone’s number.
307 //
308 // If you don’t add the “1”, you’ll receive a recorded announcement
309 // asking you to redial using it.
310 //
311 // For example, if you are calling from the USA to a cell phone
312 // in Mexico City, you would dial +52 – 1 – 55 – 1234 5678.
313 // (Note that this is different to calling a land line in Mexico City
314 // from abroad, where the number dialed would be +52 – 55 – 1234 5678)".
315 //
316 // Google's demo output:
317 // https://libphonenumber.appspot.com/phonenumberparser?number=%2b5215512345678&country=MX
318 //
319 if (this.extractAnotherNationalSignificantNumber(
320 state.getNationalDigits(),
321 state.nationalSignificantNumber,
322 state.update
323 )) {
324 return true
325 }
326 // If no format matches the phone number, then it could be
327 // "a really long IDD" (quote from a comment in Google's library).
328 // An IDD prefix is first extracted when the user has entered at least 3 digits,
329 // and then here — every time when there's a new digit and the number
330 // couldn't be formatted.
331 // For example, in Australia the default IDD prefix is `0011`,
332 // and it could even be as long as `14880011`.
333 //
334 // Could also check `!hasReceivedThreeLeadingDigits` here
335 // to filter out the case when this check duplicates the one
336 // already performed when there're 3 leading digits,
337 // but it's not a big deal, and in most cases there
338 // will be a suitable `format` when there're 3 leading digits.
339 //
340 if (this.extractIddPrefix(state)) {
341 this.extractCallingCodeAndNationalSignificantNumber(state)
342 return true
343 }
344 // Google's AsYouType formatter supports sort of an "autocorrection" feature
345 // when it "autocorrects" numbers that have been input for a country
346 // with that country's calling code.
347 // Such "autocorrection" feature looks weird, but different people have been requesting it:
348 // https://github.com/catamphetamine/libphonenumber-js/issues/376
349 // https://github.com/catamphetamine/libphonenumber-js/issues/375
350 // https://github.com/catamphetamine/libphonenumber-js/issues/316
351 if (this.fixMissingPlus(state)) {
352 this.extractCallingCodeAndNationalSignificantNumber(state)
353 return true
354 }
355 }
356
357 extractIddPrefix(state) {
358 // An IDD prefix can't be present in a number written with a `+`.
359 // Also, don't re-extract an IDD prefix if has already been extracted.
360 const {
361 international,
362 IDDPrefix,
363 digits,
364 nationalSignificantNumber
365 } = state
366 if (international || IDDPrefix) {
367 return
368 }
369 // Some users input their phone number in "out-of-country"
370 // dialing format instead of using the leading `+`.
371 // https://github.com/catamphetamine/libphonenumber-js/issues/185
372 // Detect such numbers.
373 const numberWithoutIDD = stripIddPrefix(
374 digits,
375 this.defaultCountry,
376 this.defaultCallingCode,
377 this.metadata.metadata
378 )
379 if (numberWithoutIDD !== undefined && numberWithoutIDD !== digits) {
380 // If an IDD prefix was stripped then convert the IDD-prefixed number
381 // to international number for subsequent parsing.
382 state.update({
383 IDDPrefix: digits.slice(0, digits.length - numberWithoutIDD.length)
384 })
385 this.startInternationalNumber(state)
386 return true
387 }
388 }
389
390 fixMissingPlus(state) {
391 if (!state.international) {
392 const {
393 countryCallingCode: newCallingCode,
394 number
395 } = extractCountryCallingCodeFromInternationalNumberWithoutPlusSign(
396 state.digits,
397 this.defaultCountry,
398 this.defaultCallingCode,
399 this.metadata.metadata
400 )
401 if (newCallingCode) {
402 state.update({
403 missingPlus: true
404 })
405 this.startInternationalNumber(state)
406 return true
407 }
408 }
409 }
410
411 startInternationalNumber(state) {
412 state.startInternationalNumber()
413 // If a national (significant) number has been extracted before, reset it.
414 if (state.nationalSignificantNumber) {
415 state.resetNationalSignificantNumber()
416 this.onNationalSignificantNumberChange()
417 this.hasExtractedNationalSignificantNumber = undefined
418 }
419 }
420
421 extractCallingCodeAndNationalSignificantNumber(state) {
422 if (this.extractCountryCallingCode(state)) {
423 // `this.extractCallingCode()` is currently called when the number
424 // couldn't be formatted during the standard procedure.
425 // Normally, the national prefix would be re-extracted
426 // for an international number if such number couldn't be formatted,
427 // but since it's already not able to be formatted,
428 // there won't be yet another retry, so also extract national prefix here.
429 this.extractNationalSignificantNumber(state.getNationalDigits(), state.update)
430 }
431 }
432}
433
434/**
435 * Extracts formatted phone number from text (if there's any).
436 * @param {string} text
437 * @return {string} [formattedPhoneNumber]
438 */
439function extractFormattedPhoneNumber(text) {
440 // Attempt to extract a possible number from the string passed in.
441 const startsAt = text.search(VALID_PHONE_NUMBER)
442 if (startsAt < 0) {
443 return
444 }
445 // Trim everything to the left of the phone number.
446 text = text.slice(startsAt)
447 // Trim the `+`.
448 let hasPlus
449 if (text[0] === '+') {
450 hasPlus = true
451 text = text.slice('+'.length)
452 }
453 // Trim everything to the right of the phone number.
454 text = text.replace(AFTER_PHONE_NUMBER_DIGITS_END_PATTERN, '')
455 // Re-add the previously trimmed `+`.
456 if (hasPlus) {
457 text = '+' + text
458 }
459 return text
460}
461
462/**
463 * Extracts formatted phone number digits (and a `+`) from text (if there're any).
464 * @param {string} text
465 * @return {any[]}
466 */
467function _extractFormattedDigitsAndPlus(text) {
468 // Extract a formatted phone number part from text.
469 const extractedNumber = extractFormattedPhoneNumber(text) || ''
470 // Trim a `+`.
471 if (extractedNumber[0] === '+') {
472 return [extractedNumber.slice('+'.length), true]
473 }
474 return [extractedNumber]
475}
476
477/**
478 * Extracts formatted phone number digits (and a `+`) from text (if there're any).
479 * @param {string} text
480 * @return {any[]}
481 */
482export function extractFormattedDigitsAndPlus(text) {
483 let [formattedDigits, hasPlus] = _extractFormattedDigitsAndPlus(text)
484 // If the extracted phone number part
485 // can possibly be a part of some valid phone number
486 // then parse phone number characters from a formatted phone number.
487 if (!VALID_FORMATTED_PHONE_NUMBER_PART_PATTERN.test(formattedDigits)) {
488 formattedDigits = ''
489 }
490 return [formattedDigits, hasPlus]
491}
\No newline at end of file