Skip to content

Commit

Permalink
Merge branch 'feature/query' into develop
Browse files Browse the repository at this point in the history
Implemented query methods:
1. By County
2. By Town
See #2.
  • Loading branch information
tony84727 committed Oct 24, 2017
2 parents 6ca2ff6 + 218fac7 commit 580baee
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 313 deletions.
125 changes: 97 additions & 28 deletions cwbDataSource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package twweather

import (
"bytes"
"encoding/xml"
"fmt"
"log"
"net/http"
"strconv"
"time"
)

const ApiUrl = "http://opendata.cwb.gov.tw/opendataapi"
Expand Down Expand Up @@ -36,47 +39,113 @@ func (cwb cwbDataSource) loadDataSet(dataID string) (result cwbDataSet) {
return
}

type rawWeatherElement struct {
ElementName string `xml:"elementName"`
ElementValue interface{} `xml:"elementValue>value"`
}
type StationStatus struct {
StationName string
CityName string
CitySN int
TownName string
TownSN int

latitude float64
longitude float64

type stationStatus struct {
LocationName string
WeatherElements map[string]interface{}
}

type rawStationStatus struct {
LocationName string `xml:"locationName"`
RawWeatherElement []rawWeatherElement `xml:"weatherElement"`
type StationList struct {
Locations map[string]StationStatus `xml:"location"`
}

type rawStationList struct {
Locations []rawStationStatus `xml:"location"`
type rawWeatherElement struct {
Name string
Value interface{}
}

type stationList struct {
Locations map[string]stationStatus
}
func (rawElement *rawWeatherElement) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
const timeShowFormat = "2006-01-02T15:04:05-07:00"
raw := new(struct {
Name string `xml:"elementName"`
Value string `xml:"elementValue>value"`
})
err := d.DecodeElement(raw, &start)
if err != nil {
return err
}
rawElement.Name = raw.Name

func (raw *rawStationList) Convert() *stationList {
list := make(map[string]stationStatus, 11)
for _, rawElem := range raw.Locations {
list[rawElem.LocationName] = rawElem.Convert()
valStr := raw.Value
timeStamp, err := time.Parse(timeShowFormat, valStr)
if err != nil {
f, err := strconv.ParseFloat(valStr, 64)
if err != nil {
return err
}
rawElement.Value = f
} else {
rawElement.Value = timeStamp
}
return &stationList{list}
return nil
}

func (status *rawStationStatus) Convert() (converted stationStatus) {
converted.LocationName = status.LocationName
converted.WeatherElements = status.ToMap()
return
func (status *StationStatus) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
raw := new(struct {
StationName string `xml:"locationName"`
Latitude float64 `xml:"lat"`
Longitude float64 `xml:"lon"`
WeatherElements []rawWeatherElement `xml:"weatherElement"`
Parameters []struct {
Name string `xml:"parameterName"`
Value string `xml:"parameterValue"`
} `xml:"parameter"`
})
err := d.DecodeElement(raw, &start)
if err != nil {
return err
}
status.StationName = raw.StationName
status.latitude = raw.Latitude
status.longitude = raw.Longitude
// init map
status.WeatherElements = make(map[string]interface{}, 11)
for _, element := range raw.WeatherElements {
status.WeatherElements[element.Name] = element.Value
}
for _, parameter := range raw.Parameters {
switch parameter.Name {
case "CITY":
status.CityName = parameter.Value
break
case "CITY_SN":
i, err := strconv.Atoi(parameter.Value)
if err == nil {
status.CitySN = i
}
break
case "TOWN":
status.TownName = parameter.Value
break
case "TOWN_SN":
i, err := strconv.Atoi(parameter.Value)
if err == nil {
status.TownSN = i
}
break
}
}
return nil
}

func (status rawStationStatus) ToMap() (elemMap map[string]interface{}) {
elemMap = make(map[string]interface{})
for _, element := range status.RawWeatherElement {
elemMap[element.ElementName] = element.ElementValue
func (list *StationList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
stations := new(struct {
Locations []StationStatus `xml:"location"`
})
err := d.DecodeElement(stations, &start)
if err != nil {
return err
}
return
list.Locations = make(map[string]StationStatus, 150)
for _, station := range stations.Locations {
list.Locations[station.StationName] = station
}
return nil
}
69 changes: 42 additions & 27 deletions cwbDataSource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,84 @@ package twweather

import (
"encoding/xml"
"errors"
"fmt"
"os"
"testing"
"time"
)

var (
sampleXML []byte
locationXML []byte
exampleElements = make(map[string]float64)
exampleElements = make(map[string]interface{})
)

func init() {
exampleElements["ELEV"] = 227
exampleElements["WDIR"] = 56
exampleElements["WDSD"] = 1.9
exampleElements["TEMP"] = 26.6
exampleElements["HUMD"] = 0.79
exampleElements["PRES"] = 989.1
exampleElements["SUN"] = -99
exampleElements["H_24R"] = 0.0
exampleElements["H_FX"] = -99
exampleElements["H_XD"] = -99
exampleElements["H_FXT"] = -99
exampleElements["ELEV"] = float64(227)
exampleElements["WDIR"] = float64(56)
exampleElements["WDSD"] = float64(1.9)
exampleElements["TEMP"] = float64(26.6)
exampleElements["HUMD"] = float64(0.79)
exampleElements["PRES"] = float64(989.1)
exampleElements["SUN"] = float64(-99)
exampleElements["H_24R"] = float64(0.0)
exampleElements["H_FX"] = float64(-99)
exampleElements["H_XD"] = float64(-99)
exampleElements["H_FXT"] = time.Date(2017, 10, 19, 7, 29, 0, 0, time.FixedZone("CST", 8*60*60))
}

func createTestError(format string, params ...interface{}) error {
return errors.New(fmt.Sprintf(format, params))
return fmt.Errorf(format, params...)
}

func matchExampleElements(t *testing.T, location *rawStationStatus) error {
convertedLocation := location.Convert()

func matchExampleElements(t *testing.T, station *StationStatus) error {
for name, expected := range exampleElements {
element, ok := convertedLocation.WeatherElements[name]
element, ok := station.WeatherElements[name]
if !ok {
return createTestError("Element %s not found!", name)
}
if element != expected {
return createTestError("Element %s should be %f got %v!", name, expected, element)
switch v := expected.(type) {
case time.Time:
if !v.Equal(element.(time.Time)) {
return createTestError("Element %s should be %v got %v!", name, expected, element)
}
break
default:
if element != expected {
return createTestError("Element %s should be %v got %v!", name, expected, element)
}
break
}
t.Logf("Element match %s => %f = %f", name, expected, element)

t.Logf("Element match %s => %v = %v", name, expected, element)
}
return nil
}

// Test if we can unmarshal location xml with struct stationLocation
func TestParseLocation(t *testing.T) {
station := new(rawStationStatus)
xml.Unmarshal(locationXML, &station)
if station.LocationName != "橫山" {
location := new(StationStatus)
err := xml.Unmarshal(locationXML, location)
if err != nil {
t.Fatal(err)
}
if location.StationName != "橫山" {
t.Fail()
}
if count := len(station.RawWeatherElement); count != 11 {
if count := len(location.WeatherElements); count != 11 {
t.Logf("weather element count of the sample location should be 11. Got %d", count)
t.Fail()
}
matchExampleElements(t, station)
err = matchExampleElements(t, location)
if err != nil {
t.Log(err)
t.Fail()
}
}

func TestLoadData(t *testing.T) {
t.Skip()
weather.cwbDataSource = &cwbDataSource{os.Getenv("cwbAPIKey")}
dataSet := weather.cwbDataSource.loadDataSet(StationStatusDataId)
dataSet := weather.cwbDataSource.loadDataSet(StationStatusDataID)
t.Log(string(dataSet.RawData))
}
29 changes: 29 additions & 0 deletions query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package twweather

type StationMap map[string]StationStatus

func (weather *Weather) GetStationBy(predictor func(StationStatus) bool) StationMap {
candidate := make(StationMap, 2)
for _, location := range weather.stationList.Locations {
if predictor(location) {
// return a copy
cp := location
candidate[location.StationName] = cp
}
}
return candidate
}

// GetStationByCityName returns StationMap that contains stations matched by town name.
func (weather *Weather) GetStationByTownName(townName string) StationMap {
return weather.GetStationBy(func(station StationStatus) bool {
return station.TownName == townName
})
}

// GetStationByCityName returns StationMap that contains stations matched by city name.
func (weather *Weather) GetStationsByCityName(cityName string) StationMap {
return weather.GetStationBy(func(station StationStatus) bool {
return station.CityName == cityName
})
}
44 changes: 44 additions & 0 deletions query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package twweather

import (
"fmt"
"testing"
)

func TestGetStationByTownName(t *testing.T) {
weather.UpdateStationStatusWithData(sampleXML)
stationMap := weather.GetStationByTownName("橫山鄉")
station, ok := stationMap["橫山"]
if !ok {
t.Log(stationMap)
t.Error("Cannot find station 橫山")
if station.TownSN != 78 {
t.Logf("Got Town number %v", station.TownSN)
t.Fail()
}
}
}

func testHasStation(list map[string]StationStatus, name string) error {
_, ok := list[name]
if !ok {
return fmt.Errorf("Missing station %s", name)
}
return nil
}

func TestGetStationsByCityName(t *testing.T) {
weather.UpdateStationStatusWithData(sampleXML)
stationMap := weather.GetStationsByCityName("新竹縣")
logIfError := func(err error) {
if err != nil {
t.Error(err)
}
}
if len(stationMap) != 2 {
t.Fail()
} else {
logIfError(testHasStation(stationMap, "橫山"))
logIfError(testHasStation(stationMap, "新豐"))
}
}
Loading

0 comments on commit 580baee

Please sign in to comment.