< Summary

Information
Class: AmbientServices.Utilities.StringUtilities
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Utilities/StringUtilities.cs
Tag: 332_35464845198
Line coverage
98%
Covered lines: 64
Uncovered lines: 1
Coverable lines: 65
Total lines: 94
Line coverage: 98.4%
Branch coverage
91%
Covered branches: 21
Total branches: 23
Branch coverage: 91.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
NormalizeStringWithNumberSequences(...)100%11100%
NegativePartTransform(...)100%22100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Utilities/StringUtilities.cs

#LineLine coverage
 1using System;
 2using System.Linq;
 3using System.Text;
 4
 5namespace AmbientServices.Utilities;
 6
 7/// <summary>
 8/// A static partial class that extends <see cref="string"/>.
 9/// </summary>
 10/// <remarks>
 11/// <pitch>The normalization engine behind natural string comparison in <see cref="AmbientServices.Extensions.StringExte
 12/// <pledge>Given the same <c>maxDigits</c> (at least the longest digit run in either input), ordinal comparison of two 
 13/// <plan>A single compiled regex classifies each numeric token — period- or dash-separated sequences (versions, dates; 
 14/// </remarks>
 15internal static partial class StringUtilities
 16{
 217    private static readonly char[] DecimalPointCharArray = ".,".ToCharArray();
 218    private static readonly char[] NumberSeparatorCharArray = ".,-".ToCharArray();
 219    private static readonly System.Text.RegularExpressions.Regex NumberRegex = new(
 220        @"(?<ps>(?:-?\d+)\.(?:(?:\d+)\.)+(?:\d+))" +    // finds a sequence of numbers separated by periods, such as 202
 221        @"|(?<ds>(?:-?\d+)-(?:(?:\d+)-)+(?:\d+))" +     // finds a sequence of numbers separated by dashes, such as 2020
 222        @"|(?<nr>(?<![0-9])(?:-(?:\d*)\.\d+))" +        // finds a negative real
 223        @"|(?<ni>(?<![0-9])(?:-(?:\d+)))" +             // finds a negative integer
 224        @"|(?<pr>(?<![-.,]\d*)(?:(?:\d*)\.\d+))" +      // finds a positive real
 225        @"|(?<pi>(?<![-.,]\d*)(?:\d+))",                // finds a positive integer
 226        System.Text.RegularExpressions.RegexOptions.Compiled);
 27    internal static string NormalizeStringWithNumberSequences(string str, int maxDigits)
 28    {
 229        str = NumberRegex.Replace(str,
 230            delegate (System.Text.RegularExpressions.Match m)
 231            {
 232                int matchGroup = 1;
 233                for (; matchGroup < m.Groups.Count; ++matchGroup)
 234                {
 235                    if (m.Groups[matchGroup].Captures.Count > 0) break;
 236                }
 237                // I don't think this should ever happen, but just in case...
 238                if (matchGroup >= m.Groups.Count) return m.Value;
 239                int prefixIndex = m.Index - 1;
 240                int decimalPointIndex;
 241                string wholePart;
 242                string fractionPart;
 243                string[] numberParts;
 244                // Note that the use of 1 and 4 here is to be sure that negatives sort before positives.  1 is like a si
 245                switch (matchGroup)
 246                {
 247                    case 1: // ps: period sequence
 248                        numberParts = m.Value.Split(NumberSeparatorCharArray);
 249                        return (numberParts[0].Length == 0 && m.Value[0] == '-')
 250                        ? NegativePartTransform("1", numberParts[0].PadLeft(maxDigits, '0')) + "." + string.Join(".", nu
 251                        : string.Join(".", numberParts.Select(s => "4" + s.PadLeft(maxDigits, '0')));
 252                    case 2: // ds: dash sequence
 253                        numberParts = m.Value.Split(NumberSeparatorCharArray);
 254                        return (numberParts[0].Length == 0 && m.Value[0] == '-')
 255                        ? NegativePartTransform("1", numberParts[0].PadLeft(maxDigits, '0')) + "-" + string.Join("-", nu
 256                        : string.Join("-", numberParts.Select(s => "4" + s.PadLeft(maxDigits, '0')));
 257                    case 3: // nr: negative real
 258                        decimalPointIndex = m.Value.IndexOfAny(DecimalPointCharArray, 1);
 259                        System.Diagnostics.Debug.Assert(decimalPointIndex > 0);
 260                        wholePart = NegativePartTransform("1", m.Value.Substring(1, decimalPointIndex - 1).PadLeft(maxDi
 261                        fractionPart = NegativePartTransform("4", m.Value.Substring(decimalPointIndex + 1, m.Value.Lengt
 262                        return wholePart + fractionPart;
 263                    case 4: // ni: negative integer
 264                        return NegativePartTransform("1", m.Value.Substring(1).PadLeft(maxDigits, '0'));
 265                    case 5: // pr: positive real
 266                        decimalPointIndex = m.Value.IndexOfAny(DecimalPointCharArray, 0);
 267                        System.Diagnostics.Debug.Assert(decimalPointIndex >= 0);
 268                        wholePart = "4" + m.Value.Substring(0, decimalPointIndex).PadLeft(maxDigits, '0');
 269                        fractionPart = "4" + m.Value.Substring(decimalPointIndex + 1, m.Value.Length - decimalPointIndex
 270                        return wholePart + fractionPart;
 271                    case 6: // pi: positive integer
 272                        return "4" + m.Value.PadLeft(maxDigits, '0');
 273                    default:
 274                        // this should also never happen, but just in case...
 075                        throw new InvalidOperationException("The match group number was not expected--the regex must hav
 276                }
 277            });
 278        return str;
 79    }
 80
 81    private static string NegativePartTransform(string prefix, string str)
 82    {
 83        System.Diagnostics.Debug.Assert(str[0] != '-');
 84        // use 1 as the 'negative prefix' because it should sort before the 'positive prefix' of 4
 285        StringBuilder builder = new(prefix);
 286        for (int off = 0; off < str.Length; ++off)
 87        {
 288            char c = str[off];
 89            System.Diagnostics.Debug.Assert(c >= '0' && c <= '9');
 290            builder.Append((char)('0' + (9 - (c - '0'))));
 91        }
 292        return builder.ToString();
 93    }
 94}