penguin/golifehk

@golifehk bot in Telegram

commit 3376d9eb967fc1d9970b2e140d429bce97383128

author斟酌 鵬兄 <tgckpg@gmail.com>
date2022-09-16T20:33:47Z
subjectAdded kmb and refactored query.Parse
commit 3376d9eb967fc1d9970b2e140d429bce97383128
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2022-09-16T20:33:47Z

    Added kmb and refactored query.Parse
---
 datasources/kmb/BusStop.go         |  40 ++++++++
 datasources/kmb/QueryResult.go     |  47 +++++++++
 datasources/kmb/RouteStop.go       |  44 ++++++++
 datasources/kmb/query.go           |  32 ++++++
 datasources/kmb/query_test.go      |  10 ++
 datasources/kmb/routestops.go      | 143 ++++++++++++++++++++++++++
 datasources/kmb/routestops_test.go |  12 +++
 datasources/mtr/bus/BusStop.go     |  34 +++++--
 datasources/mtr/bus/QueryResult.go |  98 +++++++++++-------
 datasources/mtr/bus/busschedule.go |   8 +-
 datasources/mtr/bus/busstops.go    |  31 ++++--
 datasources/mtr/bus/query.go       | 201 +++++--------------------------------
 datasources/mtr/bus/query_test.go  |  27 ++++-
 i18n/Generics.go                   |  10 ++
 main.go                            |  55 +++++++++-
 query/IQueryResult.go              |   5 +
 query/Searchable.go                |  40 ++++++++
 query/query.go                     |  74 ++++++++++++++
 query/query_test.go                |  79 +++++++++++++++
 utils/env.go                       |   2 +-
 utils/shortcuts.go                 |  11 ++
 21 files changed, 762 insertions(+), 241 deletions(-)

