diff --git a/docs/internal/briefing.md b/docs/internal/briefing.md index d18e025..707fee8 100644 --- a/docs/internal/briefing.md +++ b/docs/internal/briefing.md @@ -36,6 +36,10 @@ Outputs: Every registered composition entry has a builder. Unknown or unimplemented module IDs fail validation instead of being skipped. +Prompt-facing module values use local, human-readable date and time labels +where the LLM is expected to reason about report content. Canonical timestamps +remain in report metadata, source provenance, and integration artifacts. + ## Boundaries - This package selects and shapes already-collected weather facts for prompts. diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 456f100..938cd5b 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -686,7 +686,7 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) { if err != nil { t.Fatalf("decode daily summary: %v", err) } - if !ok || dailySummary["date"] != "2026-05-30" { + if !ok || dailySummary["date"] != "Saturday, May 30, 2026" { t.Fatalf("daily summary = %#v, want tomorrow date", dailySummary) } if _, ok := result.ModuleSnapshot.LookupStanza("tomorrow_planning"); !ok { @@ -1370,7 +1370,8 @@ func priorDailyModuleSnapshot(t *testing.T, resolved report.Resolved) module.Sna }}, {ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{ "morning": map[string]any{ - "period": timeutil.Period{Start: resolved.ValidPeriod.Start.Add(6 * time.Hour), End: resolved.ValidPeriod.Start.Add(10 * time.Hour)}, + "date": resolved.ValidPeriod.Start.Format(timeutil.DateLayout), + "period": resolved.ValidPeriod.Start.Add(6*time.Hour).Format("2006-01-02 at 3:04 PM") + " to " + resolved.ValidPeriod.Start.Add(10*time.Hour).Format("2006-01-02 at 3:04 PM"), "temp_range_f": "50-58", }, }}, @@ -1392,7 +1393,8 @@ func priorOutlookModuleSnapshot(t *testing.T, date string) module.Snapshot { snapshot, err := module.NewSnapshot([]module.Output{ {ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{ date + "_morning": map[string]any{ - "period": timeutil.Period{Start: mustParse(date + "T06:00:00Z"), End: mustParse(date + "T10:00:00Z")}, + "date": date, + "period": date + " at 6:00 AM to " + date + " at 10:00 AM", "temp_range_f": "50-58", "max_pop_percent": precip, "max_pop_time": "6 AM", diff --git a/internal/briefing/base_modules_test.go b/internal/briefing/base_modules_test.go index 055db27..8892347 100644 --- a/internal/briefing/base_modules_test.go +++ b/internal/briefing/base_modules_test.go @@ -62,6 +62,9 @@ func TestHourlyForecastModuleUsesValidPeriodHourlyPeriods(t *testing.T) { if period.TextDescription != "Showers likely." || period.TemperatureF == nil || *period.TemperatureF != 76 { t.Fatalf("HourlyForecast period = %#v, want hourly period facts", period) } + if period.StartTime != "2026-05-29 at 8:00 AM" || period.EndTime != "2026-05-29 at 9:00 AM" { + t.Fatalf("HourlyForecast period times = %q/%q, want friendly local time labels", period.StartTime, period.EndTime) + } if period.WindDirection != "S" || period.ProbabilityOfPrecipitationPercent == nil || *period.ProbabilityOfPrecipitationPercent != 70 { t.Fatalf("HourlyForecast period = %#v, want compass wind and precip chance", period) } @@ -78,6 +81,9 @@ func TestHourlyForecastModuleUsesValidPeriodHourlyPeriods(t *testing.T) { if strings.Contains(jsonText, "wind_direction_degrees") || strings.Contains(jsonText, "Tomorrow") { t.Fatalf("hourly json = %s, want valid-period prompt fields only", jsonText) } + if strings.Contains(jsonText, `"start_time":"2026-05-29T`) || strings.Contains(jsonText, `"end_time":"2026-05-29T`) { + t.Fatalf("hourly json = %s, want friendly local start/end times", jsonText) + } } func TestHourlyForecastModuleRejectsUnsupportedReports(t *testing.T) { @@ -107,6 +113,9 @@ func TestNarrativeForecastModuleUsesValidPeriodNarrativePeriods(t *testing.T) { if period.Name != "Today" || period.TextDescription != "Morning storms, then partly sunny." { t.Fatalf("NarrativeForecast period = %#v, want Today narrative", period) } + if period.StartTime != "2026-05-29 at 6:00 AM" || period.EndTime != "2026-05-29 at 6:00 PM" { + t.Fatalf("NarrativeForecast period times = %q/%q, want friendly local time labels", period.StartTime, period.EndTime) + } if period.IsDay == nil || !*period.IsDay || period.TemperatureF == nil || *period.TemperatureF != 81 || period.ProbabilityOfPrecipitationPercent == nil || *period.ProbabilityOfPrecipitationPercent != 60 { t.Fatalf("NarrativeForecast period = %#v, want day, temperature, and precip values", period) } @@ -126,6 +135,9 @@ func TestNarrativeForecastModuleUsesValidPeriodNarrativePeriods(t *testing.T) { if strings.Contains(jsonText, "wind_direction_degrees") { t.Fatalf("narrative json = %s, want compass wind_direction without degrees field", jsonText) } + if strings.Contains(jsonText, `"start_time":"2026-05-29T`) || strings.Contains(jsonText, `"end_time":"2026-05-29T`) { + t.Fatalf("narrative json = %s, want friendly local start/end times", jsonText) + } if strings.Contains(jsonText, "Tomorrow night") { t.Fatalf("narrative json = %s, want only valid-period narrative periods", jsonText) } diff --git a/internal/briefing/derived_daily_summary_module.go b/internal/briefing/derived_daily_summary_module.go index d00fe71..a27f75d 100644 --- a/internal/briefing/derived_daily_summary_module.go +++ b/internal/briefing/derived_daily_summary_module.go @@ -35,7 +35,7 @@ func buildDerivedDailySummaryModule(ctx ModuleContext, _ any) (*module.Output, e func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.PrecipTiming, timezone string) (DerivedDailySummaryModule, error) { value := DerivedDailySummaryModule{ - Date: summary.Date, + Date: friendlyDateLabel(summary.Date, timezone), ThunderMentioned: timing.ThunderMentioned, } conditions := map[string]struct{}{} diff --git a/internal/briefing/derived_daypart_summaries_module.go b/internal/briefing/derived_daypart_summaries_module.go index 91a6009..4f906d3 100644 --- a/internal/briefing/derived_daypart_summaries_module.go +++ b/internal/briefing/derived_daypart_summaries_module.go @@ -11,22 +11,23 @@ import ( ) type DerivedDaypartSummaryModule struct { - Period timeutil.Period `json:"period"` - TempRangeF string `json:"temp_range_f,omitempty"` - ApparentTempRangeF string `json:"apparent_temp_range_f,omitempty"` - MaxPopPercent *int `json:"max_pop_percent,omitempty"` - MaxPopTime string `json:"max_pop_time,omitempty"` - MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"` - MaxWindGustTime string `json:"max_wind_gust_time,omitempty"` - DominantCondition string `json:"dominant_condition,omitempty"` - NotableConditions []string `json:"notable_conditions,omitempty"` - Snow bool `json:"snow,omitempty"` - Ice bool `json:"ice,omitempty"` - Fog bool `json:"fog,omitempty"` - Heat bool `json:"heat,omitempty"` - Cold bool `json:"cold,omitempty"` - Wind bool `json:"wind,omitempty"` - RelevantAlertCount int `json:"relevant_alert_count,omitempty"` + Date string `json:"date,omitempty"` + Period string `json:"period,omitempty"` + TempRangeF string `json:"temp_range_f,omitempty"` + ApparentTempRangeF string `json:"apparent_temp_range_f,omitempty"` + MaxPopPercent *int `json:"max_pop_percent,omitempty"` + MaxPopTime string `json:"max_pop_time,omitempty"` + MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"` + MaxWindGustTime string `json:"max_wind_gust_time,omitempty"` + DominantCondition string `json:"dominant_condition,omitempty"` + NotableConditions []string `json:"notable_conditions,omitempty"` + Snow bool `json:"snow,omitempty"` + Ice bool `json:"ice,omitempty"` + Fog bool `json:"fog,omitempty"` + Heat bool `json:"heat,omitempty"` + Cold bool `json:"cold,omitempty"` + Wind bool `json:"wind,omitempty"` + RelevantAlertCount int `json:"relevant_alert_count,omitempty"` } func buildDerivedDaypartSummariesModule(ctx ModuleContext, _ any) (*module.Output, error) { @@ -44,7 +45,8 @@ func buildDerivedDaypartSummariesModule(ctx ModuleContext, _ any) (*module.Outpu func derivedDaypartSummaryValue(daypart forecast.DaypartSummary, timezone string) DerivedDaypartSummaryModule { value := DerivedDaypartSummaryModule{ - Period: daypart.Period, + Date: localDateLabel(daypart.Period.Start, timezone), + Period: friendlyPeriodLabel(daypart.Period, timezone), TempRangeF: rangeLabel(daypart.Temperature), ApparentTempRangeF: daypartApparentRangeLabel(daypart.ApparentTemperature), DominantCondition: daypart.DominantCondition, diff --git a/internal/briefing/derived_modules_test.go b/internal/briefing/derived_modules_test.go index afa75d3..168e341 100644 --- a/internal/briefing/derived_modules_test.go +++ b/internal/briefing/derived_modules_test.go @@ -24,6 +24,9 @@ func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) { } value := moduleValue[DerivedDailySummaryModule](t, output) + if value.Date != "Friday, May 29, 2026" { + t.Fatalf("Date = %q, want friendly local date", value.Date) + } if value.HighTempF == nil || *value.HighTempF != 88 || value.LowTempF == nil || *value.LowTempF != 64 { t.Fatalf("daily temperatures = %#v/%#v, want narrative 88/64", value.HighTempF, value.LowTempF) } @@ -147,6 +150,9 @@ func TestDerivedDaypartSummariesExposeConfiguredKeysAndHazards(t *testing.T) { if morning.TempRangeF != "58" || morning.MaxPopPercent == nil || *morning.MaxPopPercent != 60 { t.Fatalf("morning = %#v, want temp range and precip peak", morning) } + if morning.Date != "2026-05-29" || morning.Period != "2026-05-29 at 6:00 AM to 2026-05-29 at 12:00 PM" { + t.Fatalf("morning period = %q/%q, want friendly local date and period labels", morning.Date, morning.Period) + } afternoon := value["afternoon"] if !afternoon.Heat || !afternoon.Wind || afternoon.MaxWindGustMph == nil || *afternoon.MaxWindGustMph != 42 { t.Fatalf("afternoon = %#v, want heat and wind hazard values", afternoon) @@ -160,11 +166,14 @@ func TestDerivedDaypartSummariesExposeConfiguredKeysAndHazards(t *testing.T) { t.Fatalf("marshal daypart summaries: %v", err) } jsonText := string(data) - for _, field := range []string{"temp_range_f", "max_pop_percent", "max_wind_gust_mph", "dominant_condition"} { + for _, field := range []string{"date", "period", "temp_range_f", "max_pop_percent", "max_wind_gust_mph", "dominant_condition"} { if !strings.Contains(jsonText, field) { t.Fatalf("daypart json = %s, want field %s", jsonText, field) } } + if strings.Contains(jsonText, `"period":{"start"`) || strings.Contains(jsonText, `T06:00:00`) { + t.Fatalf("daypart json = %s, want friendly period label instead of raw timestamps", jsonText) + } } func TestOutdoorWindowsAndTomorrowPlanningModulesPreserveDailyContent(t *testing.T) { diff --git a/internal/briefing/hourly_forecast_module.go b/internal/briefing/hourly_forecast_module.go index 19f9fe1..672d00c 100644 --- a/internal/briefing/hourly_forecast_module.go +++ b/internal/briefing/hourly_forecast_module.go @@ -17,39 +17,39 @@ type HourlyForecastModule struct { } type HourlyForecastPeriod struct { - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - Name string `json:"name,omitempty"` - IsDay *bool `json:"is_day,omitempty"` - ConditionCode *int `json:"condition_code,omitempty"` - TextDescription string `json:"text_description,omitempty"` - TemperatureC *float64 `json:"temperature_c,omitempty"` - TemperatureF *float64 `json:"temperature_f,omitempty"` - TemperatureCMin *float64 `json:"temperature_c_min,omitempty"` - TemperatureFMin *float64 `json:"temperature_f_min,omitempty"` - TemperatureCMax *float64 `json:"temperature_c_max,omitempty"` - TemperatureFMax *float64 `json:"temperature_f_max,omitempty"` - DewpointC *float64 `json:"dewpoint_c,omitempty"` - DewpointF *float64 `json:"dewpoint_f,omitempty"` - WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` - WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` - WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"` - WindGustMph *float64 `json:"wind_gust_mph,omitempty"` - WindDirection string `json:"wind_direction,omitempty"` - BarometricPressurePa *float64 `json:"barometric_pressure_pa,omitempty"` - BarometricPressureInHg *float64 `json:"barometric_pressure_in_hg,omitempty"` - VisibilityMeters *float64 `json:"visibility_meters,omitempty"` - VisibilityMiles *float64 `json:"visibility_miles,omitempty"` - ApparentTemperatureC *float64 `json:"apparent_temperature_c,omitempty"` - ApparentTemperatureF *float64 `json:"apparent_temperature_f,omitempty"` - CloudCoverPercent *float64 `json:"cloud_cover_percent,omitempty"` - ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"` - PrecipitationAmountMm *float64 `json:"precipitation_amount_mm,omitempty"` - PrecipitationAmountIn *float64 `json:"precipitation_amount_in,omitempty"` - SnowfallDepthMM *float64 `json:"snowfall_depth_mm,omitempty"` - SnowfallDepthIn *float64 `json:"snowfall_depth_in,omitempty"` - UVIndex *float64 `json:"uv_index,omitempty"` - RelativeHumidityPercent *float64 `json:"relative_humidity_percent,omitempty"` + StartTime string `json:"start_time,omitempty"` + EndTime string `json:"end_time,omitempty"` + Name string `json:"name,omitempty"` + IsDay *bool `json:"is_day,omitempty"` + ConditionCode *int `json:"condition_code,omitempty"` + TextDescription string `json:"text_description,omitempty"` + TemperatureC *float64 `json:"temperature_c,omitempty"` + TemperatureF *float64 `json:"temperature_f,omitempty"` + TemperatureCMin *float64 `json:"temperature_c_min,omitempty"` + TemperatureFMin *float64 `json:"temperature_f_min,omitempty"` + TemperatureCMax *float64 `json:"temperature_c_max,omitempty"` + TemperatureFMax *float64 `json:"temperature_f_max,omitempty"` + DewpointC *float64 `json:"dewpoint_c,omitempty"` + DewpointF *float64 `json:"dewpoint_f,omitempty"` + WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` + WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` + WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"` + WindGustMph *float64 `json:"wind_gust_mph,omitempty"` + WindDirection string `json:"wind_direction,omitempty"` + BarometricPressurePa *float64 `json:"barometric_pressure_pa,omitempty"` + BarometricPressureInHg *float64 `json:"barometric_pressure_in_hg,omitempty"` + VisibilityMeters *float64 `json:"visibility_meters,omitempty"` + VisibilityMiles *float64 `json:"visibility_miles,omitempty"` + ApparentTemperatureC *float64 `json:"apparent_temperature_c,omitempty"` + ApparentTemperatureF *float64 `json:"apparent_temperature_f,omitempty"` + CloudCoverPercent *float64 `json:"cloud_cover_percent,omitempty"` + ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"` + PrecipitationAmountMm *float64 `json:"precipitation_amount_mm,omitempty"` + PrecipitationAmountIn *float64 `json:"precipitation_amount_in,omitempty"` + SnowfallDepthMM *float64 `json:"snowfall_depth_mm,omitempty"` + SnowfallDepthIn *float64 `json:"snowfall_depth_in,omitempty"` + UVIndex *float64 `json:"uv_index,omitempty"` + RelativeHumidityPercent *float64 `json:"relative_humidity_percent,omitempty"` } func buildHourlyForecastModule(ctx ModuleContext, _ any) (*module.Output, error) { @@ -63,7 +63,7 @@ func buildHourlyForecastModule(ctx ModuleContext, _ any) (*module.Output, error) UpdatedAt: copyTime(hourly.UpdatedAt), SourceLocation: hourly.LocationName, SourceLocationID: hourly.LocationID, - Periods: hourlyForecastPeriods(ctx.Derived.ValidPeriodHourlyPeriods), + Periods: hourlyForecastPeriods(ctx.Derived.ValidPeriodHourlyPeriods, ctx.Timezone), } if value.isEmpty() { return nil, nil @@ -71,12 +71,12 @@ func buildHourlyForecastModule(ctx ModuleContext, _ any) (*module.Output, error) return &module.Output{ID: module.HourlyForecast, StanzaName: "hourly_forecast", Value: value}, nil } -func hourlyForecastPeriods(periods []weatherdata.ForecastPeriod) []HourlyForecastPeriod { +func hourlyForecastPeriods(periods []weatherdata.ForecastPeriod, timezone string) []HourlyForecastPeriod { out := make([]HourlyForecastPeriod, 0, len(periods)) for _, period := range periods { out = append(out, HourlyForecastPeriod{ - StartTime: period.StartTime, - EndTime: period.EndTime, + StartTime: friendlyDateTimeLabel(period.StartTime, timezone), + EndTime: friendlyDateTimeLabel(period.EndTime, timezone), Name: period.Name, IsDay: copyBool(period.IsDay), ConditionCode: copyInt(period.ConditionCode), diff --git a/internal/briefing/module_format_helpers.go b/internal/briefing/module_format_helpers.go index 58be4a6..cfec401 100644 --- a/internal/briefing/module_format_helpers.go +++ b/internal/briefing/module_format_helpers.go @@ -74,6 +74,47 @@ func periodClockLabel(period timeutil.Period, timezone string) string { return clockLabel(period.Start, timezone) + "-" + clockLabel(period.End, timezone) } +func friendlyPeriodLabel(period timeutil.Period, timezone string) string { + if !period.IsValid() { + return "" + } + return friendlyDateTimeLabel(period.Start, timezone) + " to " + friendlyDateTimeLabel(period.End, timezone) +} + +func friendlyDateTimeLabel(value time.Time, timezone string) string { + if value.IsZero() { + return "" + } + location, err := timeutil.LoadLocation(timezone) + if err != nil { + location = time.UTC + } + return value.In(location).Format("2006-01-02 at 3:04 PM") +} + +func friendlyDateLabel(date string, timezone string) string { + location, err := timeutil.LoadLocation(timezone) + if err != nil { + location = time.UTC + } + parsed, err := time.ParseInLocation(timeutil.DateLayout, date, location) + if err != nil { + return date + } + return parsed.Format("Monday, January 2, 2006") +} + +func localDateLabel(value time.Time, timezone string) string { + if value.IsZero() { + return "" + } + location, err := timeutil.LoadLocation(timezone) + if err != nil { + location = time.UTC + } + return value.In(location).Format(timeutil.DateLayout) +} + func clockLabel(value time.Time, timezone string) string { location, err := timeutil.LoadLocation(timezone) if err != nil { diff --git a/internal/briefing/narrative_forecast_module.go b/internal/briefing/narrative_forecast_module.go index 07b3213..38856c3 100644 --- a/internal/briefing/narrative_forecast_module.go +++ b/internal/briefing/narrative_forecast_module.go @@ -17,23 +17,23 @@ type NarrativeForecastModule struct { } type NarrativeForecastPeriod struct { - Name string `json:"name,omitempty"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - IsDay *bool `json:"is_day,omitempty"` - TextDescription string `json:"text_description,omitempty"` - TemperatureC *float64 `json:"temperature_c,omitempty"` - TemperatureF *float64 `json:"temperature_f,omitempty"` - TemperatureCMin *float64 `json:"temperature_c_min,omitempty"` - TemperatureFMin *float64 `json:"temperature_f_min,omitempty"` - TemperatureCMax *float64 `json:"temperature_c_max,omitempty"` - TemperatureFMax *float64 `json:"temperature_f_max,omitempty"` - WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` - WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` - WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"` - WindGustMph *float64 `json:"wind_gust_mph,omitempty"` - WindDirection string `json:"wind_direction,omitempty"` - ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"` + Name string `json:"name,omitempty"` + StartTime string `json:"start_time,omitempty"` + EndTime string `json:"end_time,omitempty"` + IsDay *bool `json:"is_day,omitempty"` + TextDescription string `json:"text_description,omitempty"` + TemperatureC *float64 `json:"temperature_c,omitempty"` + TemperatureF *float64 `json:"temperature_f,omitempty"` + TemperatureCMin *float64 `json:"temperature_c_min,omitempty"` + TemperatureFMin *float64 `json:"temperature_f_min,omitempty"` + TemperatureCMax *float64 `json:"temperature_c_max,omitempty"` + TemperatureFMax *float64 `json:"temperature_f_max,omitempty"` + WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` + WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` + WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"` + WindGustMph *float64 `json:"wind_gust_mph,omitempty"` + WindDirection string `json:"wind_direction,omitempty"` + ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"` } func buildNarrativeForecastModule(ctx ModuleContext, _ any) (*module.Output, error) { @@ -47,7 +47,7 @@ func buildNarrativeForecastModule(ctx ModuleContext, _ any) (*module.Output, err UpdatedAt: copyTime(narrative.UpdatedAt), SourceLocation: narrative.LocationName, SourceLocationID: narrative.LocationID, - Periods: narrativeForecastPeriods(ctx.Derived.ValidPeriodNarrativePeriods), + Periods: narrativeForecastPeriods(ctx.Derived.ValidPeriodNarrativePeriods, ctx.Timezone), } if value.isEmpty() { return nil, nil @@ -55,13 +55,13 @@ func buildNarrativeForecastModule(ctx ModuleContext, _ any) (*module.Output, err return &module.Output{ID: module.NarrativeForecast, StanzaName: "narrative_forecast", Value: value}, nil } -func narrativeForecastPeriods(periods []weatherdata.ForecastPeriod) []NarrativeForecastPeriod { +func narrativeForecastPeriods(periods []weatherdata.ForecastPeriod, timezone string) []NarrativeForecastPeriod { out := make([]NarrativeForecastPeriod, 0, len(periods)) for _, period := range periods { out = append(out, NarrativeForecastPeriod{ Name: period.Name, - StartTime: period.StartTime, - EndTime: period.EndTime, + StartTime: friendlyDateTimeLabel(period.StartTime, timezone), + EndTime: friendlyDateTimeLabel(period.EndTime, timezone), IsDay: copyBool(period.IsDay), TextDescription: period.TextDescription, TemperatureC: copyFloat(period.TemperatureC), diff --git a/internal/changes/daily.go b/internal/changes/daily.go index 30549e3..e1bc45b 100644 --- a/internal/changes/daily.go +++ b/internal/changes/daily.go @@ -10,7 +10,6 @@ import ( "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" - "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) type Thresholds struct { @@ -84,13 +83,14 @@ type dailySummaryStanza struct { } type daypartSummaryStanza struct { - Period timeutil.Period `json:"period"` - TempRangeF string `json:"temp_range_f,omitempty"` - MaxPopPercent *int `json:"max_pop_percent,omitempty"` - MaxPopTime string `json:"max_pop_time,omitempty"` - MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"` - Snow bool `json:"snow,omitempty"` - Ice bool `json:"ice,omitempty"` + Date string `json:"date,omitempty"` + Period string `json:"period,omitempty"` + TempRangeF string `json:"temp_range_f,omitempty"` + MaxPopPercent *int `json:"max_pop_percent,omitempty"` + MaxPopTime string `json:"max_pop_time,omitempty"` + MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"` + Snow bool `json:"snow,omitempty"` + Ice bool `json:"ice,omitempty"` } type precipTimingStanza struct { diff --git a/internal/changes/daily_test.go b/internal/changes/daily_test.go index dfc18e0..086ff81 100644 --- a/internal/changes/daily_test.go +++ b/internal/changes/daily_test.go @@ -3,10 +3,8 @@ package changes import ( "strings" "testing" - "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" - "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestCompareDailyNoMeaningfulChanges(t *testing.T) { @@ -98,7 +96,7 @@ func dailySnapshot(t *testing.T, low int, high int, precip int, precipTime strin DailyPrecipitationProbability: &precip, }}, module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]daypartSummaryStanza{ - "morning": {Period: period("2026-05-29T06:00:00Z", "2026-05-29T10:00:00Z"), TempRangeF: "60-70", Snow: snow}, + "morning": {Date: "2026-05-29", Period: "2026-05-29 at 6:00 AM to 2026-05-29 at 10:00 AM", TempRangeF: "60-70", Snow: snow}, }}, module.Output{ID: module.PrecipTiming, StanzaName: "precip_timing", Value: precipTimingStanza{MaxPopPercent: &precip, MaxPopTime: precipTime}}, module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: alertDigestStanza{Relevant: relevant}}, @@ -114,10 +112,6 @@ func snapshot(t *testing.T, outputs ...module.Output) module.Snapshot { return snapshot } -func period(start string, end string) timeutil.Period { - return timeutil.Period{Start: at(start), End: at(end)} -} - func testThresholds() Thresholds { return Thresholds{ TemperatureDegrees: 5, @@ -136,11 +130,3 @@ func countType(changes []Change, changeType string) int { } return count } - -func at(value string) time.Time { - parsed, err := time.Parse(time.RFC3339, value) - if err != nil { - panic(err) - } - return parsed -} diff --git a/internal/changes/three_day.go b/internal/changes/three_day.go index f5901f3..4980f71 100644 --- a/internal/changes/three_day.go +++ b/internal/changes/three_day.go @@ -5,7 +5,6 @@ import ( "sort" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" - "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func CompareThreeDay(previous module.Snapshot, current module.Snapshot, thresholds Thresholds) ([]Change, error) { @@ -113,10 +112,7 @@ func outlookDaysFromDayparts(dayparts map[string]daypartSummaryStanza) map[strin } func daypartDate(daypart daypartSummaryStanza) string { - if !daypart.Period.Start.IsZero() { - return daypart.Period.Start.Format(timeutil.DateLayout) - } - return "" + return daypart.Date } func minInt(a *int, b *int) *int { diff --git a/internal/changes/three_day_test.go b/internal/changes/three_day_test.go index ce31d1a..d5d45cb 100644 --- a/internal/changes/three_day_test.go +++ b/internal/changes/three_day_test.go @@ -31,7 +31,8 @@ func outlookSnapshot(t *testing.T, date string, tempRange string, precip int, pr t.Helper() return snapshot(t, module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]daypartSummaryStanza{ date + "_morning": { - Period: period(date+"T06:00:00Z", date+"T10:00:00Z"), + Date: date, + Period: date + " at 6:00 AM to " + date + " at 10:00 AM", TempRangeF: tempRange, MaxPopPercent: &precip, MaxPopTime: precipTime,