From c76dcb21363af0a026873d72d5059356469d596e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Silvio=20Br=C3=A4ndle?= Date: Wed, 15 Jan 2025 13:49:39 +0000 Subject: [PATCH] Improve handling phone context in RFC3966 format. --- .../i18n/phonenumbers/PhoneNumberUtil.java | 112 ++++++++++++------ .../phonenumbers/PhoneNumberUtilTest.java | 32 ++++- 2 files changed, 109 insertions(+), 35 deletions(-) diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java index ea289fab4..8e497dd72 100644 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java @@ -3203,49 +3203,93 @@ public class PhoneNumberUtil { } /** - * Converts numberToParse to a form that we can parse and write it to nationalNumber if it is - * written in RFC3966; otherwise extract a possible number out of it and write to nationalNumber. + * Converts numberToParse to a form that we can parse and write it to outputNumber if it is + * written in RFC3966; otherwise extract a possible number out of it and write to outputNumber. */ - private void buildNationalNumberForParsing(String numberToParse, StringBuilder nationalNumber) + // @VisibleForTesting + void buildNationalNumberForParsing(String numberToParse, StringBuilder outputNumber) throws NumberParseException { - int indexOfPhoneContext = numberToParse.indexOf(Constants.RFC3966_PHONE_CONTEXT); - PhoneContext phoneContext = phoneContextParser.parse(numberToParse); - if (phoneContext != null) { - // If the phone context contains a phone number prefix, we need to capture it, whereas domains - // will be ignored. - if (phoneContext.getRawContext().charAt(0) == Constants.PLUS_SIGN) { - // Additional parameters might follow the phone context. If so, we will remove them here - // because the parameters after phone context are not important for parsing the phone - // number. - nationalNumber.append(phoneContext.getRawContext()); - } - - // 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. - int indexOfRfc3966Prefix = numberToParse.indexOf(RFC3966_PREFIX); - int indexOfNationalNumber = - (indexOfRfc3966Prefix >= 0) ? indexOfRfc3966Prefix + RFC3966_PREFIX.length() : 0; - nationalNumber.append(numberToParse.substring(indexOfNationalNumber, indexOfPhoneContext)); - } else { + 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.) - nationalNumber.append(extractPossibleNumber(numberToParse)); + outputNumber.append(extractPossibleNumber(numberToParse)); + + // Strip the isdn parameter if present + int idsnPrefixStart = outputNumber.indexOf(RFC3966_ISDN_SUBADDRESS); + if (idsnPrefixStart > 0) { + outputNumber.delete(idsnPrefixStart, outputNumber.length()); + } + return; } - // 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, - int indexOfIsdn = nationalNumber.indexOf(RFC3966_ISDN_SUBADDRESS); - if (indexOfIsdn > 0) { - nationalNumber.delete(indexOfIsdn, nationalNumber.length()); + // Note we also handle the case when "tel:" is missing. In that case, we consider the start of + // the string to be the start of the number. + int rfc3966PrefixStart = numberToParse.indexOf(RFC3966_PREFIX); + int numberStart = + (rfc3966PrefixStart >= 0) ? rfc3966PrefixStart + RFC3966_PREFIX.length() : 0; + int numberEnd = numberToParse.indexOf(";", numberStart); + + if (numberEnd < 0) { + // If there is no semicolon, we assume the rest of the string is the national number. + // This should not happen, since we expect to find a semicolon if there is a phone-context. + numberEnd = numberToParse.length(); } - // 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. + + String numberPart = numberToParse.substring(numberStart, numberEnd); + + outputNumber.append(constructE164(phoneContext, numberPart)); + + // Append the extension if present. + int extnPrefixStart = numberToParse.indexOf(RFC3966_EXTN_PREFIX); + if (extnPrefixStart >= 0) { + int extnStart = extnPrefixStart + RFC3966_EXTN_PREFIX.length(); + int extnEnd = numberToParse.indexOf(";", extnStart); + if (extnEnd < 0) { + extnEnd = numberToParse.length(); + } + outputNumber.append(DEFAULT_EXTN_PREFIX).append(numberToParse, extnStart, extnEnd); + } + } + + /** + * Attempts to construct an E164 number from the parsed phone context and numberPart and returns + * it. + * + * If the phone context is a country code, the national prefix is stripped from the numberPart and + * the E164 number is constructed from the country code and the stripped number. + * + * If the phone context is more than just a country code, we fall back to concatenating the whole + * context with the numberPart. + */ + private String constructE164(PhoneContext phoneContext, String numberPart) { + if (phoneContext.getRawContext().charAt(0) != Constants.PLUS_SIGN) { + return numberPart; + } + + if (phoneContext.getCountryCode() == null) { + // Fall back to prefixing the national number with the country calling code if the context + // is more than just a country calling code. + return phoneContext.getRawContext() + numberPart; + } + + // Get the region code and metadata from the phone context country code. + String regionCode = getRegionCodeForCountryCode(phoneContext.getCountryCode()); + PhoneMetadata regionMetadata = getMetadataForRegionOrCallingCode(phoneContext.getCountryCode(), regionCode); + + if (regionMetadata == null) { + // Fall back to prefixing the national number with the country calling code if the country + // code is invalid. + return phoneContext.getRawContext() + numberPart; + } + + StringBuilder numberWithoutNationalPrefix = new StringBuilder(numberPart); + maybeStripNationalPrefixAndCarrierCode(numberWithoutNationalPrefix, regionMetadata, + new StringBuilder()); + + return Constants.PLUS_SIGN + phoneContext.getCountryCode().toString() + + numberWithoutNationalPrefix; } /** diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java index f3bdf445d..1c0e27e31 100644 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java @@ -16,8 +16,13 @@ package com.google.i18n.phonenumbers; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; - +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; import com.google.i18n.phonenumbers.PhoneNumberUtil.ValidationResult; @@ -3279,4 +3284,29 @@ public class PhoneNumberUtilTest extends TestMetadataTestCase { } }); } + + public void testBuildNationalNumberForParsing() throws Exception { + // Test that the national prefix is stripped from the numberPart when the phone context is a + // country code. + StringBuilder nationalNumber = new StringBuilder(); + phoneUtil.buildNationalNumberForParsing("tel:033316005;phone-context=+64", nationalNumber); + assertEquals("+6433316005", nationalNumber.toString()); + nationalNumber.setLength(0); + // Test that the phone context is ignored if it is not a country code. + phoneUtil.buildNationalNumberForParsing("tel:033316005;phone-context=abc.nz", nationalNumber); + assertEquals("033316005", nationalNumber.toString()); + // Test that extensions are correctly parsed. + nationalNumber.setLength(0); + phoneUtil.buildNationalNumberForParsing("tel:033316005;ext=1234;phone-context=+64", + nationalNumber); + assertEquals("+6433316005 ext. 1234", nationalNumber.toString()); + nationalNumber.setLength(0); + phoneUtil.buildNationalNumberForParsing("tel:033316005;phone-context=+64;ext=1234", + nationalNumber); + assertEquals("+6433316005 ext. 1234", nationalNumber.toString()); + // Test that the isub parameter is removed. + nationalNumber.setLength(0); + phoneUtil.buildNationalNumberForParsing("tel:033316005;isub=1234", nationalNumber); + assertEquals("033316005", nationalNumber.toString()); + } }