diff --git a/datasources/kmb/BusStop.go b/datasources/kmb/BusStop.go
new file mode 100644
index 0000000..0019705
--- /dev/null
+++ b/datasources/kmb/BusStop.go
@@ -0,0 +1,40 @@
+package kmb
+
+import (
+    i18n "github.com/tgckpg/golifehk/i18n"
+)
+
+type BusStop struct {
+    BusStopId string `json:"stop"`
+    Latitude float64 `json:"lat,string"`
+    Longtitude float64 `json:"long,string"`
+    Name_en string `json:"name_en"`
+    Name_tc string `json:"name_tc"`
+    Name_sc string `json:"name_sc"`
+
+    // Routes[ Route ][ Direction ]
+    Routes *[] *RouteStop
+
+    i18n.Generics
+}
+
+type BusStops struct {
+    Type string `json:"type"`
+    Version string `json:"version"`
+    DateCreated string `json:"generated_timestamp"`
+    BusStops [] *BusStop `json:"data"`
+}
+
+func ( this *BusStop ) Reload() {
+    i18n_Name := map[string] string{}
+    i18n_Name["en"] = this.Name_en
+    i18n_Name["zh-Hant"] = this.Name_tc
+
+    searchData := [] *string{}
+    searchData = append( searchData, &this.Name_en )
+    searchData = append( searchData, &this.Name_tc )
+
+    this.Name = &i18n_Name
+    this.Key = &this.BusStopId
+    this.SearchData = &searchData
+}
diff --git a/datasources/kmb/QueryResult.go b/datasources/kmb/QueryResult.go
new file mode 100644
index 0000000..48ecce3
--- /dev/null
+++ b/datasources/kmb/QueryResult.go
@@ -0,0 +1,47 @@
+package kmb
+
+import (
+    "strings"
+
+    query "github.com/tgckpg/golifehk/query"
+    utils "github.com/tgckpg/golifehk/utils"
+)
+
+type QueryResult struct {
+    Lang string
+    Error error
+    Query *query.QueryObject
+}
+
+func ( this *QueryResult ) Message() ( string, error ) {
+
+    if this.Error != nil {
+        return "", this.Error
+    }
+
+    sb := strings.Builder{}
+
+    if 0 < len( *this.Query.Results ) {
+        // Print Stop Name, the print the list of routes
+        if this.Query.Key == "" {
+            for _, item := range *this.Query.Results {
+                var b *BusStop
+                b = any( item ).( *BusStop )
+
+                if b.Routes == nil {
+                    continue
+                }
+
+                utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] )
+                sb.WriteString( "\n  " )
+                for _, route := range *b.Routes {
+                    utils.WriteMDv2Text( &sb, route.RouteId )
+                    sb.WriteString( " " )
+                }
+                sb.WriteString( "\n" )
+            }
+        }
+    }
+
+    return sb.String(), nil
+}
diff --git a/datasources/kmb/RouteStop.go b/datasources/kmb/RouteStop.go
new file mode 100644
index 0000000..cde46a3
--- /dev/null
+++ b/datasources/kmb/RouteStop.go
@@ -0,0 +1,44 @@
+package kmb
+
+import (
+    query "github.com/tgckpg/golifehk/query"
+)
+
+type RouteStop struct {
+    BusStop *BusStop
+    RouteId string `json:"route"`
+    ServiceType string `json:"service_type"`
+    Direction string `json:"bound"`
+    StationSeq int `json:"seq,string"`
+    StationId string `json:"stop"`
+
+    RouteStops *map[int] *RouteStop
+    query.Searchable
+}
+
+type RouteStops struct {
+    Type string `json:"type"`
+    Version string `json:"version"`
+    DateCreated string `json:"generated_timestamp"`
+    RouteStops [] *RouteStop `json:"data"`
+}
+
+func ( routeStop RouteStop ) PrevStop() *RouteStop {
+    if v, hasKey := (*routeStop.RouteStops)[ routeStop.StationSeq - 1 ]; hasKey {
+        return v
+    }
+    return nil
+}
+
+func ( routeStop RouteStop ) NextStop() *RouteStop {
+    if v, hasKey := (*routeStop.RouteStops)[ routeStop.StationSeq + 1 ]; hasKey {
+        return v
+    }
+    return nil
+}
+
+type ByRouteId []RouteStop
+
+func (a ByRouteId) Len() int           { return len(a) }
+func (a ByRouteId) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ByRouteId) Less(i, j int) bool { return a[i].RouteId < a[j].RouteId }
diff --git a/datasources/kmb/query.go b/datasources/kmb/query.go
new file mode 100644
index 0000000..e11ac16
--- /dev/null
+++ b/datasources/kmb/query.go
@@ -0,0 +1,32 @@
+package kmb
+
+import (
+    "strings"
+    query "github.com/tgckpg/golifehk/query"
+)
+
+func Query( lang string, message string ) query.IQueryResult {
+
+    var qo *query.QueryObject
+    var err error
+
+    qr := QueryResult{ Lang: lang }
+    busStops, err := getBusStops()
+    if err != nil {
+        qr.Error = err
+        goto qrReturn
+    }
+
+    qo, err = query.Parse( strings.ToUpper( message ), busStops )
+    if err != nil {
+        qr.Error = err
+        goto qrReturn
+    }
+
+    qr.Query = qo
+
+    qrReturn:
+    var iqr query.IQueryResult
+    iqr = &qr
+    return iqr
+}
diff --git a/datasources/kmb/query_test.go b/datasources/kmb/query_test.go
new file mode 100644
index 0000000..08250b1
--- /dev/null
+++ b/datasources/kmb/query_test.go
@@ -0,0 +1,10 @@
+package kmb
+
+import (
+    "fmt"
+	"testing"
+)
+
+func TestQuerySchedule( t *testing.T ) {
+    fmt.Print( Query( "zh", "大欖" ).Message() )
+}
diff --git a/datasources/kmb/routestops.go b/datasources/kmb/routestops.go
new file mode 100644
index 0000000..4b85e4e
--- /dev/null
+++ b/datasources/kmb/routestops.go
@@ -0,0 +1,143 @@
+package kmb
+
+import (
+    "bytes"
+    "encoding/json"
+    "fmt"
+    "io"
+    "net/http"
+    "path/filepath"
+    // "strings"
+
+    query "github.com/tgckpg/golifehk/query"
+    "github.com/tgckpg/golifehk/utils"
+)
+
+var JSON_ROUTESTOPS string = filepath.Join( utils.WORKDIR, "kmb-routestops.json" )
+var JSON_BUSSTOPS string = filepath.Join( utils.WORKDIR, "kmb-busstops.json" )
+
+func readRouteStopsData( busStops *map[string] *BusStop, buff *bytes.Buffer ) error {
+
+    routeStopsData := RouteStops{}
+    err := json.Unmarshal( buff.Bytes(), &routeStopsData )
+    if err != nil {
+        return err
+    }
+
+    // routeStops[ Route ][ ServiceType ][ Direction ][ Seq ] = RouteStop
+    routeStops := map[string] *map[string] *map[ string ] *map[ int ] *RouteStop{}
+    for _, entry := range routeStopsData.RouteStops {
+
+        busStop := (*busStops)[ entry.StationId ]
+        if busStop == nil {
+            busStop = &BusStop {
+                BusStopId: entry.StationId,
+                Name_en: "???", Name_tc: "???", Name_sc: "???",
+            }
+            busStop.Reload()
+
+            (*busStops)[ entry.StationId ] = busStop
+        }
+
+        if busStop.Routes == nil {
+            busStopRoutes := [] *RouteStop{}
+            busStop.Routes = &busStopRoutes
+        }
+
+        (*busStop.Routes) = append( (*busStop.Routes), entry )
+        entry.BusStop = busStop
+
+        route := routeStops[ entry.RouteId ]
+        if route == nil {
+            route = &map[string] *map[ string ] *map[ int ] *RouteStop{} 
+            routeStops[ entry.RouteId ] = route
+        }
+
+        service := (*route)[ entry.ServiceType ]
+        if service == nil {
+            service = &map[ string ] *map[ int ] *RouteStop{}
+            (*route)[ entry.ServiceType ] = service
+        }
+
+        direction := (*service)[ entry.Direction ]
+        if direction == nil {
+            direction = &map[ int ] *RouteStop{}
+            (*service)[ entry.Direction ] = direction
+            entry.RouteStops = direction
+        }
+
+        seq := (*direction)[ entry.StationSeq ]
+        if seq == nil {
+            (*direction)[ entry.StationSeq ] = entry
+        }
+    }
+
+    return nil
+}
+
+func readBusStopsData( buff *bytes.Buffer ) ( *map[string]*BusStop, error ) {
+    busStopsData := BusStops{}
+    err := json.Unmarshal( buff.Bytes(), &busStopsData )
+    if err != nil {
+        return nil, err
+    }
+
+    busStopMap := map[string] *BusStop{}
+    for _, entry := range busStopsData.BusStops {
+
+        entry.Reload()
+
+        if _, ok := busStopMap[ entry.BusStopId ]; ok {
+            return nil, fmt.Errorf( "Duplicated BusStop: %s", entry.BusStopId )
+        }
+
+        busStopMap[ entry.BusStopId ] = entry
+    }
+    return &busStopMap, nil
+}
+
+func getBusStops() (*[] query.ISearchable, error) {
+
+    QUERY_FUNC := func() ( io.ReadCloser, error ) {
+        resp, err := http.Get( "https://data.etabus.gov.hk/v1/transport/kmb/stop" )
+        if err != nil {
+            return nil, err
+        }
+        return resp.Body, nil
+    }
+
+    buff, err := utils.CacheStream( JSON_BUSSTOPS, QUERY_FUNC, 7 * 24 * 3600 )
+    if err != nil {
+        return nil, err
+    }
+
+    busStopMap, err := readBusStopsData( buff )
+    if err != nil {
+        return nil, err
+    }
+
+    QUERY_FUNC = func() ( io.ReadCloser, error ) {
+        resp, err := http.Get( "https://data.etabus.gov.hk/v1/transport/kmb/route-stop" )
+        if err != nil {
+            return nil, err
+        }
+        return resp.Body, nil
+    }
+
+    buff, err = utils.CacheStream( JSON_ROUTESTOPS, QUERY_FUNC, 7 * 24 * 3600 )
+    if err != nil {
+        return nil, err
+    }
+
+    err = readRouteStopsData( busStopMap, buff )
+    if err != nil {
+        return nil, err
+    }
+
+    searchables := [] query.ISearchable{}
+    for _, busStop := range *busStopMap {
+        searchables = append( searchables, busStop )
+    }
+
+    return &searchables, nil
+}
diff --git a/datasources/kmb/routestops_test.go b/datasources/kmb/routestops_test.go
new file mode 100644
index 0000000..399ca2b
--- /dev/null
+++ b/datasources/kmb/routestops_test.go
@@ -0,0 +1,12 @@
+package kmb
+
+import (
+	"testing"
+)
+
+func TestRouteStops(t *testing.T) {
+    _, err := getBusStops()
+    if err != nil {
+        t.Error( err )
+    }
+}
diff --git a/datasources/mtr/bus/BusStop.go b/datasources/mtr/bus/BusStop.go
index ad7f0a6..20af3c9 100644
--- a/datasources/mtr/bus/BusStop.go
+++ b/datasources/mtr/bus/BusStop.go
@@ -1,5 +1,9 @@
 package bus
 
