namespace Enmarcha.Backend.Helpers;
public class SortingHelper
{
///
/// Generic route short name comparison. Non-numeric names sort first, then numeric by value,
/// then alphabetical tiebreak. Used for per-stop sorting where route IDs may not be available.
///
public static int SortRouteShortNames(string? a, string? b)
{
if (a == null && b == null) return 0;
if (a == null) return 1;
if (b == null) return -1;
var aDigits = new string(a.Where(char.IsDigit).ToArray());
var bDigits = new string(b.Where(char.IsDigit).ToArray());
bool aHasDigits = int.TryParse(aDigits, out int aNumber);
bool bHasDigits = int.TryParse(bDigits, out int bNumber);
if (aHasDigits != bHasDigits)
{
// Non-numeric routes (like "A" or "-") go to the beginning
return aHasDigits ? 1 : -1;
}
if (aHasDigits && bHasDigits)
{
if (aNumber != bNumber)
{
return aNumber.CompareTo(bNumber);
}
}
// If both are non-numeric, or numeric parts are equal, use alphabetical
return string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
}
///
/// Feed-aware route sort key. For Vitrasa, applies custom group ordering:
/// Circular (C*) → Regular numbered → Hospital (H*) → Others (N*, PSA*, U*, specials).
/// For other feeds, uses generic numeric-then-alphabetic ordering.
///
public static (int Group, string Prefix, int Number, string Name) GetRouteSortKey(
string? shortName, string? routeId)
{
if (string.IsNullOrEmpty(shortName))
return (99, "", int.MaxValue, shortName ?? "");
var feed = routeId?.Split(':')[0];
if (feed is "vitrasa" or "tussa")
{
int group = feed switch
{
"vitrasa" => GetVitrasaRouteGroup(shortName),
"tussa" => GetTussaRouteGroup(shortName),
_ => throw new ArgumentOutOfRangeException()
};
// For "Others" group, sub-sort by alphabetic prefix to keep N*, PSA*, U* etc. grouped
string prefix = group == 3 ? ExtractAlphaPrefix(shortName) : "";
int number = ExtractNumber(shortName);
// For routes with no number in short name (like "A"), use the GTFS route ID number
if (number == int.MaxValue && routeId != null)
{
var idPart = routeId.Split(':').Last();
if (int.TryParse(idPart, out int idNumber))
number = idNumber;
}
return (group, prefix, number, shortName);
}
// Generic: non-numeric names first, then by number, then alphabetical
int genericNumber = ExtractNumber(shortName);
bool hasDigits = genericNumber != int.MaxValue;
return (hasDigits ? 1 : 0, "", genericNumber, shortName);
}
///
/// Vitrasa route groups:
/// 0 = Circular (C1, C3d, C3i)
/// 1 = Regular numbered routes (4A, 6, 10, A, etc.)
/// 2 = Hospital (H, H1, H2, H3)
/// 3 = Others (N*, PSA*, U*, LZD, PTL)
///
private static int GetVitrasaRouteGroup(string shortName)
{
// Circular: "C" followed by a digit
if (shortName.Length > 1 && shortName[0] == 'C' && char.IsDigit(shortName[1]))
return 0;
// Hospital: starts with "H"
if (shortName[0] == 'H')
return 2;
// Night: "N" followed by a digit
if (shortName[0] == 'N' && shortName.Length > 1 && char.IsDigit(shortName[1]))
return 3;
// PSA shuttle lines
if (shortName.StartsWith("PSA", StringComparison.OrdinalIgnoreCase))
return 3;
// University: "U" followed by a digit
if (shortName[0] == 'U' && shortName.Length > 1 && char.IsDigit(shortName[1]))
return 3;
// Multi-letter codes with no digits (LZD, PTL)
if (shortName.Length >= 2 && shortName.All(char.IsLetter))
return 3;
// Everything else is regular (numbered routes like 4A, 6, 10, single letters like A)
return 1;
}
private static int GetTussaRouteGroup(string shortName)
{
if (shortName[0] == 'C')
{
return 1;
}
return 0;
}
private static int ExtractNumber(string name)
{
var digits = new string(name.Where(char.IsDigit).ToArray());
return int.TryParse(digits, out int number) ? number : int.MaxValue;
}
private static string ExtractAlphaPrefix(string name)
{
return new string(name.TakeWhile(char.IsLetter).ToArray());
}
}