diff --git a/internal/normalizers/openweather/common.go b/internal/normalizers/openweather/common.go new file mode 100644 index 0000000..5542346 --- /dev/null +++ b/internal/normalizers/openweather/common.go @@ -0,0 +1,71 @@ +// FILE: ./internal/normalizers/openweather/common.go +package openweather + +import ( + "fmt" + "strings" +) + +// This file holds provider-specific helpers that are shared across multiple +// OpenWeather normalizers (observations today; forecasts/alerts later). +// Keeping these out of observation.go helps preserve the "one normalizer per file" +// convention while avoiding duplication. + +// primaryCondition returns the "primary" weather condition from OpenWeather's +// weather array. Per OpenWeather conventions, element [0] is treated as primary. +func primaryCondition(list []owmWeather) (id int, desc string, icon string) { + if len(list) == 0 { + return 0, "", "" + } + w := list[0] + return w.ID, strings.TrimSpace(w.Description), strings.TrimSpace(w.Icon) +} + +// inferIsDay determines day/night using the best available upstream signals. +// +// Priority: +// 1. The OpenWeather icon suffix ("d" / "n") when present. +// 2. Sunrise/sunset bounds (unix seconds), if provided. +// 3. Unknown (nil) when no reliable signal is present. +func inferIsDay(icon string, dt, sunrise, sunset int64) *bool { + // Prefer icon suffix. + icon = strings.TrimSpace(icon) + if icon != "" { + last := icon[len(icon)-1] + switch last { + case 'd': + v := true + return &v + case 'n': + v := false + return &v + } + } + + // Fall back to sunrise/sunset bounds if provided. + if dt > 0 && sunrise > 0 && sunset > 0 { + v := dt >= sunrise && dt < sunset + return &v + } + + return nil +} + +// openWeatherIconURL builds the standard OpenWeather icon URL for the given icon code. +func openWeatherIconURL(icon string) string { + icon = strings.TrimSpace(icon) + if icon == "" { + return "" + } + return fmt.Sprintf("https://openweathermap.org/img/wn/%s@2x.png", icon) +} + +// openWeatherStationID returns a stable station identifier for the given response. +// Prefer the OpenWeather city ID when present; otherwise, fall back to coordinates. +func openWeatherStationID(parsed owmResponse) string { + if parsed.ID != 0 { + return fmt.Sprintf("OPENWEATHER(%d)", parsed.ID) + } + // Fallback: synthesize from coordinates. + return fmt.Sprintf("OPENWEATHER(%.5f,%.5f)", parsed.Coord.Lat, parsed.Coord.Lon) +} diff --git a/internal/normalizers/openweather/observation.go b/internal/normalizers/openweather/observation.go index 19ae86c..17d2cf1 100644 --- a/internal/normalizers/openweather/observation.go +++ b/internal/normalizers/openweather/observation.go @@ -3,7 +3,6 @@ package openweather import ( "context" - "fmt" "strings" "time" @@ -132,51 +131,3 @@ func buildObservation(parsed owmResponse) (model.WeatherObservation, time.Time, return obs, ts, nil } - -func primaryCondition(list []owmWeather) (id int, desc string, icon string) { - if len(list) == 0 { - return 0, "", "" - } - w := list[0] - return w.ID, strings.TrimSpace(w.Description), strings.TrimSpace(w.Icon) -} - -func inferIsDay(icon string, dt, sunrise, sunset int64) *bool { - // Prefer icon suffix. - icon = strings.TrimSpace(icon) - if icon != "" { - last := icon[len(icon)-1] - switch last { - case 'd': - v := true - return &v - case 'n': - v := false - return &v - } - } - - // Fall back to sunrise/sunset bounds if provided. - if dt > 0 && sunrise > 0 && sunset > 0 { - v := dt >= sunrise && dt < sunset - return &v - } - - return nil -} - -func openWeatherIconURL(icon string) string { - icon = strings.TrimSpace(icon) - if icon == "" { - return "" - } - return fmt.Sprintf("https://openweathermap.org/img/wn/%s@2x.png", icon) -} - -func openWeatherStationID(parsed owmResponse) string { - if parsed.ID != 0 { - return fmt.Sprintf("OPENWEATHER(%d)", parsed.ID) - } - // Fallback: synthesize from coordinates. - return fmt.Sprintf("OPENWEATHER(%.5f,%.5f)", parsed.Coord.Lat, parsed.Coord.Lon) -}