package engine import ( "fmt" "strconv" "time" "github.com/google/uuid" ) // ParseRecurrencePattern converts "1w", "2d", "1m" to time.Duration func ParseRecurrencePattern(pattern string) (time.Duration, error) { if len(pattern) < 2 { return 0, fmt.Errorf("invalid recurrence pattern: %s", pattern) } numStr := pattern[:len(pattern)-1] unit := pattern[len(pattern)-1] num, err := strconv.Atoi(numStr) if err != nil { return 0, fmt.Errorf("invalid number in pattern: %s", pattern) } switch unit { case 'd': return time.Duration(num) * 24 * time.Hour, nil case 'w': return time.Duration(num) * 7 * 24 * time.Hour, nil case 'm': // Approximate: 30 days return time.Duration(num) * 30 * 24 * time.Hour, nil case 'y': // Approximate: 365 days return time.Duration(num) * 365 * 24 * time.Hour, nil default: return 0, fmt.Errorf("unknown unit: %c (use d/w/m/y)", unit) } } // FormatRecurrenceDuration converts time.Duration back to "1w", "2d" format func FormatRecurrenceDuration(d time.Duration) string { days := int(d.Hours() / 24) if days%365 == 0 && days/365 > 0 { return fmt.Sprintf("%dy", days/365) } if days%30 == 0 && days/30 > 0 { return fmt.Sprintf("%dm", days/30) } if days%7 == 0 && days/7 > 0 { return fmt.Sprintf("%dw", days/7) } return fmt.Sprintf("%dd", days) } // CalculateNextDue calculates next due date based on current and recurrence func CalculateNextDue(currentDue time.Time, recurrence time.Duration) time.Time { return currentDue.Add(recurrence) } // SpawnNextInstance creates a new task instance from completed recurring task func SpawnNextInstance(completedInstance *Task) error { if completedInstance.ParentUUID == nil { return fmt.Errorf("task is not a recurring instance") } // Load template template, err := GetTask(*completedInstance.ParentUUID) if err != nil { return fmt.Errorf("failed to load template: %w", err) } if template.RecurrenceDuration == nil { return fmt.Errorf("template has no recurrence duration") } // Calculate next due date var nextDue *time.Time if completedInstance.Due != nil { next := CalculateNextDue(*completedInstance.Due, *template.RecurrenceDuration) nextDue = &next } // Check if we're past 'until' date if template.Until != nil && nextDue != nil && nextDue.After(*template.Until) { // Don't spawn, recurrence has expired return nil } // Create new instance now := time.Now() newInstance := &Task{ UUID: uuid.New(), Status: StatusPending, Description: template.Description, Project: template.Project, Priority: template.Priority, Created: now, Modified: now, Due: nextDue, Scheduled: template.Scheduled, Wait: template.Wait, Until: template.Until, ParentUUID: &template.UUID, Tags: []string{}, } if err := newInstance.Save(); err != nil { return fmt.Errorf("failed to save new instance: %w", err) } // Copy tags from template templateTags, err := template.GetTags() if err != nil { return fmt.Errorf("failed to get template tags: %w", err) } for _, tag := range templateTags { if err := newInstance.AddTag(tag); err != nil { return fmt.Errorf("failed to add tag: %w", err) } } return nil }