Merge pull request #2513 from Bond-009/alpha
Improve alpha numeric sorting
This commit is contained in:
commit
e80c9bb8c6
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
44
tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
Normal file
44
tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
Loading…
Reference in New Issue
Block a user