-// Set adds a schedule with text and returns the answer of the tool. Exactly one
-// of at and cron is not nil. at is a local time in scheduleAtLayout after now,
-// and it goes through scheduleWallTime, so a time in the hour that the clock
-// repeats is its first occurrence. cron is an expression of scheduleParseCron.
-func (s *Schedules) Set(text string, at, cron *string) (string, error) {
- text = strings.TrimSpace(text)
- if text == "" || len(text) > scheduleTextBytes {
- return "", fmt.Errorf("the text must have 1 to %d bytes", scheduleTextBytes)
- }
-
- now := s.clock()
- schedule := Schedule{
- Text: text,
- Owner: s.owner,
- }
-
- switch {
- case (at == nil) == (cron == nil):
- return "", errors.New("give either at or cron, and null for the other")
- case at != nil:
- parsed, err := time.Parse(scheduleAtLayout, *at)
- if err != nil {
- return "", fmt.Errorf("at must be a local time such as %s", now.Format(scheduleAtLayout))
- }
-
- schedule.Due = scheduleWallTime(parsed.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), now.Location())
- if !schedule.Due.After(now) {
- return "", fmt.Errorf("the time %s is not after now, %s", schedule.Due.Format(scheduleTimeLayout), now.Format(scheduleTimeLayout))
- }
- default:
- parsed, err := scheduleParseCron(*cron)
- if err != nil {
- return "", err
- }
-
- next, found := parsed.Next(now)
- if !found {
- return "", fmt.Errorf("the cron expression %q matches no time in %d years", *cron, scheduleCronYears)
- }
-
- schedule.Cron = parsed.String()
- schedule.Due = next
- }
-
- s.mutex.Lock()
- defer s.mutex.Unlock()
-
- s.file.Next++
- schedule.ID = s.file.Next
- s.file.Schedules = append(s.file.Schedules, schedule)
- err := s.save()
- if err != nil {
- return "", err
- }
-
- s.change()
-
- return fmt.Sprintf("Schedule %d set. Next: %s.", schedule.ID, schedule.Due.In(now.Location()).Format(scheduleTimeLayout)), nil
-}
-
-// List returns the answer of the tool: one line for each schedule, by due time,
-// with its ID, its next time, its cron expression or once, and its text on one
-// line.
-func (s *Schedules) List() string {
- location := s.clock().Location()
- schedules := s.sorted()
- if len(schedules) == 0 {
- return "No schedules."
- }
-
- lines := make([]string, 0, len(schedules))
- for _, schedule := range schedules {
- lines = append(lines, strconv.Itoa(schedule.ID)+" | "+schedule.Due.In(location).Format(scheduleTimeLayout)+" | "+cmp.Or(schedule.Cron, "once")+" | "+strings.Join(strings.Fields(schedule.Text), " "))
- }
-
- return strings.Join(lines, "\n")
-}
-
-// Len returns the number of schedules.
-func (s *Schedules) Len() int {
- s.mutex.Lock()
- defer s.mutex.Unlock()
-
- return len(s.file.Schedules)
-}
-
-// Remove removes the schedule id and returns the answer of the tool.
-func (s *Schedules) Remove(id int) (string, error) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
-
- index := slices.IndexFunc(s.file.Schedules, func(schedule Schedule) bool {
- return schedule.ID == id
- })
-
- if index < 0 {
- return "", fmt.Errorf("no schedule %d", id)
- }
-
- s.file.Schedules = slices.Delete(s.file.Schedules, index, index+1)
- err := s.save()
- if err != nil {
- return "", err
- }
-
- s.change()
-
- return fmt.Sprintf("Schedule %d removed.", id), nil
-}
-