Files
tasksquire/timewarrior/models.go
2026-02-01 21:30:19 +01:00

277 lines
5.1 KiB
Go

package timewarrior
import (
"fmt"
"log/slog"
"math"
"strconv"
"strings"
"time"
)
const (
dtformat = "20060102T150405Z"
)
type Intervals []*Interval
type Interval struct {
ID int `json:"-"`
Start string `json:"start,omitempty"`
End string `json:"end,omitempty"`
Tags []string `json:"tags,omitempty"`
}
func NewInterval() *Interval {
return &Interval{
Tags: make([]string, 0),
}
}
func (i *Interval) GetString(field string) string {
switch field {
case "id":
return strconv.Itoa(i.ID)
case "start":
return formatDate(i.Start, "formatted")
case "end":
if i.End == "" {
return "now"
}
return formatDate(i.End, "formatted")
case "tags":
if len(i.Tags) == 0 {
return ""
}
return strings.Join(i.Tags, " ")
case "duration":
return i.GetDuration()
case "active":
if i.End == "" {
return "●"
}
return ""
default:
slog.Error(fmt.Sprintf("Field not implemented: %s", field))
return ""
}
}
func (i *Interval) GetDuration() string {
start, err := time.Parse(dtformat, i.Start)
if err != nil {
slog.Error("Failed to parse start time:", err)
return ""
}
var end time.Time
if i.End == "" {
end = time.Now()
} else {
end, err = time.Parse(dtformat, i.End)
if err != nil {
slog.Error("Failed to parse end time:", err)
return ""
}
}
duration := end.Sub(start)
return formatDuration(duration)
}
func (i *Interval) GetStartTime() time.Time {
dt, err := time.Parse(dtformat, i.Start)
if err != nil {
slog.Error("Failed to parse time:", err)
return time.Time{}
}
return dt
}
func (i *Interval) GetEndTime() time.Time {
if i.End == "" {
return time.Now()
}
dt, err := time.Parse(dtformat, i.End)
if err != nil {
slog.Error("Failed to parse time:", err)
return time.Time{}
}
return dt
}
func (i *Interval) HasTag(tag string) bool {
for _, t := range i.Tags {
if t == tag {
return true
}
}
return false
}
func (i *Interval) AddTag(tag string) {
if !i.HasTag(tag) {
i.Tags = append(i.Tags, tag)
}
}
func (i *Interval) RemoveTag(tag string) {
for idx, t := range i.Tags {
if t == tag {
i.Tags = append(i.Tags[:idx], i.Tags[idx+1:]...)
return
}
}
}
func (i *Interval) IsActive() bool {
return i.End == ""
}
func formatDate(date string, format string) string {
if date == "" {
return ""
}
dt, err := time.Parse(dtformat, date)
if err != nil {
slog.Error("Failed to parse time:", err)
return ""
}
switch format {
case "formatted", "":
return dt.Format("2006-01-02 15:04")
case "time":
return dt.Format("15:04")
case "date":
return dt.Format("2006-01-02")
case "iso":
return dt.Format("2006-01-02T150405Z")
case "epoch":
return strconv.FormatInt(dt.Unix(), 10)
case "age":
return parseDurationVague(time.Since(dt))
case "relative":
return parseDurationVague(time.Until(dt))
default:
slog.Error(fmt.Sprintf("Date format not implemented: %s", format))
return ""
}
}
func formatDuration(d time.Duration) string {
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
if hours > 0 {
return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
}
return fmt.Sprintf("%d:%02d", minutes, seconds)
}
func parseDurationVague(d time.Duration) string {
dur := d.Round(time.Second).Abs()
days := dur.Hours() / 24
var formatted string
if dur >= time.Hour*24*365 {
formatted = fmt.Sprintf("%.1fy", days/365)
} else if dur >= time.Hour*24*90 {
formatted = strconv.Itoa(int(math.Round(days/30))) + "mo"
} else if dur >= time.Hour*24*7 {
formatted = strconv.Itoa(int(math.Round(days/7))) + "w"
} else if dur >= time.Hour*24 {
formatted = strconv.Itoa(int(days)) + "d"
} else if dur >= time.Hour {
formatted = strconv.Itoa(int(dur.Round(time.Hour).Hours())) + "h"
} else if dur >= time.Minute {
formatted = strconv.Itoa(int(dur.Round(time.Minute).Minutes())) + "min"
} else if dur >= time.Second {
formatted = strconv.Itoa(int(dur.Round(time.Second).Seconds())) + "s"
}
if d < 0 {
formatted = "-" + formatted
}
return formatted
}
var (
dateFormats = []string{
"2006-01-02",
"2006-01-02T15:04",
"20060102T150405Z",
}
specialDateFormats = []string{
"",
"now",
"today",
"yesterday",
"tomorrow",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"mon",
"tue",
"wed",
"thu",
"fri",
"sat",
"sun",
}
)
func ValidateDate(s string) error {
for _, f := range dateFormats {
if _, err := time.Parse(f, s); err == nil {
return nil
}
}
for _, f := range specialDateFormats {
if s == f {
return nil
}
}
return fmt.Errorf("invalid date")
}
func ValidateDuration(s string) error {
// TODO: implement proper duration validation
// Should accept formats like: 1h, 30m, 1h30m, etc.
return nil
}
// Summary represents time tracking summary data
type Summary struct {
Range string
TotalTime time.Duration
ByTag map[string]time.Duration
}
func (s *Summary) GetTotalString() string {
return formatDuration(s.TotalTime)
}
func (s *Summary) GetTagTime(tag string) string {
if duration, ok := s.ByTag[tag]; ok {
return formatDuration(duration)
}
return "0:00"
}