mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-07-23 14:10:40 +02:00
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>
|
/// <param name="list">The list that should get shuffled.</param>
|
||||||
/// <typeparam name="T">The type.</typeparam>
|
/// <typeparam name="T">The type.</typeparam>
|
||||||
public static void Shuffle<T>(this IList<T> list)
|
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;
|
int n = list.Count;
|
||||||
while (n > 1)
|
while (n > 1)
|
||||||
{
|
{
|
||||||
n--;
|
n--;
|
||||||
int k = _rng.Next(n + 1);
|
int k = rng.Next(n + 1);
|
||||||
T value = list[k];
|
T value = list[k];
|
||||||
list[k] = list[n];
|
list[k] = list[n];
|
||||||
list[n] = value;
|
list[n] = value;
|
||||||
|
|
|
@ -1,94 +1,132 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
|
||||||
using MediaBrowser.Controller.Sorting;
|
|
||||||
|
|
||||||
namespace 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
else if (s1 == null)
|
||||||
int thisMarker = 0, thisNumericChunk = 0;
|
|
||||||
int thatMarker = 0, thatNumericChunk = 0;
|
|
||||||
|
|
||||||
while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
|
|
||||||
{
|
{
|
||||||
if (thisMarker >= s1.Length)
|
return -1;
|
||||||
{
|
}
|
||||||
return -1;
|
else if (s2 == null)
|
||||||
}
|
{
|
||||||
else if (thatMarker >= s2.Length)
|
return 1;
|
||||||
{
|
}
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
char thisCh = s1[thisMarker];
|
|
||||||
char thatCh = s2[thatMarker];
|
|
||||||
|
|
||||||
var thisChunk = new StringBuilder();
|
int len1 = s1.Length;
|
||||||
var thatChunk = new StringBuilder();
|
int len2 = s2.Length;
|
||||||
bool thisNumeric = char.IsDigit(thisCh), thatNumeric = char.IsDigit(thatCh);
|
|
||||||
|
|
||||||
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);
|
pos1++;
|
||||||
thisMarker++;
|
|
||||||
|
|
||||||
if (thisMarker < s1.Length)
|
|
||||||
{
|
|
||||||
thisCh = s1[thisMarker];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (thatMarker < s2.Length && char.IsDigit(thatCh) == thatNumeric)
|
while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2)
|
||||||
{
|
{
|
||||||
thatChunk.Append(thatCh);
|
pos2++;
|
||||||
thatMarker++;
|
|
||||||
|
|
||||||
if (thatMarker < s2.Length)
|
|
||||||
{
|
|
||||||
thatCh = s2[thatMarker];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var span1 = s1.AsSpan(start1, pos1 - start1);
|
||||||
|
var span2 = s2.AsSpan(start2, pos2 - start2);
|
||||||
|
|
||||||
// If both chunks contain numeric characters, sort them numerically
|
if (isNum1 && isNum2)
|
||||||
if (thisNumeric && thatNumeric)
|
|
||||||
{
|
{
|
||||||
if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)
|
// Trim leading zeros so we can compare the length
|
||||||
|| !int.TryParse(thatChunk.ToString(), out thatNumericChunk))
|
// of the strings to find the largest number
|
||||||
{
|
span1 = span1.TrimStart('0');
|
||||||
return 0;
|
span2 = span2.TrimStart('0');
|
||||||
}
|
var span1Len = span1.Length;
|
||||||
|
var span2Len = span2.Length;
|
||||||
if (thisNumericChunk < thatNumericChunk)
|
if (span1Len < span2Len)
|
||||||
{
|
{
|
||||||
return -1;
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int result = thisChunk.ToString().CompareTo(thatChunk.ToString());
|
int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} while (pos1 < len1 && pos2 < len2);
|
||||||
|
|
||||||
}
|
return len1 - len2;
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public int Compare(string x, string y)
|
public int Compare(string x, string y)
|
||||||
{
|
{
|
||||||
return CompareValues(x, y);
|
return CompareValues(x, y);
|
||||||
|
|
|
@ -60,6 +60,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests
|
||||||
EndProject
|
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}"
|
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
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -201,5 +207,6 @@ Global
|
||||||
{3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
|
{3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
|
||||||
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {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}
|
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
|
||||||
|
{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
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 a new issue