From bd5b69b30fdd28ed38678ecf5a97f6839d22644b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 19:52:45 +0000 Subject: [PATCH] docs: add API differences from upstream Java reference Consolidates the existing "Known Issues" bullets from csharp/README.md with newly-verified, concrete API-shape mismatches discovered while exercising the public API as a consumer would: PhoneNumberOfflineGeocoder / PhoneNumberToCarrierMapper taking this library's own Locale type instead of System.Globalization.CultureInfo, PhoneNumbers.Extensions.PhoneNumber colliding (CS0104) with PhoneNumbers.PhoneNumber when both namespaces are used together, enum members keeping Java's SCREAMING_SNAKE_CASE, and PhoneNumber's internal-setter/Builder shape looking like a settable POCO but failing to compile (CS0200) with an object initializer. Each entry was verified against current source and, where a compiler error is claimed, against an actual build. csharp/README.md's Known Issues section now links to the new page. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW --- csharp/README.md | 6 + docs/api-differences-from-java.md | 214 ++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 docs/api-differences-from-java.md diff --git a/csharp/README.md b/csharp/README.md index 56e28ff5..0e1d511d 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -50,3 +50,9 @@ Known Issues equivalent spots (`PhoneNumberMatcher(PhoneNumberUtil, string, ...)`, `PhoneNumberUtil.IsViablePhoneNumber(string)`) instead of a comparable abstraction, so a caller building up a number in a `StringBuilder` has to call `.ToString()` first. + +See [`docs/api-differences-from-java.md`](../docs/api-differences-from-java.md) for a fuller, +example-driven reference of API-shape differences from upstream Java — including gotchas like +`PhoneNumberOfflineGeocoder`/`PhoneNumberToCarrierMapper` taking this library's own `Locale` +type rather than `System.Globalization.CultureInfo`, and the `PhoneNumbers.Extensions.PhoneNumber` +helper class sharing a simple name with `PhoneNumbers.PhoneNumber` (the data type). diff --git a/docs/api-differences-from-java.md b/docs/api-differences-from-java.md new file mode 100644 index 00000000..acf18c96 --- /dev/null +++ b/docs/api-differences-from-java.md @@ -0,0 +1,214 @@ +# API differences from upstream Java + +This port follows the upstream Java `libphonenumber` API closely — file names, class +names, and method names generally match one-to-one (`PhoneNumberUtil.java` ↔ +`PhoneNumberUtil.cs`, `isValidNumber` ↔ `IsValidNumber`, etc.), and where behavior is in +question the Java source is the source of truth. This page documents the places where the +*shape* of the C# API is not what a straight Java-to-C# reading would suggest, or not what +a C# developer would instinctively reach for. Every entry below was verified against the +current source in this repository (file paths are given so you can check them yourself). + +Use this page two ways: as a contributor porting a new upstream Java change, to know where +the port intentionally departs from a literal translation; and as a consumer, to avoid the +specific mistakes documented here. + +## Contents + +- [`Locale` is a small type local to this library, not `System.Globalization.CultureInfo`](#locale-is-a-small-type-local-to-this-library-not-systemglobalizationcultureinfo) +- [`PhoneNumbers.Extensions.PhoneNumber` and `PhoneNumbers.PhoneNumber` share a simple name](#phonenumbersextensionsphonenumber-and-phonenumbersphonenumber-share-a-simple-name) +- [Enum members keep Java's `SCREAMING_SNAKE_CASE`, not .NET `PascalCase`](#enum-members-keep-javas-screaming_snake_case-not-net-pascalcase) +- [`PhoneNumber` looks like a plain settable class but is a protobuf-style immutable message](#phonenumber-looks-like-a-plain-settable-class-but-is-a-protobuf-style-immutable-message) +- [`CharSequence` parameters become `string`, not a comparable abstraction](#charsequence-parameters-become-string-not-a-comparable-abstraction) +- [Metadata and prefix maps are a custom binary format, not protocol buffers](#metadata-and-prefix-maps-are-a-custom-binary-format-not-protocol-buffers) + +--- + +## `Locale` is a small type local to this library, not `System.Globalization.CultureInfo` + +**Java:** `PhoneNumberOfflineGeocoder.getDescriptionForNumber` and +`PhoneNumberToCarrierMapper.getNameForNumber` take a `java.util.Locale`. + +**C# (this port):** the equivalent parameter is `PhoneNumbers.Locale` — a small class +defined in [`csharp/PhoneNumbers/PhoneNumberOfflineGeocoder.cs`](../csharp/PhoneNumbers/PhoneNumberOfflineGeocoder.cs) +(despite the file name, it is *not* nested inside `PhoneNumberOfflineGeocoder`; it's a +top-level type in the `PhoneNumbers` namespace). It is constructed directly: + +```csharp +public class Locale +{ + public static readonly Locale English = new("en", "GB"); + public static readonly Locale French = new("fr", "FR"); + public static readonly Locale German = new("de", "DE"); + public static readonly Locale Italian = new("it", "IT"); + public static readonly Locale Korean = new("ko", "KR"); + public static readonly Locale SimplifiedChinese = new("zh", "CN"); + + public readonly string Language; + public readonly string Country; + + public Locale(string language, string countryCode) { ... } +} +``` + +`GetDescriptionForNumber(PhoneNumber, Locale)` and `GetNameForNumber(PhoneNumber, Locale)` +both take this type — not `System.Globalization.CultureInfo`, which is what a C# developer +would instinctively reach for given a parameter named `languageCode` next to a phone +number API. Passing a `CultureInfo` will not compile; use one of the static presets +(`Locale.English`, `Locale.French`, ...) or `new Locale("en", "US")`. + +**Why:** `java.util.Locale` has no direct BCL equivalent with the same two-part +language/country shape, so the port ported the small pieces of `java.util.Locale` actually +used (language + country, and `getDisplayCountry`) into a dedicated type instead of +reshaping the API around `CultureInfo`. One known gap: `java.util.Locale.getScript()` has +no equivalent here, so callers cannot express a script subtag — see the comment in +[`PhoneNumberToCarrierMapper.cs`](../csharp/PhoneNumbers/PhoneNumberToCarrierMapper.cs) +(`GetNameForValidNumber`), which always passes an empty script to the underlying prefix +reader. + +--- + +## `PhoneNumbers.Extensions.PhoneNumber` and `PhoneNumbers.PhoneNumber` share a simple name + +**Java:** no equivalent — upstream Java ships a single artifact, so this situation cannot +arise there. + +**C# (this port):** the main package's data type is `PhoneNumbers.PhoneNumber` (the +protobuf-derived phone number value — see +[`csharp/PhoneNumbers/Phonenumber.cs`](../csharp/PhoneNumbers/Phonenumber.cs)). The +separate `libphonenumber-csharp.extensions` package adds a *static helper class* with the +same simple name, `PhoneNumbers.Extensions.PhoneNumber`, exposing `TryParse` / +`TryParseValid` (see +[`csharp/PhoneNumbers.Extensions/PhoneNumber.cs`](../csharp/PhoneNumbers.Extensions/PhoneNumber.cs)). + +If a file has both `using PhoneNumbers;` and `using PhoneNumbers.Extensions;`, an unqualified +reference to `PhoneNumber` is genuinely ambiguous and fails to compile: + +``` +error CS0104: 'PhoneNumber' is an ambiguous reference between +'PhoneNumbers.PhoneNumber' and 'PhoneNumbers.Extensions.PhoneNumber' +``` + +Work around it with a fully-qualified call, e.g.: + +```csharp +if (PhoneNumbers.Extensions.PhoneNumber.TryParse(input, out PhoneNumbers.PhoneNumber number)) +{ + ... +} +``` + +or a `using` alias (`using PhoneNumberHelper = PhoneNumbers.Extensions.PhoneNumber;`) if +the ambiguity comes up often in one file. + +**Why:** this is a naming collision incidental to how the Extensions package is organized +— `PhoneNumber` reads naturally as "the static helper for working with a `PhoneNumber`", +mirroring the type it wraps, but that convenience is exactly what collides. It is not a +Java/C# behavioral difference, just something worth knowing before it surprises you with a +compiler error in code that otherwise looks correct. + +--- + +## Enum members keep Java's `SCREAMING_SNAKE_CASE`, not .NET `PascalCase` + +**Java:** enum constants use Java's constant-naming convention, e.g. +`PhoneNumberType.FIXED_LINE`, `PhoneNumberFormat.E164`, `ErrorType.INVALID_COUNTRY_CODE`. + +**C# (this port):** the same spelling is kept as-is instead of being translated to the +.NET-idiomatic `PascalCase` a C# developer would expect (`PhoneNumberType.FixedLine`, for +example, does not exist — it's `PhoneNumberType.FIXED_LINE`). This is consistent across +every public enum in the library, not just one: + +- [`PhoneNumberType`](../csharp/PhoneNumbers/PhoneNumberType.cs) — `FIXED_LINE`, `MOBILE`, `TOLL_FREE`, `UNKNOWN`, ... +- [`PhoneNumberFormat`](../csharp/PhoneNumbers/PhoneNumberFormat.cs) — `E164`, `INTERNATIONAL`, `NATIONAL`, `RFC3966` +- [`NumberParseException.ErrorType`](../csharp/PhoneNumbers/NumberParseException.cs) — `INVALID_COUNTRY_CODE`, `NOT_A_NUMBER`, ... +- `PhoneNumberUtil.MatchType`, `PhoneNumberUtil.ValidationResult`, `PhoneNumberUtil.Leniency` (all in [`PhoneNumberUtil.cs`](../csharp/PhoneNumbers/PhoneNumberUtil.cs)) +- [`ShortNumberInfo.ShortNumberCost`](../csharp/PhoneNumbers/ShortNumberInfo.cs) +- `PhoneNumber.Types.CountryCodeSource` (in [`Phonenumber.cs`](../csharp/PhoneNumbers/Phonenumber.cs)) + +**Why:** a straight rename to `PascalCase` would be a purely cosmetic diff against every +Java release this port tracks, for no behavioral gain, and it's easy to keep matching +Java exactly (`grep`-able) at the cost of a naming-convention violation IDEs and analyzers +will flag. Worth knowing going in so `PhoneNumberType.FixedLine` doesn't cost you a +"did you mean" compiler round-trip. + +--- + +## `PhoneNumber` looks like a plain settable class but is a protobuf-style immutable message + +**Java:** `com.google.i18n.phonenumbers.Phonenumber.PhoneNumber` is a generated protobuf +message: private fields, `getXxx()`/`hasXxx()` accessors, and mutation only through a +nested `Builder` (`PhoneNumber.newBuilder().setCountryCode(1)...build()`). + +**C# (this port):** [`csharp/PhoneNumbers/Phonenumber.cs`](../csharp/PhoneNumbers/Phonenumber.cs) +mirrors that same protobuf shape rather than turning it into a plain C# POCO. `CountryCode`, +`NationalNumber`, `Extension`, etc. are real public properties (so they read like an +ordinary mutable class), but every setter is `internal`: + +```csharp +public int CountryCode { get; internal set; } +public bool HasCountryCode => CountryCode != 0; +``` + +That means an object initializer that looks perfectly reasonable to a C# developer — + +```csharp +var number = new PhoneNumber { CountryCode = 1, NationalNumber = 4155551234 }; // CS0200 +``` + +— fails to compile (the setters aren't accessible outside the assembly). The only ways to +get a `PhoneNumber` are `PhoneNumberUtil.Parse(...)` or the protobuf-style builder: + +```csharp +var number = PhoneNumber.CreateBuilder() + .SetCountryCode(1) + .SetNationalNumber(4155551234) + .Build(); +``` + +**Why:** this one *does* match Java faithfully — Java's protobuf messages are equally +immutable outside their builder, so a straight port preserves that. The trap is purely +that C#'s auto-property syntax makes `PhoneNumber` *look* like an ordinary settable class +at a glance (Java has no equivalent syntax to be misled by), so the internal setters come +as a surprise the first time you reach for an object initializer instead of `Parse` or +`CreateBuilder()`. + +--- + +## `CharSequence` parameters become `string`, not a comparable abstraction + +**Java:** several entry points accept `CharSequence` (e.g. `PhoneNumberMatcher`'s +constructor, `isViablePhoneNumber`), letting callers pass a `String`, `StringBuilder`, or +`StringBuffer` without copying. + +**C# (this port):** the equivalent spots take `string` (`PhoneNumberMatcher(PhoneNumberUtil, string, ...)`, +`PhoneNumberUtil.IsViablePhoneNumber(string)`) rather than a comparable abstraction such as +`ReadOnlySpan` or a generic `IEnumerable`, so a caller building a number up in +a `StringBuilder` has to call `.ToString()` first. + +**Why:** `CharSequence` has no single natural .NET analogue that preserves both the +zero-copy behavior and the API ergonomics — `ReadOnlySpan` can't be a field or used +across `async`/iterator boundaries the way this code is structured, and a generic +interface would add an abstraction layer for a case (`StringBuilder` input) that is rare +in practice. `string` was chosen as the pragmatic equivalent. + +--- + +## Metadata and prefix maps are a custom binary format, not protocol buffers + +**Java:** metadata (`PhoneNumberMetadata.xml`, etc.) is compiled to protocol buffers and +read from those at run time. + +**C# (this port):** the XML source files in `resources/` are converted to a custom +per-region binary format at build time by `PhoneNumbers.MetadataBuilder`; the published +assembly embeds those binaries (gzip-compressed) and never reads XML or protocol buffers +at run time. Geocoding, timezone, and carrier prefix maps go through the same +build-time-binary-and-embed pipeline — no zip files or text files are needed to run the +library or its tests. See `IMetadataLoader` / `EmbeddedResourceMetadataLoader` in +`csharp/PhoneNumbers/` for the read side. + +**Why:** avoids taking a runtime dependency on Google's protobuf C# library purely to +deserialize static, build-time-known data, and keeps the library trim/AOT-friendly (no +protobuf reflection at run time). This does not change any public method's behavior — it's +an internal storage detail — but it does mean `PhoneNumber`'s protobuf-*shaped* API (see +above) is not backed by actual protobuf serialization in this port, which can be confusing +if you come from the Java side expecting `.proto`-generated wire compatibility.