From 84c4efbc2ea3e730b1148e57a790ac05d52fda8d Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 15 Jan 2026 10:41:56 -0600 Subject: [PATCH] normalizers/openweather: extract shared helpers into common.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor OpenWeather normalizers to improve structure and reuse by moving provider-specific helper functions out of observation.go and into a new common.go. This keeps observation.go focused on schema matching and domain mapping, preserves the “one normalizer per file” convention, and establishes a clear home for helpers that will be shared by future OpenWeather forecast and alert normalizers. No functional behavior changes; this is a pure internal refactor. --- internal/normalizers/openweather/common.go | 71 +++++++++++++++++++ .../normalizers/openweather/observation.go | 49 ------------- 2 files changed, 71 insertions(+), 49 deletions(-) create mode 100644 internal/normalizers/openweather/common.go 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) -}