penguin/golifehk

@golifehk bot in Telegram

commit 36e077c9100f202c4d2027cb08868fa0d1da9fec

author斟酌 鵬兄 <tgckpg@gmail.com>
date2025-12-16T12:17:45Z
subjectProperly handle REFERENCE_ID for future changes
commit 36e077c9100f202c4d2027cb08868fa0d1da9fec
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2025-12-16T12:17:45Z

    Properly handle REFERENCE_ID for future changes
---
 datasources/mtr/bus/BusStop.go     |   3 +
 datasources/mtr/bus/QueryResult.go | 324 ++++++++++++++++++++++---------------
 datasources/mtr/bus/busstops.go    |  18 +--
 datasources/mtr/bus/query.go       | 113 +++++++------
 4 files changed, 260 insertions(+), 198 deletions(-)

diff --git a/datasources/mtr/bus/BusStop.go b/datasources/mtr/bus/BusStop.go
index 8e6ea31..c11fb52 100644
--- a/datasources/mtr/bus/BusStop.go
+++ b/datasources/mtr/bus/BusStop.go
@@ -18,6 +18,9 @@ type BusStop struct {
 	// RouteStops[ StationSeq ] = BusStop
 	RouteStops *map[int]*BusStop
 
+	// AltRoutes
+	AltRoutes *map[string]*BusStop
+
 	i18n.Generics
 }
 
diff --git a/datasources/mtr/bus/QueryResult.go b/datasources/mtr/bus/QueryResult.go
index ebe9859..9371715 100644
--- a/datasources/mtr/bus/QueryResult.go
+++ b/datasources/mtr/bus/QueryResult.go
@@ -1,148 +1,206 @@
 package bus
 
 import (
-    "fmt"
-    "sort"
-    "strings"
+	"fmt"
+	"sort"
+	"strconv"
+	"strings"
 
-    query "github.com/tgckpg/golifehk/query"
-    utils "github.com/tgckpg/golifehk/utils"
+	query "github.com/tgckpg/golifehk/query"
+	utils "github.com/tgckpg/golifehk/utils"
 )
 
 type QueryResult struct {
-    Schedules *map[*BusStop] *BusStopBuses
-    Lang string
-    Error error
-    Query *query.QueryObject
+	Schedules *map[*BusStop]*BusStopBuses
+	Lang      string
+	Error     error
+	Query     *query.QueryObject
 }
 
-func writeShortRoute( lang *string, sb *strings.Builder, b *BusStop ) {
-    if b.PrevStop() != nil {
-        utils.WriteMDv2Text( sb, (*b.PrevStop().Name)[ *lang ] )
-        sb.WriteString( " \\> " )
-    }
+func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) {
+	if b.PrevStop() != nil {
+		utils.WriteMDv2Text(sb, (*b.PrevStop().Name)[*lang])
+		sb.WriteString(" \\> ")
+	}
 
-    sb.WriteString( "*" )
-    utils.WriteMDv2Text( sb, (*b.Name)[ *lang ] )
-    sb.WriteString( "*" )
+	sb.WriteString("*")
+	utils.WriteMDv2Text(sb, (*b.Name)[*lang])
+	sb.WriteString("*")
 
-    if b.NextStop() != nil {
-        sb.WriteString( " \\> " )
-        utils.WriteMDv2Text( sb, (*b.NextStop().Name)[ *lang ] )
-    }
+	if b.NextStop() != nil {
+		sb.WriteString(" \\> ")
+		utils.WriteMDv2Text(sb, (*b.NextStop().Name)[*lang])
+	}
 
-    sb.WriteString( "\n" )
+	sb.WriteString("\n")
 }
 
