package app import ( "fmt" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/collect" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) type plannedBatchReport struct { Resolved report.Resolved OutputPath string } func planBatchRun(req BatchRequest, now time.Time, collection collect.Result) ([]plannedBatchReport, error) { location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone) if err != nil { return nil, err } batch, err := report.BatchForCommandName(string(req.Batch)) if err != nil { return nil, err } registry, err := reportRegistry(req.Config) if err != nil { return nil, err } resolveReq := report.ResolveRequest{ Now: now, Location: location, } var planned []plannedBatchReport switch batch { case report.Morning: planned, err = appendPlannedReport(planned, registry, report.Today, resolveReq) if err != nil { return nil, err } planned, err = appendPlannedReport(planned, registry, report.Tomorrow, resolveReq) if err != nil { return nil, err } case report.Evening: planned, err = appendPlannedReport(planned, registry, report.Tomorrow, resolveReq) if err != nil { return nil, err } default: return nil, fmt.Errorf("unknown batch %q", batch) } var hourly *weatherdata.ForecastRun if collection.Bundle != nil { hourly = collection.Bundle.Hourly } for _, date := range eligibleDailyDates(hourly, now, location) { dailyReq := resolveReq dailyReq.Date = date planned, err = appendPlannedReport(planned, registry, report.Daily, dailyReq) if err != nil { return nil, err } } return planned, nil } func appendPlannedReport(planned []plannedBatchReport, registry report.Registry, id report.ID, req report.ResolveRequest) ([]plannedBatchReport, error) { resolved, err := registry.Resolve(id, req) if err != nil { return nil, err } return append(planned, plannedBatchReport{Resolved: resolved}), nil } func eligibleDailyDates(hourly *weatherdata.ForecastRun, now time.Time, location *time.Location) []time.Time { if hourly == nil || location == nil || hourly.Product != "hourly" || len(hourly.Periods) == 0 { return nil } hourlyStarts := make(map[time.Time]struct{}, len(hourly.Periods)) var maxLocalDate time.Time for _, period := range hourly.Periods { if !isHourlyPeriod(period) { continue } start := period.StartTime hourlyStarts[instantKey(start)] = struct{}{} localDate := localDateStart(start, location) if maxLocalDate.IsZero() || localDate.After(maxLocalDate) { maxLocalDate = localDate } } if len(hourlyStarts) == 0 || maxLocalDate.IsZero() { return nil } startDate := localDateStart(now.In(location).AddDate(0, 0, 2), location) var dates []time.Time for candidate := startDate; !candidate.After(maxLocalDate); candidate = candidate.AddDate(0, 0, 1) { if hasFullHourlyCoverage(candidate, location, hourlyStarts) { dates = append(dates, candidate) } } return dates } func isHourlyPeriod(period weatherdata.ForecastPeriod) bool { if period.StartTime.IsZero() || period.EndTime.IsZero() { return false } return period.EndTime.Equal(period.StartTime.Add(time.Hour)) } func hasFullHourlyCoverage(date time.Time, location *time.Location, hourlyStarts map[time.Time]struct{}) bool { day := timeutil.CivilDay(date, location) for required := day.Start; required.Before(day.End); required = required.Add(time.Hour) { if _, ok := hourlyStarts[instantKey(required)]; !ok { return false } } return true } func instantKey(value time.Time) time.Time { return value.UTC() } func localDateStart(value time.Time, location *time.Location) time.Time { local := value.In(location) return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, location) }