catamphetamine / libphonenumber-js

A simpler (and smaller) rewrite of Google Android's libphonenumber library in javascript
https://catamphetamine.gitlab.io/libphonenumber-js/
MIT License
2.77k stars 218 forks source link

International phone number without a plus sign parsed invalid #376

Closed blasterpistol closed 4 years ago

blasterpistol commented 4 years ago

formatInternational() doubles the country code. It's a common case to parse a phone without a plus sign. For exampl, sms gateways accept both variants: with or without a plus, also many apps exports phones without a plus (yes, it's completely incorrect, but this happens everywhere). Can it be handled automatically or should a person decide by himself add a plus or not, even when the country code is specified?

parsePhoneNumberFromString('375447521111', 'BY')
PhoneNumber

  country: "BY"

  countryCallingCode: "375"

  metadata: {version: "1.7.40", country_calling_codes: Object, countries: Object, nonGeographic: Object}

  nationalNumber: "375447521111"

  number: "+375375447521111"
catamphetamine commented 4 years ago

Yes, this feature has been requested numerous times. I've added a section in the readme to clarify why I haven't included such feature in the readme. And, there're a workaround code sample for those who still want to autocorrect such numbers. Report if the provided code sample works. https://github.com/catamphetamine/libphonenumber-js#missing- I've also added an item in the "Differences from libphonenumber" section.

blasterpistol commented 4 years ago

Thank you for the clarification. This snippet will help a lot, but I think it should be slightly corrected, since parsing doesn't always return undefined:

import { parsePhoneNumberFromString, getCountryCallingCode } from 'libphonenumber-js'

export default function autoFixParseNumber(number, country) {
  const phoneNumber = parsePhoneNumberFromString(number, country)
  // Here
  if (phoneNumber && phoneNumber.isValid()) {
    return phoneNumber
  }
  if (country && number[0] !== '+') {
    if (number.indexOf(getCountryCallingCode(country)) === 0) {
      return parsePhoneNumberFromString('+' + number, country)
    }
  }
}
catamphetamine commented 4 years ago

@testarossaaaaa Hmm, actually that's a valid correction. Isn't the isValid() check too strict though? isValid() returns true if the number matches the exact regular expressions for the country. For example, for mobile numbers in France it's 700\d{6}|(?:6\d|7[3-9])\d{7}. There's also another way just to validate phone number length: isPossible(). For France it's possible_lengths: [9]. For Germany, it's possible_lengths: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], so isPossible() check wouldn't be so reliable for Germany (+49), because it could be a "possible" 10-digit "local" number, or it could be a "possible" 8-digit "local" number plus the 49 country calling code prepended to it, so just isPossible() wouldn't tell those two apart (49 is a valid "area code" in Germany). isValid() is more precise, but that's only if you're looking for only "valid" numbers, dismissing all other "possible" numbers.

How does Google behave in this scenario? https://github.com/google/libphonenumber/blob/master/FAQ.md#why-wasnt-the-country-code-removed-when-parsing Seems that they do an isPossible() check.

  /**
   * Tries to extract a country calling code from a number. This method will return zero if no
   * country calling code is considered to be present. Country calling codes are extracted in the
   * following ways:
   * <ul>
   *  <li> by stripping the international dialing prefix of the region the person is dialing from,
   *       if this is present in the number, and looking at the next digits
   *  <li> by stripping the '+' sign if present and then looking at the next digits
   *  <li> by comparing the start of the number and the country calling code of the default region.
   *       If the number is not considered possible for the numbering plan of the default region
   *       initially, but starts with the country calling code of this region, validation will be
   *       reattempted after stripping this country calling code. If this number is considered a
   *       possible number, then the first digits will be considered the country calling code and
   *       removed as such.
   * </ul>
   */

Source: https://github.com/google/libphonenumber/blob/876268eb1ad6cdc1b7b5bef17fc5e43052702d57/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L2738-L2752

// Check to see if the number starts with the country calling code for the default region. If
// so, we remove the country calling code, and do some checks on the validity of the number
// before and after.
int defaultCountryCode = defaultRegionMetadata.getCountryCode();
String defaultCountryCodeString = String.valueOf(defaultCountryCode);
String normalizedNumber = fullNumber.toString();
if (normalizedNumber.startsWith(defaultCountryCodeString)) {
  StringBuilder potentialNationalNumber =
      new StringBuilder(normalizedNumber.substring(defaultCountryCodeString.length()));
  PhoneNumberDesc generalDesc = defaultRegionMetadata.getGeneralDesc();
  maybeStripNationalPrefixAndCarrierCode(
      potentialNationalNumber, defaultRegionMetadata, null /* Don't need the carrier code */);
  // 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.
  if ((!matcherApi.matchNationalNumber(fullNumber, generalDesc, false)
          && matcherApi.matchNationalNumber(potentialNationalNumber, generalDesc, false))
      || testNumberLength(fullNumber, defaultRegionMetadata) == ValidationResult.TOO_LONG) {
    nationalNumber.append(potentialNationalNumber);
    if (keepRawInput) {
      phoneNumber.setCountryCodeSource(CountryCodeSource.FROM_NUMBER_WITHOUT_PLUS_SIGN);
    }
    phoneNumber.setCountryCode(defaultCountryCode);
    return defaultCountryCode;
  }
}

Source: https://github.com/google/libphonenumber/blob/876268eb1ad6cdc1b7b5bef17fc5e43052702d57/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L2806-L2831

So, first they check whether a non-international number starts with the default country's calling code. If it does, they temporarily construct a shortened number for it. Then they choose that shortened number over the initial number if:

Seems reasonable. In fact, I guess I'll make it part of the library. Maybe some time later today.

blasterpistol commented 4 years ago

Thank you for deciding to add this to the library. This is much more than I expected. In my case I use the min version so isValid() almost the same as isPossible().

catamphetamine commented 4 years ago

@testarossaaaaa Alright, released libphonenumber-js@1.7.41. Seems to work. Thanks for the suggestion: previously I declined such feature requests, but your issue finally made me look into how Google does this and it turned out to be quite simple.

blasterpistol commented 4 years ago

Works like a charm!