-func ( this QueryResult ) Message() ( string, error ) {
-
-    if this.Error != nil {
-        return "", this.Error
-    }
-
-    sb := strings.Builder{}
-
-    if this.Schedules == nil {
-
-        q := *this.Query
-
-        if len( *q.Results ) == 0 {
-            terms := make( []string, len(*q.SearchTerms), len(*q.SearchTerms) )
-            for i, term := range *q.SearchTerms {
-                terms[i] = term.Org
-            }
-            return "", fmt.Errorf( "Not Found: \"%s\"", strings.Join( terms, "\", \"" ) )
-        }
-
-        if q.Key == "" {
-            sort.Sort( query.ByKey( *q.Results ) )
-            for _, entry := range *q.Results {
-                busStop := any( entry ).( *BusStop )
-                utils.WriteMDv2Text( &sb, busStop.RouteId )
-                sb.WriteString( " " )
-                writeShortRoute( &this.Lang, &sb, busStop )
-            }
-        } else if 1 == len( *q.SearchTerms ) {
-
-            // Route listing
-            st := map[string] *BusStop{}
-            keys := [] string{}
-
-            for _, entry := range *q.Results {
-
-                busStop := any( entry ).( *BusStop )
-                if _, ok := st[ busStop.Direction ]; ok {
-                    continue
-                }
-
-                st[ busStop.Direction ] = busStop
-                keys = append( keys, busStop.Direction )
-
-                for st[ busStop.Direction ].PrevStop() != nil {
-                    st[ busStop.Direction ] = st[ busStop.Direction ].PrevStop()
-                }
-            }
-
-            sort.Strings( keys )
-
-            for _, d := range keys {
-                b := st[ d ]
-                utils.WriteMDv2Text( &sb, q.Key )
-
-                if d == "O" {
-                    sb.WriteString( "↑" )
-                } else if d == "I" {
-                    sb.WriteString( "↓" )
-                } else {
-                    sb.WriteString( "\\?" )
-                }
-                sb.WriteString( "\n " )
-
-                for {
-                    utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] )
-                    b = b.NextStop()
-                    if b == nil {
-                        break
-                    }
-
-                    sb.WriteString( " \\> " )
-                }
-                sb.WriteString( "\n" )
-            }
-
-        } else {
-            return "", fmt.Errorf( "%s", "Unreachable condition occured!?" )
-        }
-    } else {
-        if 0 < len( *this.Schedules ) {
-
-            busStops := [] *BusStop{}
-
-            for b, _ := range *this.Schedules {
-                busStops = append( busStops, b )
-            }
-
-            sort.Sort( ByRoute( busStops ) )
-
-            for _, busStop := range busStops {
-                buses := (*this.Schedules)[ busStop ]
-
-                writeShortRoute( &this.Lang, &sb, busStop )
-                for _, bus := range buses.Buses {
-                    sb.WriteString( "  \\* " )
-                    if bus.ETAText == "" {
-                        utils.WriteMDv2Text( &sb, bus.ETDText )
-                    } else {
-                        utils.WriteMDv2Text( &sb, bus.ETAText )
-                    }
-                    sb.WriteString( "\n" )
-                }
-                sb.WriteString( "\n" )
-            }
-        } else {
-            utils.WriteMDv2Text( &sb, "Schedules are empty...perhaps Out of Service Time?" )
-        }
-    }
-
-    return sb.String(), nil
+func (this QueryResult) Message() (string, error) {
+
+	if this.Error != nil {
+		return "", this.Error
+	}
+
+	sb := strings.Builder{}
+
+	if this.Schedules == nil {
+
+		q := *this.Query
+
+		if len(*q.Results) == 0 {
+			terms := make([]string, len(*q.SearchTerms), len(*q.SearchTerms))
+			for i, term := range *q.SearchTerms {
+				terms[i] = term.Org
+			}
+			return "", fmt.Errorf("Not Found: \"%s\"", strings.Join(terms, "\", \""))
+		}
+
+		if q.Key == "" {
+			sort.Sort(query.ByKey(*q.Results))
+			for _, entry := range *q.Results {
+				busStop := any(entry).(*BusStop)
+				utils.WriteMDv2Text(&sb, busStop.RouteId)
+				sb.WriteString(" ")
+				writeShortRoute(&this.Lang, &sb, busStop)
+			}
+		} else if 1 == len(*q.SearchTerms) {
+
+			// Route listing
+			busLines := map[string]map[string]*BusStop{}
+
+			for _, entry := range *q.Results {
+
+				busStop := any(entry).(*BusStop)
+				busRoutes := busLines[busStop.ReferenceId]
+
+				if busRoutes == nil {
+					busRoutes = make(map[string]*BusStop)
+					busLines[busStop.ReferenceId] = busRoutes
+				}
+
+				if _, ok := busRoutes[busStop.Direction]; ok {
+					continue
+				}
+
+				busRoutes[busStop.Direction] = busStop
+
+				for busRoutes[busStop.Direction].PrevStop() != nil {
+					busRoutes[busStop.Direction] = busRoutes[busStop.Direction].PrevStop()
+				}
+			}
+
+			// Sort Bus Lines
+			lineKeys := make([]string, 0, len(busLines))
+			for k := range busLines {
+				lineKeys = append(lineKeys, k)
+			}
+
+			sort.Slice(lineKeys, func(i, j int) bool {
+				ai, an := splitKey(lineKeys[i])
+				bi, bn := splitKey(lineKeys[j])
+
+				// different base → normal string compare
+				if ai != bi {
+					return ai < bi
+				}
+
+				// same base: base key comes first
+				if an == -1 && bn != -1 {
+					return true
+				}
+				if an != -1 && bn == -1 {
+					return false
+				}
+
+				// both have suffix → numeric compare
+				return an < bn
+			})
+
+			for _, lineKey := range lineKeys {
+				busRoutes := busLines[lineKey]
+
+				// Sort route directions
+				dirKeys := make([]string, 0, len(busRoutes))
+				for k := range busRoutes {
+					dirKeys = append(dirKeys, k)
+				}
+
+				sort.Strings(dirKeys)
+
+				for _, d := range dirKeys {
+					b := busRoutes[d]
+
+					utils.WriteMDv2Text(&sb, lineKey)
+
+					if d == "O" {
+						sb.WriteString("↑")
+					} else if d == "I" {
+						sb.WriteString("↓")
+					} else {
+						sb.WriteString("\\?")
+					}
+					sb.WriteString("\n ")
+
+					for {
+						utils.WriteMDv2Text(&sb, (*b.Name)[this.Lang])
+						b = b.NextStop()
+						if b == nil {
+							break
+						}
+
+						sb.WriteString(" \\> ")
+					}
+					sb.WriteString("\n")
+				}
+			}
+
+		} else {
+			return "", fmt.Errorf("%s", "Unreachable condition occured!?")
+		}
+	} else {
+		if 0 < len(*this.Schedules) {
+
+			busStops := []*BusStop{}
+
+			for b, _ := range *this.Schedules {
+				busStops = append(busStops, b)
+			}
+
+			sort.Sort(ByRoute(busStops))
+
+			for _, busStop := range busStops {
+				buses := (*this.Schedules)[busStop]
+
+				writeShortRoute(&this.Lang, &sb, busStop)
+				for _, bus := range buses.Buses {
+					sb.WriteString("  \\* ")
+					if bus.ETAText == "" {
+						utils.WriteMDv2Text(&sb, bus.ETDText)
+					} else {
+						utils.WriteMDv2Text(&sb, bus.ETAText)
+					}
+					sb.WriteString("\n")
+				}
+				sb.WriteString("\n")
+			}
+		} else {
+			utils.WriteMDv2Text(&sb, "Schedules are empty...perhaps Out of Service Time?")
+		}
+	}
+
+	return sb.String(), nil
+}
+
+func splitKey(s string) (base string, num int) {
+	parts := strings.SplitN(s, "-", 2)
+	if len(parts) == 1 {
+		return s, -1 // base key, no suffix
+	}
+
+	n, err := strconv.Atoi(parts[1])
+	if err != nil {
+		return s, -1 // fallback: treat as base
+	}
+
+	return parts[0], n
 }
