// FILE: ./internal/providers/openmeteo/time.go package openmeteo import ( "fmt" "strings" "time" ) // ParseTime parses timestamps as returned by Open-Meteo. // // Open-Meteo commonly returns timestamps in one of these forms: // // - RFC3339 / RFC3339Nano with an explicit timezone suffix, e.g. // "2026-01-10T18:30:00Z" or "2026-01-10T12:30:00-06:00" // // - A local time string WITHOUT a timezone suffix, typically: // "YYYY-MM-DDTHH:MM" (and sometimes "YYYY-MM-DDTHH:MM:SS") // In that case, the timezone is provided separately via the top-level // "timezone" field (IANA name) and/or "utc_offset_seconds". // // Parsing strategy: // // 1. If the timestamp includes a timezone suffix (RFC3339/RFC3339Nano), // treat it as authoritative. // // 2. Otherwise, attempt to interpret the timestamp in the provided IANA // timezone name (if loadable), then fall back to a fixed zone derived // from utcOffsetSeconds. // // This helper lives in internal/providers so BOTH sources and normalizers can // share Open-Meteo quirks without duplicating logic. func ParseTime(s string, tz string, utcOffsetSeconds int) (time.Time, error) { s = strings.TrimSpace(s) if s == "" { return time.Time{}, fmt.Errorf("empty time") } // If the server returned a timezone-aware RFC3339 timestamp, accept it. if t, err := time.Parse(time.RFC3339Nano, s); err == nil { return t, nil } if t, err := time.Parse(time.RFC3339, s); err == nil { return t, nil } // Open-Meteo "local timestamp" formats (no timezone suffix). // Commonly "2006-01-02T15:04"; sometimes seconds are present. layouts := []string{ "2006-01-02T15:04:05", "2006-01-02T15:04", } // Prefer the IANA timezone name if it's valid on this system. if tz = strings.TrimSpace(tz); tz != "" { if loc, err := time.LoadLocation(tz); err == nil { for _, layout := range layouts { if t, err := time.ParseInLocation(layout, s, loc); err == nil { return t, nil } } } } // Fall back to a fixed zone derived from the offset seconds. zoneName := tz if zoneName == "" { zoneName = "open-meteo" } loc := time.FixedZone(zoneName, utcOffsetSeconds) for _, layout := range layouts { if t, err := time.ParseInLocation(layout, s, loc); err == nil { return t, nil } } return time.Time{}, fmt.Errorf("unsupported open-meteo timestamp format: %q", s) }