Repository: marmelroy/PhoneNumberKit (swift)

Issue:
`PhoneNumberKit.parse` returns wrong phone number
<!-- Thanks for helping PhoneNumberKit! Before you submit your issue, please make sure you followed our checklist and check the appropriate boxes by putting an x in the [ ]: [x] -->

### New Issue Checklist

- [x] Updated PhoneNumberKit to the latest version
- [x] Phone number formatted correctly on [JavaScript version](https://htmlpreview.github.io/?https://github.com/google/libphonenumber/blob/master/javascript/i18n/phonenumbers/demo-compiled.html)
- [x] I searched for [existing GitHub issues](https://github.com/marmelroy/PhoneNumberKit)
- [x] I am aware that this library is not responsible of adding/removing/changing phone number formats and any request should be done at [libphonenumber repo](https://github.com/google/libphonenumber)

### Steps to reproduce
Call `try? phoneNumberKit.parse("393 903 5695", withRegion: "IT")`

##### Expected result
Correct phone number object is returned.

##### Actual result
Incorrect phone number object is returned. `numberString` and `nationalNumber` are cut off.

### Environment
Via SPM.

### Logs
```
(lldb) po try? phoneNumberKit.parse("393 903 569", withRegion: "IT")
▿ Optional<PhoneNumber>
  ▿ some : PhoneNumber
    - numberString : "393 903 569"
    - countryCode : 39
    - leadingZero : false
    - nationalNumber : 393903569
    - numberExtension : nil
    - type : PhoneNumberKit.PhoneNumberType.mobile
    ▿ regionID : Optional<String>
      - some : "IT"
(lldb) po try? phoneNumberKit.parse("393 903 5695", withRegion: "IT")
▿ Optional<PhoneNumber>
  ▿ some : PhoneNumber
    - numberString : "035695" // This number is cut off. Should be `"393 903 5695"`.
    - countryCode : 39
    - leadingZero : true
    - nationalNumber : 35695 // This number is cut off. Should be `3939035695`.
    - numberExtension : nil
    - type : PhoneNumberKit.PhoneNumberType.fixedLine
    ▿ regionID : Optional<String>
      - some : "IT"
```

Pull request description:
- Improved logic to validate numbers that starts with the same number a…s country code.
- Improve documentation
- Fix #847

Patch:
diff --git a/PhoneNumberKit/ParseManager.swift b/PhoneNumberKit/ParseManager.swift
index b90f5a9b..d24b7b75 100644
--- a/PhoneNumberKit/ParseManager.swift
+++ b/PhoneNumberKit/ParseManager.swift
@@ -26,58 +26,92 @@ final class ParseManager {
     /// - parameter ignoreType:   Avoids number type checking for faster performance.
     func parse(_ numberString: String, withRegion region: String, ignoreType: Bool) throws -> PhoneNumber {
         guard let metadataManager = metadataManager, let regexManager = regexManager else { throw PhoneNumberError.generalError }
-        // Make sure region is in uppercase so that it matches metadata (1)
+        
+        // STEP 1: Normalize region code
+        // Ensure region is uppercase to match metadata keys (e.g., "us" -> "US")
         let region = region.uppercased()
-        // Extract number (2)
+        
+        // STEP 2: Extract and normalize the phone number
+        // Find the actual phone number within the input string using data detector
         var nationalNumber = numberString
         let match = try regexManager.phoneDataDetectorMatch(numberString)
         let matchedNumber = nationalNumber.substring(with: match.range)
-        // Replace Arabic and Persian numerals and let the rest unchanged
+        
+        // Normalize digits by replacing Arabic and Persian numerals with standard digits
+        // while keeping other characters unchanged for further processing
         nationalNumber = regexManager.stringByReplacingOccurrences(matchedNumber, map: PhoneNumberPatterns.allNormalizationMappings, keepUnmapped: true)
 
-        // Strip and extract extension (3)
+        // STEP 3: Handle phone number extensions
+        // Extract and normalize any extension (e.g., "ext 123", "x123") from the number
         var numberExtension: String?
         if let rawExtension = parser.stripExtension(&nationalNumber) {
             numberExtension = self.parser.normalizePhoneNumber(rawExtension)
         }
-        // Country code parse (4)
+        
+        // STEP 4: Extract country code from the number
+        // Get metadata for the specified region and attempt to extract country code
         guard var regionMetadata = metadataManager.filterTerritories(byCountry: region) else {
             throw PhoneNumberError.invalidCountryCode
         }
+        
         let countryCode: UInt64
         do {
+            // Try to extract country code normally
             countryCode = try self.parser.extractCountryCode(nationalNumber, nationalNumber: &nationalNumber, metadata: regionMetadata)
         } catch {
+            // Fallback: Remove any leading plus signs and try again
+            // This handles cases where the number has formatting like "+1" but extraction failed
             let plusRemovedNumberString = regexManager.replaceStringByRegex(PhoneNumberPatterns.leadingPlusCharsPattern, string: nationalNumber as String)
             countryCode = try self.parser.extractCountryCode(plusRemovedNumberString, nationalNumber: &nationalNumber, metadata: regionMetadata)
         }
 
-        // Normalized number (5)
+        // STEP 5: Final number normalization and validation
+        // Normalize the remaining national number (remove spaces, dashes, etc.)
         nationalNumber = self.parser.normalizePhoneNumber(nationalNumber)
+        
+        // Handle special case where no country code was extracted (countryCode == 0)
         if countryCode == 0 {
+            // Check if the number incorrectly includes the country code as part of the national number
             if nationalNumber.hasPrefix(String(regionMetadata.countryCode)) {
                 let potentialNationalNumber = String(nationalNumber.dropFirst(String(regionMetadata.countryCode).count))
-                if let result = try? parse(potentialNationalNumber, withRegion: regionMetadata.codeID, ignoreType: ignoreType) {
-                    return result
+                
+                // Validate that removing the country code prefix leaves a valid national number
+                if let generalNumberDesc = regionMetadata.generalDesc,
+                   regexManager.hasValue(generalNumberDesc.nationalNumberPattern),
+                   parser.isNumberMatchingDesc(potentialNationalNumber, numberDesc: generalNumberDesc) {
+                    
+                    // Attempt to create a valid phone number with the corrected national number
+                    let correctedNumberString = potentialNationalNumber
+                    if let result = try validPhoneNumber(from: potentialNationalNumber, using: regionMetadata, countryCode: regionMetadata.countryCode, ignoreType: ignoreType, numberString: correctedNumberString, numberExtension: numberExtension) {
+                        return result
+                    }
                 }
             }
 
+            // Last attempt: try to parse the number as-is with the region's default country code
             if let result = try validPhoneNumber(from: nationalNumber, using: regionMetadata, countryCode: regionMetadata.countryCode, ignoreType: ignoreType, numberString: numberString, numberExtension: numberExtension) {
                 return result
             }
+
+            // If all attempts fail, the number is invalid
             throw PhoneNumberError.invalidNumber
         }
 
-        // If country code is not default, grab correct metadata (6)
+        // STEP 6: Update metadata if extracted country code differs from region's default
+        // This handles cases where the number contains a country code different from the region parameter
+        // For example: parsing a US number (+1) while specifying region as "CA" (Canada, also +1)
         if countryCode != regionMetadata.countryCode, let countryMetadata = metadataManager.mainTerritory(forCode: countryCode) {
             regionMetadata = countryMetadata
         }
 
+        // Attempt to create a valid phone number with the extracted country code
         if let result = try validPhoneNumber(from: nationalNumber, using: regionMetadata, countryCode: countryCode, ignoreType: ignoreType, numberString: numberString, numberExtension: numberExtension) {
             return result
         }
 
-        // If everything fails, iterate through other territories with the same country code (7)
+        // STEP 7: Final fallback - try all territories with the same country code
+        // Some country codes are shared by multiple territories (e.g., +1 for US, CA, etc.)
+        // Try each territory's metadata to see if the number is valid in any of them
         var possibleResults: Set<PhoneNumber> = []
         if let metadataList = metadataManager.filterTerritories(byCode: countryCode) {
             for metadata in metadataList where regionMetadata.codeID != metadata.codeID {
@@ -87,10 +121,17 @@ final class ParseManager {
             }
         }
 
+        // Return results based on how many valid interpretations were found
         switch possibleResults.count {
-        case 0: throw PhoneNumberError.invalidNumber
-        case 1: return possibleResults.first!
-        default: throw PhoneNumberError.ambiguousNumber(phoneNumbers: possibleResults)
+        case 0: 
+            // No valid interpretation found
+            throw PhoneNumberError.invalidNumber
+        case 1: 
+            // Exactly one valid interpretation - return it
+            return possibleResults.first!
+        default: 
+            // Multiple valid interpretations - ambiguous number
+            throw PhoneNumberError.ambiguousNumber(phoneNumbers: possibleResults)
         }
     }
 
@@ -168,6 +209,7 @@ final class ParseManager {
            regexManager.hasValue(generalNumberDesc.nationalNumberPattern) == false || parser.isNumberMatchingDesc(nationalNumber, numberDesc: generalNumberDesc) == false {
             return nil
         }
+
         // Finalize remaining parameters and create phone number object (3)
         let leadingZero = nationalNumber.hasPrefix("0")
         guard let finalNationalNumber = UInt64(nationalNumber) else {


Test patch:
diff --git a/PhoneNumberKitTests/PhoneNumberUtilityTests.swift b/PhoneNumberKitTests/PhoneNumberUtilityTests.swift
index 83855105..04c889a0 100644
--- a/PhoneNumberKitTests/PhoneNumberUtilityTests.swift
+++ b/PhoneNumberKitTests/PhoneNumberUtilityTests.swift
@@ -478,4 +478,15 @@ final class PhoneNumberUtilityTests: XCTestCase {
             XCTAssertEqual(formatted, "+491713369876")
         }
     }
+
+    func testValidITNumbers() throws {
+        let numbers = ["3939035695","00393939035695", "+393939035695", "393939035695"]
+        try numbers.forEach {
+            let phoneNumber = try sut.parse($0, withRegion: "IT")
+            XCTAssertNotNil(phoneNumber)
+
+            let formatted = sut.format(phoneNumber, toType: .e164)
+            XCTAssertEqual(formatted, "+393939035695")
+        }
+    }
 }