Merge pull request #2513 from Bond-009/alpha

Improve alpha numeric sorting
This commit is contained in:
Vasily 2020-03-05 15:01:05 +03:00 committed by GitHub
commit e80c9bb8c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 54 deletions

View File

@ -16,12 +16,23 @@ namespace MediaBrowser.Common.Extensions
/// <param name="list">The list that should get shuffled.</param>
/// <typeparam name="T">The type.</typeparam>
public static void Shuffle<T>(this IList<T> list)
{
list.Shuffle(_rng);
}
/// <summary>
/// Shuffles the items in a list.
/// </summary>
/// <param name="list">The list that should get shuffled.</param>
/// <param name="rng">The random number generator to use.</param>
/// <typeparam name="T">The type.</typeparam>
public static void Shuffle<T>(this IList<T> list, Random rng)
{
int n = list.Count;
while (n > 1)
{
n--;
int k = _rng.Next(n + 1);
int k = rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;

View File

@ -1,94 +1,132 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
using MediaBrowser.Controller.Sorting;
namespace MediaBrowser.Controller.Sorting
{
public class AlphanumComparator : IComparer<string>
public class AlphanumComparator : IComparer<string?>
{
public static int CompareValues(string s1, string s2)
public static int CompareValues(string? s1, string? s2)
{
if (s1 == null || s2 == null)
if (s1 == null && s2 == null)
{
return 0;
}
int thisMarker = 0, thisNumericChunk = 0;
int thatMarker = 0, thatNumericChunk = 0;
while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
else if (s1 == null)
{
if (thisMarker >= s1.Length)
{
return -1;
}
else if (thatMarker >= s2.Length)
{
return 1;
}
char thisCh = s1[thisMarker];
char thatCh = s2[thatMarker];
return -1;
}
else if (s2 == null)
{
return 1;
}
var thisChunk = new StringBuilder();
var thatChunk = new StringBuilder();
bool thisNumeric = char.IsDigit(thisCh), thatNumeric = char.IsDigit(thatCh);
int len1 = s1.Length;
int len2 = s2.Length;
while (thisMarker < s1.Length && char.IsDigit(thisCh) == thisNumeric)
// Early return for empty strings
if (len1 == 0 && len2 == 0)
{
return 0;
}
else if (len1 == 0)
{
return -1;
}
else if (len2 == 0)
{
return 1;
}
int pos1 = 0;
int pos2 = 0;
do
{
int start1 = pos1;
int start2 = pos2;
bool isNum1 = char.IsDigit(s1[pos1++]);
bool isNum2 = char.IsDigit(s2[pos2++]);
while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1)
{
thisChunk.Append(thisCh);
thisMarker++;
if (thisMarker < s1.Length)
{
thisCh = s1[thisMarker];
}
pos1++;
}
while (thatMarker < s2.Length && char.IsDigit(thatCh) == thatNumeric)
while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2)
{
thatChunk.Append(thatCh);
thatMarker++;
if (thatMarker < s2.Length)
{
thatCh = s2[thatMarker];
}
pos2++;
}
var span1 = s1.AsSpan(start1, pos1 - start1);
var span2 = s2.AsSpan(start2, pos2 - start2);
// If both chunks contain numeric characters, sort them numerically
if (thisNumeric && thatNumeric)
if (isNum1 && isNum2)
{
if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)
|| !int.TryParse(thatChunk.ToString(), out thatNumericChunk))
{
return 0;
}
if (thisNumericChunk < thatNumericChunk)
// Trim leading zeros so we can compare the length
// of the strings to find the largest number
span1 = span1.TrimStart('0');
span2 = span2.TrimStart('0');
var span1Len = span1.Length;
var span2Len = span2.Length;
if (span1Len < span2Len)
{
return -1;
}
else if (span1Len > span2Len)
{
return 1;
}
else if (span1Len >= 20) // Number is probably too big for a ulong
{
// Trim all the first digits that are the same
int i = 0;
while (i < span1Len && span1[i] == span2[i])
{
i++;
}
if (thisNumericChunk > thatNumericChunk)
// If there are no more digits it's the same number
if (i == span1Len)
{
continue;
}
// Only need to compare the most significant digit
span1 = span1.Slice(i, 1);
span2 = span2.Slice(i, 1);
}
if (!ulong.TryParse(span1, out var num1)
|| !ulong.TryParse(span2, out var num2))
{
return 0;
}
else if (num1 < num2)
{
return -1;
}
else if (num1 > num2)
{
return 1;
}
}
else
{
int result = thisChunk.ToString().CompareTo(thatChunk.ToString());
int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
if (result != 0)
{
return result;
}
}
} while (pos1 < len1 && pos2 < len2);
}
return 0;
return len1 - len2;
}
/// <inheritdoc />
public int Compare(string x, string y)
{
return CompareValues(x, y);

View File

@ -60,6 +60,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -170,6 +172,10 @@ Global
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.Build.0 = Release|Any CPU
{462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -201,5 +207,6 @@ Global
{3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,44 @@
using System;
using System.Linq;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Sorting;
using Xunit;
namespace Jellyfin.Controller.Tests
{
public class AlphanumComparatorTests
{
private readonly Random _rng = new Random(42);
// InlineData is pre-sorted
[Theory]
[InlineData(null, "", "1", "9", "10", "a", "z")]
[InlineData("50F", "100F", "SR9", "SR100")]
[InlineData("image-1.jpg", "image-02.jpg", "image-4.jpg", "image-9.jpg", "image-10.jpg", "image-11.jpg", "image-22.jpg")]
[InlineData("Hard drive 2GB", "Hard drive 20GB")]
[InlineData("b", "e", "è", "ě", "f", "g", "k")]
[InlineData("123456789", "123456789a", "abc", "abcd")]
[InlineData("12345678912345678912345678913234567891", "123456789123456789123456789132345678912")]
[InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567891")]
[InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")]
[InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")]
[InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")]
public void AlphanumComparatorTest(params string?[] strings)
{
var copy = (string?[])strings.Clone();
if (strings.Length == 2)
{
var tmp = copy[0];
copy[0] = copy[1];
copy[1] = tmp;
}
else
{
copy.Shuffle(_rng);
}
Array.Sort(copy, new AlphanumComparator());
Assert.True(strings.SequenceEqual(copy));
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
</ItemGroup>
</Project>