// Package facts defines collected and derived report facts. package facts import ( "fmt" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) type CollectedFacts struct { FetchedAt time.Time Observation *weatherdata.Observation Current *weatherdata.Current Hourly *weatherdata.ForecastRun Narrative *weatherdata.ForecastRun Alerts *weatherdata.AlertRun Discussion *weatherdata.Discussion Daily *weatherdata.ForecastRun WeatherStory *weatherdata.WeatherStory SourceProvenance []weatherdata.Source SourceWarnings []weatherdata.SourceWarning } func BuildCollected(bundle *weatherdata.Bundle) CollectedFacts { if bundle == nil { return CollectedFacts{} } return CollectedFacts{ FetchedAt: bundle.FetchedAt, Observation: bundle.Observation, Current: bundle.Current, Hourly: bundle.Hourly, Narrative: bundle.Narrative, Alerts: bundle.Alerts, Discussion: bundle.Discussion, Daily: bundle.Daily, WeatherStory: bundle.WeatherStory, SourceProvenance: append([]weatherdata.Source(nil), bundle.Sources...), SourceWarnings: append([]weatherdata.SourceWarning(nil), bundle.Warnings...), } } func (f CollectedFacts) Bundle() *weatherdata.Bundle { return &weatherdata.Bundle{ FetchedAt: f.FetchedAt, Observation: f.Observation, Current: f.Current, Hourly: f.Hourly, Narrative: f.Narrative, Alerts: f.Alerts, Discussion: f.Discussion, Daily: f.Daily, WeatherStory: f.WeatherStory, Sources: append([]weatherdata.Source(nil), f.SourceProvenance...), Warnings: append([]weatherdata.SourceWarning(nil), f.SourceWarnings...), } } type BuildDerivedRequest struct { Resolved report.Resolved Timezone string Dayparts []forecast.DaypartDefinition Collected CollectedFacts } type DerivedFacts struct { ValidPeriodHourlyPeriods []weatherdata.ForecastPeriod ValidPeriodNarrativePeriods []weatherdata.ForecastPeriod ValidPeriodDailyPeriods []weatherdata.ForecastPeriod AlertOverlaps []forecast.AlertOverlap DailySummaries []forecast.DailySummary DaypartSummaries []forecast.DaypartSummary PrecipTiming forecast.PrecipTiming StormWindowSummary *forecast.DaypartSummary } func (f DerivedFacts) FirstDailySummary() *forecast.DailySummary { if len(f.DailySummaries) == 0 { return nil } return &f.DailySummaries[0] } func BuildDerived(req BuildDerivedRequest) (DerivedFacts, error) { if !req.Resolved.ValidPeriod.IsValid() { return DerivedFacts{}, fmt.Errorf("resolved valid period is required") } location, err := timeutil.LoadLocation(req.Timezone) if err != nil { return DerivedFacts{}, fmt.Errorf("load report timezone %q: %w", req.Timezone, err) } bundle := req.Collected.Bundle() period := req.Resolved.ValidPeriod derived := DerivedFacts{ ValidPeriodHourlyPeriods: forecast.SelectHourlyPeriods(req.Collected.Hourly, period), ValidPeriodNarrativePeriods: forecast.SelectHourlyPeriods(req.Collected.Narrative, period), ValidPeriodDailyPeriods: forecast.SelectHourlyPeriods(req.Collected.Daily, period), AlertOverlaps: forecast.AlertOverlaps(req.Collected.Alerts, period), } derived.PrecipTiming = forecast.BuildPrecipTiming(derived.ValidPeriodHourlyPeriods) switch req.Resolved.Definition.ID { case report.DailyToday, report.DailyTomorrow: summary, err := forecast.BuildDailySummary(bundle, period.Start, location, req.Dayparts) if err != nil { return DerivedFacts{}, err } derived.DailySummaries = []forecast.DailySummary{*summary} case report.ThreeDay, report.Weekend: summaries, err := forecast.BuildPeriodDailySummaries(bundle, period, location, req.Dayparts) if err != nil { return DerivedFacts{}, err } derived.DailySummaries = summaries case report.Storm: summary := forecast.SummarizeDaypart("storm window", period, derived.ValidPeriodHourlyPeriods) summary.AlertOverlaps = derived.AlertOverlaps derived.StormWindowSummary = &summary default: return DerivedFacts{}, fmt.Errorf("derived facts are not implemented for report %q", req.Resolved.Definition.ID) } derived.DaypartSummaries = collectDaypartSummaries(derived) return derived, nil } func collectDaypartSummaries(derived DerivedFacts) []forecast.DaypartSummary { var out []forecast.DaypartSummary for _, summary := range derived.DailySummaries { out = append(out, summary.Dayparts...) } if derived.StormWindowSummary != nil { out = append(out, *derived.StormWindowSummary) } return out }