aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Helpers/SortingHelper.cs
blob: fe39521c7751a8939c2b82f43bfed9789adbeaaa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
namespace Enmarcha.Backend.Helpers;

public class SortingHelper
{
    /// <summary>
    /// 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.
    /// </summary>
    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);
    }

    /// <summary>
    /// 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.
    /// </summary>
    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);
    }

    /// <summary>
    /// 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)
    /// </summary>
    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());
    }
}