Merge pull request #10 from wneessen/more_curweather_datapoints

More CurrentWeather datapoints and refactoring
This commit is contained in:
Winni Neessen 2023-06-27 19:31:44 +02:00 committed by GitHub
commit f48392d553
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 380 additions and 120 deletions

View file

@ -140,7 +140,7 @@ func (a *AstronomicalInfo) Sunset() DateTime {
// available in the AstronomicalInfo it will return DateTime in
// which the "not available" field will be true.
func (a *AstronomicalInfo) SunsetByDateString(ds string) DateTime {
t, err := time.Parse("2006-01-02", ds)
t, err := time.Parse(DateFormat, ds)
if err != nil {
return DateTime{na: true}
}
@ -206,7 +206,7 @@ func (a *AstronomicalInfo) Sunrise() DateTime {
// available in the AstronomicalInfo it will return DateTime in
// which the "not available" field will be true.
func (a *AstronomicalInfo) SunriseByDateString(ds string) DateTime {
t, err := time.Parse("2006-01-02", ds)
t, err := time.Parse(DateFormat, ds)
if err != nil {
return DateTime{na: true}
}

View file

@ -53,11 +53,21 @@ const (
// ConditionMap is a map to associate a specific ConditionType to a nicely
// formatted, human readable string
var ConditionMap = map[ConditionType]string{
CondCloudy: "Cloudy", CondFog: "Fog", CondFreezingRain: "Freezing rain",
CondOvercast: "Overcast", CondPartlyCloudy: "Partly cloudy", CondRain: "Rain",
CondRainHeavy: "Heavy rain", CondShowers: "Showers", CondShowersHeavy: "Heavy showers",
CondSnow: "Snow", CondSnowHeavy: "Heavy snow", CondSnowRain: "Sleet",
CondSunshine: "Clear sky", CondThunderStorm: "Thunderstorm", CondUnknown: "Unknown",
CondCloudy: "Cloudy",
CondFog: "Fog",
CondFreezingRain: "Freezing rain",
CondOvercast: "Overcast",
CondPartlyCloudy: "Partly cloudy",
CondRain: "Rain",
CondRainHeavy: "Heavy rain",
CondShowers: "Showers",
CondShowersHeavy: "Heavy showers",
CondSnow: "Snow",
CondSnowHeavy: "Heavy snow",
CondSnowRain: "Sleet",
CondSunshine: "Clear sky",
CondThunderStorm: "Thunderstorm",
CondUnknown: "Unknown",
}
// Condition is a type wrapper of an WeatherData for holding

View file

@ -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
@ -62,31 +64,6 @@ type APICurrentWeatherData struct {
// WeatherSymbol is a text representation of the current weather
// conditions
WeatherSymbol *APIString `json:"weatherSymbol,omitempty"`
/*
// DewPointMean represents the mean dewpoint in °C
DewpointMean *APIFloat `json:"dewpointMean,omitempty"`
// GlobalRadiation10m represents the sum of global radiation over the last
// 10 minutes in kJ/m²
GlobalRadiation10m *APIFloat `json:"globalRadiation10m,omitempty"`
// GlobalRadiation1h represents the sum of global radiation over the last
// 1 hour in kJ/m²
GlobalRadiation1h *APIFloat `json:"globalRadiation1h,omitempty"`
// GlobalRadiation24h represents the sum of global radiation over the last
// 24 hour in kJ/m²
GlobalRadiation24h *APIFloat `json:"globalRadiation24h,omitempty"`
// TemperatureMax represents the maximum temperature in °C
TemperatureMax *APIFloat `json:"tempMax,omitempty"`
// TemperatureMean represents the mean temperature in °C
TemperatureMean *APIFloat `json:"tempMean,omitempty"`
// TemperatureMin represents the minimum temperature in °C
TemperatureMin *APIFloat `json:"tempMin,omitempty"`
// Temperature5cm represents the temperature 5cm above ground in °C
Temperature5cm *APIFloat `json:"temp5cm,omitempty"`
// Temperature5cm represents the minimum temperature 5cm above
// ground in °C
Temperature5cmMin *APIFloat `json:"temp5cmMin,omitempty"`
*/
}
// CurrentWeatherByCoordinates returns the CurrentWeather values for the given coordinates
@ -146,7 +123,7 @@ func (cw CurrentWeather) Dewpoint() Temperature {
// If the data point is not available in the CurrentWeather it will return
// Humidity in which the "not available" field will be true.
func (cw CurrentWeather) HumidityRelative() Humidity {
if cw.Data.Dewpoint == nil {
if cw.Data.HumidityRelative == nil {
return Humidity{na: true}
}
v := Humidity{
@ -249,7 +226,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 +245,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.

View file

@ -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

View file

@ -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

View file

@ -36,5 +36,5 @@ func (dt DateTime) Value() time.Time {
// String satisfies the fmt.Stringer interface for the DateTime type
// The date will returned as time.RFC3339 format
func (dt DateTime) String() string {
return dt.dv.Format(time.RFC3339)
return dt.Value().Format(time.RFC3339)
}

View file

@ -10,8 +10,8 @@ import (
"time"
)
// Density is a type wrapper of an WeatherData for holding density
// values in WeatherData
// Density is a type wrapper of WeatherData for holding density
// values in kg/m³ in WeatherData
type Density WeatherData
// IsAvailable returns true if an Density value was
@ -20,8 +20,7 @@ func (d Density) IsAvailable() bool {
return !d.na
}
// DateTime returns true if an Density value was
// available at time of query
// DateTime returns the DateTime of the queried Density value
func (d Density) DateTime() time.Time {
return d.dt
}
@ -38,7 +37,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 {

View file

@ -12,10 +12,10 @@ import (
)
const (
// DirectionMinAngel is the minimum angel for a direction
DirectionMinAngel = 0
// DirectionMaxAngel is the maximum angel for a direction
DirectionMaxAngel = 360
// DirectionMinAngle is the minimum angel for a direction
DirectionMinAngle = 0
// DirectionMaxAngle is the maximum angel for a direction
DirectionMaxAngle = 360
)
// WindDirAbbrMap is a map to associate a wind direction degree value with
@ -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 {
@ -84,7 +84,7 @@ func (d Direction) Source() Source {
// Direction returns the abbreviation string for a given Direction type
func (d Direction) Direction() string {
if d.fv < DirectionMinAngel || d.fv > DirectionMaxAngel {
if d.fv < DirectionMinAngle || d.fv > DirectionMaxAngle {
return ErrUnsupportedDirection
}
if ds, ok := WindDirAbbrMap[d.fv]; ok {
@ -95,7 +95,7 @@ func (d Direction) Direction() string {
// DirectionFull returns the full string for a given Direction type
func (d Direction) DirectionFull() string {
if d.fv < DirectionMinAngel || d.fv > DirectionMaxAngel {
if d.fv < DirectionMinAngle || d.fv > DirectionMaxAngle {
return ErrUnsupportedDirection
}
if ds, ok := WindDirFullMap[d.fv]; ok {
@ -125,8 +125,8 @@ func findDirection(v float64, m map[float64]string) string {
break
}
}
sr := math.Mod(v, sv)
er := math.Mod(ev, v)
sr := v - sv
er := ev - v
if er > sr {
return m[sv]
}

37
direction_test.go Normal file
View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"testing"
)
func TestFindDirection(t *testing.T) {
// Prepare test cases
tt := []struct {
v float64
dm map[float64]string
er string
}{
{15, WindDirAbbrMap, "NbE"},
{47, WindDirAbbrMap, "NE"},
{200, WindDirAbbrMap, "SSW"},
{330, WindDirAbbrMap, "NWbN"},
{15, WindDirFullMap, "North by East"},
{47, WindDirFullMap, "Northeast"},
{200, WindDirFullMap, "South-Southwest"},
{330, WindDirFullMap, "Northwest by North"},
}
// Run tests
for _, tc := range tt {
t.Run("", func(t *testing.T) {
r := findDirection(tc.v, tc.dm)
if tc.er != r {
t.Errorf("findDirection failed, expected: %s, got: %s", tc.er, r)
}
})
}
}

2
doc.go
View file

@ -6,4 +6,4 @@
package meteologix
// VERSION represents the current version of the package
const VERSION = "0.1.0"
const VERSION = "0.1.1"

View file

@ -43,7 +43,7 @@ type GeoLocation struct {
// This method makes use of the OSM Nominatim API
func (c *Client) GetGeoLocationByName(ci string) (GeoLocation, error) {
ga, err := c.GetGeoLocationsByName(ci)
if len(ga) < 1 {
if err != nil || len(ga) < 1 {
return GeoLocation{}, err
}
return ga[0], nil

85
height.go Normal file
View file

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// 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 the timestamp associated with the Height value
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())
}

View file

@ -13,9 +13,9 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
)
@ -49,11 +49,13 @@ type APIError struct {
// NewHTTPClient returns a new HTTP client
func NewHTTPClient(c *Config) *HTTPClient {
tc := &tls.Config{
MaxVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12,
}
ht := http.Transport{TLSClientConfig: tc}
hc := &http.Client{Transport: &ht}
ht := &http.Transport{TLSClientConfig: tc}
hc := &http.Client{
Timeout: HTTPClientTimeout,
Transport: ht,
}
return &HTTPClient{c, hc}
}
@ -78,31 +80,28 @@ func (hc *HTTPClient) GetWithTimeout(u string, t time.Duration) ([]byte, error)
// User authentication (only required for Meteologix API calls)
if strings.HasPrefix(u, APIBaseURL) {
hc.setAuthHeader(hr)
hc.setAuthentication(hr)
}
sr, err := hc.Do(hr)
if err != nil {
return nil, err
}
defer func() {
if err = sr.Body.Close(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "failed to close HTTP request body", err)
if sr == nil {
return nil, errors.New("nil response received")
}
defer func(b io.ReadCloser) {
if err = b.Close(); err != nil {
log.Printf("failed to close HTTP request body: %s", err)
}
}()
}(sr.Body)
if !strings.HasPrefix(sr.Header.Get("Content-Type"), MIMETypeJSON) {
return nil, ErrNonJSONResponse
}
buf := &bytes.Buffer{}
bw := bufio.NewWriter(buf)
_, err = io.Copy(bw, sr.Body)
if err != nil {
return nil, fmt.Errorf("failed to copy HTTP response body to buffer: %w", err)
}
if sr.StatusCode >= 400 {
var ae APIError
if err = json.Unmarshal(buf.Bytes(), &ae); err != nil {
if sr.StatusCode >= http.StatusBadRequest {
ae := new(APIError)
if err = json.NewDecoder(sr.Body).Decode(ae); err != nil {
return nil, fmt.Errorf("failed to unmarshal error JSON: %w", err)
}
if ae.Code < 1 {
@ -111,15 +110,25 @@ func (hc *HTTPClient) GetWithTimeout(u string, t time.Duration) ([]byte, error)
if ae.Details == "" {
ae.Details = sr.Status
}
return nil, ae
return nil, *ae
}
buf := &bytes.Buffer{}
bw := bufio.NewWriter(buf)
_, err = io.Copy(bw, sr.Body)
if err != nil {
return nil, fmt.Errorf("failed to copy HTTP response body to buffer: %w", err)
}
if err = bw.Flush(); err != nil {
return nil, fmt.Errorf("failed to flush buffer: %w", err)
}
return buf.Bytes(), nil
}
// setAuthHeader sets the corresponding user authentication header. If an API Key is set, this
// setAuthentication sets the corresponding user authentication header. If an API Key is set, this
// will be preferred, alternatively a username/authPass combination for HTTP Basic auth can
// be used
func (hc *HTTPClient) setAuthHeader(hr *http.Request) {
func (hc *HTTPClient) setAuthentication(hr *http.Request) {
if hc.apiKey != "" {
hr.Header.Set("X-API-Key", hc.Config.apiKey)
return

View file

@ -20,8 +20,8 @@ func (h Humidity) IsAvailable() bool {
return !h.na
}
// DateTime returns true if an Humidity value was
// available at time of query
// DateTime returns the timestamp of when the humidity
// measurement was taken.
func (h Humidity) DateTime() time.Time {
return h.dt
}
@ -38,8 +38,8 @@ func (h Humidity) Source() Source {
}
// Value returns the float64 value of an Humidity
// If the Humidity is not available in the Observation
// Vaule will return math.NaN instead.
// If the Humidity is not available in the WeatherData
// Value will return math.NaN instead.
func (h Humidity) Value() float64 {
if h.na {
return math.NaN()

View file

@ -20,8 +20,7 @@ func (p Precipitation) IsAvailable() bool {
return !p.na
}
// DateTime returns true if an Precipitation value was
// available at time of query
// DateTime returns the DateTime when the Precipitation value was recorded
func (p Precipitation) DateTime() time.Time {
return p.dt
}
@ -38,7 +37,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 {

View file

@ -20,8 +20,7 @@ func (p Pressure) IsAvailable() bool {
return !p.na
}
// DateTime returns true if an Pressure value was
// available at time of query
// DateTime returns the date and time of the Pressure reading
func (p Pressure) DateTime() time.Time {
return p.dt
}
@ -38,7 +37,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 {

View file

@ -20,14 +20,14 @@ func (r Radiation) IsAvailable() bool {
return !r.na
}
// DateTime returns true if an Radiation value was
// available at time of query
// DateTime returns the time.Time object representing the date and time
// at which the Radiation value was queried
func (r Radiation) DateTime() time.Time {
return r.dt
}
// 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 {

View file

@ -29,15 +29,14 @@ func (s Speed) IsAvailable() bool {
return !s.na
}
// DateTime returns true if an Speed value was
// available at time of query
// DateTime returns the DateTime when the Speed was checked
func (s Speed) DateTime() time.Time {
return s.dt
}
// 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 {

View file

@ -18,16 +18,42 @@ 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
// PrecisionUnknown is weather station of unknown precision
// PrecisionSuperHigh represents the precision level of data corresponding
// to a resolution of less than or approximately equal to 4 kilometers.
// This is the highest level of precision, usually associated with highly
// detailed measurements or observations.
PrecisionSuperHigh Precision = iota
// PrecisionHigh represents the precision level of data corresponding to a
// resolution between 4 kilometers and 10 kilometers. This is a high precision
// level, suitable for most operational needs that require a balance between
// detail and processing requirements.
PrecisionHigh
// PrecisionStandard represents the precision level of data corresponding to
// a resolution of 10 kilometers or more. This is the standard level of
// precision, generally used for large-scale analysis and modeling.
PrecisionStandard
// PrecisionUnknown is used when the precision level of a weather station
// is unknown. This constant can be used as a placeholder when the resolution
// data is not available.
PrecisionUnknown
)
// Precision levels defined as strings to allow for clear, consistent
// use throughout the application.
const (
// PrecisionStringSuperHigh represents the super high precision level string.
PrecisionStringSuperHigh = "SUPER_HIGH"
// PrecisionStringHigh represents the high precision level string.
PrecisionStringHigh = "HIGH"
// PrecisionStringStandard represents the standard precision level string.
PrecisionStringStandard = "STANDARD"
// PrecisionStringUnknown represents an unknown precision level string.
PrecisionStringUnknown = "UNKNOWN"
)
var (
// ErrRadiusTooSmall is returned if a given radius value is too small
ErrRadiusTooSmall = errors.New("given radius is too small")
@ -156,13 +182,13 @@ func (c *Client) StationSearchByCoordinatesWithinRadius(la, lo float64, ra int)
func (p *Precision) UnmarshalJSON(s []byte) error {
v := string(s)
v = strings.ReplaceAll(v, `"`, ``)
switch strings.ToLower(v) {
case "high":
switch strings.ToUpper(v) {
case PrecisionStringSuperHigh:
*p = PrecisionSuperHigh
case PrecisionStringHigh:
*p = PrecisionHigh
case "medium":
*p = PrecisionMedium
case "low":
*p = PrecisionLow
case PrecisionStringStandard:
*p = PrecisionStandard
default:
*p = PrecisionUnknown
}
@ -172,15 +198,15 @@ 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 PrecisionStringSuperHigh
case PrecisionHigh:
return "HIGH"
case PrecisionMedium:
return "MEDIUM"
case PrecisionLow:
return "LOW"
return PrecisionStringHigh
case PrecisionStandard:
return PrecisionStringStandard
case PrecisionUnknown:
return "UNKNOWN"
return PrecisionStringUnknown
default:
return "UNKNOWN"
return PrecisionStringUnknown
}
}

View file

@ -46,7 +46,8 @@ func TestClient_StationSearchByLocation_Fail(t *testing.T) {
t.Errorf("StationSearchByLocation was supposed to fail but didn't")
}
if err != nil && !errors.As(err, &APIError{}) {
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't")
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't: %s",
err)
}
c = New(WithAPIKey("invalid"))
_, err = c.StationSearchByLocation("Cologne, Germany")
@ -55,7 +56,8 @@ func TestClient_StationSearchByLocation_Fail(t *testing.T) {
return
}
if err != nil && !errors.As(err, &APIError{}) {
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't")
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't: %s",
err)
}
}
@ -164,16 +166,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 +211,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"},
}

View file

@ -20,14 +20,14 @@ func (t Temperature) IsAvailable() bool {
return !t.na
}
// DateTime returns true if an Temperature value was
// available at time of query
// DateTime returns the time at which the temperature data was
// generated or fetched
func (t Temperature) DateTime() time.Time {
return t.dt
}
// 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 {