From 580c4676e915b97ba06d7eff3b7821efcd75ceed Mon Sep 17 00:00:00 2001 From: Thomas Clegg Date: Wed, 5 Aug 2026 19:57:34 -0500 Subject: [PATCH 1/2] perf: match slices without materialising them when parsing --- csharp/PhoneNumbers/PhoneNumberUtil.cs | 24 ++++++++++++++++++++++-- csharp/PhoneNumbers/PhoneRegex.cs | 9 +++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/csharp/PhoneNumbers/PhoneNumberUtil.cs b/csharp/PhoneNumbers/PhoneNumberUtil.cs index 1a09a86ad..24bd2e477 100644 --- a/csharp/PhoneNumbers/PhoneNumberUtil.cs +++ b/csharp/PhoneNumbers/PhoneNumberUtil.cs @@ -2540,7 +2540,7 @@ internal bool MaybeStripNationalPrefixAndCarrierCode(StringBuilder number, strin { // If the original number was viable, and the resultant number is not, we return. if (isViableOriginalNumber && - !nationalNumberRule.IsMatchAll(numberString.Substring(prefixMatch.Length))) + !IsMatchAllFrom(nationalNumberRule, numberString, prefixMatch.Length)) return false; if (getCarrier && numOfGroups > 1 && prefixMatch.Groups[numOfGroups - 1].Success) carrierCode = prefixMatch.Groups[1].Value; @@ -2572,12 +2572,32 @@ internal bool MaybeStripNationalPrefixAndCarrierCode(StringBuilder number, strin /// The non-normalized telephone number that we wish to strip the extension from. /// The same number as a string /// The phone extension. + /// + /// Whether the rule matches everything from onwards. Both of these + /// only ever fed a slice to a regex, so on targets with span matching the slice never becomes + /// a string. + /// + private static bool IsMatchAllFrom(PhoneRegex rule, string value, int start) => +#if NET7_0_OR_GREATER + rule.IsMatchAll(value.AsSpan(start)); +#else + rule.IsMatchAll(value.Substring(start)); +#endif + + /// Whether the first characters look like a phone number. + private static bool IsViablePhoneNumberPrefix(string number, int length) => +#if NET7_0_OR_GREATER + length >= MIN_LENGTH_FOR_NSN && ValidPhoneNumber().IsMatch(number.AsSpan(0, length)); +#else + IsViablePhoneNumber(number.Substring(0, length)); +#endif + static string MaybeStripExtension(StringBuilder number, string numberString) { var m = ExtnPattern().Match(numberString); // If we find a potential extension, and the number preceding this is a viable number, we assume // it is an extension. - if (m.Success && IsViablePhoneNumber(numberString.Substring(0, m.Index))) + if (m.Success && IsViablePhoneNumberPrefix(numberString, m.Index)) { // The numbers are captured into groups in the regular expression. for (int i = 1, length = m.Groups.Count; i < length; i++) diff --git a/csharp/PhoneNumbers/PhoneRegex.cs b/csharp/PhoneNumbers/PhoneRegex.cs index 04edf707a..1a2003afb 100644 --- a/csharp/PhoneNumbers/PhoneRegex.cs +++ b/csharp/PhoneNumbers/PhoneRegex.cs @@ -60,6 +60,15 @@ public PhoneRegex(string pattern, RegexOptions options) public string Replace(string value, string replacement) => regex.Value.Replace(value, replacement); public bool IsMatchAll(string value) => allRegex.Value.IsMatch(value); + +#if NET7_0_OR_GREATER + /// + /// Lets callers test a slice without materialising it. Internal because a public member here + /// would exist on the net8.0 and net10.0 assets but not on netstandard2.0. At a major version + /// the string overloads should become span overloads outright - see issue #375. + /// + internal bool IsMatchAll(ReadOnlySpan value) => allRegex.Value.IsMatch(value); +#endif public Match MatchAll(string value) => allRegex.Value.Match(value); public bool IsMatchBeginning(string value) => beginRegex.Value.IsMatch(value); From 848dd21a3f82d4f4ea64df651aabf3e7ab8d6fac Mon Sep 17 00:00:00 2001 From: Thomas Clegg Date: Wed, 5 Aug 2026 20:04:54 -0500 Subject: [PATCH 2/2] fix: keep the helpers clear of MaybeStripExtension's doc comment --- csharp/PhoneNumbers/PhoneNumberUtil.cs | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/csharp/PhoneNumbers/PhoneNumberUtil.cs b/csharp/PhoneNumbers/PhoneNumberUtil.cs index 24bd2e477..733d2aca5 100644 --- a/csharp/PhoneNumbers/PhoneNumberUtil.cs +++ b/csharp/PhoneNumbers/PhoneNumberUtil.cs @@ -678,6 +678,26 @@ public static bool IsViablePhoneNumber(string number) return ValidPhoneNumber().IsMatch(number); } + /// + /// Whether the rule matches everything from onwards. Both of these + /// only ever fed a slice to a regex, so on targets with span matching the slice never becomes + /// a string. + /// + private static bool IsMatchAllFrom(PhoneRegex rule, string value, int start) => +#if NET7_0_OR_GREATER + rule.IsMatchAll(value.AsSpan(start)); +#else + rule.IsMatchAll(value.Substring(start)); +#endif + + /// Whether the first characters look like a phone number. + private static bool IsViablePhoneNumberPrefix(string number, int length) => +#if NET7_0_OR_GREATER + length >= MIN_LENGTH_FOR_NSN && ValidPhoneNumber().IsMatch(number.AsSpan(0, length)); +#else + IsViablePhoneNumber(number.Substring(0, length)); +#endif + private static void Normalize(StringBuilder number) { if (IsValidAlphaPhone(number)) @@ -2572,26 +2592,6 @@ internal bool MaybeStripNationalPrefixAndCarrierCode(StringBuilder number, strin /// The non-normalized telephone number that we wish to strip the extension from. /// The same number as a string /// The phone extension. - /// - /// Whether the rule matches everything from onwards. Both of these - /// only ever fed a slice to a regex, so on targets with span matching the slice never becomes - /// a string. - /// - private static bool IsMatchAllFrom(PhoneRegex rule, string value, int start) => -#if NET7_0_OR_GREATER - rule.IsMatchAll(value.AsSpan(start)); -#else - rule.IsMatchAll(value.Substring(start)); -#endif - - /// Whether the first characters look like a phone number. - private static bool IsViablePhoneNumberPrefix(string number, int length) => -#if NET7_0_OR_GREATER - length >= MIN_LENGTH_FOR_NSN && ValidPhoneNumber().IsMatch(number.AsSpan(0, length)); -#else - IsViablePhoneNumber(number.Substring(0, length)); -#endif - static string MaybeStripExtension(StringBuilder number, string numberString) { var m = ExtnPattern().Match(numberString);