diff --git a/csharp/PhoneNumbers/PhoneNumberUtil.cs b/csharp/PhoneNumbers/PhoneNumberUtil.cs index 79ce721a..b4d32300 100644 --- a/csharp/PhoneNumbers/PhoneNumberUtil.cs +++ b/csharp/PhoneNumbers/PhoneNumberUtil.cs @@ -869,8 +869,10 @@ public static string GetCountryMobileToken(int countryCallingCode) /// should be stripped from the number. If this is false, they /// will be left unchanged in the number. /// The normalized string version of the phone number. +#if !(NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER) private static string NormalizeHelper(string number, Func normalizationReplacements, bool removeNonMatches) => NormalizeHelper(new StringBuilder(number), normalizationReplacements, removeNonMatches).ToString(); +#endif private static StringBuilder NormalizeHelper(StringBuilder number, Func normalizationReplacements, bool removeNonMatches) { @@ -985,27 +987,26 @@ private static bool DescHasData(PhoneNumberDesc desc) || desc.HasNationalNumberPattern; } + // Pre-filtered: excludes FIXED_LINE_OR_MOBILE (convenience aggregate) and UNKNOWN (non-type). + private static readonly PhoneNumberType[] s_checkablePhoneNumberTypes = + [ + PhoneNumberType.FIXED_LINE, PhoneNumberType.MOBILE, PhoneNumberType.TOLL_FREE, + PhoneNumberType.PREMIUM_RATE, PhoneNumberType.SHARED_COST, PhoneNumberType.VOIP, + PhoneNumberType.PERSONAL_NUMBER, PhoneNumberType.PAGER, PhoneNumberType.UAN, + PhoneNumberType.VOICEMAIL, + ]; + /// /// Returns the types we have metadata for based on the PhoneMetadata object passed in, which must /// be non-null. /// - /// - /// private HashSet GetSupportedTypesForMetadata(PhoneMetadata metadata) { var types = new HashSet(); - foreach (PhoneNumberType type in Enum.GetValues(typeof(PhoneNumberType))) + foreach (var type in s_checkablePhoneNumberTypes) { - if (type == PhoneNumberType.FIXED_LINE_OR_MOBILE || type == PhoneNumberType.UNKNOWN) - { - // Never return FIXED_LINE_OR_MOBILE (it is a convenience type, and represents that a - // particular number type can't be determined) or UNKNOWN (the non-type). - continue; - } if (DescHasData(GetNumberDescByType(metadata, type))) - { types.Add(type); - } } return types; } @@ -2305,6 +2306,14 @@ private int MaybeExtractCountryCode(string number, PhoneMetadata defaultRegionMe nationalNumber, keepRawInput, phoneNumber); } + private static bool StringBuilderStartsWith(StringBuilder sb, string value) + { + if (sb.Length < value.Length) return false; + for (var i = 0; i < value.Length; i++) + if (sb[i] != value[i]) return false; + return true; + } + // Same as the string overload above, but takes a StringBuilder workspace directly so callers // that already hold a StringBuilder can avoid an extra string<->StringBuilder round-trip. // The fullNumber buffer is always mutated (Normalize is unconditional, and any leading '+' or @@ -2355,9 +2364,10 @@ private int MaybeExtractCountryCode(StringBuilder fullNumber, PhoneMetadata defa // before and after. var defaultCountryCode = defaultRegionMetadata.CountryCode; var defaultCountryCodeString = defaultCountryCode.ToString(); - var normalizedNumber = fullNumber.ToString(); - if (normalizedNumber.StartsWith(defaultCountryCodeString, StringComparison.Ordinal)) + // Avoid the ToString() allocation unless the prefix actually matches (common case: it won't). + if (StringBuilderStartsWith(fullNumber, defaultCountryCodeString)) { + var normalizedNumber = fullNumber.ToString(); var potentialNationalNumberString = normalizedNumber.Substring(defaultCountryCodeString.Length); // Use a separate buffer for the strip-CC trial so callers that share fullNumber as // their workspace are not corrupted when this branch decides not to commit. @@ -2371,7 +2381,7 @@ private int MaybeExtractCountryCode(StringBuilder fullNumber, PhoneMetadata defa var validNumberPattern = generalDesc.GetNationalNumberPattern(); if ((!validNumberPattern.IsMatchAll(normalizedNumber) && validNumberPattern.IsMatchAll(potentialNationalNumberString)) || - TestNumberLength(normalizedNumber.Length, defaultRegionMetadata) == ValidationResult.TOO_LONG) + TestNumberLength(fullNumber.Length, defaultRegionMetadata) == ValidationResult.TOO_LONG) { nationalNumber.Append(potentialNationalNumberString); if (keepRawInput) @@ -2529,7 +2539,11 @@ 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 NET7_0_OR_GREATER + if (m.Success && IsViablePhoneNumberSpan(numberString.AsSpan(0, m.Index))) +#else if (m.Success && IsViablePhoneNumber(numberString.Substring(0, m.Index))) +#endif { // 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/PhoneNumberUtil.net.cs b/csharp/PhoneNumbers/PhoneNumberUtil.net.cs index e8105231..91d631be 100644 --- a/csharp/PhoneNumbers/PhoneNumberUtil.net.cs +++ b/csharp/PhoneNumbers/PhoneNumberUtil.net.cs @@ -606,6 +606,15 @@ private static void NormalizeHelper(ref Span span, } } + private static string NormalizeHelper(string number, Func normalizationReplacements, bool removeNonMatches) + { + if (number.Length == 0) return string.Empty; + Span result = stackalloc char[number.Length]; + var resultLength = 0; + NormalizeHelper(ref result, ref resultLength, number, normalizationReplacements, removeNonMatches); + return new string(result.Slice(0, resultLength)); + } + private static bool IsValidAlphaPhone(string number) { for (int alpha = 0, i = 0; i < number.Length; i++) @@ -750,6 +759,14 @@ private void PrefixNumberWithCountryCallingCode(ref Span span, return; } } + +#if NET7_0_OR_GREATER + internal static bool IsViablePhoneNumberSpan(ReadOnlySpan number) + { + if (number.Length < MIN_LENGTH_FOR_NSN) return false; + return ValidPhoneNumber().IsMatch(number); + } +#endif } } #endif