penguin/golifehk

@golifehk bot in Telegram

commit 7e327abbae5bd4b0188551469783b79565e27781

author斟酌 鵬兄 <tgckpg@gmail.com>
date2022-09-13T13:42:51Z
subjectInitial commit
commit 7e327abbae5bd4b0188551469783b79565e27781
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2022-09-13T13:42:51Z

    Initial commit
---
 .gitignore                      |   2 +
 businfo/mtr/busschedule.go      |  78 ++++++++++++++++++++++
 businfo/mtr/busschedule_test.go |  12 ++++
 businfo/mtr/busstops.go         | 141 ++++++++++++++++++++++++++++++++++++++++
 businfo/mtr/busstops_test.go    |  22 +++++++
 businfo/mtr/k66.json            |   0
 businfo/mtr/query.go            |  91 ++++++++++++++++++++++++++
 businfo/mtr/query_test.go       |  16 +++++
 go.mod                          |   3 +
 handlers/index.go               |  11 ++++
 main.go                         |  12 ++++
 utils/env.go                    |  12 ++++
 utils/shortcuts.go              |  45 +++++++++++++
 utils/shortcuts_test.go         |  29 +++++++++
 14 files changed, 474 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..343a25c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.csv
+*.json
diff --git a/businfo/mtr/busschedule.go b/businfo/mtr/busschedule.go
new file mode 100644
index 0000000..c796394
--- /dev/null
+++ b/businfo/mtr/busschedule.go
@@ -0,0 +1,78 @@
+package mtr
+
+import (
+    "fmt"
+    // "byte"
+    // "net/http"
+    "io"
+    "os"
+    "encoding/json"
+)
+
+type Location struct {
+    latitude float32
+    longitude float32
+}
+
+type Bus struct {
+    ETA int `json:"arrivalTimeInSecond,string"`
+    ETAText string `json:"arrivalTimeText"`
+    BusId string `json:"busId"`
+    BusLocation Location `json:"busLocation"`
+    ETD string `json:"departureTimeInSecond"`
+    ETDText string `json:"departureTimeText"`
+    Delayed string `json:"isDelayed"`
+    Scheduled string `json:"isScheduled"`
+    LineRef string `json:"lineRef"`
+}
+
+type BusStopSchedules struct {
+    Buses [] Bus `json:"bus"`
+    Suspended string `json:"isSuspended"`
+}
+
+type BusSchedule struct {
+    RefreshTime int `json:"appRefreshTimeInSecond,string"`
+    BusStops [] BusStopSchedules `json:"busStop"`
+}
+
+func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
+
+    /*
+    values := map[string]string { "language": lang , "routeName": routeName }
+
+    jsonValue, _ := json.Marshal(values)
+
+    resp, err := http.Post(
+        "https://rt.data.gov.hk/v1/transport/mtr/bus/getSchedule",
+        "application/json",
+        bytes.NewBuffer( jsonValue ),
+    )
+
+    if err != nil {
+        return nil, err
+    }
+
+    defer resp.Body.Close()
+
+    data, err := io.ReadAll( resp.Body )
+    /*/
+    f, err := os.Open( "abc.json" )
+    defer f.Close()
+
+    data, err := io.ReadAll( f )
+    //*/
+    if err != nil {
+        return nil, err
+    }
+
+    schedules := BusSchedule{}
+
+    err = json.Unmarshal( data, &schedules )
+    if err != nil {
+        return nil, err
+    }
+
+    fmt.Printf( "%+v", schedules )
+    return &schedules, nil
+}
diff --git a/businfo/mtr/busschedule_test.go b/businfo/mtr/busschedule_test.go
new file mode 100644
index 0000000..9f8ccf6
--- /dev/null
+++ b/businfo/mtr/busschedule_test.go
@@ -0,0 +1,12 @@
+package mtr
+
+import (
+	"testing"
+)
+
+func TestGetSchedule(t *testing.T) {
+    _, err := getSchedule( "en", "K66" )
+    if err != nil {
+        t.Error( err )
+    }
+}
diff --git a/businfo/mtr/busstops.go b/businfo/mtr/busstops.go
new file mode 100644
index 0000000..a1a05ad
--- /dev/null
+++ b/businfo/mtr/busstops.go
@@ -0,0 +1,141 @@
+package mtr
+
+import (
+    "encoding/csv"
+    "fmt"
+    "io"
+    "net/http"
+    "os"
+    "path/filepath"
+    "strconv"
+    "strings"
+
+    "github.com/tgckpg/golifehk/utils"
+)
+
+type BusStop struct {
+    RouteId string
+    Direction string
+    StationSeq string
+    StationId string
+    Latitude float64
+    Longtitude float64
+    Name_zhant string
+    Name_en string
+}
+
+var mBusStops *map[string]BusStop
+
+var DEF_CSV string = filepath.Join( utils.WORKDIR, "mtr_bus_stops.csv" )
+
+func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
+
+    reader := csv.NewReader( r )
+    entries, err := reader.ReadAll()
+    if err != nil {
+        return nil, err
+    }
+
+    busStops := map[string]BusStop{}
+    var headers []string
+    for i, line := range entries {
+        if i == 0 {
+            headers = line
+            line[0] = strings.TrimLeft( line[0], utils.BOM )
+            continue
+        }
+
+        var entry BusStop
+        for j, value := range line {
+            switch headers[j] {
+                case "ROUTE_ID":
+                    entry.RouteId = value
+                case "DIRECTION":
+                    entry.Direction = value
+                case "STATION_SEQNO":
+                    entry.StationSeq = value
+                case "STATION_ID":
+                    entry.StationId = value
+                case "STATION_LATITUDE":
+                    v, _ := strconv.ParseFloat( value, 64 )
+                    entry.Latitude = v
+                case "STATION_LONGITUDE":
+                    v, _ := strconv.ParseFloat( value, 64 )
+                    entry.Longtitude = v
+                case "STATION_NAME_CHI":
+                    entry.Name_zhant = value
+                case "STATION_NAME_ENG":
+                    entry.Name_en = value
+                default:
+                    return nil, fmt.Errorf( "Unknown header \"%s\"", headers[j] )
+            }
+        }
+
+        if _, t := busStops[ entry.StationId ]; t {
+            return nil, fmt.Errorf( "Duplicated entry %+v", entry )
+        }
+
+        busStops[ entry.StationId ] = entry
+    }
+    return &busStops, nil
+}
+
+func getBusStops() (*map[string]BusStop, error) {
+
+    if mBusStops != nil {
+        return mBusStops, nil
+    }
+
+    mBusStops, err := pullLocal()
+    if mBusStops != nil {
+        return mBusStops, err
+    }
+
+    mBusStops, err = pullRemote()
+    if mBusStops != nil {
+        return mBusStops, err
+    }
+
+    return nil, err
+}
+
+func pullRemote() (*map[string]BusStop, error) {
+
+    err := os.MkdirAll( filepath.Dir( DEF_CSV ), 0750 )
+    if err != nil {
+        return nil, err
+    }
+
+    resp, err := http.Get( "https://opendata.mtr.com.hk/data/mtr_bus_stops.csv" )
+    if err != nil {
+        return nil, err
+    }
+
+    defer resp.Body.Close()
+
+    f, err := os.OpenFile( DEF_CSV, os.O_CREATE | os.O_RDWR, 0644 )
+    if err != nil {
+        return nil, err
+    }
+
+    defer f.Close()
+
+    _, err = io.Copy( f, resp.Body )
+    if err != nil {
+        return nil, err
+    }
+
+    f.Seek( 0, 0 )
+    return readBusStopData( f )
+}
+
+func pullLocal() (*map[string]BusStop, error) {
+    f, err := os.Open( DEF_CSV )
+    if err != nil {
+        return nil, err
+    }
+
+    defer f.Close()
+
+    return readBusStopData( f )
+}
diff --git a/businfo/mtr/busstops_test.go b/businfo/mtr/busstops_test.go
new file mode 100644
index 0000000..aa81062
--- /dev/null
+++ b/businfo/mtr/busstops_test.go
@@ -0,0 +1,22 @@
+package mtr
+
+import (
+    "fmt"
+	"testing"
+)
+
+func TestAll(t *testing.T) {
+    /*
+    entries, err := pullRemote()
+    if err != nil {
+        t.Error( err )
+    }
+    fmt.Println( entries )
+    */
+
+    entries, err := pullLocal()
+    if err != nil {
+        t.Error( err )
+    }
+    fmt.Println( entries )
+}
diff --git a/businfo/mtr/k66.json b/businfo/mtr/k66.json
new file mode 100644
index 0000000..e69de29
diff --git a/businfo/mtr/query.go b/businfo/mtr/query.go
new file mode 100644
index 0000000..e0e61d6
--- /dev/null
+++ b/businfo/mtr/query.go
@@ -0,0 +1,91 @@
+package mtr
+
+import (
+    "fmt"
+    "strings"
+    "github.com/tgckpg/golifehk/utils"
+)
+
+type QueryObject struct {
+    Route string
+    BusStops []BusStop
+}
+
+type Term struct {
+    Value string
+    ProblyRoute bool
+}
+
+func test( entry BusStop, val string ) bool {
+    switch true {
+    case strings.Contains( entry.Name_zhant, val ):
+        fallthrough
+    case strings.Contains( 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 = ""
+    var searches = []string{}
+    matches := []BusStop{}
+
+    terms := []Term{}
+    for _, val := range strings.Split( line, " " ) {
+        val = strings.ToUpper( strings.Trim( val, " " ) )
+        term := Term{
+            Value: val,
+            ProblyRoute: strings.ContainsAny( val, utils.ROUTE_CHARS ),
+        }
+        terms = append( terms, term )
+    }
+
+    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 
+            }
+
+            searches = append( searches, term.Value )
+            if test( entry, term.Value ) {
+                matches = append( matches, entry )
+                break
+            }
+        }
+    }
+
+    if route != "" && 0 < len( searches ) {
+        matches_in := []BusStop{}
+        for _, entry := range matches {
+            if entry.RouteId != route {
+                continue
+            }
+
+            for _, val := range searches {
+                if test( entry, val ) {
+                    matches_in = append( matches_in, entry )
+                    break
+                }
+            }
+        }
+        matches = matches_in
+    }
+
+
+    return &QueryObject{ Route: route, BusStops: matches }, err
+}
diff --git a/businfo/mtr/query_test.go b/businfo/mtr/query_test.go
new file mode 100644
index 0000000..db0feb0
--- /dev/null
+++ b/businfo/mtr/query_test.go
@@ -0,0 +1,16 @@
+package mtr
+
+import (
+    "fmt"
+	"testing"
+)
+
+func TestQuerySchedule(t *testing.T) {
+    qo, err := parse( "K74 天瑞" )
+    if err != nil {
+        t.Error( err )
+    }
+
+    fmt.Println( qo )
+}
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..15f974b
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/tgckpg/golifehk
+
+go 1.19
diff --git a/handlers/index.go b/handlers/index.go
new file mode 100644
index 0000000..634ed0f
--- /dev/null
+++ b/handlers/index.go
@@ -0,0 +1,11 @@
+package handlers
+
+import (
+    "fmt"
+    "html"
+    "net/http"
+)
+
+func Index(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..7552782
--- /dev/null
+++ b/main.go
@@ -0,0 +1,12 @@
+package main   
+
+import (
+    "log"
+    "net/http"
+    "github.com/tgckpg/golifehk/handlers"
+)
+
+func main() {
+    http.HandleFunc( "/", handlers.Index )
+    log.Fatal(http.ListenAndServe(":8000", nil))
+}
diff --git a/utils/env.go b/utils/env.go
new file mode 100644
index 0000000..ec007e0
--- /dev/null
+++ b/utils/env.go
@@ -0,0 +1,12 @@
+package utils
+import (
+    "bytes"
+    "os"
+    "path/filepath"
+)
+
+var WORKDIR string = TryGetEnv( "GOLIFEHK_WORKDIR", filepath.Join( os.TempDir(), "golifehk" ) )
+
+var BOM string = bytes.NewBuffer([]byte{ 0xEF, 0xBB, 0xBF }).String()
+
+const ROUTE_CHARS string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
diff --git a/utils/shortcuts.go b/utils/shortcuts.go
new file mode 100644
index 0000000..b2164ea
--- /dev/null
+++ b/utils/shortcuts.go
@@ -0,0 +1,45 @@
+package utils
+
+import (
+    "os"
+    "strconv"
+)
+
+func TryGetEnv[T any]( name string, fallback T ) T {
+    v := os.Getenv( name )
+
+    if v != "" {
+        switch any( fallback ).(type) {
+        case uint64:
+            p, err := strconv.ParseUint( v, 10, 64 )
+            if err == nil {
+                return any( uint64( p ) ).(T)
+            }
+        case uint32:
+            p, err := strconv.ParseUint( v, 10, 32 )
+            if err == nil {
+                return any( uint32( p ) ).(T)
+            }
+        case int:
+            p, err := strconv.ParseInt( v, 10, 32 )
+            if err == nil {
+                return any( int( p ) ).(T)
+            }
+        case float64:
+            p, err := strconv.ParseFloat( v, 64 )
+            if err == nil {
+                return any( float64( p ) ).(T)
+            }
+        case float32:
+            p, err := strconv.ParseFloat( v, 32 )
+            if err == nil {
+                return any( float32( p ) ).(T)
+            }
+        default:
+            return any( v ).(T)
+        }
+
+    }
+
+    return fallback
+}
diff --git a/utils/shortcuts_test.go b/utils/shortcuts_test.go
new file mode 100644
index 0000000..be16740
--- /dev/null
+++ b/utils/shortcuts_test.go
@@ -0,0 +1,29 @@
+package utils
+
+import (
+    "os"
+    "reflect"
+	"testing"
+)
+
+func testType[T any]( t *testing.T, name string, fallback T ) T {
+    got := TryGetEnv( "ABC", fallback )
+    a := reflect.TypeOf( got ).Kind()
+    b := reflect.TypeOf( fallback ).Kind()
+    if a != b {
+        t.Errorf( "%s is not of type %s", any( got ), b )
+    }
+    return got
+}
+
+func TestAll(t *testing.T) {
+    testType( t, "ABC", "Test" )
+    testType( t, "ABC", 11 )
+
+    os.Setenv( "ABC", "22" )
+    got := testType( t, "ABC", 11 )
+    if got != 22 {
+        t.Errorf( "Expected 22, got %d", got )
+    }
+}
+