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" or "lugo")
{
int group = feed switch
{
"vitrasa" => GetVitrasaRouteGroup(shortName),
"tussa" => GetTussaRouteGroup(shortName),
"lugo" => GetLugoRouteGroup(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 GetLugoRouteGroup(string shortName)
{
if (char.IsLetter(shortName[0]))
{
return 50000;
}
// Sort something like 2,6,1.1,1.2,1.4,3.1,5.1,PAZO1,PAZO2,CDL
// 1.1, 1.2, 1.4, 2, 3.1, 5.1, 6, then PAZO1, PAZO2, then CDL
if (shortName.Contains('.'))
{
var parts = shortName.Split('.', 2);
if (int.TryParse(parts[0], out int main) && int.TryParse(parts[1], out int sub))
{
return main * 100 + sub; // 1.1 -> 101, 1.2 -> 102, etc.
}
}
string numericPart = string.Empty;
foreach (char c in shortName)
{
if (char.IsDigit(c))
{
numericPart += c;
}
else
{
break;
}
}
if (int.TryParse(numericPart, out int leadingNumber))
{
return leadingNumber * 100 + 50; // Sort by leading number if present, with a large multiplier to come after pure numbers
}
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());
}
}