Files
weatherfeeder/internal/providers/openmeteo/time.go

80 lines
2.3 KiB
Go

// 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)
}