diff --git a/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs b/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs index b4352000..da72a3da 100644 --- a/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs @@ -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 versions = await _browsingService.GetVersionsAsync(softwareId); + versions.Sort((a, b) => NaturalStringComparer.Instance.Compare(a.VersionString, b.VersionString)); foreach(SoftwareVersionDto version in versions) { diff --git a/Marechai.Data/NaturalStringComparer.cs b/Marechai.Data/NaturalStringComparer.cs new file mode 100644 index 00000000..e7c842c6 --- /dev/null +++ b/Marechai.Data/NaturalStringComparer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +namespace Marechai.Data; + +/// +/// 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". +/// +public sealed class NaturalStringComparer : IComparer +{ + 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); + } +} diff --git a/Marechai/Pages/Software/View.razor.cs b/Marechai/Pages/Software/View.razor.cs index f54f3a08..e04d1887 100644 --- a/Marechai/Pages/Software/View.razor.cs +++ b/Marechai/Pages/Software/View.razor.cs @@ -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);