package timertxt import ( "errors" "fmt" "regexp" "sort" "strings" "time" ) var ( // DateLayout is used for formatting time.Time into timer.txt date format and vice-versa. DateLayout = time.RFC3339 addonTagRx = regexp.MustCompile(`(^|\s+)([\w-]+):(\S+)`) // Match additional tags date: '... due:2012-12-12 ...' contextRx = regexp.MustCompile(`(^|\s+)@(\S+)`) // Match contexts: '@Context ...' or '... @Context ...' projectRx = regexp.MustCompile(`(^|\s+)\+(\S+)`) // Match projects: '+Project...' or '... +Project ...') ) type Timer struct { Id int `json:"id"` // Internal timer id Original string `json:"original"` // Original raw timer text StartDate time.Time `json:"startDate"` FinishDate time.Time `json:"finishDate"` Finished bool `json:"finished"` Notes string `json:"notes"` // Notes part of timer text Projects []string `json:"projects"` Contexts []string `json:"contexts"` AdditionalTags map[string]string `json:"additionalTags"` // Addon tags will be available here } // String returns a complete timer string in timer.txt format. // // Contexts, Projects, and additional tags are alphabetically sorted, // and appended at the end in the following order: // Contexts, Projects, Tags // // For example: // "2019-02-15T11:43:00-0600 Working on Go Library @home @personal +timertxt customTag1:Important! due:Today" // "x 2019-02-15T06:00:00-0600 2019-02-15T10:00:00-0600 Creating Go Library Repo @home @personal +timertxt customTag1:Important! due:Today" func (timer Timer) String() string { var text string if timer.Finished { text += "x " } text += fmt.Sprintf("%s ", timer.StartDate.Format(DateLayout)) if !timer.FinishDate.IsZero() { text += fmt.Sprintf("%s", timer.FinishDate.Format(DateLayout)) } if len(timer.Notes) > 0 { text += " " + timer.Notes } if len(timer.Contexts) > 0 { sort.Strings(timer.Contexts) for _, context := range timer.Contexts { text += fmt.Sprintf(" @%s", context) } } if len(timer.Projects) > 0 { sort.Strings(timer.Projects) for _, project := range timer.Projects { text += fmt.Sprintf(" +%s", project) } } if len(timer.AdditionalTags) > 0 { // Sort map alphabetically by keys keys := make([]string, 0, len(timer.AdditionalTags)) for key := range timer.AdditionalTags { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { text += fmt.Sprintf(" %s:%s", key, timer.AdditionalTags[key]) } } return text } // NewTimer creates a new empty Timer with default values. (StartDate is set to Now()) func NewTimer() *Timer { timer := Timer{} timer.StartDate = time.Now() timer.AdditionalTags = make(map[string]string) return &timer } // ParseTimer parses the input text string into a Timer struct func ParseTimer(text string) (*Timer, error) { var err error timer := Timer{ AdditionalTags: make(map[string]string), } timer.Original = strings.Trim(text, "\t\n\r ") originalParts := strings.Fields(timer.Original) // Check for finished if originalParts[0] == "x" { timer.Finished = true originalParts = originalParts[1:] } if timer.StartDate, err = time.Parse(DateLayout, originalParts[0]); err != nil { return nil, errors.New("Unable to parse StartDate: " + err.Error()) } originalParts = originalParts[1:] if timer.Finished { // If it's finished, there _must_ be a finished date if timer.FinishDate, err = time.Parse(DateLayout, originalParts[0]); err != nil { return nil, errors.New("Timer marked finished, but failed to parse FinishDate: " + err.Error()) } originalParts = originalParts[1:] } var notes []string for _, v := range originalParts { if strings.HasPrefix(v, "@") { v = strings.TrimPrefix(v, "@") // Contexts timer.Contexts = append(timer.Contexts, v) } else if strings.HasPrefix(v, "+") { // Projects v = strings.TrimPrefix(v, "+") timer.Projects = append(timer.Projects, v) } else if strings.Contains(v, ":") { // Additional tags tagPts := strings.Split(v, ":") if tagPts[0] != "" && tagPts[1] != "" { timer.AdditionalTags[tagPts[0]] = tagPts[1] } } else { notes = append(notes, v) } } timer.Notes = strings.Join(notes, " ") return &timer, nil } // Timer returns a complete timer string in timer.txt format. // See *Timer.String() for further information func (timer *Timer) Timer() string { return timer.String() } // Finish sets Timer.Finished to true if the timer hasn't already been finished. // Also sets Timer.FinishDate to time.Now() func (timer *Timer) Finish() { if !timer.Finished { timer.Finished = true timer.FinishDate = time.Now() } } // Reopen sets Timer.Finished to 'false' if the timer was finished // Also resets Timer.FinishDate func (timer *Timer) Reopen() { if timer.Finished { timer.Finished = false timer.FinishDate = time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) // time.IsZero() value } } func (timer *Timer) Duration() time.Duration { end := time.Now() if !timer.FinishDate.IsZero() { end = timer.FinishDate } return end.Sub(timer.StartDate) } func (timer *Timer) StartsToday() bool { currTime := time.Now() dur := int64(currTime.Hour())*int64(time.Hour) + int64(currTime.Minute())*int64(time.Minute) return int64(time.Since(timer.StartDate)) < dur } func (timer *Timer) EndsToday() bool { currTime := time.Now() dur := int64(currTime.Hour())*int64(time.Hour) + int64(currTime.Minute())*int64(time.Minute) return int64(time.Since(timer.FinishDate)) < dur } func (timer *Timer) ActiveToday() bool { return timer.ActiveOnDay(time.Now()) } func (timer *Timer) ActiveOnDay(t time.Time) bool { f := "2006/01/02" tStr := t.Format(f) // If StartDate or FinishDate is _on_ t, true if timer.StartDate.Format(f) == tStr || timer.FinishDate.Format(f) == tStr { return true } // Otherwise, if StartDate is before t and FinishDate is after t return timer.StartDate.Before(t) && timer.FinishDate.After(t) } func (timer *Timer) HasContext(context string) bool { for _, v := range timer.Contexts { if v == context { return true } } return false } func (timer *Timer) HasProject(project string) bool { for _, v := range timer.Projects { if v == project { return true } } return false } func (timer *Timer) SetTag(name, val string) { timer.AdditionalTags[name] = val } func (timer *Timer) HasTag(name string) bool { _, ok := timer.AdditionalTags[name] return ok } func (timer *Timer) GetTag(name string) string { return timer.AdditionalTags[name] } // LooksLike returns true if the passed in timer looks like this func (timer *Timer) LooksLike(tmr *Timer) bool { if tmr.StartDate != timer.StartDate || tmr.FinishDate != timer.FinishDate || tmr.Finished != timer.Finished || tmr.Notes != timer.Notes || len(tmr.Projects) != len(timer.Projects) || len(tmr.Contexts) != len(timer.Contexts) || len(tmr.AdditionalTags) != len(timer.AdditionalTags) { return false } for _, prj := range tmr.Projects { if !timer.HasProject(prj) { return false } } for _, ctx := range tmr.Contexts { if !timer.HasContext(ctx) { return false } } for key, val := range tmr.AdditionalTags { if !timer.HasTag(key) || timer.GetTag(key) != val { return false } } return true }