diff --git a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs index 79745aac2abfa..b6b34095c39a4 100644 --- a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs +++ b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs @@ -316,12 +316,12 @@ internal static bool TryFindCGroupPathForSubsystem(CGroupVersion cgroupVersion, { using (var reader = new StreamReader(procCGroupFilePath)) { + Span lineParts = stackalloc Range[4]; string? line; while ((line = reader.ReadLine()) != null) { - string[] lineParts = line.Split(':'); - - if (lineParts.Length != 3) + ReadOnlySpan lineSpan = line; + if (lineSpan.Split(lineParts, ':') != 3) { // Malformed line. continue; @@ -333,13 +333,13 @@ internal static bool TryFindCGroupPathForSubsystem(CGroupVersion cgroupVersion, // list. See man page for cgroups for /proc/[pid]/cgroups format, e.g: // hierarchy-ID:controller-list:cgroup-path // 5:cpuacct,cpu,cpuset:/daemons - if (Array.IndexOf(lineParts[1].Split(','), subsystem) < 0) + if (Array.IndexOf(line[lineParts[1]].Split(','), subsystem) < 0) { // Not the relevant entry. continue; } - path = lineParts[2]; + path = line[lineParts[2]]; return true; } else if (cgroupVersion == CGroupVersion.CGroup2) @@ -347,9 +347,9 @@ internal static bool TryFindCGroupPathForSubsystem(CGroupVersion cgroupVersion, // cgroup v2: Find the first entry that matches the cgroup v2 hierarchy: // 0::$PATH - if ((lineParts[0] == "0") && (lineParts[1].Length == 0)) + if (lineSpan[lineParts[0]] is "0" && lineSpan[lineParts[1]].IsEmpty) { - path = lineParts[2]; + path = line[lineParts[2]]; return true; } } diff --git a/src/libraries/Common/src/System/Drawing/ColorConverterCommon.cs b/src/libraries/Common/src/System/Drawing/ColorConverterCommon.cs index a2b03128a4bcd..bf69d5cc5fe3b 100644 --- a/src/libraries/Common/src/System/Drawing/ColorConverterCommon.cs +++ b/src/libraries/Common/src/System/Drawing/ColorConverterCommon.cs @@ -53,27 +53,17 @@ public static Color ConvertFromString(string strValue, CultureInfo culture) } } - // Nope. Parse the RGBA from the text. - // - string[] tokens = text.Split(sep); - int[] values = new int[tokens.Length]; - for (int i = 0; i < values.Length; i++) - { - values[i] = unchecked(IntFromString(tokens[i], culture)); - } - - // We should now have a number of parsed integer values. // We support 1, 3, or 4 arguments: - // // 1 -- full ARGB encoded // 3 -- RGB // 4 -- ARGB - // - return values.Length switch + ReadOnlySpan textSpan = text; + Span tokens = stackalloc Range[5]; + return textSpan.Split(tokens, sep) switch { - 1 => PossibleKnownColor(Color.FromArgb(values[0])), - 3 => PossibleKnownColor(Color.FromArgb(values[0], values[1], values[2])), - 4 => PossibleKnownColor(Color.FromArgb(values[0], values[1], values[2], values[3])), + 1 => PossibleKnownColor(Color.FromArgb(IntFromString(textSpan[tokens[0]], culture))), + 3 => PossibleKnownColor(Color.FromArgb(IntFromString(textSpan[tokens[0]], culture), IntFromString(textSpan[tokens[1]], culture), IntFromString(textSpan[tokens[2]], culture))), + 4 => PossibleKnownColor(Color.FromArgb(IntFromString(textSpan[tokens[0]], culture), IntFromString(textSpan[tokens[1]], culture), IntFromString(textSpan[tokens[2]], culture), IntFromString(textSpan[tokens[3]], culture))), _ => throw new ArgumentException(SR.Format(SR.InvalidColor, text)), }; } @@ -96,7 +86,7 @@ private static Color PossibleKnownColor(Color color) return color; } - private static int IntFromString(string text, CultureInfo culture) + private static int IntFromString(ReadOnlySpan text, CultureInfo culture) { text = text.Trim(); @@ -104,34 +94,23 @@ private static int IntFromString(string text, CultureInfo culture) { if (text[0] == '#') { - return IntFromString(text.Substring(1), 16); + return Convert.ToInt32(text.Slice(1).ToString(), 16); } - else if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - || text.StartsWith("&h", StringComparison.OrdinalIgnoreCase)) + else if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || text.StartsWith("&h", StringComparison.OrdinalIgnoreCase)) { - return IntFromString(text.Substring(2), 16); + return Convert.ToInt32(text.Slice(2).ToString(), 16); } else { Debug.Assert(culture != null); var formatInfo = (NumberFormatInfo?)culture.GetFormat(typeof(NumberFormatInfo)); - return IntFromString(text, formatInfo); + return int.Parse(text, provider: formatInfo); } } catch (Exception e) { - throw new ArgumentException(SR.Format(SR.ConvertInvalidPrimitive, text, nameof(Int32)), e); + throw new ArgumentException(SR.Format(SR.ConvertInvalidPrimitive, text.ToString(), nameof(Int32)), e); } } - - private static int IntFromString(string value, int radix) - { - return Convert.ToInt32(value, radix); - } - - private static int IntFromString(string value, NumberFormatInfo? formatInfo) - { - return int.Parse(value, NumberStyles.Integer, formatInfo); - } } } diff --git a/src/libraries/System.Data.Common/src/System/Data/XDRSchema.cs b/src/libraries/System.Data.Common/src/System/Data/XDRSchema.cs index ed0968e36765a..8b976e69a889e 100644 --- a/src/libraries/System.Data.Common/src/System/Data/XDRSchema.cs +++ b/src/libraries/System.Data.Common/src/System/Data/XDRSchema.cs @@ -297,16 +297,17 @@ private static NameType FindNameType(string name) private static Type ParseDataType(string dt, string dtValues) { string strType = dt; - string[] parts = dt.Split(':'); - if (parts.Length > 2) + Span parts = stackalloc System.Range[3]; + switch (dt.AsSpan().Split(parts, ':')) { - throw ExceptionBuilder.InvalidAttributeValue("type", dt); - } - else if (parts.Length == 2) - { - // CONSIDER: check that we have valid prefix - strType = parts[1]; + case 2: + // CONSIDER: check that we have valid prefix + strType = dt[parts[1]]; + break; + + case > 2: + throw ExceptionBuilder.InvalidAttributeValue("type", dt); } NameType nt = FindNameType(strType); diff --git a/src/libraries/System.Diagnostics.FileVersionInfo/src/System/Diagnostics/FileVersionInfo.Unix.cs b/src/libraries/System.Diagnostics.FileVersionInfo/src/System/Diagnostics/FileVersionInfo.Unix.cs index e56737fb9d248..7e6078c0c8a04 100644 --- a/src/libraries/System.Diagnostics.FileVersionInfo/src/System/Diagnostics/FileVersionInfo.Unix.cs +++ b/src/libraries/System.Diagnostics.FileVersionInfo/src/System/Diagnostics/FileVersionInfo.Unix.cs @@ -199,22 +199,23 @@ private static void ParseVersion(string? versionString, out int major, out int m major = minor = build = priv = 0; - if (versionString != null) + ReadOnlySpan versionSpan = versionString; + + Span parts = stackalloc Range[5]; + parts = parts.Slice(0, versionSpan.Split(parts, '.')); + + if (parts.Length <= 4 && parts.Length > 0) { - string[] parts = versionString.Split('.'); - if (parts.Length <= 4 && parts.Length > 0) + major = ParseUInt16UntilNonDigit(versionSpan[parts[0]], out bool endedEarly); + if (!endedEarly && parts.Length > 1) { - major = ParseUInt16UntilNonDigit(parts[0], out bool endedEarly); - if (!endedEarly && parts.Length > 1) + minor = ParseUInt16UntilNonDigit(versionSpan[parts[1]], out endedEarly); + if (!endedEarly && parts.Length > 2) { - minor = ParseUInt16UntilNonDigit(parts[1], out endedEarly); - if (!endedEarly && parts.Length > 2) + build = ParseUInt16UntilNonDigit(versionSpan[parts[2]], out endedEarly); + if (!endedEarly && parts.Length > 3) { - build = ParseUInt16UntilNonDigit(parts[2], out endedEarly); - if (!endedEarly && parts.Length > 3) - { - priv = ParseUInt16UntilNonDigit(parts[3], out _); - } + priv = ParseUInt16UntilNonDigit(versionSpan[parts[3]], out _); } } } @@ -225,7 +226,7 @@ private static void ParseVersion(string? versionString, out int major, out int m /// The string to parse. /// Whether parsing ended prior to reaching the end of the input. /// The parsed value. - private static ushort ParseUInt16UntilNonDigit(string s, out bool endedEarly) + private static ushort ParseUInt16UntilNonDigit(ReadOnlySpan s, out bool endedEarly) { endedEarly = false; ushort result = 0; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs index 4ec733be4644d..d0eb17f573267 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Windows.cs @@ -191,7 +191,7 @@ private static bool IsRemoteMachineCore(string machineName) { ReadOnlySpan baseName = machineName.AsSpan(machineName.StartsWith('\\') ? 2 : 0); return - !baseName.Equals(".", StringComparison.Ordinal) && + baseName is not "." && !baseName.Equals(Interop.Kernel32.GetComputerName(), StringComparison.OrdinalIgnoreCase); } @@ -499,7 +499,7 @@ private static ProcessInfo[] GetProcessInfos(PerformanceCounterLib library, int ReadOnlySpan instanceName = PERF_INSTANCE_DEFINITION.GetName(in instance, data.Slice(instancePos)); - if (instanceName.Equals("_Total", StringComparison.Ordinal)) + if (instanceName is "_Total") { // continue } diff --git a/src/libraries/System.Drawing.Primitives/src/System.Drawing.Primitives.csproj b/src/libraries/System.Drawing.Primitives/src/System.Drawing.Primitives.csproj index 697be7783e250..f200cd051fb17 100644 --- a/src/libraries/System.Drawing.Primitives/src/System.Drawing.Primitives.csproj +++ b/src/libraries/System.Drawing.Primitives/src/System.Drawing.Primitives.csproj @@ -1,4 +1,4 @@ - + System.Drawing $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent) @@ -37,6 +37,7 @@ + diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index c67966a60604e..d33b59ea50c11 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -315,6 +315,10 @@ public static void Sort(this System.Span keys, System.Span(this System.Span keys, System.Span items, System.Comparison comparison) { } public static void Sort(this System.Span span, TComparer comparer) where TComparer : System.Collections.Generic.IComparer? { } public static void Sort(this System.Span keys, System.Span items, TComparer comparer) where TComparer : System.Collections.Generic.IComparer? { } + public static int Split(this System.ReadOnlySpan source, System.Span destination, char separator, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; } + public static int Split(this System.ReadOnlySpan source, System.Span destination, System.ReadOnlySpan separator, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; } + public static int SplitAny(this System.ReadOnlySpan source, System.Span destination, System.ReadOnlySpan separators, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; } + public static int SplitAny(this System.ReadOnlySpan source, System.Span destination, System.ReadOnlySpan separators, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; } public static bool StartsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value, System.StringComparison comparisonType) { throw null; } public static bool StartsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value) where T : System.IEquatable? { throw null; } public static bool StartsWith(this System.Span span, System.ReadOnlySpan value) where T : System.IEquatable? { throw null; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs index 22935964ebc9e..5c63f095229db 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs @@ -226,7 +226,7 @@ private static bool TryReadPercentEncodedAlpnProtocolName(string value, int star } break; case 5: - if (span.SequenceEqual("clear")) + if (span is "clear") { result = "clear"; return true; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/ContentDispositionHeaderValue.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/ContentDispositionHeaderValue.cs index 94d50128c5d33..105a58e93869d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/ContentDispositionHeaderValue.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/ContentDispositionHeaderValue.cs @@ -470,9 +470,13 @@ private static bool TryDecodeMime(string? input, [NotNullWhen(true)] out string? return false; } - string[] parts = processedInput.Split('?'); + Span parts = stackalloc Range[6]; + ReadOnlySpan processedInputSpan = processedInput; // "=, encodingName, encodingType, encodedData, =" - if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" || parts[2].ToLowerInvariant() != "b") + if (processedInputSpan.Split(parts, '?') != 5 || + processedInputSpan[parts[0]] is not "\"=" || + processedInputSpan[parts[4]] is not "=\"" || + !processedInputSpan[parts[2]].Equals("b", StringComparison.OrdinalIgnoreCase)) { // Not encoded. // This does not support multi-line encoding. @@ -482,8 +486,8 @@ private static bool TryDecodeMime(string? input, [NotNullWhen(true)] out string? try { - Encoding encoding = Encoding.GetEncoding(parts[1]); - byte[] bytes = Convert.FromBase64String(parts[3]); + Encoding encoding = Encoding.GetEncoding(processedInput[parts[1]]); + byte[] bytes = Convert.FromBase64String(processedInput[parts[3]]); output = encoding.GetString(bytes, 0, bytes.Length); return true; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs index 6ab56c8843348..43b12177bed0c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs @@ -234,10 +234,11 @@ private HttpEnvironmentProxy(Uri? httpProxy, Uri? httpsProxy, string? bypassList // UriBuilder does not handle that now e.g. does not distinguish between empty and missing. if (user == "" && password == "") { - string[] tokens = uri.ToString().Split('/', 3); - if (tokens.Length == 3) + Span tokens = stackalloc Range[3]; + ReadOnlySpan uriSpan = uri.ToString(); + if (uriSpan.Split(tokens, '/') == 3) { - uri = new Uri($"{tokens[0]}//:@{tokens[2]}"); + uri = new Uri($"{uriSpan[tokens[0]]}//:@{uriSpan[tokens[2]]}"); } } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs b/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs index 829785bf4cb65..b2b38ebfd3e26 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs @@ -614,13 +614,15 @@ internal bool IsLocalDomain(string host) } // Test for "127.###.###.###" without using regex. - string[] ipParts = host.Split('.'); - if (ipParts != null && ipParts.Length == 4 && ipParts[0] == "127") + ReadOnlySpan hostSpan = host; + Span ipParts = stackalloc Range[5]; + ipParts = ipParts.Slice(0, hostSpan.Split(ipParts, '.')); + if (ipParts.Length == 4 && hostSpan[ipParts[0]] is "127") { int i; for (i = 1; i < ipParts.Length; i++) { - string part = ipParts[i]; + ReadOnlySpan part = hostSpan[ipParts[i]]; switch (part.Length) { case 3: diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs index fa025d64f5d55..ab72a79c966cc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs @@ -505,7 +505,7 @@ public virtual CultureInfo Parent parentName = "zh-Hant"; } } - else if (_name.Length > 8 && _name.AsSpan(2, 4).Equals("-Han", StringComparison.Ordinal) && _name[7] == '-') // cultures like zh-Hant-* and zh-Hans-* + else if (_name.Length > 8 && _name.AsSpan(2, 4) is "-Han" && _name[7] == '-') // cultures like zh-Hant-* and zh-Hans-* { if (_name[6] == 't') // zh-Hant-* { diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/JapaneseCalendar.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/JapaneseCalendar.Nls.cs index 90365e2d2deca..1ae65aa57432d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/JapaneseCalendar.Nls.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/JapaneseCalendar.Nls.cs @@ -184,29 +184,32 @@ private static int CompareEraRanges(EraInfo a, EraInfo b) // Get Strings // // Needs to be a certain length e_a_E_A at least (7 chars, exactly 4 groups) - string[] names = data.Split('_'); - // Should have exactly 4 parts // 0 - Era Name // 1 - Abbreviated Era Name // 2 - English Era Name // 3 - Abbreviated English Era Name - if (names.Length != 4) return null; + Span names = stackalloc Range[5]; + ReadOnlySpan dataSpan = data; + if (dataSpan.Split(names, '_') == 4) + { + ReadOnlySpan eraName = dataSpan[names[0]]; + ReadOnlySpan abbreviatedEraName = dataSpan[names[1]]; + ReadOnlySpan englishEraName = dataSpan[names[2]]; + ReadOnlySpan abbreviatedEnglishEraName = dataSpan[names[3]]; - // Each part should have data in it - if (names[0].Length == 0 || - names[1].Length == 0 || - names[2].Length == 0 || - names[3].Length == 0) - return null; + // Each part should have data in it + if (!eraName.IsEmpty && !abbreviatedEraName.IsEmpty && !englishEraName.IsEmpty && !abbreviatedEnglishEraName.IsEmpty) + { + // Now we have an era we can build + // Note that the era # and max era year need cleaned up after sorting + // Don't use the full English Era Name (names[2]) + return new EraInfo(0, year, month, day, year - 1, 1, 0, + eraName.ToString(), abbreviatedEraName.ToString(), abbreviatedEnglishEraName.ToString()); + } + } - // - // Now we have an era we can build - // Note that the era # and max era year need cleaned up after sorting - // Don't use the full English Era Name (names[2]) - // - return new EraInfo(0, year, month, day, year - 1, 1, 0, - names[0], names[1], names[3]); + return null; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs index 5a99506447922..44299c2238ed9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs @@ -3235,6 +3235,339 @@ private static void SliceLongerSpanToMatchShorterLength(ref ReadOnlySpan s Debug.Assert(span.Length == other.Length); } + /// + /// Parses the source for the specified , populating the span + /// with instances representing the regions between the separators. + /// + /// The source span to parse. + /// The destination span into which the resulting ranges are written. + /// A character that delimits the regions in this instance. + /// A bitwise combination of the enumeration values that specifies whether to trim whitespace and include empty ranges. + /// The number of ranges written into . + /// + /// + /// Delimiter characters are not included in the elements of the returned array. + /// + /// + /// If the span is empty, or if the specifies and is empty, + /// or if specifies both and and the is + /// entirely whitespace, no ranges are written to the destination. + /// + /// + /// If the span does not contain , or if 's length is 1, a single range will be output containing the entire , + /// subject to the processing implied by . + /// + /// + /// If there are more regions in than will fit in , the first length minus 1 ranges are + /// stored in , and a range for the remainder of is stored in . + /// + /// + public static int Split(this ReadOnlySpan source, Span destination, char separator, StringSplitOptions options = StringSplitOptions.None) + { + string.CheckStringSplitOptions(options); + + return SplitCore(source, destination, new ReadOnlySpan(in separator), default, isAny: true, options); + } + + /// + /// Parses the source for the specified , populating the span + /// with instances representing the regions between the separators. + /// + /// The source span to parse. + /// The destination span into which the resulting ranges are written. + /// A character that delimits the regions in this instance. + /// A bitwise combination of the enumeration values that specifies whether to trim whitespace and include empty ranges. + /// The number of ranges written into . + /// + /// + /// Delimiter characters are not included in the elements of the returned array. + /// + /// + /// If the span is empty, or if the specifies and is empty, + /// or if specifies both and and the is + /// entirely whitespace, no ranges are written to the destination. + /// + /// + /// If the span does not contain , or if 's length is 1, a single range will be output containing the entire , + /// subject to the processing implied by . + /// + /// + /// If there are more regions in than will fit in , the first length minus 1 ranges are + /// stored in , and a range for the remainder of is stored in . + /// + /// + public static int Split(this ReadOnlySpan source, Span destination, ReadOnlySpan separator, StringSplitOptions options = StringSplitOptions.None) + { + string.CheckStringSplitOptions(options); + + // If the separator is an empty string, the whole input is considered the sole range. + if (separator.IsEmpty) + { + if (!destination.IsEmpty) + { + int startInclusive = 0, endExclusive = source.Length; + + if ((options & StringSplitOptions.TrimEntries) != 0) + { + (startInclusive, endExclusive) = TrimSplitEntry(source, startInclusive, endExclusive); + } + + if (startInclusive != endExclusive || (options & StringSplitOptions.RemoveEmptyEntries) == 0) + { + destination[0] = startInclusive..endExclusive; + return 1; + } + } + + return 0; + } + + return SplitCore(source, destination, separator, default, isAny: false, options); + } + + /// + /// Parses the source for one of the specified , populating the span + /// with instances representing the regions between the separators. + /// + /// The source span to parse. + /// The destination span into which the resulting ranges are written. + /// Any number of characters that may delimit the regions in this instance. If empty, all Unicode whitespace characters are used as the separators. + /// A bitwise combination of the enumeration values that specifies whether to trim whitespace and include empty ranges. + /// The number of ranges written into . + /// + /// + /// Delimiter characters are not included in the elements of the returned array. + /// + /// + /// If the span is empty, or if the specifies and is empty, + /// or if specifies both and and the is + /// entirely whitespace, no ranges are written to the destination. + /// + /// + /// If the span does not contain any of the , or if 's length is 1, a single range will be output containing the entire , + /// subject to the processing implied by . + /// + /// + /// If there are more regions in than will fit in , the first length minus 1 ranges are + /// stored in , and a range for the remainder of is stored in . + /// + /// + public static int SplitAny(this ReadOnlySpan source, Span destination, ReadOnlySpan separators, StringSplitOptions options = StringSplitOptions.None) + { + string.CheckStringSplitOptions(options); + + // If the separators list is empty, whitespace is used as separators. In that case, we want to ignore TrimEntries if specified, + // since TrimEntries also impacts whitespace. + if (separators.IsEmpty) + { + options &= ~StringSplitOptions.TrimEntries; + } + + return SplitCore(source, destination, separators, default, isAny: true, options); + } + + /// + /// Parses the source for one of the specified , populating the span + /// with instances representing the regions between the separators. + /// + /// The source span to parse. + /// The destination span into which the resulting ranges are written. + /// Any number of strings that may delimit the regions in this instance. If empty, all Unicode whitespace characters are used as the separators. + /// A bitwise combination of the enumeration values that specifies whether to trim whitespace and include empty ranges. + /// The number of ranges written into . + /// + /// + /// Delimiter characters are not included in the elements of the returned array. + /// + /// + /// If the span is empty, or if the specifies and is empty, + /// or if specifies both and and the is + /// entirely whitespace, no ranges are written to the destination. + /// + /// + /// If the span does not contain any of the , or if 's length is 1, a single range will be output containing the entire , + /// subject to the processing implied by . + /// + /// + /// If there are more regions in than will fit in , the first length minus 1 ranges are + /// stored in , and a range for the remainder of is stored in . + /// + /// + public static int SplitAny(this ReadOnlySpan source, Span destination, ReadOnlySpan separators, StringSplitOptions options = StringSplitOptions.None) + { + string.CheckStringSplitOptions(options); + + // If the separators list is empty, whitespace is used as separators. In that case, we want to ignore TrimEntries if specified, + // since TrimEntries also impacts whitespace. + if (separators.IsEmpty) + { + options &= ~StringSplitOptions.TrimEntries; + } + + return SplitCore(source, destination, default, separators!, isAny: true, options); + } + + /// Core implementation for all of the Split{Any}AsRanges methods. + /// The source span to parse. + /// The destination span into which the resulting ranges are written. + /// Either a single separator (one or more characters in length) or multiple individual 1-character separators. + /// Strings to use as separators instead of . + /// true if the separators are a set; false if should be treated as a single separator. + /// A bitwise combination of the enumeration values that specifies whether to trim whitespace and include empty ranges. + /// The number of ranges written into . + /// This implementation matches the various quirks of string.Split. + private static int SplitCore( + ReadOnlySpan source, Span destination, + ReadOnlySpan separatorOrSeparators, ReadOnlySpan stringSeparators, bool isAny, + StringSplitOptions options) + { + // If the destination is empty, there's nothing to do. + if (destination.IsEmpty) + { + return 0; + } + + bool keepEmptyEntries = (options & StringSplitOptions.RemoveEmptyEntries) == 0; + bool trimEntries = (options & StringSplitOptions.TrimEntries) != 0; + + // If the input is empty, then we either return an empty range as the sole range, or if empty entries + // are to be removed, we return nothing. + if (source.Length == 0) + { + if (keepEmptyEntries) + { + destination[0] = default; + return 1; + } + + return 0; + } + + int startInclusive = 0, endExclusive; + + // If the destination has only one slot, then we need to return the whole input, subject to the options. + if (destination.Length == 1) + { + endExclusive = source.Length; + if (trimEntries) + { + (startInclusive, endExclusive) = TrimSplitEntry(source, startInclusive, endExclusive); + } + + if (startInclusive != endExclusive || keepEmptyEntries) + { + destination[0] = startInclusive..endExclusive; + return 1; + } + + return 0; + } + + scoped ValueListBuilder separatorList = new ValueListBuilder(stackalloc int[string.StackallocIntBufferSizeLimit]); + scoped ValueListBuilder lengthList = default; + + int separatorLength; + int rangeCount = 0; + if (!stringSeparators.IsEmpty) + { + lengthList = new ValueListBuilder(stackalloc int[string.StackallocIntBufferSizeLimit]); + string.MakeSeparatorListAny(source, stringSeparators, ref separatorList, ref lengthList); + separatorLength = -1; // Will be set on each iteration of the loop + } + else if (isAny) + { + string.MakeSeparatorListAny(source, separatorOrSeparators, ref separatorList); + separatorLength = 1; + } + else + { + string.MakeSeparatorList(source, separatorOrSeparators, ref separatorList); + separatorLength = separatorOrSeparators.Length; + } + + // Try to fill in all but the last slot in the destination. The last slot is reserved for whatever remains + // after the last discovered separator. If the options specify that empty entries are to be removed, then we + // need to skip past all of those here as well, including any that occur at the beginning of the last entry, + // which is why we enter the loop if remove empty entries is set, even if we've already added enough entries. + int separatorIndex = 0; + Span destinationMinusOne = destination.Slice(0, destination.Length - 1); + while (separatorIndex < separatorList.Length && (rangeCount < destinationMinusOne.Length || !keepEmptyEntries)) + { + endExclusive = separatorList[separatorIndex]; + if (separatorIndex < lengthList.Length) + { + separatorLength = lengthList[separatorIndex]; + } + separatorIndex++; + + // Trim off whitespace from the start and end of the range. + int untrimmedEndEclusive = endExclusive; + if (trimEntries) + { + (startInclusive, endExclusive) = TrimSplitEntry(source, startInclusive, endExclusive); + } + + // If the range is not empty or we're not ignoring empty ranges, store it. + Debug.Assert(startInclusive <= endExclusive); + if (startInclusive != endExclusive || keepEmptyEntries) + { + // If we're not keeping empty entries, we may have entered the loop even if we'd + // already written enough ranges. Now that we know this entry isn't empty, we + // need to validate there's still room remaining. + if ((uint)rangeCount >= (uint)destinationMinusOne.Length) + { + break; + } + + destinationMinusOne[rangeCount] = startInclusive..endExclusive; + rangeCount++; + } + + // Reset to be just past the separator, and loop around to go again. + startInclusive = untrimmedEndEclusive + separatorLength; + } + + separatorList.Dispose(); + lengthList.Dispose(); + + // Either we found at least destination.Length - 1 ranges or we didn't find any more separators. + // If we still have a last destination slot available and there's anything left in the source, + // put a range for the remainder of the source into the destination. + if ((uint)rangeCount < (uint)destination.Length) + { + endExclusive = source.Length; + if (trimEntries) + { + (startInclusive, endExclusive) = TrimSplitEntry(source, startInclusive, endExclusive); + } + + if (startInclusive != endExclusive || keepEmptyEntries) + { + destination[rangeCount] = startInclusive..endExclusive; + rangeCount++; + } + } + + // Return how many ranges were written. + return rangeCount; + } + + /// Updates the starting and ending markers for a range to exclude whitespace. + private static (int StartInclusive, int EndExclusive) TrimSplitEntry(ReadOnlySpan source, int startInclusive, int endExclusive) + { + while (startInclusive < endExclusive && char.IsWhiteSpace(source[startInclusive])) + { + startInclusive++; + } + + while (endExclusive > startInclusive && char.IsWhiteSpace(source[endExclusive - 1])) + { + endExclusive--; + } + + return (startInclusive, endExclusive); + } + /// Writes the specified interpolated string to the character span. /// The span to which the interpolated string should be formatted. /// The interpolated string. diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs index 13c5a4ed4475d..dae42bc06496d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameParser.cs @@ -190,37 +190,39 @@ private AssemblyNameParts Parse() private Version ParseVersion(string attributeValue) { - string[] parts = attributeValue.Split('.'); - if (parts.Length > 4) + ReadOnlySpan attributeValueSpan = attributeValue; + Span parts = stackalloc Range[5]; + parts = parts.Slice(0, attributeValueSpan.Split(parts, '.')); + if (parts.Length is < 2 or > 4) + { ThrowInvalidAssemblyName(); + } + Span versionNumbers = stackalloc ushort[4]; for (int i = 0; i < versionNumbers.Length; i++) { - if (i >= parts.Length) - versionNumbers[i] = ushort.MaxValue; - else + if ((uint)i >= (uint)parts.Length) { - // Desktop compat: TryParse is a little more forgiving than Fusion. - for (int j = 0; j < parts[i].Length; j++) - { - if (!char.IsDigit(parts[i][j])) - ThrowInvalidAssemblyName(); - } + versionNumbers[i] = ushort.MaxValue; + break; + } - if (!ushort.TryParse(parts[i], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out versionNumbers[i])) - { - ThrowInvalidAssemblyName(); - } + if (!ushort.TryParse(attributeValueSpan[parts[i]], NumberStyles.None, NumberFormatInfo.InvariantInfo, out versionNumbers[i])) + { + ThrowInvalidAssemblyName(); } } - if (versionNumbers[0] == ushort.MaxValue || versionNumbers[1] == ushort.MaxValue) + if (versionNumbers[0] == ushort.MaxValue || + versionNumbers[1] == ushort.MaxValue) + { ThrowInvalidAssemblyName(); - if (versionNumbers[2] == ushort.MaxValue) - return new Version(versionNumbers[0], versionNumbers[1]); - if (versionNumbers[3] == ushort.MaxValue) - return new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2]); - return new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2], versionNumbers[3]); + } + + return + versionNumbers[2] == ushort.MaxValue ? new Version(versionNumbers[0], versionNumbers[1]) : + versionNumbers[3] == ushort.MaxValue ? new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2]) : + new Version(versionNumbers[0], versionNumbers[1], versionNumbers[2], versionNumbers[3]); } private static string ParseCulture(string attributeValue) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/FrameworkName.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/FrameworkName.cs index ad22a86e5c548..973a13aafabf2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/FrameworkName.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/FrameworkName.cs @@ -110,18 +110,21 @@ public FrameworkName(string frameworkName) { ArgumentException.ThrowIfNullOrEmpty(frameworkName); - string[] components = frameworkName.Split(ComponentSeparator); + ReadOnlySpan frameworkNameSpan = frameworkName; + Span components = stackalloc Range[4]; + int numComponents = frameworkNameSpan.Split(components, ComponentSeparator); // Identifier and Version are required, Profile is optional. - if (components.Length < 2 || components.Length > 3) + if (numComponents is not (2 or 3)) { throw new ArgumentException(SR.Argument_FrameworkNameTooShort, nameof(frameworkName)); } + components = components.Slice(0, numComponents); // // 1) Parse the "Identifier", which must come first. Trim any whitespace // - _identifier = components[0].Trim(); + _identifier = frameworkNameSpan[components[0]].Trim().ToString(); if (_identifier.Length == 0) { @@ -137,7 +140,7 @@ public FrameworkName(string frameworkName) for (int i = 1; i < components.Length; i++) { // Get the key/value pair separated by '=' - string component = components[i]; + ReadOnlySpan component = frameworkNameSpan[components[i]]; int separatorIndex = component.IndexOf(KeyValueSeparator); if (separatorIndex < 0 || separatorIndex != component.LastIndexOf(KeyValueSeparator)) @@ -146,8 +149,8 @@ public FrameworkName(string frameworkName) } // Get the key and value, trimming any whitespace - ReadOnlySpan key = component.AsSpan(0, separatorIndex).Trim(); - ReadOnlySpan value = component.AsSpan(separatorIndex + 1).Trim(); + ReadOnlySpan key = component.Slice(0, separatorIndex).Trim(); + ReadOnlySpan value = component.Slice(separatorIndex + 1).Trim(); // // 2) Parse the required "Version" key value diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index 8522fd72c411f..5cdd7d87d4530 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -28,7 +28,7 @@ private static class IndexOfAnyValuesStorage IndexOfAnyValues.Create("\r\n\f\u0085\u2028\u2029"); } - private const int StackallocIntBufferSizeLimit = 128; + internal const int StackallocIntBufferSizeLimit = 128; private static void FillStringChecked(string dest, int destPos, string src) { @@ -1324,7 +1324,7 @@ private string[] SplitInternal(ReadOnlySpan separators, int count, StringS var sepListBuilder = new ValueListBuilder(stackalloc int[StackallocIntBufferSizeLimit]); - MakeSeparatorList(separators, ref sepListBuilder); + MakeSeparatorListAny(this, separators, ref sepListBuilder); ReadOnlySpan sepList = sepListBuilder.AsSpan(); // Handle the special case of no replaces. @@ -1401,7 +1401,7 @@ private string[] SplitInternal(string? separator, string?[]? separators, int cou var sepListBuilder = new ValueListBuilder(stackalloc int[StackallocIntBufferSizeLimit]); var lengthListBuilder = new ValueListBuilder(stackalloc int[StackallocIntBufferSizeLimit]); - MakeSeparatorList(separators!, ref sepListBuilder, ref lengthListBuilder); + MakeSeparatorListAny(this, separators, ref sepListBuilder, ref lengthListBuilder); ReadOnlySpan sepList = sepListBuilder.AsSpan(); ReadOnlySpan lengthList = lengthListBuilder.AsSpan(); @@ -1447,7 +1447,7 @@ private string[] SplitInternal(string separator, int count, StringSplitOptions o { var sepListBuilder = new ValueListBuilder(stackalloc int[StackallocIntBufferSizeLimit]); - MakeSeparatorList(separator, ref sepListBuilder); + MakeSeparatorList(this, separator, ref sepListBuilder); ReadOnlySpan sepList = sepListBuilder.AsSpan(); if (sepList.Length == 0) { @@ -1578,16 +1578,17 @@ private string[] SplitWithPostProcessing(ReadOnlySpan sepList, ReadOnlySpan /// /// Uses ValueListBuilder to create list that holds indexes of separators in string. /// + /// The source to parse. /// of separator chars /// to store indexes - private void MakeSeparatorList(ReadOnlySpan separators, ref ValueListBuilder sepListBuilder) + internal static void MakeSeparatorListAny(ReadOnlySpan source, ReadOnlySpan separators, ref ValueListBuilder sepListBuilder) { // Special-case no separators to mean any whitespace is a separator. if (separators.Length == 0) { - for (int i = 0; i < Length; i++) + for (int i = 0; i < source.Length; i++) { - if (char.IsWhiteSpace(this[i])) + if (char.IsWhiteSpace(source[i])) { sepListBuilder.Append(i); } @@ -1601,15 +1602,15 @@ private void MakeSeparatorList(ReadOnlySpan separators, ref ValueListBuild sep0 = separators[0]; sep1 = separators.Length > 1 ? separators[1] : sep0; sep2 = separators.Length > 2 ? separators[2] : sep1; - if (Vector128.IsHardwareAccelerated && Length >= Vector128.Count * 2) + if (Vector128.IsHardwareAccelerated && source.Length >= Vector128.Count * 2) { - MakeSeparatorListVectorized(ref sepListBuilder, sep0, sep1, sep2); + MakeSeparatorListVectorized(source, ref sepListBuilder, sep0, sep1, sep2); return; } - for (int i = 0; i < Length; i++) + for (int i = 0; i < source.Length; i++) { - char c = this[i]; + char c = source[i]; if (c == sep0 || c == sep1 || c == sep2) { sepListBuilder.Append(i); @@ -1626,10 +1627,9 @@ private void MakeSeparatorList(ReadOnlySpan separators, ref ValueListBuild var map = new ProbabilisticMap(separators); ref uint charMap = ref Unsafe.As(ref map); - for (int i = 0; i < Length; i++) + for (int i = 0; i < source.Length; i++) { - char c = this[i]; - if (ProbabilisticMap.Contains(ref charMap, separators, c)) + if (ProbabilisticMap.Contains(ref charMap, separators, source[i])) { sepListBuilder.Append(i); } @@ -1638,7 +1638,7 @@ private void MakeSeparatorList(ReadOnlySpan separators, ref ValueListBuild } } - private void MakeSeparatorListVectorized(ref ValueListBuilder sepListBuilder, char c, char c2, char c3) + private static void MakeSeparatorListVectorized(ReadOnlySpan sourceSpan, ref ValueListBuilder sepListBuilder, char c, char c2, char c3) { // Redundant test so we won't prejit remainder of this method // on platforms where it is not supported @@ -1647,12 +1647,12 @@ private void MakeSeparatorListVectorized(ref ValueListBuilder sepListBuilde throw new PlatformNotSupportedException(); } - Debug.Assert(Length >= Vector128.Count); + Debug.Assert(sourceSpan.Length >= Vector128.Count); nuint offset = 0; - nuint lengthToExamine = (nuint)(uint)Length; + nuint lengthToExamine = (uint)sourceSpan.Length; - ref ushort source = ref Unsafe.As(ref _firstChar); + ref ushort source = ref Unsafe.As(ref MemoryMarshal.GetReference(sourceSpan)); Vector128 v1 = Vector128.Create((ushort)c); Vector128 v2 = Vector128.Create((ushort)c2); @@ -1695,39 +1695,42 @@ private void MakeSeparatorListVectorized(ref ValueListBuilder sepListBuilde /// /// Uses ValueListBuilder to create list that holds indexes of separators in string. /// + /// The source to parse. /// separator string /// to store indexes - private void MakeSeparatorList(string separator, ref ValueListBuilder sepListBuilder) + internal static void MakeSeparatorList(ReadOnlySpan source, ReadOnlySpan separator, ref ValueListBuilder sepListBuilder) { - Debug.Assert(!IsNullOrEmpty(separator), "!string.IsNullOrEmpty(separator)"); + Debug.Assert(!separator.IsEmpty, "Empty separator"); - int currentSepLength = separator.Length; - - for (int i = 0; i < Length; i++) + int i = 0; + while (!source.IsEmpty) { - if (this[i] == separator[0] && currentSepLength <= Length - i) + int index = source.IndexOf(separator); + if (index < 0) { - if (currentSepLength == 1 - || this.AsSpan(i, currentSepLength).SequenceEqual(separator)) - { - sepListBuilder.Append(i); - i += currentSepLength - 1; - } + break; } + + i += index; + sepListBuilder.Append(i); + + i += separator.Length; + source = source.Slice(index + separator.Length); } } /// /// Uses ValueListBuilder to create list that holds indexes of separators in string and list that holds length of separator strings. /// + /// The source to parse. /// separator strngs /// for separator indexes /// for separator length values - private void MakeSeparatorList(string?[] separators, ref ValueListBuilder sepListBuilder, ref ValueListBuilder lengthListBuilder) + internal static void MakeSeparatorListAny(ReadOnlySpan source, ReadOnlySpan separators, ref ValueListBuilder sepListBuilder, ref ValueListBuilder lengthListBuilder) { - Debug.Assert(separators != null && separators.Length > 0, "separators != null && separators.Length > 0"); + Debug.Assert(!separators.IsEmpty, "Zero separators"); - for (int i = 0; i < Length; i++) + for (int i = 0; i < source.Length; i++) { for (int j = 0; j < separators.Length; j++) { @@ -1737,10 +1740,9 @@ private void MakeSeparatorList(string?[] separators, ref ValueListBuilder s continue; } int currentSepLength = separator.Length; - if (this[i] == separator[0] && currentSepLength <= Length - i) + if (source[i] == separator[0] && currentSepLength <= source.Length - i) { - if (currentSepLength == 1 - || this.AsSpan(i, currentSepLength).SequenceEqual(separator)) + if (currentSepLength == 1 || source.Slice(i, currentSepLength).SequenceEqual(separator)) { sepListBuilder.Append(i); lengthListBuilder.Append(currentSepLength); @@ -1752,7 +1754,7 @@ private void MakeSeparatorList(string?[] separators, ref ValueListBuilder s } } - private static void CheckStringSplitOptions(StringSplitOptions options) + internal static void CheckStringSplitOptions(StringSplitOptions options) { const StringSplitOptions AllValidFlags = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs index 6152acc0e8a52..c748b9afa609a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -752,14 +752,16 @@ private static string GetLocalizedNameByMuiNativeResource(string resource) // filePath = "C:\Windows\System32\tzres.dll" // resourceId = -100 // - string[] resources = resource.Split(','); + ReadOnlySpan resourceSpan = resource; + Span resources = stackalloc Range[3]; + resources = resources.Slice(0, resourceSpan.Split(resources, ',')); if (resources.Length != 2) { return string.Empty; } // Get the resource ID - if (!int.TryParse(resources[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int resourceId)) + if (!int.TryParse(resourceSpan[resources[1]], NumberStyles.Integer, CultureInfo.InvariantCulture, out int resourceId)) { return string.Empty; } @@ -772,7 +774,7 @@ private static string GetLocalizedNameByMuiNativeResource(string resource) string system32 = Environment.SystemDirectory; // trim the string "@tzres.dll" to "tzres.dll" and append the "mui" file extension to it. - string tzresDll = $"{resources[0].AsSpan().TrimStart('@')}.mui"; + string tzresDll = $"{resourceSpan[resources[0]].TrimStart('@')}.mui"; try { diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Schema/Inference/Infer.cs b/src/libraries/System.Private.Xml/src/System/Xml/Schema/Inference/Infer.cs index 1f84753bbb66b..f519d6468b22f 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Schema/Inference/Infer.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Schema/Inference/Infer.cs @@ -1768,7 +1768,7 @@ internal static int InferSimpleType(string s, ref bool bNeedsRangeCheck) //else case 'I': //try to match "INF" INF: - if (s.AsSpan(i).SequenceEqual("INF")) + if (s.AsSpan(i) is "INF") return TF_float | TF_double | TF_string; else return TF_string; case '.': //try to match ".9999" decimal/float/double diff --git a/src/libraries/System.Runtime/tests/System/String.SplitTests.cs b/src/libraries/System.Runtime/tests/System/String.SplitTests.cs index 388778ac5beae..ce7450c72c2db 100644 --- a/src/libraries/System.Runtime/tests/System/String.SplitTests.cs +++ b/src/libraries/System.Runtime/tests/System/String.SplitTests.cs @@ -1,130 +1,167 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using Xunit; namespace System.Tests { + // These tests validate both String.Split and MemoryExtensions.Split, as they have equivalent semantics, with the + // former creating a new array to store the results and the latter writing the results into a supplied span. + public static class StringSplitTests { [Fact] public static void SplitInvalidCount() { - const string value = "a,b"; - const int count = -1; - const StringSplitOptions options = StringSplitOptions.None; - - AssertExtensions.Throws("count", () => value.Split(',', count)); - AssertExtensions.Throws("count", () => value.Split(',', count, options)); - AssertExtensions.Throws("count", () => value.Split(new[] { ',' }, count)); - AssertExtensions.Throws("count", () => value.Split(new[] { ',' }, count, options)); - AssertExtensions.Throws("count", () => value.Split(",", count)); - AssertExtensions.Throws("count", () => value.Split(",", count, options)); - AssertExtensions.Throws("count", () => value.Split(new[] { "," }, count, options)); + const string Value = "a,b"; + const int Count = -1; + const StringSplitOptions Options = StringSplitOptions.None; + + AssertExtensions.Throws("count", () => Value.Split(',', Count)); + AssertExtensions.Throws("count", () => Value.Split(',', Count, Options)); + AssertExtensions.Throws("count", () => Value.Split(new[] { ',' }, Count)); + AssertExtensions.Throws("count", () => Value.Split(new[] { ',' }, Count, Options)); + AssertExtensions.Throws("count", () => Value.Split(",", Count)); + AssertExtensions.Throws("count", () => Value.Split(",", Count, Options)); + AssertExtensions.Throws("count", () => Value.Split(new[] { "," }, Count, Options)); } [Fact] public static void SplitInvalidOptions() { - const string value = "a,b"; - const int count = int.MaxValue; - const StringSplitOptions optionsTooLow = StringSplitOptions.None - 1; - const StringSplitOptions optionsTooHigh = (StringSplitOptions)0x04; - - AssertExtensions.Throws("options", () => value.Split(',', optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(',', optionsTooHigh)); - AssertExtensions.Throws("options", () => value.Split(',', count, optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(',', count, optionsTooHigh)); - AssertExtensions.Throws("options", () => value.Split(new[] { ',' }, optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(new[] { ',' }, optionsTooHigh)); - AssertExtensions.Throws("options", () => value.Split(new[] { ',' }, count, optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(new[] { ',' }, count, optionsTooHigh)); - AssertExtensions.Throws("options", () => value.Split(",", optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(",", optionsTooHigh)); - AssertExtensions.Throws("options", () => value.Split(",", count, optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(",", count, optionsTooHigh)); - AssertExtensions.Throws("options", () => value.Split(new[] { "," }, optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(new[] { "," }, optionsTooHigh)); - AssertExtensions.Throws("options", () => value.Split(new[] { "," }, count, optionsTooLow)); - AssertExtensions.Throws("options", () => value.Split(new[] { "," }, count, optionsTooHigh)); + const string Value = "a,b"; + const int Count = 0; + + foreach (StringSplitOptions options in new[] { StringSplitOptions.None - 1, (StringSplitOptions)0x04 }) + { + AssertExtensions.Throws("options", () => Value.Split(',', options)); + AssertExtensions.Throws("options", () => Value.Split(',', Count, options)); + AssertExtensions.Throws("options", () => Value.Split(new[] { ',' }, options)); + AssertExtensions.Throws("options", () => Value.Split(new[] { ',' }, Count, options)); + AssertExtensions.Throws("options", () => Value.Split(",", options)); + AssertExtensions.Throws("options", () => Value.Split(",", Count, options)); + AssertExtensions.Throws("options", () => Value.Split(new[] { "," }, options)); + AssertExtensions.Throws("options", () => Value.Split(new[] { "," }, Count, options)); + + AssertExtensions.Throws("options", () => Value.AsSpan().Split(Span.Empty, ',', options)); + AssertExtensions.Throws("options", () => Value.AsSpan().Split(Span.Empty, ",", options)); + AssertExtensions.Throws("options", () => Value.AsSpan().SplitAny(Span.Empty, ",", options)); + AssertExtensions.Throws("options", () => Value.AsSpan().SplitAny(Span.Empty, new[] { "," }, options)); + } } [Fact] public static void SplitZeroCountEmptyResult() { - const string value = "a,b"; - const int count = 0; - const StringSplitOptions options = StringSplitOptions.None; + const string Value = "a,b"; + const int Count = 0; + const StringSplitOptions Options = StringSplitOptions.None; string[] expected = new string[0]; - Assert.Equal(expected, value.Split(',', count)); - Assert.Equal(expected, value.Split(',', count, options)); - Assert.Equal(expected, value.Split(new[] { ',' }, count)); - Assert.Equal(expected, value.Split(new[] { ',' }, count, options)); - Assert.Equal(expected, value.Split(",", count)); - Assert.Equal(expected, value.Split(",", count, options)); - Assert.Equal(expected, value.Split(new[] { "," }, count, options)); + Assert.Equal(expected, Value.Split(',', Count)); + Assert.Equal(expected, Value.Split(',', Count, Options)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Count)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Count, Options)); + Assert.Equal(expected, Value.Split(",", Count)); + Assert.Equal(expected, Value.Split(",", Count, Options)); + Assert.Equal(expected, Value.Split(new[] { "," }, Count, Options)); + + Assert.Equal(0, Value.AsSpan().Split(Span.Empty, ',', Options)); + Assert.Equal(0, Value.AsSpan().Split(Span.Empty, ",", Options)); + Assert.Equal(0, Value.AsSpan().SplitAny(Span.Empty, ",", Options)); } [Fact] public static void SplitEmptyValueWithRemoveEmptyEntriesOptionEmptyResult() { - string value = string.Empty; - const int count = int.MaxValue; - const StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries; + const string Value = ""; + const int Count = int.MaxValue; + const StringSplitOptions Options = StringSplitOptions.RemoveEmptyEntries; - string[] expected = new string[0]; + string[] expected = Array.Empty(); + + Assert.Equal(expected, Value.Split(',', Options)); + Assert.Equal(expected, Value.Split(',', Count, Options)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Options)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Count, Options)); + Assert.Equal(expected, Value.Split(",", Options)); + Assert.Equal(expected, Value.Split(",", Count, Options)); + Assert.Equal(expected, Value.Split(new[] { "," }, Options)); + Assert.Equal(expected, Value.Split(new[] { "," }, Count, Options)); - Assert.Equal(expected, value.Split(',', options)); - Assert.Equal(expected, value.Split(',', count, options)); - Assert.Equal(expected, value.Split(new[] { ',' }, options)); - Assert.Equal(expected, value.Split(new[] { ',' }, count, options)); - Assert.Equal(expected, value.Split(",", options)); - Assert.Equal(expected, value.Split(",", count, options)); - Assert.Equal(expected, value.Split(new[] { "," }, options)); - Assert.Equal(expected, value.Split(new[] { "," }, count, options)); + Range[] ranges = new Range[10]; + Assert.Equal(0, Value.AsSpan().Split(ranges, ',', Options)); + Assert.Equal(0, Value.AsSpan().Split(ranges, ",", Options)); + Assert.Equal(0, Value.AsSpan().SplitAny(ranges, ",", Options)); } [Fact] public static void SplitOneCountSingleResult() { - const string value = "a,b"; - const int count = 1; - const StringSplitOptions options = StringSplitOptions.None; - - string[] expected = new[] { value }; - - Assert.Equal(expected, value.Split(',', count)); - Assert.Equal(expected, value.Split(',', count, options)); - Assert.Equal(expected, value.Split(new[] { ',' }, count)); - Assert.Equal(expected, value.Split(new[] { ',' }, count, options)); - Assert.Equal(expected, value.Split(",", count)); - Assert.Equal(expected, value.Split(",", count, options)); - Assert.Equal(expected, value.Split(new[] { "," }, count, options)); + const string Value = "a,b"; + const int Count = 1; + const StringSplitOptions Options = StringSplitOptions.None; + + string[] expected = new[] { Value }; + + Assert.Equal(expected, Value.Split(',', Count)); + Assert.Equal(expected, Value.Split(',', Count, Options)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Count)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Count, Options)); + Assert.Equal(expected, Value.Split(",", Count)); + Assert.Equal(expected, Value.Split(",", Count, Options)); + Assert.Equal(expected, Value.Split(new[] { "," }, Count, Options)); + + Range[] ranges = new Range[1]; + Assert.Equal(1, Value.AsSpan().Split(ranges, ',', Options)); + Assert.Equal(0..3, ranges[0]); + Array.Clear(ranges); + + Assert.Equal(1, Value.AsSpan().Split(ranges, ",", Options)); + Assert.Equal(0..3, ranges[0]); + Array.Clear(ranges); + + Assert.Equal(1, Value.AsSpan().SplitAny(ranges, ",", Options)); + Assert.Equal(0..3, ranges[0]); + Array.Clear(ranges); } [Fact] public static void SplitNoMatchSingleResult() { - const string value = "a b"; - const int count = int.MaxValue; - const StringSplitOptions options = StringSplitOptions.None; - - string[] expected = new[] { value }; - - Assert.Equal(expected, value.Split(',')); - Assert.Equal(expected, value.Split(',', options)); - Assert.Equal(expected, value.Split(',', count, options)); - Assert.Equal(expected, value.Split(new[] { ',' })); - Assert.Equal(expected, value.Split(new[] { ',' }, options)); - Assert.Equal(expected, value.Split(new[] { ',' }, count)); - Assert.Equal(expected, value.Split(new[] { ',' }, count, options)); - Assert.Equal(expected, value.Split(",")); - Assert.Equal(expected, value.Split(",", options)); - Assert.Equal(expected, value.Split(",", count, options)); - Assert.Equal(expected, value.Split(new[] { "," }, options)); - Assert.Equal(expected, value.Split(new[] { "," }, count, options)); + const string Value = "a b"; + const int Count = int.MaxValue; + const StringSplitOptions Options = StringSplitOptions.None; + + string[] expected = new[] { Value }; + + Assert.Equal(expected, Value.Split(',')); + Assert.Equal(expected, Value.Split(',', Options)); + Assert.Equal(expected, Value.Split(',', Count, Options)); + Assert.Equal(expected, Value.Split(new[] { ',' })); + Assert.Equal(expected, Value.Split(new[] { ',' }, Options)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Count)); + Assert.Equal(expected, Value.Split(new[] { ',' }, Count, Options)); + Assert.Equal(expected, Value.Split(",")); + Assert.Equal(expected, Value.Split(",", Options)); + Assert.Equal(expected, Value.Split(",", Count, Options)); + Assert.Equal(expected, Value.Split(new[] { "," }, Options)); + Assert.Equal(expected, Value.Split(new[] { "," }, Count, Options)); + + Range[] ranges = new Range[10]; + Assert.Equal(1, Value.AsSpan().Split(ranges, ',', Options)); + Assert.Equal(0..3, ranges[0]); + Array.Clear(ranges); + + Assert.Equal(1, Value.AsSpan().Split(ranges, ",", Options)); + Assert.Equal(0..3, ranges[0]); + Array.Clear(ranges); + + Assert.Equal(1, Value.AsSpan().SplitAny(ranges, ",", Options)); + Assert.Equal(0..3, ranges[0]); + Array.Clear(ranges); } private const int M = int.MaxValue; @@ -479,9 +516,23 @@ public static void SplitCharSeparator(string value, char separator, int count, S Assert.Equal(expected, value.Split(new[] { separator })); Assert.Equal(expected, value.Split(separator.ToString())); } + + Range[] ranges = new Range[count == int.MaxValue ? value.Length + 1 : count]; + + Assert.Equal(expected.Length, value.AsSpan().Split(ranges, separator, options)); + Assert.Equal(expected, ranges.Take(expected.Length).Select(r => value[r]).ToArray()); + + Assert.Equal(expected.Length, value.AsSpan().Split(ranges, separator.ToString(), options)); + Assert.Equal(expected, ranges.Take(expected.Length).Select(r => value[r]).ToArray()); } [Theory] + [InlineData("", null, 0, StringSplitOptions.None, new string[0])] + [InlineData("", "", 0, StringSplitOptions.None, new string[0])] + [InlineData("", "separator", 0, StringSplitOptions.None, new string[0])] + [InlineData(" a , b ,c ", "", M, StringSplitOptions.TrimEntries, new[] { "a , b ,c" })] + [InlineData(" ", "", M, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries, new string[0])] + [InlineData(" ", "", M, StringSplitOptions.TrimEntries, new[] { "" })] [InlineData("a,b,c", null, M, StringSplitOptions.None, new[] { "a,b,c" })] [InlineData("a,b,c", "", M, StringSplitOptions.None, new[] { "a,b,c" })] [InlineData("aaabaaabaaa", "aa", M, StringSplitOptions.None, new[] { "", "ab", "ab", "a" })] @@ -516,6 +567,10 @@ public static void SplitStringSeparator(string value, string separator, int coun { Assert.Equal(expected, value.Split(separator)); } + + Range[] ranges = new Range[count == int.MaxValue ? value.Length + 1 : count]; + Assert.Equal(expected.Length, value.AsSpan().Split(ranges, separator, options)); + Assert.Equal(expected, ranges.Take(expected.Length).Select(r => value[r]).ToArray()); } [Fact] @@ -559,6 +614,10 @@ public static void SplitCharArraySeparator(string value, char[] separators, int { Assert.Equal(expected, value.Split(separators, count, options)); Assert.Equal(expected, value.Split(ToStringArray(separators), count, options)); + + Range[] ranges = new Range[count == int.MaxValue ? value.Length + 1 : count]; + Assert.Equal(expected.Length, value.AsSpan().SplitAny(ranges, separators, options)); + Assert.Equal(expected, ranges.Take(expected.Length).Select(r => value[r]).ToArray()); } [Theory] @@ -598,6 +657,10 @@ public static void SplitCharArraySeparator(string value, char[] separators, int public static void SplitStringArraySeparator(string value, string[] separators, int count, StringSplitOptions options, string[] expected) { Assert.Equal(expected, value.Split(separators, count, options)); + + Range[] ranges = new Range[count == int.MaxValue ? value.Length + 1 : count]; + Assert.Equal(expected.Length, value.AsSpan().SplitAny(ranges, separators, options)); + Assert.Equal(expected, ranges.Take(expected.Length).Select(r => value[r]).ToArray()); } private static string[] ToStringArray(char[] source)