commit 3376d9eb967fc1d9970b2e140d429bce97383128
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2022-09-16T20:33:47Z |
| subject | Added 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 )