diff --git a/datasources/mtr/bus/busstops.go b/datasources/mtr/bus/busstops.go
index aa0410b..da47e13 100644
--- a/datasources/mtr/bus/busstops.go
+++ b/datasources/mtr/bus/busstops.go
@@ -4,7 +4,6 @@ import (
 	"encoding/csv"
 	"fmt"
 	"io"
-	"log"
 	"net/http"
 	"path/filepath"
 	"strconv"
@@ -63,21 +62,16 @@ func readBusStopData(r io.Reader) (*map[string]*BusStop, error) {
 			}
 		}
 
-		// Ignoring special route for now as getSchedule does not reflect
-		// which bus is a special route
-		if entry.ReferenceId != entry.RouteId {
-			log.Printf("Ignoring special Route: %s", entry.ReferenceId)
-			continue
-		}
+		entryId := fmt.Sprintf("%s:%s", entry.ReferenceId, entry.StationId)
 
-		if busStops[entry.StationId] != nil {
+		if busStops[entryId] != nil {
 			return nil, fmt.Errorf("Duplicated entry %+v", entry)
 		}
 
-		routeDir, hasKey := routeStops[entry.RouteId]
+		routeDir, hasKey := routeStops[entry.ReferenceId]
 		if !hasKey {
-			routeStops[entry.RouteId] = map[string]map[int]*BusStop{}
-			routeDir = routeStops[entry.RouteId]
+			routeStops[entry.ReferenceId] = map[string]map[int]*BusStop{}
+			routeDir = routeStops[entry.ReferenceId]
 		}
 
 		route, hasKey := routeDir[entry.Direction]
@@ -94,7 +88,7 @@ func readBusStopData(r io.Reader) (*map[string]*BusStop, error) {
 		entry.RouteStops = &route
 		entry.Reload()
 
-		busStops[entry.StationId] = &entry
+		busStops[entryId] = &entry
 	}
 	return &busStops, nil
 }
