Implement natural string sorting for software versions

This commit is contained in:
2026-04-26 22:00:10 +01:00
parent 5253c7baf9
commit dfd846eba4
3 changed files with 79 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.ApiClient.Models;
using Marechai.App.Navigation;
using Marechai.Data;
using Marechai.App.Presentation.Views;
using Marechai.App.Services;
using Marechai.App.Services.Caching;
@@ -182,6 +183,7 @@ public partial class SoftwareViewViewModel : ObservableObject, IRegionAware
// Load versions (without nested releases)
List<SoftwareVersionDto> versions = await _browsingService.GetVersionsAsync(softwareId);
versions.Sort((a, b) => NaturalStringComparer.Instance.Compare(a.VersionString, b.VersionString));
foreach(SoftwareVersionDto version in versions)
{

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
namespace Marechai.Data;
/// <summary>
/// Compares strings using natural sort order, where numeric segments are compared by value rather than
/// lexicographically. For example, "4.00.96" sorts before "4.00.111".
/// </summary>
public sealed class NaturalStringComparer : IComparer<string>
{
public static readonly NaturalStringComparer Instance = new();
public int Compare(string x, string y)
{
if(ReferenceEquals(x, y)) return 0;
if(x is null) return -1;
if(y is null) return 1;
int ix = 0, iy = 0;
while(ix < x.Length && iy < y.Length)
{
bool xIsDigit = char.IsDigit(x[ix]);
bool yIsDigit = char.IsDigit(y[iy]);
if(xIsDigit && yIsDigit)
{
// Skip leading zeros and compare numeric segments by value
int xStart = ix;
int yStart = iy;
while(ix < x.Length && x[ix] == '0') ix++;
while(iy < y.Length && y[iy] == '0') iy++;
int xNumStart = ix;
int yNumStart = iy;
while(ix < x.Length && char.IsDigit(x[ix])) ix++;
while(iy < y.Length && char.IsDigit(y[iy])) iy++;
int xLen = ix - xNumStart;
int yLen = iy - yNumStart;
// Longer numeric segment is larger
if(xLen != yLen) return xLen.CompareTo(yLen);
// Same length: compare digit by digit
for(int i = 0; i < xLen; i++)
{
int cmp = x[xNumStart + i].CompareTo(y[yNumStart + i]);
if(cmp != 0) return cmp;
}
// Values equal — more leading zeros means smaller (e.g., "007" < "7" is debatable, keep stable)
int xLeadingZeros = xNumStart - xStart;
int yLeadingZeros = yNumStart - yStart;
if(xLeadingZeros != yLeadingZeros) return xLeadingZeros.CompareTo(yLeadingZeros);
}
else
{
int cmp = char.ToUpperInvariant(x[ix]).CompareTo(char.ToUpperInvariant(y[iy]));
if(cmp != 0) return cmp;
ix++;
iy++;
}
}
return x.Length.CompareTo(y.Length);
}
}

View File

@@ -28,6 +28,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Marechai.ApiClient.Models;
using Marechai.Data;
using Microsoft.AspNetCore.Components;
namespace Marechai.Pages.Software;
@@ -80,6 +81,7 @@ public partial class View
_companies = await Service.GetCompaniesAsync(Id);
_versions = await Service.GetVersionsAsync(Id);
_versions.Sort((a, b) => NaturalStringComparer.Instance.Compare(a.VersionString, b.VersionString));
// Load all non-compilation releases for this software (flat list)
_releases = await Service.GetReleasesBySoftwareAsync(Id);