140 lines
4.0 KiB
Go
140 lines
4.0 KiB
Go
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
|
|
OutputCopyName 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
|
|
outputCopyName := "daily-" + date.In(location).Format(timeutil.DateLayout) + ".md"
|
|
planned, err = appendPlannedReport(planned, registry, report.Daily, dailyReq, outputCopyName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return planned, nil
|
|
}
|
|
|
|
func appendPlannedReport(planned []plannedBatchReport, registry report.Registry, id report.ID, req report.ResolveRequest, outputCopyName string) ([]plannedBatchReport, error) {
|
|
resolved, err := registry.Resolve(id, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(planned, plannedBatchReport{
|
|
Resolved: resolved,
|
|
OutputCopyName: outputCopyName,
|
|
}), 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)
|
|
}
|