diff --git a/.github/workflows/build_and_run_unit_tests_linux.yml b/.github/workflows/build_and_run_unit_tests_linux.yml
index f69a6ae54..c3e6fe88e 100644
--- a/.github/workflows/build_and_run_unit_tests_linux.yml
+++ b/.github/workflows/build_and_run_unit_tests_linux.yml
@@ -19,6 +19,7 @@ jobs:
run: |
(cd resources/geocoding; zip -r ../../resources/geocoding.zip *)
(cd resources/test/geocoding; zip -r ../../../resources/test/testgeocoding.zip *)
+ (cd resources/test/carrier; zip -r ../../../resources/test/testcarrier.zip *)
- name: Restore dependencies
run: dotnet restore
working-directory: ./csharp
diff --git a/.github/workflows/run_all_tests_and_upload_code_coverage.yml b/.github/workflows/run_all_tests_and_upload_code_coverage.yml
index 34e5692f1..1fc1df20d 100644
--- a/.github/workflows/run_all_tests_and_upload_code_coverage.yml
+++ b/.github/workflows/run_all_tests_and_upload_code_coverage.yml
@@ -20,6 +20,7 @@ jobs:
run: |
Compress-Archive -Path "resources\geocoding\*" -DestinationPath "resources\geocoding.zip"
Compress-Archive -Path "resources\test\geocoding\*" -DestinationPath "resources\test\testgeocoding.zip"
+ Compress-Archive -Path "resources\test\carrier\*" -DestinationPath "resources\test\testcarrier.zip"
- name: Run tests
run: dotnet test csharp/PhoneNumbers.sln --configuration Release --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
- name: Upload coverage reports to Codecov
diff --git a/.gitignore b/.gitignore
index a15e51bef..98782a0d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -202,6 +202,8 @@ FakesAssemblies/
# Zipped Geocoding Data
geocoding.zip
testgeocoding.zip
+/resources/carrier.zip
+/resources/test/testcarrier.zip
# Performance tests artifacts
csharp/PhoneNumbers.PerformanceTest/BenchmarkDotNet.Artifacts/
diff --git a/README.md b/README.md
index 2bc94c420..a35fd2fad 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,20 @@ var regionCode = phoneNumberUtil.GetRegionCodeForNumber(phoneNumber);
Console.WriteLine(regionCode); // US
```
+### Get the carrier name for a phone number
+```csharp
+using PhoneNumbers;
+
+var phoneNumberUtil = PhoneNumberUtil.GetInstance();
+var carrierMapper = PhoneNumberToCarrierMapper.GetInstance();
+var phoneNumber = phoneNumberUtil.Parse("+917503397672", null);
+var carrierName = carrierMapper.GetNameForNumber(phoneNumber, Locale.English);
+
+Console.WriteLine(carrierName); // Vodafone
+```
+
+> **Note:** Carrier data reflects the original network allocation. If the country supports mobile number portability, the number may have since moved to a different carrier. Use `GetSafeDisplayName` to return an empty string in those regions.
+
## Features
* Parsing/formatting/validating phone numbers for all countries/regions of the world.
@@ -94,6 +108,7 @@ Console.WriteLine(regionCode); // US
* IsPossibleNumber - quickly guessing whether a number is a possible phone number by using only the length information, much faster than a full validation.
* AsYouTypeFormatter - formats phone numbers on-the-fly when users enter each digit.
* FindNumbers - finds numbers in text input
+* PhoneNumberToCarrierMapper - looks up the carrier name originally assigned to a mobile or pager number, with locale-aware output and a safe-display mode for regions with mobile number portability.
See [PhoneNumberUtil.cs](csharp/PhoneNumbers/PhoneNumberUtil.cs) for the various methods and properties available.
@@ -119,21 +134,31 @@ For more information on metadata usage, please refer to the [main repository faq
## Running tests locally
-To run tests locally, you will need a zip version of the `geocoding.zip` file stored in the `resources` folder
-and `testgeocoding.zip` file stored in the `resources/test` folder.
+To run tests locally, you will need zip versions of the geocoding and carrier data files.
+
+| Zip file | Source directory |
+|---|---|
+| `resources/geocoding.zip` | `resources/geocoding/` |
+| `resources/carrier.zip` | `resources/carrier/` |
+| `resources/test/testgeocoding.zip` | `resources/test/geocoding/` |
+| `resources/test/testcarrier.zip` | `resources/test/carrier/` |
-On linux, you can run the following commands to generate the zip accordingly
+On linux, you can run the following commands to generate the zips accordingly
```bash
(cd resources/geocoding; zip -r ../../resources/geocoding.zip *)
+(cd resources/carrier; zip -r ../../resources/carrier.zip *)
(cd resources/test/geocoding; zip -r ../../../resources/test/testgeocoding.zip *)
+(cd resources/test/carrier; zip -r ../../../resources/test/testcarrier.zip *)
```
For windows, you can use the following powershell script
```powershell
Compress-Archive -Path "resources\geocoding\*" -DestinationPath "resources\geocoding.zip"
+Compress-Archive -Path "resources\carrier\*" -DestinationPath "resources\carrier.zip"
Compress-Archive -Path "resources\test\geocoding\*" -DestinationPath "resources\test\testgeocoding.zip"
+Compress-Archive -Path "resources\test\carrier\*" -DestinationPath "resources\test\testcarrier.zip"
```
## Contributing
diff --git a/appveyor.yml b/appveyor.yml
index 93d7e4b33..5f772190c 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -25,7 +25,9 @@ branches:
before_build:
- dotnet restore csharp -s https://api.nuget.org/v3/index.json
- ps: Compress-Archive -Path "resources\geocoding\*" -DestinationPath "resources\geocoding.zip"
+ - ps: Compress-Archive -Path "resources\carrier\*" -DestinationPath "resources\carrier.zip"
- ps: Compress-Archive -Path "resources\test\geocoding\*" -DestinationPath "resources\test\testgeocoding.zip"
+ - ps: Compress-Archive -Path "resources\test\carrier\*" -DestinationPath "resources\test\testcarrier.zip"
build_script:
- dotnet pack -c Release csharp\PhoneNumbers
- dotnet pack -c Release csharp\PhoneNumbers.Extensions
diff --git a/csharp/PhoneNumbers.Test/PhoneNumbers.Test.csproj b/csharp/PhoneNumbers.Test/PhoneNumbers.Test.csproj
index 64f08ce01..8a6348861 100644
--- a/csharp/PhoneNumbers.Test/PhoneNumbers.Test.csproj
+++ b/csharp/PhoneNumbers.Test/PhoneNumbers.Test.csproj
@@ -33,8 +33,10 @@
+
+
diff --git a/csharp/PhoneNumbers.Test/TestPhoneNumberToCarrierMapper.cs b/csharp/PhoneNumbers.Test/TestPhoneNumberToCarrierMapper.cs
new file mode 100644
index 000000000..660925883
--- /dev/null
+++ b/csharp/PhoneNumbers.Test/TestPhoneNumberToCarrierMapper.cs
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2013 The Libphonenumber Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Reflection;
+using Xunit;
+
+namespace PhoneNumbers.Test
+{
+ ///
+ /// Mirrors TestPhoneNumberToCarrierMapper's synthetic-data tests using testcarrier.zip
+ /// to exercise the zip code path (LoadFileNamesFromZip + GetManifestZipFileStream).
+ ///
+ [Collection("TestZippedCarrierTestCase")]
+ public class TestZippedPhoneNumberToCarrierMapper
+ {
+ private static readonly PhoneNumberToCarrierMapper s_zippedMapper =
+ new PhoneNumberToCarrierMapper("testcarrier.", Assembly.GetExecutingAssembly());
+
+ // Same numbers as the testCarrierMapper tests in TestPhoneNumberToCarrierMapper.
+ private static readonly PhoneNumber s_ukMobile1Test =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7387654321L).Build();
+ private static readonly PhoneNumber s_ukFixed1Test =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(1123456789L).Build();
+ private static readonly PhoneNumber s_ukPager =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7601234567L).Build();
+ private static readonly PhoneNumber s_ukInvalidTest =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7301234L).Build();
+ private static readonly PhoneNumber s_aoMobile1Test =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(917654321L).Build();
+
+ [Fact]
+ public void TestGetNameForMobilePortableRegion()
+ {
+ Assert.Equal("British carrier", s_zippedMapper.GetNameForNumber(s_ukMobile1Test, Locale.English));
+ Assert.Equal("British carrier", s_zippedMapper.GetNameForNumber(s_ukMobile1Test, Locale.French));
+ Assert.Equal("", s_zippedMapper.GetSafeDisplayName(s_ukMobile1Test, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNonMobilePortableRegion()
+ {
+ Assert.Equal("Angolan carrier", s_zippedMapper.GetNameForNumber(s_aoMobile1Test, Locale.English));
+ Assert.Equal("Angolan carrier", s_zippedMapper.GetSafeDisplayName(s_aoMobile1Test, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForPagerNumber()
+ {
+ Assert.Equal("British pager", s_zippedMapper.GetNameForNumber(s_ukPager, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForFixedLineNumber()
+ {
+ Assert.Equal("", s_zippedMapper.GetNameForNumber(s_ukFixed1Test, Locale.English));
+ Assert.Equal("British fixed line carrier", s_zippedMapper.GetNameForValidNumber(s_ukFixed1Test, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForInvalidNumber()
+ {
+ Assert.Equal("", s_zippedMapper.GetNameForNumber(s_ukInvalidTest, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameWithSwedishLocale()
+ {
+ Assert.Equal("Brittisk operatör", s_zippedMapper.GetNameForNumber(s_ukMobile1Test, new Locale("sv", "SE")));
+ }
+ }
+
+ [Collection("TestMetadataTestCase")]
+ public class TestPhoneNumberToCarrierMapper
+ {
+ private readonly PhoneNumberToCarrierMapper carrierMapper = PhoneNumberToCarrierMapper.GetInstance();
+
+ // Test-data mapper backed by resources/test/carrier/
+ private static readonly PhoneNumberToCarrierMapper testCarrierMapper =
+ new PhoneNumberToCarrierMapper("carrier.", Assembly.GetExecutingAssembly());
+
+ // ── Production-data numbers (used with carrierMapper / GetInstance()) ─────────────────
+
+ // UK mobile: 447106 -> O2, 447306 -> Virgin Mobile (en/44.txt)
+ private static readonly PhoneNumber UK_MOBILE1 =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7106123456L).Build();
+ private static readonly PhoneNumber UK_MOBILE2 =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7306123456L).Build();
+ private static readonly PhoneNumber UK_FIXED1 =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(2071234567L).Build();
+ // Too short to be valid — UNKNOWN type
+ private static readonly PhoneNumber UK_INVALID_NUMBER =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7301234L).Build();
+
+ // Angola mobile: 24491 -> Movicel, 24492 -> UNITEL (en/244.txt)
+ private static readonly PhoneNumber AO_MOBILE1 =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(912345678L).Build();
+ private static readonly PhoneNumber AO_MOBILE2 =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(923456789L).Build();
+ private static readonly PhoneNumber AO_FIXED1 =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(222333444L).Build();
+ // Too short to be valid — UNKNOWN type
+ private static readonly PhoneNumber AO_INVALID_NUMBER =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(123456L).Build();
+ // Prefix 24498 is not present in en/244.txt
+ private static readonly PhoneNumber AO_NUMBER_WITH_MISSING_PREFIX =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(985000000L).Build();
+
+ // US FIXED_LINE_OR_MOBILE — no carrier data for this prefix in en/1.txt
+ private static readonly PhoneNumber US_FIXED_OR_MOBILE =
+ new PhoneNumber.Builder().SetCountryCode(1).SetNationalNumber(6502530000L).Build();
+
+ // No carrier data files exist for these country codes
+ private static readonly PhoneNumber NUMBER_WITH_INVALID_COUNTRY_CODE =
+ new PhoneNumber.Builder().SetCountryCode(999).SetNationalNumber(2423651234L).Build();
+ private static readonly PhoneNumber INTERNATIONAL_TOLL_FREE =
+ new PhoneNumber.Builder().SetCountryCode(800).SetNationalNumber(12345678L).Build();
+
+ // ── Test-data numbers (used with testCarrierMapper, match prefixes in resources/test/carrier/) ─
+
+ // en/44.txt: 4411 -> "British fixed line carrier", 4473 -> "British carrier", 44760 -> "British pager"
+ // sv/44.txt: 4473 -> "Brittisk operatör"
+ private static readonly PhoneNumber UK_MOBILE1_TEST =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7387654321L).Build();
+ private static readonly PhoneNumber UK_MOBILE2_TEST =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7487654321L).Build();
+ private static readonly PhoneNumber UK_FIXED1_TEST =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(1123456789L).Build();
+ private static readonly PhoneNumber UK_FIXED2_TEST =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(2987654321L).Build();
+ private static readonly PhoneNumber UK_PAGER =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7601234567L).Build();
+ private static readonly PhoneNumber UK_INVALID_TEST =
+ new PhoneNumber.Builder().SetCountryCode(44).SetNationalNumber(7301234L).Build();
+
+ // en/244.txt: 244917 -> "Angolan carrier", 244262 -> "Angolan fixed line carrier"
+ private static readonly PhoneNumber AO_MOBILE1_TEST =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(917654321L).Build();
+ private static readonly PhoneNumber AO_MOBILE2_TEST =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(927654321L).Build();
+ private static readonly PhoneNumber AO_FIXED1_TEST =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(22254321L).Build();
+ private static readonly PhoneNumber AO_FIXED2_TEST =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(26254321L).Build();
+ private static readonly PhoneNumber AO_INVALID_TEST =
+ new PhoneNumber.Builder().SetCountryCode(244).SetNationalNumber(101234L).Build();
+
+ // en/1650.txt: 1650212 -> "US carrier", 1650213 -> "US carrier2"
+ // NANPA data is split by area code prefix (e.g. 1650), so we need a file per prefix.
+ private static readonly PhoneNumber US_FIXED_OR_MOBILE_TEST =
+ new PhoneNumber.Builder().SetCountryCode(1).SetNationalNumber(6502123456L).Build();
+ // Area code 212 (New York) — no split file exists in the test data, so lookup returns "".
+ private static readonly PhoneNumber s_usNanpaNoDataTest =
+ new PhoneNumber.Builder().SetCountryCode(1).SetNationalNumber(2128120000L).Build();
+
+ [Fact]
+ public void TestGetNameForMobilePortableRegion()
+ {
+ // UK supports mobile number portability: GetNameForNumber still returns the carrier.
+ Assert.Equal("O2", carrierMapper.GetNameForNumber(UK_MOBILE1, Locale.English));
+ // No French carrier data for UK — falls back to English.
+ Assert.Equal("O2", carrierMapper.GetNameForNumber(UK_MOBILE1, Locale.French));
+ // GetSafeDisplayName returns empty because UK has MNP.
+ Assert.Equal("", carrierMapper.GetSafeDisplayName(UK_MOBILE1, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNonMobilePortableRegion()
+ {
+ // Angola does not support MNP: both methods return the carrier.
+ Assert.Equal("Movicel", carrierMapper.GetNameForNumber(AO_MOBILE1, Locale.English));
+ Assert.Equal("Movicel", carrierMapper.GetSafeDisplayName(AO_MOBILE1, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForFixedLineNumber()
+ {
+ // Fixed-line numbers are not mobile type: GetNameForNumber returns "".
+ Assert.Equal("", carrierMapper.GetNameForNumber(AO_FIXED1, Locale.English));
+ Assert.Equal("", carrierMapper.GetNameForNumber(UK_FIXED1, Locale.English));
+ // GetNameForValidNumber skips the type check but there is no carrier data for fixed lines.
+ Assert.Equal("", carrierMapper.GetNameForValidNumber(AO_FIXED1, Locale.English));
+ Assert.Equal("", carrierMapper.GetNameForValidNumber(UK_FIXED1, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForFixedOrMobileNumber()
+ {
+ // FIXED_LINE_OR_MOBILE is treated as mobile by GetNameForNumber.
+ // No carrier data exists for US prefix 1-6502..., so "" is returned.
+ Assert.Equal("", carrierMapper.GetNameForNumber(US_FIXED_OR_MOBILE, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNumberWithNoDataFile()
+ {
+ // No carrier data file for country code 999 or 800.
+ Assert.Equal("", carrierMapper.GetNameForNumber(NUMBER_WITH_INVALID_COUNTRY_CODE, Locale.English));
+ Assert.Equal("", carrierMapper.GetNameForNumber(INTERNATIONAL_TOLL_FREE, Locale.English));
+ Assert.Equal("", carrierMapper.GetNameForValidNumber(NUMBER_WITH_INVALID_COUNTRY_CODE, Locale.English));
+ Assert.Equal("", carrierMapper.GetNameForValidNumber(INTERNATIONAL_TOLL_FREE, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNumberWithMissingPrefix()
+ {
+ // Prefix 24498 is absent from en/244.txt — returns "" regardless of number type.
+ Assert.Equal("", carrierMapper.GetNameForNumber(AO_NUMBER_WITH_MISSING_PREFIX, Locale.English));
+ Assert.Equal("", carrierMapper.GetNameForValidNumber(AO_NUMBER_WITH_MISSING_PREFIX, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForInvalidNumber()
+ {
+ // UNKNOWN-type numbers are not mobile, so GetNameForNumber returns "".
+ Assert.Equal("", carrierMapper.GetNameForNumber(UK_INVALID_NUMBER, Locale.English));
+ Assert.Equal("", carrierMapper.GetNameForNumber(AO_INVALID_NUMBER, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForValidNumber()
+ {
+ // GetNameForValidNumber skips type checking and returns the carrier directly.
+ Assert.Equal("O2", carrierMapper.GetNameForValidNumber(UK_MOBILE1, Locale.English));
+ Assert.Equal("Virgin Mobile", carrierMapper.GetNameForValidNumber(UK_MOBILE2, Locale.English));
+ Assert.Equal("Movicel", carrierMapper.GetNameForValidNumber(AO_MOBILE1, Locale.English));
+ Assert.Equal("UNITEL", carrierMapper.GetNameForValidNumber(AO_MOBILE2, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetSafeDisplayName()
+ {
+ // UK supports MNP — always returns "".
+ Assert.Equal("", carrierMapper.GetSafeDisplayName(UK_MOBILE1, Locale.English));
+ // Angola does not support MNP — returns the carrier name.
+ Assert.Equal("Movicel", carrierMapper.GetSafeDisplayName(AO_MOBILE1, Locale.English));
+ // Fixed-line: GetSafeDisplayName calls GetNameForNumber which returns "" for non-mobile type.
+ Assert.Equal("", carrierMapper.GetSafeDisplayName(UK_FIXED1, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameFallbackToEnglish()
+ {
+ // French has no carrier data for UK, so the result falls back to English.
+ Assert.Equal("O2", carrierMapper.GetNameForValidNumber(UK_MOBILE1, Locale.French));
+ // Korean never falls back to English (zh, ja, ko are excluded).
+ Assert.Equal("", carrierMapper.GetNameForValidNumber(UK_MOBILE1, Locale.Korean));
+ }
+
+ // ── Tests using synthetic test carrier data ─────────────────────────────────────────────
+
+ [Fact]
+ public void TestGetNameForMobilePortableRegion_WithTestData()
+ {
+ // UK has MNP: GetNameForNumber still resolves the original carrier.
+ Assert.Equal("British carrier", testCarrierMapper.GetNameForNumber(UK_MOBILE1_TEST, Locale.English));
+ // French has no test data for UK — falls back to English.
+ Assert.Equal("British carrier", testCarrierMapper.GetNameForNumber(UK_MOBILE1_TEST, Locale.French));
+ // GetSafeDisplayName returns "" because UK supports MNP.
+ Assert.Equal("", testCarrierMapper.GetSafeDisplayName(UK_MOBILE1_TEST, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNonMobilePortableRegion_WithTestData()
+ {
+ // Angola has no MNP: both methods return the carrier.
+ Assert.Equal("Angolan carrier", testCarrierMapper.GetNameForNumber(AO_MOBILE1_TEST, Locale.English));
+ Assert.Equal("Angolan carrier", testCarrierMapper.GetSafeDisplayName(AO_MOBILE1_TEST, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForPagerNumber()
+ {
+ // PAGER is treated as mobile by GetNameForNumber — carrier data is returned.
+ Assert.Equal("British pager", testCarrierMapper.GetNameForNumber(UK_PAGER, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForFixedOrMobileNumber_WithCarrierData()
+ {
+ // FIXED_LINE_OR_MOBILE is treated as mobile, so carrier data is returned.
+ Assert.Equal("US carrier", testCarrierMapper.GetNameForNumber(US_FIXED_OR_MOBILE_TEST, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForFixedLineNumber_WithTestData()
+ {
+ // Fixed-line type: GetNameForNumber skips non-mobile types — returns "".
+ Assert.Equal("", testCarrierMapper.GetNameForNumber(AO_FIXED1_TEST, Locale.English));
+ Assert.Equal("", testCarrierMapper.GetNameForNumber(UK_FIXED1_TEST, Locale.English));
+ // GetNameForValidNumber bypasses the type check — returns the carrier from the data file.
+ Assert.Equal("Angolan fixed line carrier", testCarrierMapper.GetNameForValidNumber(AO_FIXED2_TEST, Locale.English));
+ Assert.Equal("British fixed line carrier", testCarrierMapper.GetNameForValidNumber(UK_FIXED1_TEST, Locale.English));
+ // No carrier data for this UK fixed-line prefix — returns "" even without type check.
+ Assert.Equal("", testCarrierMapper.GetNameForValidNumber(UK_FIXED2_TEST, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNumberWithMissingPrefix_WithTestData()
+ {
+ // Prefixes not in the test data return "" regardless of number type.
+ Assert.Equal("", testCarrierMapper.GetNameForNumber(UK_MOBILE2_TEST, Locale.English));
+ Assert.Equal("", testCarrierMapper.GetNameForNumber(AO_MOBILE2_TEST, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForInvalidNumber_WithTestData()
+ {
+ Assert.Equal("", testCarrierMapper.GetNameForNumber(UK_INVALID_TEST, Locale.English));
+ Assert.Equal("", testCarrierMapper.GetNameForNumber(AO_INVALID_TEST, Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNumberWithSwedishLocale()
+ {
+ // Swedish test data (sv/44.txt) has an entry for prefix 4473.
+ Assert.Equal("Brittisk operatör",
+ testCarrierMapper.GetNameForNumber(UK_MOBILE1_TEST, new Locale("sv", "SE")));
+ }
+
+ // ── NANPA data-split tests ──────────────────────────────────────────────────────────────
+ // As the NANPA carrier data is split into per-area-code files (e.g. en/1650.txt),
+ // GetDescriptionForNumber must use phonePrefix = 1000 + (nationalNumber / 10_000_000)
+ // for country code 1 instead of the country code itself.
+
+ [Fact]
+ public void TestGetNameForNanpaNumberWithSplitData()
+ {
+ // 6502123456 → prefix 1650 → en/1650.txt → "US carrier"
+ Assert.Equal("US carrier",
+ testCarrierMapper.GetNameForNumber(US_FIXED_OR_MOBILE_TEST, Locale.English));
+ // 6502133456 → prefix 1650 → en/1650.txt → "US carrier2"
+ Assert.Equal("US carrier2",
+ testCarrierMapper.GetNameForValidNumber(
+ new PhoneNumber.Builder().SetCountryCode(1).SetNationalNumber(6502133456L).Build(),
+ Locale.English));
+ }
+
+ [Fact]
+ public void TestGetNameForNanpaNumberWithNoSplitFile()
+ {
+ // Area code 212 has no en/1212.txt split file — returns "".
+ Assert.Equal("", testCarrierMapper.GetNameForValidNumber(s_usNanpaNoDataTest, Locale.English));
+ }
+ }
+}
diff --git a/csharp/PhoneNumbers/PhoneNumberToCarrierMapper.cs b/csharp/PhoneNumbers/PhoneNumberToCarrierMapper.cs
new file mode 100644
index 000000000..9e394cd7a
--- /dev/null
+++ b/csharp/PhoneNumbers/PhoneNumberToCarrierMapper.cs
@@ -0,0 +1,110 @@
+#nullable disable
+/*
+ * Copyright (C) 2013 The Libphonenumber Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Reflection;
+
+namespace PhoneNumbers
+{
+ ///
+ /// A phone prefix mapper which provides carrier information related to a phone number.
+ ///
+ /// Carrier data is the one the number was originally allocated to. If the country supports mobile
+ /// number portability the number might not belong to the returned carrier anymore.
+ ///
+ ///
+ public class PhoneNumberToCarrierMapper
+ {
+ // Corresponds to resources/carrier/ embedded with LinkBase="carrier".
+ private const string MAPPING_DATA_DIRECTORY = "carrier.";
+
+ private static readonly Lazy s_instance =
+ new Lazy(() => new PhoneNumberToCarrierMapper(MAPPING_DATA_DIRECTORY));
+
+ private readonly PhoneNumberUtil phoneUtil = PhoneNumberUtil.GetInstance();
+ private readonly PrefixFileReader prefixFileReader;
+
+ // @VisibleForTesting
+ internal PhoneNumberToCarrierMapper(string phonePrefixDataDirectory, Assembly asm = null)
+ {
+ prefixFileReader = new PrefixFileReader(phonePrefixDataDirectory, asm);
+ }
+
+ ///
+ /// Gets the singleton instance.
+ ///
+ public static PhoneNumberToCarrierMapper GetInstance() => s_instance.Value;
+
+ ///
+ /// Returns a carrier name for the given phone number, in the language provided.
+ ///
+ /// The carrier name is the one the number was originally allocated to. If the country supports
+ /// mobile number portability the number might not belong to the returned carrier anymore.
+ /// If no mapping is found an empty string is returned.
+ ///
+ ///
+ /// This method assumes the validity of the number passed in has already been checked, and that
+ /// the number is suitable for carrier lookup. We consider mobile and pager numbers possible
+ /// candidates for carrier lookup.
+ ///
+ ///
+ /// A valid phone number for which we want to get a carrier name.
+ /// The language code in which the name should be written.
+ /// A carrier name for the given phone number, or empty string if not found.
+ public string GetNameForValidNumber(PhoneNumber number, Locale languageCode)
+ {
+ return prefixFileReader.GetDescriptionForNumber(
+ number, languageCode.Language, "", languageCode.Country);
+ }
+
+ ///
+ /// Gets the name of the carrier for the given phone number, in the language provided.
+ /// As per but explicitly checks the validity of
+ /// the number passed in, and returns empty string if the number is not a mobile or pager number.
+ ///
+ /// The phone number for which we want to get a carrier name.
+ /// The language code in which the name should be written.
+ /// A carrier name for the given phone number, or empty string if the number passed
+ /// in is invalid or is not a mobile/pager number.
+ public string GetNameForNumber(PhoneNumber number, Locale languageCode)
+ {
+ if (IsMobile(phoneUtil.GetNumberType(number)))
+ return GetNameForValidNumber(number, languageCode);
+ return "";
+ }
+
+ ///
+ /// Gets the name of the carrier for the given phone number only when it is 'safe' to display
+ /// to users. A carrier name is considered safe if the number is valid and for a region that
+ /// doesn't support mobile number portability.
+ ///
+ /// The phone number for which we want to get a carrier name.
+ /// The language code in which the name should be written.
+ /// A carrier name that is safe to display to users, or the empty string.
+ public string GetSafeDisplayName(PhoneNumber number, Locale languageCode)
+ {
+ if (phoneUtil.IsMobileNumberPortableRegion(phoneUtil.GetRegionCodeForNumber(number)))
+ return "";
+ return GetNameForNumber(number, languageCode);
+ }
+
+ private static bool IsMobile(PhoneNumberType numberType) =>
+ numberType == PhoneNumberType.MOBILE
+ || numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE
+ || numberType == PhoneNumberType.PAGER;
+ }
+}
diff --git a/csharp/PhoneNumbers/PhoneNumberUtil.cs b/csharp/PhoneNumbers/PhoneNumberUtil.cs
index 551a2aca7..cba796825 100644
--- a/csharp/PhoneNumbers/PhoneNumberUtil.cs
+++ b/csharp/PhoneNumbers/PhoneNumberUtil.cs
@@ -1,4 +1,4 @@
-#nullable disable
+#nullable disable
/*
* Copyright (C) 2009 Google Inc.
*
@@ -2997,5 +2997,20 @@ public bool CanBeInternationallyDialled(PhoneNumber number)
var nationalSignificantNumber = GetNationalSignificantNumber(number);
return !IsNumberMatchingDesc(nationalSignificantNumber, metadata.NoInternationalDialling);
}
+
+ ///
+ /// Returns true if the supplied region supports mobile number portability. Returns false for
+ /// invalid, unknown or regions that don't support mobile number portability.
+ ///
+ /// The region for which we want to know whether it supports mobile
+ /// number portability or not.
+ /// True if the region supports mobile number portability, false otherwise.
+ public bool IsMobileNumberPortableRegion(string regionCode)
+ {
+ var metadata = GetMetadataForRegion(regionCode);
+ if (metadata == null)
+ return false;
+ return metadata.MobileNumberPortableRegion;
+ }
}
}
diff --git a/csharp/PhoneNumbers/PhoneNumbers.csproj b/csharp/PhoneNumbers/PhoneNumbers.csproj
index 9d41d0751..7d6250e33 100644
--- a/csharp/PhoneNumbers/PhoneNumbers.csproj
+++ b/csharp/PhoneNumbers/PhoneNumbers.csproj
@@ -44,6 +44,14 @@
+
+
+
+
+
+
+
+
diff --git a/csharp/PhoneNumbers/PrefixFileReader.cs b/csharp/PhoneNumbers/PrefixFileReader.cs
new file mode 100644
index 000000000..4e9a782e4
--- /dev/null
+++ b/csharp/PhoneNumbers/PrefixFileReader.cs
@@ -0,0 +1,218 @@
+#nullable disable
+/*
+ * Copyright (C) 2013 The Libphonenumber Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Reflection;
+
+namespace PhoneNumbers
+{
+ ///
+ /// A helper class that handles file loading and prefix-based lookup of phone number mappings.
+ ///
+ internal class PrefixFileReader
+ {
+ private static readonly char[] s_pathSeparators = { '/', '\\' };
+
+ private readonly MappingFileProvider mappingFileProvider;
+ private readonly ConcurrentDictionary> availablePhonePrefixMaps =
+ new ConcurrentDictionary>();
+ // Caches GetFileName results to avoid StringBuilder allocations on every lookup.
+ private readonly ConcurrentDictionary<(int, string, string, string), string> _fileNameCache =
+ new ConcurrentDictionary<(int, string, string, string), string>();
+ // Pre-allocated delegates to avoid closure re-allocation on every GetOrAdd call.
+ private readonly Func<(int, string, string, string), string> _fileNameFactory;
+ private readonly Func> _areaCodeMapFactory;
+ private readonly string phonePrefixDataDirectory;
+ private readonly string phoneDataZipFile;
+ private readonly Assembly assembly;
+ // Normalized entry name -> original FullName, built during construction for O(1) zip lookup.
+ private readonly Dictionary _zipEntryIndex;
+
+ internal PrefixFileReader(string phonePrefixDataDirectory, Assembly asm = null)
+ {
+ SortedDictionary> files;
+ asm ??= typeof(PrefixFileReader).Assembly;
+ var prefix = asm.GetName().Name + "." + phonePrefixDataDirectory;
+
+ var zipFile = prefix + "zip";
+ var zipStream = asm.GetManifestResourceStream(zipFile);
+
+ if (zipStream != null)
+ {
+ using (zipStream)
+ {
+ files = LoadFileNamesFromZip(zipStream, out var entryIndex);
+ _zipEntryIndex = entryIndex;
+ }
+ phoneDataZipFile = zipFile;
+ }
+ else
+ {
+ files = LoadFileNamesFromManifestResources(asm, prefix);
+ }
+
+ mappingFileProvider = new MappingFileProvider();
+ mappingFileProvider.ReadFileConfigs(files);
+ assembly = asm;
+ this.phonePrefixDataDirectory = prefix;
+ _fileNameFactory = k => mappingFileProvider.GetFileName(k.Item1, k.Item2, k.Item3, k.Item4);
+ _areaCodeMapFactory = key => new Lazy(() => LoadAreaCodeMapFromFile(key));
+ }
+
+ // For zipped data: entries follow the pattern "lang/cc.txt".
+ private static SortedDictionary> LoadFileNamesFromZip(
+ Stream zipStream, out Dictionary entryIndex)
+ {
+ var files = new SortedDictionary>();
+ entryIndex = new Dictionary(StringComparer.Ordinal);
+ using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
+ foreach (var entry in archive.Entries)
+ {
+ if (string.IsNullOrWhiteSpace(entry.Name))
+ continue;
+
+ entryIndex[entry.FullName.Replace("/", ".").Replace("\\", ".")] = entry.FullName;
+
+ var pathParts = entry.FullName.Split(s_pathSeparators, StringSplitOptions.RemoveEmptyEntries);
+ if (pathParts.Length < 2)
+ continue;
+
+ var language = pathParts[0];
+ var ccPart = pathParts[pathParts.Length - 1].Split('.')[0];
+ if (!int.TryParse(ccPart, out var country))
+ continue;
+
+ if (!files.TryGetValue(country, out var languages))
+ files[country] = languages = new HashSet();
+ languages.Add(language);
+ }
+ return files;
+ }
+
+ // For unzipped data: resources are "{AssemblyName}.{prefix}{lang}.{cc}.txt".
+ private static SortedDictionary> LoadFileNamesFromManifestResources(
+ Assembly asm, string prefix)
+ {
+ var files = new SortedDictionary>();
+ var names = asm.GetManifestResourceNames()
+ .Where(n => n.StartsWith(prefix, StringComparison.Ordinal));
+ foreach (var n in names)
+ {
+ // filePart e.g. "en.44.txt" or "zh_Hant.852.txt"
+ var filePart = n.Substring(prefix.Length);
+ var parts = filePart.Split('.');
+ // Minimum: [lang, cc, "txt"] => length 3
+ if (parts.Length < 3)
+ continue;
+
+ // Last segment is "txt", second-to-last is the country calling code.
+ // Everything before is the language (joined with '_' to reconstruct "zh_Hant" etc.)
+ var ccIdx = parts.Length - 2;
+ if (!int.TryParse(parts[ccIdx], out var country))
+ continue;
+
+ var lang = string.Join("_", parts.Take(ccIdx));
+ if (lang.Length == 0)
+ continue;
+
+ if (!files.TryGetValue(country, out var languages))
+ files[country] = languages = new HashSet();
+ languages.Add(lang);
+ }
+ return files;
+ }
+
+ ///
+ /// Returns a text description in the given language for the given phone number.
+ /// Falls back to English when no mapping exists for the requested language,
+ /// except for Chinese, Japanese, and Korean.
+ ///
+ internal string GetDescriptionForNumber(PhoneNumber number, string lang, string script, string region)
+ {
+ var countryCallingCode = number.CountryCode;
+ // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number
+ // prefix of 4 digits for NANPA instead, e.g. 1650.
+ var phonePrefix = countryCallingCode != 1
+ ? countryCallingCode
+ : (int)(1000 + number.NationalNumber / 10000000);
+ var phonePrefixDescriptions = GetPhonePrefixDescriptions(phonePrefix, lang, script, region);
+ var description = phonePrefixDescriptions?.Lookup(number);
+ if (string.IsNullOrEmpty(description) && MayFallBackToEnglish(lang))
+ {
+ var defaultMap = GetPhonePrefixDescriptions(phonePrefix, "en", "", "");
+ if (defaultMap == null)
+ return "";
+ description = defaultMap.Lookup(number);
+ }
+ return description ?? "";
+ }
+
+ private static bool MayFallBackToEnglish(string lang) =>
+ !lang.Equals("zh") && !lang.Equals("ja") && !lang.Equals("ko");
+
+ private AreaCodeMap GetPhonePrefixDescriptions(int prefixMapKey, string language, string script, string region)
+ {
+ var fileName = _fileNameCache.GetOrAdd((prefixMapKey, language, script, region), _fileNameFactory);
+ if (fileName.Length == 0)
+ return null;
+
+ return availablePhonePrefixMaps.GetOrAdd(fileName, _areaCodeMapFactory).Value;
+ }
+
+ private AreaCodeMap LoadAreaCodeMapFromFile(string fileName)
+ {
+ var fp = phoneDataZipFile != null
+ ? GetManifestZipFileStream(assembly, phoneDataZipFile, fileName, _zipEntryIndex)
+ : GetManifestFileStream(assembly, phonePrefixDataDirectory, fileName);
+
+ using (fp)
+ {
+ return AreaCodeParser.ParseAreaCodeMap(fp);
+ }
+ }
+
+ private static Stream GetManifestFileStream(Assembly asm, string phonePrefixDataDirectory, string fileName)
+ {
+ return asm.GetManifestResourceStream(phonePrefixDataDirectory + fileName);
+ }
+
+ private static Stream GetManifestZipFileStream(Assembly asm, string phoneDataZipFile, string fileName,
+ Dictionary entryIndex)
+ {
+ using var zipStream = asm.GetManifestResourceStream(phoneDataZipFile);
+ if (zipStream == null)
+ throw new InvalidOperationException("Manifest zip file stream was null.");
+
+ using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
+ var entry = entryIndex.TryGetValue(fileName, out var originalName)
+ ? archive.GetEntry(originalName)
+ : archive.Entries.First(p => p.FullName.Replace("/", ".").Replace("\\", ".") == fileName);
+ if (entry == null)
+ throw new InvalidOperationException($"Entry '{fileName}' not found in zip.");
+ using var entryStream = entry.Open();
+ var fileStream = new MemoryStream();
+ entryStream.CopyTo(fileStream);
+ fileStream.Position = 0;
+ return fileStream;
+ }
+ }
+}
diff --git a/resources/test/carrier/en/1.txt b/resources/test/carrier/en/1650.txt
similarity index 100%
rename from resources/test/carrier/en/1.txt
rename to resources/test/carrier/en/1650.txt