diff --git a/datasources/mtr/bus/query.go b/datasources/mtr/bus/query.go
index a337812..3e9ed04 100644
--- a/datasources/mtr/bus/query.go
+++ b/datasources/mtr/bus/query.go
@@ -1,59 +1,66 @@
 package bus
 
 import (
-    "strings"
-    query "github.com/tgckpg/golifehk/query"
+	"log"
+	"strings"
+
+	query "github.com/tgckpg/golifehk/query"
 )
 
-func Query( lang string, message string ) query.IQueryResult {
-
-    var qBusStops *query.QueryObject
-    var err error
-
-    qr := QueryResult{ Lang: lang }
-    busStops, err := getBusStops()
-    if err != nil {
-        qr.Error = err
-        goto qrReturn
-    }
-
-    qBusStops, err = query.Parse( strings.ToUpper( message ), busStops )
-    if err != nil {
-        qr.Error = err
-        goto qrReturn
-    }
-
-    qr.Query = qBusStops
-    if 0 < len( *qBusStops.Results ) && 1 < len( *qBusStops.SearchTerms ) {
-        schedules, err := getSchedule( lang, qBusStops.Key )
-        if err != nil {
-            qr.Error = err
-            goto qrReturn
-        }
-
-        if len( schedules.BusStops ) == 0 {
-            qr.Schedules = &map[*BusStop] *BusStopBuses{}
-            goto qrReturn
-        }
-
-        matches := map[*BusStop] *BusStopBuses{}
-        for _, entry := range *qBusStops.Results {
-            busStop := any( entry ).( *BusStop )
-
-            for _, busStopSch := range schedules.BusStops {
-                if busStopSch.BusStopId == busStop.StationId {
-                    matches[busStop] = &busStopSch
-                    break
-                }
-            }
-
-        }
-
-        qr.Schedules = &matches
-    }
-
-    qrReturn:
-    var iqr query.IQueryResult
-    iqr = &qr
-    return iqr
+func Query(lang string, message string) query.IQueryResult {
+
+	var qBusStops *query.QueryObject
+	var err error
+
+	qr := QueryResult{Lang: lang}
+	busStops, err := getBusStops()
+	if err != nil {
+		qr.Error = err
+		goto qrReturn
+	}
+
+	qBusStops, err = query.Parse(strings.ToUpper(message), busStops)
+	if err != nil {
+		qr.Error = err
+		goto qrReturn
+	}
+
+	qr.Query = qBusStops
+	if 0 < len(*qBusStops.Results) && 1 < len(*qBusStops.SearchTerms) {
+		schedules, err := getSchedule(lang, qBusStops.Key)
+		if err != nil {
+			qr.Error = err
+			goto qrReturn
+		}
+
+		if len(schedules.BusStops) == 0 {
+			qr.Schedules = &map[*BusStop]*BusStopBuses{}
+			goto qrReturn
+		}
+
+		matches := map[*BusStop]*BusStopBuses{}
+		for _, entry := range *qBusStops.Results {
+			busStop := any(entry).(*BusStop)
+
+			for _, busStopSch := range schedules.BusStops {
+				if busStopSch.BusStopId == busStop.StationId {
+					if busStop.RouteId != busStop.ReferenceId {
+						// There were no indicator for special routes from getSchedule API
+						log.Printf("Ignoring special route matches: %s", busStop.ReferenceId)
+						continue
+					}
+					matches[busStop] = &busStopSch
+					break
+				}
+			}
+
+		}
+
+		qr.Schedules = &matches
+	}
+
+qrReturn:
+	var iqr query.IQueryResult
+	iqr = &qr
+	return iqr
 }