+import (
+    i18n "github.com/tgckpg/golifehk/i18n"
+)
+
 type BusStop struct {
     RouteId string
     Direction string
@@ -7,27 +11,39 @@ type BusStop struct {
     StationId string
     Latitude float64
     Longtitude float64
+    Name_zh string
+    Name_en string
 
-    Name *map[string] string
+    // RouteStops[ StationSeq ] = BusStop
     RouteStops *map[int] *BusStop
+
+    i18n.Generics
 }
 
-func ( busStop BusStop ) PrevStop() *BusStop {
-    if v, hasKey := (*busStop.RouteStops)[ busStop.StationSeq - 1 ]; hasKey {
+func ( this *BusStop ) PrevStop() *BusStop {
+    if v, hasKey := (*this.RouteStops)[ this.StationSeq - 1 ]; hasKey {
         return v
     }
     return nil
 }
 
-func ( busStop BusStop ) NextStop() *BusStop {
-    if v, hasKey := (*busStop.RouteStops)[ busStop.StationSeq + 1 ]; hasKey {
+func ( this *BusStop ) NextStop() *BusStop {
+    if v, hasKey := (*this.RouteStops)[ this.StationSeq + 1 ]; hasKey {
         return v
     }
     return nil
 }
 
-type ByRouteId []BusStop
+func ( this *BusStop ) Reload() {
+    i18n_Name := map[string] string{}
+    i18n_Name["en"] = this.Name_en
+    i18n_Name["zh-Hant"] = this.Name_zh
 
-func (a ByRouteId) Len() int           { return len(a) }
-func (a ByRouteId) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a ByRouteId) Less(i, j int) bool { return a[i].RouteId < a[j].RouteId }
+    searchData := [] *string{}
+    searchData = append( searchData, &this.Name_en )
+    searchData = append( searchData, &this.Name_zh )
+
+    this.Name = &i18n_Name
+    this.Key = &this.RouteId
+    this.SearchData = &searchData
+}
diff --git a/datasources/mtr/bus/QueryResult.go b/datasources/mtr/bus/QueryResult.go
index ba59b4c..dc7db64 100644
--- a/datasources/mtr/bus/QueryResult.go
+++ b/datasources/mtr/bus/QueryResult.go
@@ -4,68 +4,87 @@ import (
     "fmt"
     "sort"
     "strings"
+
+    query "github.com/tgckpg/golifehk/query"
+    utils "github.com/tgckpg/golifehk/utils"
 )
 
 type QueryResult struct {
-    Schedules *map[BusStop] *BusStopBuses
+    Schedules *map[*BusStop] *BusStopBuses
     Lang string
     Error error
-    Query *QueryObject
-}
-
-var MARKDOWN_ESC []string = []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
-func _escape( t string ) string {
-    for _, c := range MARKDOWN_ESC {
-        t = strings.Replace( t, c, "\\" + c, -1 )
-    }
-    return t
+    Query *query.QueryObject
 }
 
 func writeShortRoute( lang *string, sb *strings.Builder, b *BusStop ) {
     if b.PrevStop() != nil {
-        sb.WriteString( _escape( (*b.PrevStop().Name)[ *lang ] ) )
+        utils.WriteMDv2Text( sb, (*b.PrevStop().Name)[ *lang ] ) 
         sb.WriteString( " \\> " )
     }
 
     sb.WriteString( "*" )
-    sb.WriteString( _escape( (*b.Name)[ *lang ] ) )
+    utils.WriteMDv2Text( sb, (*b.Name)[ *lang ] ) 
     sb.WriteString( "*" )
 
     if b.NextStop() != nil {
         sb.WriteString( " \\> " )
-        sb.WriteString( _escape( (*b.NextStop().Name)[ *lang ] ) )
+        utils.WriteMDv2Text( sb, (*b.NextStop().Name)[ *lang ] ) 
     }
 
     sb.WriteString( "\n" )
 }
 
-func ( result QueryResult ) Message() string {
+func ( this QueryResult ) Message() ( string, error ) {
 
-    if result.Error != nil {
-        return fmt.Sprintf( "%s", result.Error )
+    if this.Error != nil {
+        return "", this.Error
     }
 
     sb := strings.Builder{}
 
-    if result.Schedules == nil {
+    if this.Schedules == nil {
 
-        query := *result.Query
-        if query.Route == "" {
-            sort.Sort( ByRouteId( *query.BusStops ) )
-            for _, busStop := range *query.BusStops {
-                sb.WriteString( _escape( busStop.RouteId ) )
+        q := *this.Query
+        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( &result.Lang, &sb, &busStop )
+                writeShortRoute( &this.Lang, &sb, busStop )
+            }
+        } else if 1 == len( *q.SearchTerms ) && 0 < len( *q.Results ) {
+
+            // 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()
+                }
             }
-        } else if query.BusStops == nil && query.RouteStarts != nil {
-            for d, b := range *query.RouteStarts {
-                sb.WriteString( query.Route )
+
+            sort.Strings( keys )
+
+            for _, d := range keys {
+                b := st[ d ]
+                sb.WriteString( q.Key )
                 sb.WriteString( "\\-" )
-                sb.WriteString( _escape( d ) )
+                utils.WriteMDv2Text( &sb, d ) 
                 sb.WriteString( "\n " )
 
                 for {
-                    sb.WriteString( _escape( (*b.Name)[ result.Lang ] ) )
+                    utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] ) 
                     b = b.NextStop()
                     if b == nil {
                         break
@@ -75,28 +94,35 @@ func ( result QueryResult ) Message() string {
                 }
                 sb.WriteString( "\n" )
             }
+
+        } else if 1 < len( *q.SearchTerms ) && 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, "\", \"" ) )
         } else {
-            sb.WriteString( _escape( "Unreachable condition occured!?" ) )
+            return "", fmt.Errorf( "%s", "Unreachable condition occured!?" )
         }
     } else {
-        if 0 < len(*result.Schedules) {
-            for busStop, buses := range *result.Schedules {
-                writeShortRoute( &result.Lang, &sb, &busStop )
+        if 0 < len( *this.Schedules ) {
+            for busStop, buses := range *this.Schedules {
+                writeShortRoute( &this.Lang, &sb, busStop )
                 for _, bus := range buses.Buses {
                     sb.WriteString( "  \\* " )
                     if bus.ETAText == "" {
-                        sb.WriteString( _escape( bus.ETDText ) )
+                        utils.WriteMDv2Text( &sb, bus.ETDText ) 
                     } else {
-                        sb.WriteString( _escape( bus.ETAText ) )
+                        utils.WriteMDv2Text( &sb, bus.ETAText ) 
                     }
                     sb.WriteString( "\n" )
                 }
                 sb.WriteString( "\n" )
             }
         } else {
-            sb.WriteString( _escape( "Schedules are empty...perhaps Out of Service Time?" ) )
+            utils.WriteMDv2Text( &sb, "Schedules are empty...perhaps Out of Service Time?" ) 
         }
     }
 
-    return sb.String()
+    return sb.String(), nil
 }
diff --git a/datasources/mtr/bus/busschedule.go b/datasources/mtr/bus/busschedule.go
index f70676d..76a6f95 100644
--- a/datasources/mtr/bus/busschedule.go
+++ b/datasources/mtr/bus/busschedule.go
@@ -45,9 +45,15 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
         "mtr_bsch" + "-" + lang + "-" + routeName + ".json",
     )
 
+    postLang := "en"
+    if lang == "zh-Hant" {
+        postLang = "zh"
+
+    }
+
     QUERY_FUNC := func() ( io.ReadCloser, error ) {
         // Query Remote
-        values := map[string]string { "language": lang , "routeName": routeName }
+        values := map[string]string { "language": postLang, "routeName": routeName }
         jsonValue, _ := json.Marshal(values)
         resp, err := http.Post(
             "https://rt.data.gov.hk/v1/transport/mtr/bus/getSchedule",
diff --git a/datasources/mtr/bus/busstops.go b/datasources/mtr/bus/busstops.go
index ee88fec..8b642ed 100644
--- a/datasources/mtr/bus/busstops.go
+++ b/datasources/mtr/bus/busstops.go
@@ -9,12 +9,13 @@ import (
     "strconv"
     "strings"
 
+    query "github.com/tgckpg/golifehk/query"
     "github.com/tgckpg/golifehk/utils"
 )
 
 var CSV_BUSSTOPS string = filepath.Join( utils.WORKDIR, "mtr_bus_stops.csv" )
 
-func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
+func readBusStopData( r io.Reader ) ( *map[string] *BusStop, error ) {
 
     reader := csv.NewReader( r )
     entries, err := reader.ReadAll()
@@ -22,7 +23,7 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
         return nil, err
     }
 
-    busStops := map[string]BusStop{}
+    busStops := map[string] *BusStop{}
     routeStops := map[string] map[string] map[int] *BusStop{}
 
     var headers []string
@@ -34,7 +35,6 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
         }
 
         var entry BusStop
-        i18n_Name := map[string] string{}
 
         for j, value := range line {
             switch headers[j] {
@@ -54,15 +54,15 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
                     v, _ := strconv.ParseFloat( value, 64 )
                     entry.Longtitude = v
                 case "STATION_NAME_CHI":
-                    i18n_Name["zh"] = value
+                    entry.Name_zh = value
                 case "STATION_NAME_ENG":
-                    i18n_Name["en"] = value
+                    entry.Name_en = value
                 default:
                     return nil, fmt.Errorf( "Unknown header \"%s\"", headers[j] )
             }
         }
 
-        if _, t := busStops[ entry.StationId ]; t {
+        if busStops[ entry.StationId ] != nil {
             return nil, fmt.Errorf( "Duplicated entry %+v", entry )
         }
 
@@ -83,15 +83,15 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
             route[ entry.StationSeq ] = &entry
         }
 
-        entry.Name = &i18n_Name
         entry.RouteStops = &route
+        entry.Reload()
 
-        busStops[ entry.StationId ] = entry
+        busStops[ entry.StationId ] = &entry
     }
     return &busStops, nil
 }
 
-func getBusStops() (*map[string]BusStop, error) {
+func getBusStops() (*[] query.ISearchable, error) {
 
     QUERY_FUNC := func() ( io.ReadCloser, error ) {
         resp, err := http.Get( "https://opendata.mtr.com.hk/data/mtr_bus_stops.csv" )
@@ -106,5 +106,16 @@ func getBusStops() (*map[string]BusStop, error) {
         return nil, err
     }
 
-    return readBusStopData( buff )
+    busStopMap, err := readBusStopData( buff )
+    if err != nil {
+        return nil, err
+    }
+
+    searchables := [] query.ISearchable{}
+    for _, busStop := range *busStopMap {
+        searchables = append( searchables, busStop )
+    }
+
+    return &searchables, nil
+
 }
diff --git a/datasources/mtr/bus/query.go b/datasources/mtr/bus/query.go
index 4741e65..a337812 100644
--- a/datasources/mtr/bus/query.go
+++ b/datasources/mtr/bus/query.go
@@ -1,76 +1,44 @@
 package bus
 
 import (
-    "errors"
-    "fmt"
     "strings"
-    "github.com/tgckpg/golifehk/utils"
+    query "github.com/tgckpg/golifehk/query"
 )
 
-type qTerm struct {
-    Org string
-    Value string
-    ProblyRoute bool
-}
-
-type QueryObject struct {
-    Route string
-    BusStops *[]BusStop
-    RouteStarts *map[string] *BusStop
-    SearchTerms *[] *qTerm
-}
+func Query( lang string, message string ) query.IQueryResult {
 
-func NotFound ( terms *[] *qTerm ) string {
-    sb := strings.Builder{}
-    sb.WriteString( "Not Found" )
-    if 0 < len( *terms ) {
-        sb.WriteString( ":" )
-        for _, term := range *terms {
-            sb.WriteString( " \"" )
-            sb.WriteString( term.Org )
-            sb.WriteString( "\"" )
-        }
-    }
-
-    return sb.String()
-}
-
-func Query( lang string, message string ) *QueryResult {
+    var qBusStops *query.QueryObject
+    var err error
 
     qr := QueryResult{ Lang: lang }
-
-    qo, err := parse( message )
+    busStops, err := getBusStops()
     if err != nil {
         qr.Error = err
-        return &qr
+        goto qrReturn
     }
 
-    qr.Query = qo
-
-    if qo.Route != "" {
-
-        if qo.BusStops == nil {
-            return &qr
-        }
-
-        if len( *qo.BusStops ) == 0 {
-            qr.Error = errors.New( NotFound( qo.SearchTerms ) )
-            return &qr
-        }
+    qBusStops, err = query.Parse( strings.ToUpper( message ), busStops )
+    if err != nil {
+        qr.Error = err
+        goto qrReturn
+    }
 
-        schedules, err := getSchedule( lang, qo.Route )
+    qr.Query = qBusStops
+    if 0 < len( *qBusStops.Results ) && 1 < len( *qBusStops.SearchTerms ) {
+        schedules, err := getSchedule( lang, qBusStops.Key )
         if err != nil {
             qr.Error = err
-            return &qr
+            goto qrReturn
         }
 
         if len( schedules.BusStops ) == 0 {
-            qr.Schedules = &map[BusStop] *BusStopBuses{}
-            return &qr
+            qr.Schedules = &map[*BusStop] *BusStopBuses{}
+            goto qrReturn
         }
 
-        matches := map[BusStop] *BusStopBuses{}
-        for _, busStop := range *qo.BusStops {
+        matches := map[*BusStop] *BusStopBuses{}
+        for _, entry := range *qBusStops.Results {
+            busStop := any( entry ).( *BusStop )
 
             for _, busStopSch := range schedules.BusStops {
                 if busStopSch.BusStopId == busStop.StationId {
@@ -82,131 +50,10 @@ func Query( lang string, message string ) *QueryResult {
         }
 
         qr.Schedules = &matches
-        return &qr
-    } else {
-        if len( *qo.BusStops ) == 0 {
-            return &QueryResult{ Error: errors.New( NotFound( qo.SearchTerms ) ) }
-        }
-
-        return &qr
-    }
-
-}
-
-func test( entry BusStop, val string ) bool {
-    switch true {
-    case strings.Contains( strings.ToUpper( (*entry.Name)["zh"] ), val ):
-        fallthrough
-    case strings.Contains( strings.ToUpper( (*entry.Name)["en"] ), val ):
-        return true
-    }
-    return false
-}
-
-func parse( line string ) ( *QueryObject, error ) {
-    busStops, err := getBusStops()
-
-    if err != nil {
-        return nil, err
-    }
-
-    var route string = ""
-    matches := []BusStop{}
-
-    // Sanitize and assume properties for each of the keywords
-    terms := [] *qTerm{}
-    for _, val := range strings.Split( line, " " ) {
-
-        if val == "" {
-            continue
-        }
-
-        term := qTerm{
-            Org: val,
-            Value: strings.ToUpper( strings.Trim( val, " " ) ),
-            ProblyRoute: strings.ContainsAny( val, utils.ROUTE_CHARS ),
-        }
-        terms = append( terms, &term )
-    }
-
-    // Search for route name first, otherwise search in other props
-    for _, entry := range *busStops {
-
-        // Search for RouteId
-        for _, term := range terms {
-
-            if term.ProblyRoute && term.Value == entry.RouteId {
-                if route != "" && route != term.Value {
-                    return nil, fmt.Errorf( "Cannot %s & %s", route, term.Value )
-                }
-                matches = append( matches, entry )
-                route = entry.RouteId
-                break 
-            }
-
-            if test( entry, term.Value ) {
-                matches = append( matches, entry )
-                break
-            }
-        }
-    }
-
-    searchTerms := [] *qTerm{}
-    matches_in := []BusStop{}
-
-    for _, term := range terms {
-        if term.ProblyRoute {
-            continue
-        }
-        searchTerms = append( searchTerms, term )
-    }
-
-    if 0 < len( searchTerms ) {
-        // If route found, filter out all other route
-        // then search the terms within that route
-        for _, entry := range matches {
-            if route != "" && entry.RouteId != route {
-                continue
-            }
-
-            for _, term := range searchTerms {
-                if test( entry, term.Value ) {
-                    matches_in = append( matches_in, entry )
-                    break
-                }
-            }
-        }
-
-        matches = matches_in
-
-        return &QueryObject{
-            Route: route,
-            BusStops: &matches,
-            SearchTerms: &searchTerms,
-        }, err
-
-    } else if route != "" {
-        // Route listing
-        st := map[string] *BusStop{}
-
-        for _, entry := range *busStops {
-            if entry.RouteId != route {
-                continue
-            }
-
-            if _, ok := st[ entry.Direction ]; ok {
-                continue
-            }
-
-            st[ entry.Direction ] = &entry
-
-            for st[ entry.Direction ].PrevStop() != nil {
-                st[ entry.Direction ] = st[ entry.Direction ].PrevStop()
-            }
-        }
-
-        return &QueryObject{ Route: route, RouteStarts: &st, BusStops: nil }, nil
     }
 
-    return nil, fmt.Errorf( "Cannot parse: %s", line )
+    qrReturn:
+    var iqr query.IQueryResult
+    iqr = &qr
+    return iqr
 }
diff --git a/datasources/mtr/bus/query_test.go b/datasources/mtr/bus/query_test.go
index b1decb2..cb93d9b 100644
--- a/datasources/mtr/bus/query_test.go
+++ b/datasources/mtr/bus/query_test.go
@@ -2,11 +2,32 @@ package bus
 
 import (
     "fmt"
+    "strings"
 	"testing"
 )
 
-func TestQuerySchedule( t *testing.T ) {
-    qo := Query( "zh", "水" )
+func TestQuery( t *testing.T ) {
+    qo := Query( "zh-Hant", "K73" )
+    mesg, err := qo.Message()
+    if err != nil {
+        t.Errorf( "Unexpected Error: %s", err )
+    }
 
-    fmt.Print( qo.Message() )
+    if !strings.Contains( mesg, "K73\\-O" ) {
+        t.Errorf( "Expected Route Listing, got \"%s\" instead", mesg )
+    }
+
+    qo = Query( "zh-Hant", "K76 池" )
+    mesg, err = qo.Message()
+    if err == nil {
+        t.Errorf( "Expecting error, got \"%s\" instead", mesg )
+    }
+
+    qo = Query( "zh-Hant", "K73 池" )
+    mesg, err = qo.Message()
+    if err != nil {
+        t.Errorf( "Unexpected Error: %s", err )
+    }
+
+    fmt.Println( mesg )
 }
diff --git a/i18n/Generics.go b/i18n/Generics.go
new file mode 100644
index 0000000..ab9deb3
--- /dev/null
+++ b/i18n/Generics.go
@@ -0,0 +1,10 @@
+package i18n
+
+import (
+    query "github.com/tgckpg/golifehk/query"
+)
+
+type Generics struct {
+    Name *map[string] string
+    query.Searchable
+}
diff --git a/main.go b/main.go
index eb32d7a..58f9d1b 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,11 @@
 package main   
 
 import (
+    "fmt"
     "log"
     "os"
     mtrbus "github.com/tgckpg/golifehk/datasources/mtr/bus"
+    kmb "github.com/tgckpg/golifehk/datasources/kmb"
     tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 )
 
@@ -29,13 +31,58 @@ func main() {
 
         log.Printf( "[%s] %s", update.Message.From.UserName, update.Message.Text )
 
-        result := mtrbus.Query( "zh", update.Message.Text )
+        mesg, err := mtrbus.Query( "zh-Hant", update.Message.Text ).Message()
+
+        if update.Message.Chat.IsGroup() {
+            if err == nil {
+                msg := tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
+                msg.ParseMode = "MarkdownV2"
+                msg.ReplyToMessageID = update.Message.MessageID
+
+                bot.Send( msg )
+            }
+
+        } else {
+
+            var msg tgbotapi.MessageConfig
+            if err == nil {
+                msg = tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
+                msg.ParseMode = "MarkdownV2"
+            } else {
+                msg = tgbotapi.NewMessage( update.Message.Chat.ID, fmt.Sprintf( "%s", err ) )
+            }
 
-        if result.Error == nil || !update.Message.Chat.IsGroup() {
-            msg := tgbotapi.NewMessage( update.Message.Chat.ID, result.Message() )
-            msg.ParseMode = "MarkdownV2"
             msg.ReplyToMessageID = update.Message.MessageID
+            bot.Send( msg )
+        }
 
+        mesg, err2 := kmb.Query( "zh-Hant", update.Message.Text ).Message()
+
+        if err != nil && err2 != nil {
+            continue
+        }
+
+        err = err2
+        if update.Message.Chat.IsGroup() {
+            if err == nil {
+                msg := tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
+                msg.ParseMode = "MarkdownV2"
+                msg.ReplyToMessageID = update.Message.MessageID
+
+                bot.Send( msg )
+            }
+
+        } else {
+
+            var msg tgbotapi.MessageConfig
+            if err == nil {
+                msg = tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
+                msg.ParseMode = "MarkdownV2"
+            } else {
+                msg = tgbotapi.NewMessage( update.Message.Chat.ID, fmt.Sprintf( "%s", err ) )
+            }
+
+            msg.ReplyToMessageID = update.Message.MessageID
             bot.Send( msg )
         }
     }
diff --git a/query/IQueryResult.go b/query/IQueryResult.go
new file mode 100644
index 0000000..1b30c8e
--- /dev/null
+++ b/query/IQueryResult.go
@@ -0,0 +1,5 @@
+package query
+
+type IQueryResult interface {
+    Message() ( string, error )
+}
diff --git a/query/Searchable.go b/query/Searchable.go
new file mode 100644
index 0000000..6706ba8
--- /dev/null
+++ b/query/Searchable.go
@@ -0,0 +1,40 @@
+package query
+
+import (
+    "strings"
+)
+
+type ISearchable interface {
+    Test( string ) bool
+    GetKey() *string
+}
+
+type Searchable struct {
+    Key *string
+    SearchData *[] *string
+}
+
+func ( this *Searchable ) Test( val string ) bool {
+
+    data := this.SearchData
+    if data == nil {
+        return false
+    }
+
+    for _, v := range *data {
+        if strings.Contains( *v, val ) {
+            return true
+        }
+    }
+    return false
+}
+
+func ( this *Searchable ) GetKey() *string {
+    return this.Key
+}
+
+type ByKey [] ISearchable
+
+func (a ByKey) Len() int           { return len(a) }
+func (a ByKey) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ByKey) Less(i, j int) bool { return *a[i].GetKey() < *a[j].GetKey() }
diff --git a/query/query.go b/query/query.go
new file mode 100644
index 0000000..cdfa05d
--- /dev/null
+++ b/query/query.go
@@ -0,0 +1,74 @@
+package query
+
+import (
+    "fmt"
+    "strings"
+)
+
+type QTerm struct {
+    Org string
+    Value string
+    IsKey bool
+}
+
+type QueryObject struct {
+    Key string
+    SearchTerms *[] *QTerm
+    Results *[] ISearchable
+}
+
+func Parse( line string, entries *[] ISearchable ) ( *QueryObject, error ) {
+
+    var Key string = ""
+
+    // Sanitize and assume properties for each of the keywords
+    terms := [] *QTerm{}
+    for _, val := range strings.Split( line, " " ) {
+
+        if val == "" {
+            continue
+        }
+
+        term := QTerm{
+            Org: val,
+            Value: strings.Trim( val, " " ),
+        }
+        terms = append( terms, &term )
+    }
+
+    lookForKey:
+    for _, term := range terms {
+        for _, entry := range *entries {
+            if term.Value == *entry.GetKey() {
+                Key = term.Value
+                term.IsKey = true
+                break lookForKey
+            }
+        }
+    }
+
+    matches := [] ISearchable{}
+
+    if Key != "" && len( terms ) == 1 {
+        for _, entry := range *entries {
+            if Key == *entry.GetKey() {
+                matches = append( matches, entry )
+            }
+        }
+        return &QueryObject{ Key: Key, Results: &matches, SearchTerms: &terms }, nil
+    } else if 0 < len( terms ) {
+        for _, entry := range *entries {
+            for _, term := range terms {
+                if term.IsKey {
+                    continue
+                }
+                if ( Key == "" || Key == *entry.GetKey() ) && entry.Test( term.Value ) {
+                    matches = append( matches, entry )
+                }
+            }
+        }
+        return &QueryObject{ Key: Key, Results: &matches, SearchTerms: &terms }, nil
+    }
+
+    return nil, fmt.Errorf( "Cannot parse: %s", line )
+}
diff --git a/query/query_test.go b/query/query_test.go
new file mode 100644
index 0000000..56c19be
--- /dev/null
+++ b/query/query_test.go
@@ -0,0 +1,79 @@
+package query
+
+import (
+	"testing"
+)
+
+type TestItem struct {
+    Searchable
+}
+
+func TestQuery( t *testing.T ) {
+    testItems := [] ISearchable{}
+
+    s1234 := "1234"
+    sApple := "Apple"
+    s0000 := "0000"
+
+    sDat0 := "Dat0"
+    data0 := [] *string{}
+    data0 = append( data0, &s1234 )
+    data0 = append( data0, &sApple )
+    t0 := TestItem{}
+    t0.Key = &sDat0
+    t0.SearchData = &data0
+    testItems = append( testItems, &t0 )
+
+    sDat1 := "Dat1"
+    data1 := [] *string{}
+    data1 = append( data1, &s0000 )
+    data1 = append( data1, &sApple )
+    t1 := TestItem{}
+    t1.Key = &sDat1
+    t1.SearchData = &data1
+    testItems = append( testItems, &t1 ) 
+
+    sDat2 := "Dat2"
+    data2 := [] *string{}
+    data2 = append( data2, &sDat0 )
+    t2 := TestItem{}
+    t2.Key = &sDat2
+    t2.SearchData = &data2
+    testItems = append( testItems, &t2 ) 
+
+    qo, err := Parse( "Apple", &testItems )
+    if err != nil {
+        t.Error( err )
+    }
+
+    if len(*qo.Results) != 2 {
+        t.Error( "Expected 2 results when searching for \"Apple\"" )
+    }
+
+    qo, err = Parse( "0000", &testItems )
+    if err != nil {
+        t.Error( err )
+    }
+
+    if len(*qo.Results) != 1 {
+        t.Error( "Expected 1 results when searching for \"0000\"" )
+    }
+
+    qo, err = Parse( "Dat0", &testItems )
+    if err != nil {
+        t.Error( err )
+    }
+
+    if len(*qo.Results) != 1 {
+        t.Error( "Expected 1 results when searching for \"Dat0\"" )
+    }
+
+    qo, err = Parse( "Dat2 Dat0", &testItems )
+    if err != nil {
+        t.Error( err )
+    }
+
+    if len(*qo.Results) != 1 {
+        t.Error( "Expected 1 results when searching for \"Dat2 Dat0\"" )
+    }
+}
diff --git a/utils/env.go b/utils/env.go
index ec007e0..85aa0c0 100644
--- a/utils/env.go
+++ b/utils/env.go
@@ -9,4 +9,4 @@ var WORKDIR string = TryGetEnv( "GOLIFEHK_WORKDIR", filepath.Join( os.TempDir(),
 
 var BOM string = bytes.NewBuffer([]byte{ 0xEF, 0xBB, 0xBF }).String()
 
-const ROUTE_CHARS string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
+const KEY_CHARS string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
diff --git a/utils/shortcuts.go b/utils/shortcuts.go
index b2164ea..23995f4 100644
--- a/utils/shortcuts.go
+++ b/utils/shortcuts.go
@@ -3,8 +3,19 @@ package utils
 import (
     "os"
     "strconv"
+    "strings"
 )
 
+var MARKDOWN_ESC []string = []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
+
+func WriteMDv2Text( sb *strings.Builder, t string ) {
+    for _, c := range MARKDOWN_ESC {
+        t = strings.Replace( t, c, "\\" + c, -1 )
+    }
+
+    sb.WriteString( t )
+}
+
 func TryGetEnv[T any]( name string, fallback T ) T {
     v := os.Getenv( name )