Implement friendly time formatting in select modules
This commit is contained in:
@@ -36,6 +36,10 @@ Outputs:
|
|||||||
Every registered composition entry has a builder. Unknown or unimplemented
|
Every registered composition entry has a builder. Unknown or unimplemented
|
||||||
module IDs fail validation instead of being skipped.
|
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
|
## Boundaries
|
||||||
|
|
||||||
- This package selects and shapes already-collected weather facts for prompts.
|
- This package selects and shapes already-collected weather facts for prompts.
|
||||||
|
|||||||
@@ -686,7 +686,7 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("decode daily summary: %v", err)
|
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)
|
t.Fatalf("daily summary = %#v, want tomorrow date", dailySummary)
|
||||||
}
|
}
|
||||||
if _, ok := result.ModuleSnapshot.LookupStanza("tomorrow_planning"); !ok {
|
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{
|
{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{
|
||||||
"morning": 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",
|
"temp_range_f": "50-58",
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
@@ -1392,7 +1393,8 @@ func priorOutlookModuleSnapshot(t *testing.T, date string) module.Snapshot {
|
|||||||
snapshot, err := module.NewSnapshot([]module.Output{
|
snapshot, err := module.NewSnapshot([]module.Output{
|
||||||
{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{
|
{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{
|
||||||
date + "_morning": 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",
|
"temp_range_f": "50-58",
|
||||||
"max_pop_percent": precip,
|
"max_pop_percent": precip,
|
||||||
"max_pop_time": "6 AM",
|
"max_pop_time": "6 AM",
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ func TestHourlyForecastModuleUsesValidPeriodHourlyPeriods(t *testing.T) {
|
|||||||
if period.TextDescription != "Showers likely." || period.TemperatureF == nil || *period.TemperatureF != 76 {
|
if period.TextDescription != "Showers likely." || period.TemperatureF == nil || *period.TemperatureF != 76 {
|
||||||
t.Fatalf("HourlyForecast period = %#v, want hourly period facts", period)
|
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 {
|
if period.WindDirection != "S" || period.ProbabilityOfPrecipitationPercent == nil || *period.ProbabilityOfPrecipitationPercent != 70 {
|
||||||
t.Fatalf("HourlyForecast period = %#v, want compass wind and precip chance", period)
|
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") {
|
if strings.Contains(jsonText, "wind_direction_degrees") || strings.Contains(jsonText, "Tomorrow") {
|
||||||
t.Fatalf("hourly json = %s, want valid-period prompt fields only", jsonText)
|
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) {
|
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." {
|
if period.Name != "Today" || period.TextDescription != "Morning storms, then partly sunny." {
|
||||||
t.Fatalf("NarrativeForecast period = %#v, want Today narrative", period)
|
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 {
|
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)
|
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") {
|
if strings.Contains(jsonText, "wind_direction_degrees") {
|
||||||
t.Fatalf("narrative json = %s, want compass wind_direction without degrees field", jsonText)
|
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") {
|
if strings.Contains(jsonText, "Tomorrow night") {
|
||||||
t.Fatalf("narrative json = %s, want only valid-period narrative periods", jsonText)
|
t.Fatalf("narrative json = %s, want only valid-period narrative periods", jsonText)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func buildDerivedDailySummaryModule(ctx ModuleContext, _ any) (*module.Output, e
|
|||||||
|
|
||||||
func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.PrecipTiming, timezone string) (DerivedDailySummaryModule, error) {
|
func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.PrecipTiming, timezone string) (DerivedDailySummaryModule, error) {
|
||||||
value := DerivedDailySummaryModule{
|
value := DerivedDailySummaryModule{
|
||||||
Date: summary.Date,
|
Date: friendlyDateLabel(summary.Date, timezone),
|
||||||
ThunderMentioned: timing.ThunderMentioned,
|
ThunderMentioned: timing.ThunderMentioned,
|
||||||
}
|
}
|
||||||
conditions := map[string]struct{}{}
|
conditions := map[string]struct{}{}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DerivedDaypartSummaryModule struct {
|
type DerivedDaypartSummaryModule struct {
|
||||||
Period timeutil.Period `json:"period"`
|
Date string `json:"date,omitempty"`
|
||||||
|
Period string `json:"period,omitempty"`
|
||||||
TempRangeF string `json:"temp_range_f,omitempty"`
|
TempRangeF string `json:"temp_range_f,omitempty"`
|
||||||
ApparentTempRangeF string `json:"apparent_temp_range_f,omitempty"`
|
ApparentTempRangeF string `json:"apparent_temp_range_f,omitempty"`
|
||||||
MaxPopPercent *int `json:"max_pop_percent,omitempty"`
|
MaxPopPercent *int `json:"max_pop_percent,omitempty"`
|
||||||
@@ -44,7 +45,8 @@ func buildDerivedDaypartSummariesModule(ctx ModuleContext, _ any) (*module.Outpu
|
|||||||
|
|
||||||
func derivedDaypartSummaryValue(daypart forecast.DaypartSummary, timezone string) DerivedDaypartSummaryModule {
|
func derivedDaypartSummaryValue(daypart forecast.DaypartSummary, timezone string) DerivedDaypartSummaryModule {
|
||||||
value := DerivedDaypartSummaryModule{
|
value := DerivedDaypartSummaryModule{
|
||||||
Period: daypart.Period,
|
Date: localDateLabel(daypart.Period.Start, timezone),
|
||||||
|
Period: friendlyPeriodLabel(daypart.Period, timezone),
|
||||||
TempRangeF: rangeLabel(daypart.Temperature),
|
TempRangeF: rangeLabel(daypart.Temperature),
|
||||||
ApparentTempRangeF: daypartApparentRangeLabel(daypart.ApparentTemperature),
|
ApparentTempRangeF: daypartApparentRangeLabel(daypart.ApparentTemperature),
|
||||||
DominantCondition: daypart.DominantCondition,
|
DominantCondition: daypart.DominantCondition,
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) {
|
|||||||
}
|
}
|
||||||
value := moduleValue[DerivedDailySummaryModule](t, output)
|
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 {
|
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)
|
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 {
|
if morning.TempRangeF != "58" || morning.MaxPopPercent == nil || *morning.MaxPopPercent != 60 {
|
||||||
t.Fatalf("morning = %#v, want temp range and precip peak", morning)
|
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"]
|
afternoon := value["afternoon"]
|
||||||
if !afternoon.Heat || !afternoon.Wind || afternoon.MaxWindGustMph == nil || *afternoon.MaxWindGustMph != 42 {
|
if !afternoon.Heat || !afternoon.Wind || afternoon.MaxWindGustMph == nil || *afternoon.MaxWindGustMph != 42 {
|
||||||
t.Fatalf("afternoon = %#v, want heat and wind hazard values", afternoon)
|
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)
|
t.Fatalf("marshal daypart summaries: %v", err)
|
||||||
}
|
}
|
||||||
jsonText := string(data)
|
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) {
|
if !strings.Contains(jsonText, field) {
|
||||||
t.Fatalf("daypart json = %s, want field %s", 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) {
|
func TestOutdoorWindowsAndTomorrowPlanningModulesPreserveDailyContent(t *testing.T) {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ type HourlyForecastModule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HourlyForecastPeriod struct {
|
type HourlyForecastPeriod struct {
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime string `json:"start_time,omitempty"`
|
||||||
EndTime time.Time `json:"end_time"`
|
EndTime string `json:"end_time,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
IsDay *bool `json:"is_day,omitempty"`
|
IsDay *bool `json:"is_day,omitempty"`
|
||||||
ConditionCode *int `json:"condition_code,omitempty"`
|
ConditionCode *int `json:"condition_code,omitempty"`
|
||||||
@@ -63,7 +63,7 @@ func buildHourlyForecastModule(ctx ModuleContext, _ any) (*module.Output, error)
|
|||||||
UpdatedAt: copyTime(hourly.UpdatedAt),
|
UpdatedAt: copyTime(hourly.UpdatedAt),
|
||||||
SourceLocation: hourly.LocationName,
|
SourceLocation: hourly.LocationName,
|
||||||
SourceLocationID: hourly.LocationID,
|
SourceLocationID: hourly.LocationID,
|
||||||
Periods: hourlyForecastPeriods(ctx.Derived.ValidPeriodHourlyPeriods),
|
Periods: hourlyForecastPeriods(ctx.Derived.ValidPeriodHourlyPeriods, ctx.Timezone),
|
||||||
}
|
}
|
||||||
if value.isEmpty() {
|
if value.isEmpty() {
|
||||||
return nil, nil
|
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
|
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))
|
out := make([]HourlyForecastPeriod, 0, len(periods))
|
||||||
for _, period := range periods {
|
for _, period := range periods {
|
||||||
out = append(out, HourlyForecastPeriod{
|
out = append(out, HourlyForecastPeriod{
|
||||||
StartTime: period.StartTime,
|
StartTime: friendlyDateTimeLabel(period.StartTime, timezone),
|
||||||
EndTime: period.EndTime,
|
EndTime: friendlyDateTimeLabel(period.EndTime, timezone),
|
||||||
Name: period.Name,
|
Name: period.Name,
|
||||||
IsDay: copyBool(period.IsDay),
|
IsDay: copyBool(period.IsDay),
|
||||||
ConditionCode: copyInt(period.ConditionCode),
|
ConditionCode: copyInt(period.ConditionCode),
|
||||||
|
|||||||
@@ -74,6 +74,47 @@ func periodClockLabel(period timeutil.Period, timezone string) string {
|
|||||||
return clockLabel(period.Start, timezone) + "-" + clockLabel(period.End, timezone)
|
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 {
|
func clockLabel(value time.Time, timezone string) string {
|
||||||
location, err := timeutil.LoadLocation(timezone)
|
location, err := timeutil.LoadLocation(timezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ type NarrativeForecastModule struct {
|
|||||||
|
|
||||||
type NarrativeForecastPeriod struct {
|
type NarrativeForecastPeriod struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime string `json:"start_time,omitempty"`
|
||||||
EndTime time.Time `json:"end_time"`
|
EndTime string `json:"end_time,omitempty"`
|
||||||
IsDay *bool `json:"is_day,omitempty"`
|
IsDay *bool `json:"is_day,omitempty"`
|
||||||
TextDescription string `json:"text_description,omitempty"`
|
TextDescription string `json:"text_description,omitempty"`
|
||||||
TemperatureC *float64 `json:"temperature_c,omitempty"`
|
TemperatureC *float64 `json:"temperature_c,omitempty"`
|
||||||
@@ -47,7 +47,7 @@ func buildNarrativeForecastModule(ctx ModuleContext, _ any) (*module.Output, err
|
|||||||
UpdatedAt: copyTime(narrative.UpdatedAt),
|
UpdatedAt: copyTime(narrative.UpdatedAt),
|
||||||
SourceLocation: narrative.LocationName,
|
SourceLocation: narrative.LocationName,
|
||||||
SourceLocationID: narrative.LocationID,
|
SourceLocationID: narrative.LocationID,
|
||||||
Periods: narrativeForecastPeriods(ctx.Derived.ValidPeriodNarrativePeriods),
|
Periods: narrativeForecastPeriods(ctx.Derived.ValidPeriodNarrativePeriods, ctx.Timezone),
|
||||||
}
|
}
|
||||||
if value.isEmpty() {
|
if value.isEmpty() {
|
||||||
return nil, nil
|
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
|
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))
|
out := make([]NarrativeForecastPeriod, 0, len(periods))
|
||||||
for _, period := range periods {
|
for _, period := range periods {
|
||||||
out = append(out, NarrativeForecastPeriod{
|
out = append(out, NarrativeForecastPeriod{
|
||||||
Name: period.Name,
|
Name: period.Name,
|
||||||
StartTime: period.StartTime,
|
StartTime: friendlyDateTimeLabel(period.StartTime, timezone),
|
||||||
EndTime: period.EndTime,
|
EndTime: friendlyDateTimeLabel(period.EndTime, timezone),
|
||||||
IsDay: copyBool(period.IsDay),
|
IsDay: copyBool(period.IsDay),
|
||||||
TextDescription: period.TextDescription,
|
TextDescription: period.TextDescription,
|
||||||
TemperatureC: copyFloat(period.TemperatureC),
|
TemperatureC: copyFloat(period.TemperatureC),
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Thresholds struct {
|
type Thresholds struct {
|
||||||
@@ -84,7 +83,8 @@ type dailySummaryStanza struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type daypartSummaryStanza struct {
|
type daypartSummaryStanza struct {
|
||||||
Period timeutil.Period `json:"period"`
|
Date string `json:"date,omitempty"`
|
||||||
|
Period string `json:"period,omitempty"`
|
||||||
TempRangeF string `json:"temp_range_f,omitempty"`
|
TempRangeF string `json:"temp_range_f,omitempty"`
|
||||||
MaxPopPercent *int `json:"max_pop_percent,omitempty"`
|
MaxPopPercent *int `json:"max_pop_percent,omitempty"`
|
||||||
MaxPopTime string `json:"max_pop_time,omitempty"`
|
MaxPopTime string `json:"max_pop_time,omitempty"`
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package changes
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCompareDailyNoMeaningfulChanges(t *testing.T) {
|
func TestCompareDailyNoMeaningfulChanges(t *testing.T) {
|
||||||
@@ -98,7 +96,7 @@ func dailySnapshot(t *testing.T, low int, high int, precip int, precipTime strin
|
|||||||
DailyPrecipitationProbability: &precip,
|
DailyPrecipitationProbability: &precip,
|
||||||
}},
|
}},
|
||||||
module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]daypartSummaryStanza{
|
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.PrecipTiming, StanzaName: "precip_timing", Value: precipTimingStanza{MaxPopPercent: &precip, MaxPopTime: precipTime}},
|
||||||
module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: alertDigestStanza{Relevant: relevant}},
|
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
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
func period(start string, end string) timeutil.Period {
|
|
||||||
return timeutil.Period{Start: at(start), End: at(end)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testThresholds() Thresholds {
|
func testThresholds() Thresholds {
|
||||||
return Thresholds{
|
return Thresholds{
|
||||||
TemperatureDegrees: 5,
|
TemperatureDegrees: 5,
|
||||||
@@ -136,11 +130,3 @@ func countType(changes []Change, changeType string) int {
|
|||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func at(value string) time.Time {
|
|
||||||
parsed, err := time.Parse(time.RFC3339, value)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
"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) {
|
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 {
|
func daypartDate(daypart daypartSummaryStanza) string {
|
||||||
if !daypart.Period.Start.IsZero() {
|
return daypart.Date
|
||||||
return daypart.Period.Start.Format(timeutil.DateLayout)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func minInt(a *int, b *int) *int {
|
func minInt(a *int, b *int) *int {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ func outlookSnapshot(t *testing.T, date string, tempRange string, precip int, pr
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
return snapshot(t, module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]daypartSummaryStanza{
|
return snapshot(t, module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]daypartSummaryStanza{
|
||||||
date + "_morning": {
|
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,
|
TempRangeF: tempRange,
|
||||||
MaxPopPercent: &precip,
|
MaxPopPercent: &precip,
|
||||||
MaxPopTime: precipTime,
|
MaxPopTime: precipTime,
|
||||||
|
|||||||
Reference in New Issue
Block a user