Integrate timewarrior
This commit is contained in:
277
timewarrior/models.go
Normal file
277
timewarrior/models.go
Normal file
@ -0,0 +1,277 @@
|
||||
package timewarrior
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
}
|
||||
Reference in New Issue
Block a user