diff --git a/curweather.go b/curweather.go index b5e92a8..b939e54 100644 --- a/curweather.go +++ b/curweather.go @@ -50,6 +50,8 @@ type APICurrentWeatherData struct { PressureQFE *APIFloat `json:"pressure,omitempty"` // SnowAmount represents the the amount of snow in kg/m3 SnowAmount *APIFloat `json:"snowAmount,omitempty"` + // SnowHeight represents the the height of snow in m + SnowHeight *APIFloat `json:"snowHeight,omitempty"` // Temperature represents the temperature in °C Temperature *APIFloat `json:"temp,omitempty"` // WindDirection represents the direction from which the wind @@ -249,7 +251,7 @@ func (cw CurrentWeather) PressureQFE() Pressure { return v } -// SnowAmount returns the temperature data point as Density. +// SnowAmount returns the amount of snow data point as Density. // If the data point is not available in the CurrentWeather it will return // Density in which the "not available" field will be true. func (cw CurrentWeather) SnowAmount() Density { @@ -268,6 +270,25 @@ func (cw CurrentWeather) SnowAmount() Density { return v } +// SnowHeight returns the snow height data point as Height. +// If the data point is not available in the CurrentWeather it will return +// Height in which the "not available" field will be true. +func (cw CurrentWeather) SnowHeight() Height { + if cw.Data.SnowHeight == nil { + return Height{na: true} + } + v := Height{ + dt: cw.Data.SnowHeight.DateTime, + n: FieldSnowHeight, + s: SourceUnknown, + fv: cw.Data.SnowHeight.Value, + } + if cw.Data.SnowHeight.Source != nil { + v.s = StringToSource(*cw.Data.SnowHeight.Source) + } + return v +} + // Temperature returns the temperature data point as Temperature. // If the data point is not available in the CurrentWeather it will return // Temperature in which the "not available" field will be true. diff --git a/curweather_test.go b/curweather_test.go index 4fc4771..4681050 100644 --- a/curweather_test.go +++ b/curweather_test.go @@ -626,6 +626,103 @@ func TestClient_CurrentWeatherByLocation_SnowAmount(t *testing.T) { } } +func TestClient_CurrentWeatherByLocation_SnowHeight(t *testing.T) { + tt := []struct { + // Location name + loc string + // CurWeather height + h *Height + }{ + {"Ehrenfeld, Germany", &Height{ + dt: time.Date(2023, 5, 23, 6, 0, 0, 0, time.UTC), + s: SourceAnalysis, + fv: 1.23, + }}, + {"Berlin, Germany", &Height{ + dt: time.Date(2023, 5, 23, 6, 0, 0, 0, time.UTC), + s: SourceAnalysis, + fv: 0.003, + }}, + {"Neermoor, Germany", nil}, + } + c := New(withMockAPI()) + if c == nil { + t.Errorf("failed to create new Client, got nil") + return + } + for _, tc := range tt { + t.Run(tc.loc, func(t *testing.T) { + cw, err := c.CurrentWeatherByLocation(tc.loc) + if err != nil { + t.Errorf("CurrentWeatherByLocation failed: %s", err) + return + } + if tc.h != nil && tc.h.String() != cw.SnowHeight().String() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "string: %s, got: %s", tc.h.String(), cw.SnowHeight()) + } + if tc.h != nil && tc.h.Value() != cw.SnowHeight().Value() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "float: %f, got: %f", tc.h.Value(), cw.SnowHeight().Value()) + } + if tc.h != nil && tc.h.MeterString() != cw.SnowHeight().MeterString() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "string: %s, got: %s", tc.h.MeterString(), cw.SnowHeight().MeterString()) + } + if tc.h != nil && tc.h.Meter() != cw.SnowHeight().Meter() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "float: %f, got: %f", tc.h.Meter(), cw.SnowHeight().Meter()) + } + if tc.h != nil && tc.h.CentiMeterString() != cw.SnowHeight().CentiMeterString() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "string: %s, got: %s", tc.h.CentiMeterString(), cw.SnowHeight().CentiMeterString()) + } + if tc.h != nil && tc.h.CentiMeter() != cw.SnowHeight().CentiMeter() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "float: %f, got: %f", tc.h.CentiMeter(), cw.SnowHeight().CentiMeter()) + } + if tc.h != nil && tc.h.MilliMeterString() != cw.SnowHeight().MilliMeterString() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "string: %s, got: %s", tc.h.MilliMeterString(), cw.SnowHeight().MilliMeterString()) + } + if tc.h != nil && tc.h.MilliMeter() != cw.SnowHeight().MilliMeter() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "float: %f, got: %f", tc.h.MilliMeter(), cw.SnowHeight().MilliMeter()) + } + if tc.h != nil && cw.SnowHeight().Source() != tc.h.s { + t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s", + tc.h.s, cw.SnowHeight().Source()) + } + if tc.h != nil && tc.h.dt.Unix() != cw.SnowHeight().DateTime().Unix() { + t.Errorf("CurrentWeatherByLocation failed, expected datetime: %s, got: %s", + tc.h.dt.Format(time.RFC3339), cw.SnowHeight().DateTime().Format(time.RFC3339)) + } + if tc.h == nil { + if cw.SnowHeight().IsAvailable() { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "to have no data, but got: %s", cw.SnowHeight()) + } + if !math.IsNaN(cw.SnowHeight().Value()) { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "to return NaN, but got: %s", cw.SnowHeight().String()) + } + if !math.IsNaN(cw.SnowHeight().Meter()) { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "to return NaN, but got: %f", cw.SnowHeight().Meter()) + } + if !math.IsNaN(cw.SnowHeight().CentiMeter()) { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "to return NaN, but got: %f", cw.SnowHeight().CentiMeter()) + } + if !math.IsNaN(cw.SnowHeight().MilliMeter()) { + t.Errorf("CurrentWeatherByLocation failed, expected snow height "+ + "to return NaN, but got: %f", cw.SnowHeight().MilliMeter()) + } + } + }) + } +} + func TestClient_CurrentWeatherByLocation_Temperature(t *testing.T) { tt := []struct { // Location name diff --git a/datatype.go b/datatype.go index 13dd426..e22814f 100644 --- a/datatype.go +++ b/datatype.go @@ -46,6 +46,8 @@ const ( FieldPressureQFE // FieldSnowAmount represents the SnowAmount data point FieldSnowAmount + // FieldSnowHeight represents the SnowHeight data point + FieldSnowHeight // FieldSunrise represents the Sunrise data point FieldSunrise // FieldSunset represents the Sunset data point diff --git a/density.go b/density.go index e270997..b336eb3 100644 --- a/density.go +++ b/density.go @@ -38,7 +38,7 @@ func (d Density) Source() Source { } // Value returns the float64 value of an Density -// If the Density is not available in the Observation +// If the Density is not available in the WeatherData // Vaule will return math.NaN instead. func (d Density) Value() float64 { if d.na { diff --git a/direction.go b/direction.go index 219b921..14fcca2 100644 --- a/direction.go +++ b/direction.go @@ -62,7 +62,7 @@ func (d Direction) DateTime() time.Time { } // Value returns the float64 value of an Direction in degrees -// If the Direction is not available in the Observation +// If the Direction is not available in the WeatherData // Vaule will return math.NaN instead. func (d Direction) Value() float64 { if d.na { diff --git a/height.go b/height.go new file mode 100644 index 0000000..60583cf --- /dev/null +++ b/height.go @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2023 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package meteologix + +import ( + "fmt" + "math" + "time" +) + +// Height is a type wrapper of an WeatherData for holding height +// values in WeatherData (based on meters a default unit) +type Height WeatherData + +// IsAvailable returns true if an Height value was +// available at time of query +func (h Height) IsAvailable() bool { + return !h.na +} + +// DateTime returns true if an Height value was +// available at time of query +func (h Height) DateTime() time.Time { + return h.dt +} + +// String satisfies the fmt.Stringer interface for the Height type +func (h Height) String() string { + return fmt.Sprintf("%.3fm", h.fv) +} + +// Source returns the Source of Height +// If the Source is not available it will return SourceUnknown +func (h Height) Source() Source { + return h.s +} + +// Value returns the float64 value of an Height +// If the Height is not available in the WeatherData +// Vaule will return math.NaN instead. +func (h Height) Value() float64 { + if h.na { + return math.NaN() + } + return h.fv +} + +// Meter returns the Height type value as float64 in meters. +// This is an alias for the Value() method +func (h Height) Meter() float64 { + return h.Value() +} + +// MeterString returns the Height type as formatted string in meters +// This is an alias for the String() method +func (h Height) MeterString() string { + return h.String() +} + +// CentiMeter returns the Height type value as float64 in centimeters. +func (h Height) CentiMeter() float64 { + if h.na { + return math.NaN() + } + return h.fv / 100 +} + +// CentiMeterString returns the Height type as formatted string in centimeters +func (h Height) CentiMeterString() string { + return fmt.Sprintf("%.3fcm", h.CentiMeter()) +} + +// MilliMeter returns the Height type value as float64 in milliimeters. +func (h Height) MilliMeter() float64 { + if h.na { + return math.NaN() + } + return h.fv / 1000 +} + +// MilliMeterString returns the Height type as formatted string in millimeters +func (h Height) MilliMeterString() string { + return fmt.Sprintf("%.3fmm", h.MilliMeter()) +} diff --git a/humidity.go b/humidity.go index 82ae3f8..6dc1175 100644 --- a/humidity.go +++ b/humidity.go @@ -38,7 +38,7 @@ func (h Humidity) Source() Source { } // Value returns the float64 value of an Humidity -// If the Humidity is not available in the Observation +// If the Humidity is not available in the WeatherData // Vaule will return math.NaN instead. func (h Humidity) Value() float64 { if h.na { diff --git a/precipitation.go b/precipitation.go index 215289f..d91f4c7 100644 --- a/precipitation.go +++ b/precipitation.go @@ -38,7 +38,7 @@ func (p Precipitation) Source() Source { } // Value returns the float64 value of an Precipitation -// If the Precipitation is not available in the Observation +// If the Precipitation is not available in the WeatherData // Vaule will return math.NaN instead. func (p Precipitation) Value() float64 { if p.na { diff --git a/pressure.go b/pressure.go index efcdc4f..d279ac1 100644 --- a/pressure.go +++ b/pressure.go @@ -38,7 +38,7 @@ func (p Pressure) Source() Source { } // Value returns the float64 value of an Pressure -// If the Pressure is not available in the Observation +// If the Pressure is not available in the WeatherData // Vaule will return math.NaN instead. func (p Pressure) Value() float64 { if p.na { diff --git a/radiation.go b/radiation.go index 4b09f70..1959df3 100644 --- a/radiation.go +++ b/radiation.go @@ -27,7 +27,7 @@ func (r Radiation) DateTime() time.Time { } // Value returns the float64 value of an Radiation -// If the Radiation is not available in the Observation +// If the Radiation is not available in the WeatherData // Vaule will return math.NaN instead. func (r Radiation) Value() float64 { if r.na { diff --git a/speed.go b/speed.go index b1abb16..3570c4d 100644 --- a/speed.go +++ b/speed.go @@ -37,7 +37,7 @@ func (s Speed) DateTime() time.Time { // Value returns the float64 value of an Speed in meters // per second. -// If the Speed is not available in the Observation +// If the Speed is not available in the WeatherData // Vaule will return math.NaN instead. func (s Speed) Value() float64 { if s.na { diff --git a/station.go b/station.go index 4ea327d..1ad46f4 100644 --- a/station.go +++ b/station.go @@ -18,12 +18,12 @@ import ( const DefaultRadius int = 10 const ( - // PrecisionHigh is a high precision weather station - PrecisionHigh Precision = iota - // PrecisionMedium is a medium precision weather station - PrecisionMedium - // PrecisionLow is a low precision weather station - PrecisionLow + // PrecisionSuperHigh represents data of < ~4km resolution + PrecisionSuperHigh Precision = iota + // PrecisionHigh represents data of >= ~4km but < ~10km resolution + PrecisionHigh + // PrecisionStandard represents data of >= ~10km resolution + PrecisionStandard // PrecisionUnknown is weather station of unknown precision PrecisionUnknown ) @@ -157,12 +157,12 @@ func (p *Precision) UnmarshalJSON(s []byte) error { v := string(s) v = strings.ReplaceAll(v, `"`, ``) switch strings.ToLower(v) { + case "super_high": + *p = PrecisionSuperHigh case "high": *p = PrecisionHigh - case "medium": - *p = PrecisionMedium - case "low": - *p = PrecisionLow + case "standard": + *p = PrecisionStandard default: *p = PrecisionUnknown } @@ -172,12 +172,12 @@ func (p *Precision) UnmarshalJSON(s []byte) error { // String satisfies the fmt.Stringer interface for the Precision type func (p *Precision) String() string { switch *p { + case PrecisionSuperHigh: + return "SUPER_HIGH" case PrecisionHigh: return "HIGH" - case PrecisionMedium: - return "MEDIUM" - case PrecisionLow: - return "LOW" + case PrecisionStandard: + return "STANDARD" case PrecisionUnknown: return "UNKNOWN" default: diff --git a/station_test.go b/station_test.go index a8e6322..d7abc83 100644 --- a/station_test.go +++ b/station_test.go @@ -164,16 +164,16 @@ func TestPrecision_UnmarshalJSON(t *testing.T) { // Should fail sf bool }{ + { + "Super high precision", []byte(`{"precision":"SUPER_HIGH"}`), PrecisionSuperHigh, + false, + }, { "High precision", []byte(`{"precision":"HIGH"}`), PrecisionHigh, false, }, { - "Medium precision", []byte(`{"precision":"MEDIUM"}`), PrecisionMedium, - false, - }, - { - "Low precision", []byte(`{"precision":"LOW"}`), PrecisionLow, + "Standard precision", []byte(`{"precision":"STANDARD"}`), PrecisionStandard, false, }, { @@ -209,9 +209,9 @@ func TestPrecision_String(t *testing.T) { // Expected string es string }{ + {"Super high precision", PrecisionSuperHigh, "SUPER_HIGH"}, {"High precision", PrecisionHigh, "HIGH"}, - {"Medium precision", PrecisionMedium, "MEDIUM"}, - {"Low precision", PrecisionLow, "LOW"}, + {"Standard precision", PrecisionStandard, "STANDARD"}, {"Unknown precision", PrecisionUnknown, "UNKNOWN"}, {"Undefined precision", 999, "UNKNOWN"}, } diff --git a/temperature.go b/temperature.go index 5235569..07a48c7 100644 --- a/temperature.go +++ b/temperature.go @@ -27,7 +27,7 @@ func (t Temperature) DateTime() time.Time { } // Value returns the float64 value of an Temperature -// If the Temperature is not available in the Observation +// If the Temperature is not available in the WeatherData // Vaule will return math.NaN instead. func (t Temperature) Value() float64 { if t.na {