diff --git a/internal/control/cli/controller.go b/internal/control/cli/controller.go index 9aa94084..a4be60c6 100644 --- a/internal/control/cli/controller.go +++ b/internal/control/cli/controller.go @@ -1963,18 +1963,84 @@ func (c *Controller) getCurrentMonthEvents() ([]*model.Event, error) { return events, nil } +func (c *Controller) ensureCurrentEventVisible() { + e := c.data.CurrentEvent + if e != nil { + c.ensureEventsPaneTimestampWithinVisibleScroll(e.Start) + c.ensureEventsPaneTimestampWithinVisibleScroll(e.End) + } +} + func (c *Controller) switchToNextEventInDay() { - if c.data.CurrentEvent != nil { - c.ensureEventsPaneTimestampWithinVisibleScroll(c.data.CurrentEvent.Start) - c.ensureEventsPaneTimestampWithinVisibleScroll(c.data.CurrentEvent.End) + defer c.ensureCurrentEventVisible() + + if c.data.CurrentEvent == nil { + candidate, err := c.dataProvider.GetEventAfter(c.data.CurrentDate.ToGotime()) + if err != nil { + c.log.Error().Err(err).Stringer("date", c.data.CurrentDate).Msg("could not get next for current date") + return + } + if model.DateFromGotime(candidate.Start) == c.data.CurrentDate { + c.data.CurrentEvent = candidate + c.log.Debug().Stringer("event", candidate).Msg("switched to next event") + return + } + c.log.Debug().Msg("no event on current day") + return + } + + next, err := c.dataProvider.GetEventAfter(c.data.CurrentEvent.End) + if err != nil { + c.log.Error().Err(err).Stringer("date", c.data.CurrentDate).Msg("could not get next for current date") + return + } + if next == nil { + c.log.Info().Msg("there is no next event") + return + } + if model.DateFromGotime(next.Start) != c.data.CurrentDate { + c.log.Info().Msg("next event is on a different day") + return } + + c.data.CurrentEvent = next + c.log.Debug().Stringer("event", next).Msg("switched to next event") } func (c *Controller) switchToPreviousEventInDay() { - if c.data.CurrentEvent != nil { - c.ensureEventsPaneTimestampWithinVisibleScroll(c.data.CurrentEvent.Start) - c.ensureEventsPaneTimestampWithinVisibleScroll(c.data.CurrentEvent.End) + defer c.ensureCurrentEventVisible() + + if c.data.CurrentEvent == nil { + candidate, err := c.dataProvider.GetEventBefore(c.data.CurrentDate.ToGotime().Add(24 * time.Hour)) + if err != nil { + c.log.Error().Err(err).Stringer("date", c.data.CurrentDate).Msg("could not get prev for current date") + return + } + if model.DateFromGotime(candidate.Start) == c.data.CurrentDate { + c.data.CurrentEvent = candidate + c.log.Debug().Stringer("event", candidate).Msg("switched to prev event") + return + } + c.log.Debug().Msg("no event on current day") + return } + + prev, err := c.dataProvider.GetEventBefore(c.data.CurrentEvent.Start) + if err != nil { + c.log.Error().Err(err).Stringer("date", c.data.CurrentDate).Msg("could not get prev for current date") + return + } + if prev == nil { + c.log.Info().Msg("there is no prev event") + return + } + if model.DateFromGotime(prev.Start) != c.data.CurrentDate { + c.log.Info().Msg("prev event is on a different day") + return + } + + c.data.CurrentEvent = prev + c.log.Debug().Stringer("event", prev).Msg("switched to prev event") } func (c *Controller) moveEventsForwardPushing() error { diff --git a/internal/control/cli/timesheet.go b/internal/control/cli/timesheet.go index 30a0b3a1..50d862d2 100644 --- a/internal/control/cli/timesheet.go +++ b/internal/control/cli/timesheet.go @@ -88,12 +88,12 @@ func (command *TimesheetCommand) Execute(args []string) error { styledCategories.Add(cat, style) } - startDate, err := model.FromString(command.FromDay) + startDate, err := model.DateFromString(command.FromDay) if err != nil { log.Fatal().Msgf("from date '%s' invalid", command.FromDay) } currentDate := startDate - finalDate, err := model.FromString(command.TilDay) + finalDate, err := model.DateFromString(command.TilDay) if err != nil { log.Fatal().Msgf("til date '%s' invalid", command.TilDay) } diff --git a/internal/control/cli/tui.go b/internal/control/cli/tui.go index 53c9be1a..4e93dfd1 100644 --- a/internal/control/cli/tui.go +++ b/internal/control/cli/tui.go @@ -73,7 +73,7 @@ func (command *TUICommand) Execute(_ []string) error { if command.Day == "" { initialDay = model.Date{Year: now.Year(), Month: int(now.Month()), Day: now.Day()} } else { - initialDay, err = model.FromString(command.Day) + initialDay, err = model.DateFromString(command.Day) if err != nil { return fmt.Errorf("could not parse given date (%w)", err) } diff --git a/internal/model/date_and_time.go b/internal/model/date_and_time.go index a5942ae5..6653a157 100644 --- a/internal/model/date_and_time.go +++ b/internal/model/date_and_time.go @@ -105,8 +105,14 @@ func (d Date) Valid() bool { return true } -// FromString creates a date from a string in the format "YYYY-MM-DD". -func FromString(s string) (Date, error) { +type DateSlice []Date + +func (a DateSlice) Len() int { return len(a) } +func (a DateSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a DateSlice) Less(i, j int) bool { return a[i].IsBefore(a[j]) } + +// DateFromString creates a date from a string in the format "YYYY-MM-DD". +func DateFromString(s string) (Date, error) { var result Date var err error diff --git a/internal/model/goal.go b/internal/model/goal.go index d8bcced5..684c1e2c 100644 --- a/internal/model/goal.go +++ b/internal/model/goal.go @@ -47,11 +47,11 @@ func NewRangedGoalFromConfig(cfg []config.RangedGoal) (*RangedGoal, error) { result := RangedGoal{} for i := range cfg { - start, err := FromString(cfg[i].Start) + start, err := DateFromString(cfg[i].Start) if err != nil { return nil, fmt.Errorf("error parsing start date of range no. %d: %w", i, err) } - end, err := FromString(cfg[i].End) + end, err := DateFromString(cfg[i].End) if err != nil { return nil, fmt.Errorf("error parsing start date of range no. %d: %w", i, err) } diff --git a/internal/storage/providers/files_provider.go b/internal/storage/providers/files_provider.go index badceb9d..7adbcf9e 100644 --- a/internal/storage/providers/files_provider.go +++ b/internal/storage/providers/files_provider.go @@ -2,6 +2,9 @@ package providers import ( "fmt" + "os" + "regexp" + "sort" "sync" "time" @@ -14,6 +17,8 @@ import ( const notSameDayEventErrorMsg = string("event does not start and end on the same day") +var fileDateNamingRegex = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) + // FilesDataProvider ... type FilesDataProvider struct { BasePath string @@ -96,15 +101,74 @@ func (p *FilesDataProvider) RemoveEvents([]storage.EventIdentifier) error { return nil } -// TODO: doc GetEventAfter -func (p *FilesDataProvider) GetEventAfter(time.Time) (*model.Event, error) { - p.log.Fatal().Msg("TODO IMPL(GetEventAfter)") +// GetEventAfter retrieves the first event after the specified time. +func (p *FilesDataProvider) GetEventAfter(t time.Time) (*model.Event, error) { + p.log.Debug().Msgf("getting first event after %s", t.String()) + defer p.log.Debug().Msgf("done getting first event after %s", t.String()) + + availableDates, err := p.getAvailableDates() + if err != nil { + return nil, fmt.Errorf("error getting available dates (%w)", err) + } + p.log.Trace().Msgf("have %d available dates", len(availableDates)) + + sort.Sort(model.DateSlice(availableDates)) + + dateForT := model.DateFromGotime(t) + + for _, d := range availableDates { + if d.IsBefore(dateForT) { + p.log.Trace().Msgf("skipping date '%s' because it is before the target time", d.String()) + continue + } + p.log.Trace().Msgf("getting file handler for date '%s'", d.String()) + fh, err := p.getFileHandler(d) + if err != nil { + return nil, fmt.Errorf("error getting file handler for date '%s', which should not happen since the file should exist (%w)", d.String(), err) + } + for _, event := range fh.data.Events { + if event.Start == t || event.Start.After(t) { + p.log.Trace().Msgf("found event starting after target time: %s", event.String()) + return event, nil + } + } + } return nil, nil } // TODO: doc GetEventBefore -func (p *FilesDataProvider) GetEventBefore(time.Time) (*model.Event, error) { - p.log.Fatal().Msg("TODO IMPL(GetEventBefore)") +func (p *FilesDataProvider) GetEventBefore(t time.Time) (*model.Event, error) { + p.log.Debug().Msgf("getting last event before %s", t.String()) + defer p.log.Debug().Msgf("done getting last event before %s", t.String()) + + availableDates, err := p.getAvailableDates() + if err != nil { + return nil, fmt.Errorf("error getting available dates (%w)", err) + } + p.log.Trace().Msgf("have %d available dates", len(availableDates)) + + sort.Sort(sort.Reverse(model.DateSlice(availableDates))) + + dateForT := model.DateFromGotime(t) + + for _, d := range availableDates { + if d.IsAfter(dateForT) { + p.log.Trace().Msgf("skipping date '%s' because it is after the target time", d.String()) + continue + } + p.log.Trace().Msgf("getting file handler for date '%s'", d.String()) + fh, err := p.getFileHandler(d) + if err != nil { + return nil, fmt.Errorf("error getting file handler for date '%s', which should not happen since the file should exist (%w)", d.String(), err) + } + for i := len(fh.data.Events) - 1; i >= 0; i-- { + event := fh.data.Events[i] + if event.End == t || event.End.Before(t) { + p.log.Trace().Msgf("found event ending before target time: %s", event.String()) + return event, nil + } + } + } return nil, nil } @@ -279,3 +343,32 @@ func eventStartsAndEndsOnSameDate(e *model.Event) bool { func timesOnSameDate(a, b time.Time) bool { return a.Year() != b.Year() || a.YearDay() != b.YearDay() } + +// NOTE: this function is fine, but its use could be improved, because we really should only need to call this once +func (p *FilesDataProvider) getAvailableDates() ([]model.Date, error) { + p.log.Debug().Msg("getting available dates") + defer p.log.Debug().Msg("done getting available dates") + + files, err := os.ReadDir(p.BasePath) + if err != nil { + return nil, fmt.Errorf("error reading directory (%w)", err) + } + var dates []model.Date + for _, f := range files { + if f.IsDir() { + p.log.Trace().Msgf("skipping directory '%s'", f.Name()) + continue + } + if !fileDateNamingRegex.MatchString(f.Name()) { + p.log.Trace().Msgf("skipping non-date file '%s'", f.Name()) + continue + } + d, err := model.DateFromString(f.Name()) + if err != nil { + return nil, fmt.Errorf("error parsing date from file name '%s' (%w)", f.Name(), err) + } + dates = append(dates, d) + } + return dates, nil + +}