Compare commits

...

10 Commits
v1.0.0 ... main

4 changed files with 242 additions and 75 deletions

View File

@ -2,6 +2,6 @@
A Go library for working with timer.txt files.
A timer.txt file is a plain text file for timekeeping purposes. It stems from the same motivations as todo.txt (http://todotxt.org/).
A timer.txt file is a plain text file for timekeeping purposes. It stems from the same motivations as todo.txt (http://todotxt.org/ ).
This library is very similar to https://github.com/br0xen/go-todotxt, which is a fork of https://github.com/JamesClonk/go-todotxt.

33
sort.go
View File

@ -8,28 +8,33 @@ import (
// Flags for defining sort element and order.
const (
SORT_UNFINISHED_START = iota
SORT_START_DATE_ASC
SORT_START_DATE_DESC
SORT_FINISH_DATE_ASC
SORT_FINISH_DATE_DESC
SortUnfinishedStart = iota
SortStartDateAsc
SortStartDateDesc
SortFinishDateAsc
SortFinishDateDesc
SortError
)
// Sort allows a TimerList to be sorted by certain predefined fields.
// See constants SORT_* for fields and sort order.
// See constants Sort* for fields and sort order.
func (timerlist *TimerList) Sort(sortFlag int) error {
switch sortFlag {
case SORT_UNFINISHED_START:
case SortUnfinishedStart:
timerlist.sortByUnfinishedThenStart()
case SORT_START_DATE_ASC, SORT_START_DATE_DESC:
case SortStartDateAsc, SortStartDateDesc:
timerlist.sortByStartDate(sortFlag)
case SORT_FINISH_DATE_ASC, SORT_FINISH_DATE_DESC:
case SortFinishDateAsc, SortFinishDateDesc:
timerlist.sortByFinishDate(sortFlag)
default:
return errors.New("Unrecognized sort option")
}
timerlist.SortFlag = sortFlag
return nil
}
func (timerlist *TimerList) refresh() {
timerlist.Sort(timerlist.SortFlag)
}
type timerlistSort struct {
timerlists TimerList
@ -37,15 +42,15 @@ type timerlistSort struct {
}
func (ts *timerlistSort) Len() int {
return len(ts.timerlists)
return len(ts.timerlists.Timers)
}
func (ts *timerlistSort) Swap(l, r int) {
ts.timerlists[l], ts.timerlists[r] = ts.timerlists[r], ts.timerlists[l]
ts.timerlists.Timers[l], ts.timerlists.Timers[r] = ts.timerlists.Timers[r], ts.timerlists.Timers[l]
}
func (ts *timerlistSort) Less(l, r int) bool {
return ts.by(ts.timerlists[l], ts.timerlists[r])
return ts.by(ts.timerlists.Timers[l], ts.timerlists.Timers[r])
}
func (timerlist *TimerList) sortBy(by func(t1, t2 *Timer) bool) *TimerList {
@ -73,14 +78,14 @@ func sortByDate(asc bool, date1, date2 time.Time) bool {
func (timerlist *TimerList) sortByStartDate(order int) *TimerList {
timerlist.sortBy(func(t1, t2 *Timer) bool {
return sortByDate(order == SORT_START_DATE_ASC, t1.StartDate, t2.StartDate)
return sortByDate(order == SortStartDateAsc, t1.StartDate, t2.StartDate)
})
return timerlist
}
func (timerlist *TimerList) sortByFinishDate(order int) *TimerList {
timerlist.sortBy(func(t1, t2 *Timer) bool {
return sortByDate(order == SORT_FINISH_DATE_ASC, t1.FinishDate, t2.FinishDate)
return sortByDate(order == SortFinishDateAsc, t1.FinishDate, t2.FinishDate)
})
return timerlist
}

View File

@ -19,15 +19,15 @@ var (
)
type Timer struct {
Id int // Internal timer id
Original string // Original raw timer text
StartDate time.Time
FinishDate time.Time
Finished bool
Notes string // Notes part of timer text
Projects []string
Contexts []string
AdditionalTags map[string]string // Addon tags will be available here
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.
@ -212,6 +212,10 @@ func (timer *Timer) HasProject(project string) bool {
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
@ -220,3 +224,32 @@ func (timer *Timer) HasTag(name string) bool {
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
}

View File

@ -6,32 +6,61 @@ import (
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
"time"
)
// TimerList represents a list of timer.txt timer entries.
// It is usually loasded from a whole timer.txt file.
type TimerList []*Timer
// It is usually loaded from a whole timer.txt file.
type TimerList struct {
Timers []*Timer `json:"timers"`
SortFlag int `json:"sortFlag"`
}
// NewTimerList creates a new empty TimerList.
func NewTimerList() *TimerList {
return &TimerList{}
}
func NewTimerList() *TimerList { return &TimerList{Timers: []*Timer{}} }
func (timerlist *TimerList) Size() int { return len(timerlist.Timers) }
func (timerlist *TimerList) GetTimerSlice() []*Timer { return timerlist.Timers }
func (timerlist *TimerList) Size() int {
return len([]*Timer(*timerlist))
func (timerlist *TimerList) Contains(t *Timer) bool {
for _, tmr := range timerlist.Timers {
if tmr == t {
return true
}
}
return false
}
func (timerlist *TimerList) GetActiveOrMostRecent() (*Timer, error) {
var found *Timer
var latest time.Time
activeTimers := timerlist.Filter(func(t *Timer) bool { return !t.Finished })
if len(activeTimers.Timers) > 0 {
return activeTimers.GetMostRecentTimer()
}
func (timerlist *TimerList) GetTimerSlice() []*Timer {
return []*Timer(*timerlist)
for _, t := range timerlist.Timers {
if t.FinishDate.IsZero() {
if t.StartDate.After(latest) {
found = t
latest = t.StartDate
}
} else {
if t.FinishDate.After(latest) {
latest = t.FinishDate
found = t
}
}
}
if found == nil {
return nil, errors.New("No timer found")
}
return found, nil
}
func (timerlist *TimerList) GetMostRecentTimer() (*Timer, error) {
var found *Timer
var latest time.Time
for i := range *timerlist {
t := ([]*Timer(*timerlist))[i]
for _, t := range timerlist.Timers {
if t.FinishDate.IsZero() {
if t.StartDate.After(latest) {
found = t
@ -75,20 +104,100 @@ func (timerlist *TimerList) GetTimersWithProject(project string) *TimerList {
})
}
func (timerlist *TimerList) GetContexts() []string {
var ret []string
added := make(map[string]bool)
for _, tmr := range timerlist.Timers {
for _, c := range tmr.Contexts {
if !added[c] {
ret = append(ret, c)
added[c] = true
}
}
}
sort.Strings(ret)
return ret
}
func (timerlist *TimerList) GetProjects() []string {
var ret []string
added := make(map[string]bool)
for _, tmr := range timerlist.Timers {
for _, p := range tmr.Projects {
if !added[p] {
ret = append(ret, p)
added[p] = true
}
}
}
sort.Strings(ret)
return ret
}
func (timerlist *TimerList) GetTagKVList() []string {
var ret []string
added := make(map[string]bool)
for _, tmr := range timerlist.Timers {
for k, v := range tmr.AdditionalTags {
tag := fmt.Sprintf("%s:%s", k, v)
if !added[tag] {
ret = append(ret, tag)
added[tag] = true
}
}
}
sort.Strings(ret)
return ret
}
func (timerlist *TimerList) GetTagKeys() []string {
var ret []string
added := make(map[string]bool)
for _, tmr := range timerlist.Timers {
for k := range tmr.AdditionalTags {
if !added[k] {
ret = append(ret, k)
added[k] = true
}
}
}
sort.Strings(ret)
return ret
}
func (timerlist *TimerList) GetTagValuesForKey(key string) []string {
var ret []string
added := make(map[string]bool)
for _, tmr := range timerlist.Timers {
if v, ok := tmr.AdditionalTags[key]; ok && !added[v] {
ret = append(ret, v)
added[v] = true
}
}
sort.Strings(ret)
return ret
}
func (timerlist *TimerList) GetActiveTimers() *TimerList {
t := *NewTimerList()
for _, v := range *timerlist {
for _, v := range timerlist.Timers {
if v.FinishDate.IsZero() {
t = append(t, v)
t.Timers = append(t.Timers, v)
}
}
return &t
}
func (timerlist *TimerList) GetNextId() int {
nextId := 0
for _, v := range timerlist.Timers {
if v.Id > nextId {
nextId = v.Id
}
}
return nextId + 1
}
// String returns a complete list of timers in timer.txt format.
func (timerlist *TimerList) String() string {
var ret string
for _, timer := range *timerlist {
for _, timer := range timerlist.Timers {
ret += fmt.Sprintf("%s\n", timer.String())
}
return ret
@ -96,24 +205,25 @@ func (timerlist *TimerList) String() string {
// AddTimer prepends a Timer to the current TimerList and takes care to set the Timer.Id correctly
func (timerlist *TimerList) AddTimer(timer *Timer) {
// The new timer is going to be id 1
timer.Id = 1
for _, t := range *timerlist {
// Everything else gets incremented
t.Id++
}
// Now prepend the timer to the slice
*timerlist = append(*timerlist, &Timer{})
copy((*timerlist)[1:], (*timerlist)[0:])
(*timerlist)[0] = timer
timer.Id = timerlist.GetNextId()
timerlist.Timers = append(timerlist.Timers, timer)
timerlist.refresh()
}
// AddTimers adds all passed in timers to the list, sorts the list, then updates the Timer.Id values.
func (timerlist *TimerList) AddTimers(timers []*Timer) {
for _, v := range timers {
timerlist.AddTimer(v)
}
}
func (timerlist *TimerList) Combine(other *TimerList) { timerlist.AddTimers(other.Timers) }
// GetTimer returns the Timer with the given timer 'id' from the TimerList.
// Returns an error if Timer could not be found.
func (timerlist *TimerList) GetTimer(id int) (*Timer, error) {
for i := range *timerlist {
if ([]*Timer(*timerlist))[i].Id == id {
return ([]*Timer(*timerlist))[i], nil
for i := range timerlist.Timers {
if timerlist.Timers[i].Id == id {
return timerlist.Timers[i], nil
}
}
return nil, errors.New("timer not found")
@ -122,38 +232,40 @@ func (timerlist *TimerList) GetTimer(id int) (*Timer, error) {
// RemoveTimerById removes any Timer with given Timer 'id' from the TimerList.
// Returns an error if no Timer was removed.
func (timerlist *TimerList) RemoveTimerById(id int) error {
var newList TimerList
found := false
for _, t := range *timerlist {
if t.Id != id {
newList = append(newList, t)
} else {
var remIdx int
var t *Timer
for remIdx, t = range timerlist.Timers {
if t.Id == id {
found = true
break
}
}
if !found {
return errors.New("timer not found")
}
*timerlist = newList
timerlist.Timers = append(timerlist.Timers[:remIdx], timerlist.Timers[remIdx+1:]...)
timerlist.refresh()
return nil
}
// RemoveTimer removes any Timer from the TimerList with the same String representation as the given Timer.
// Returns an error if no Timer was removed.
func (timerlist *TimerList) RemoveTimer(timer Timer) error {
var newList TimerList
found := false
for _, t := range *timerlist {
if t.String() != timer.String() {
newList = append(newList, t)
} else {
var remIdx int
var t *Timer
for remIdx, t = range timerlist.Timers {
if t.String() == timer.String() {
found = true
break
}
}
if !found {
return errors.New("timer not found")
}
*timerlist = newList
timerlist.Timers = append(timerlist.Timers[:remIdx], timerlist.Timers[remIdx+1:]...)
timerlist.refresh()
return nil
}
@ -177,18 +289,26 @@ func (timerlist *TimerList) ArchiveTimerToFile(timer Timer, filename string) err
// bool), and returns a new TimerList. The original TimerList is not modified.
func (timerlist *TimerList) Filter(predicate func(*Timer) bool) *TimerList {
var newList TimerList
for _, t := range *timerlist {
for _, t := range timerlist.Timers {
if predicate(t) {
newList = append(newList, t)
newList.Timers = append(newList.Timers, t)
}
}
return &newList
}
func (timerlist *TimerList) ResetIds() {
timerId := 1
for _, v := range timerlist.Timers {
v.Id = timerId
timerId++
}
}
// LoadFromFile loads a TimerList from *os.File.
// Note: This will clear the current TimerList and overwrite it's contents with whatever is in *os.File.
func (timerlist *TimerList) LoadFromFile(file *os.File) error {
*timerlist = []*Timer{} // Empty timerlist
timerlist.Timers = []*Timer{} // Empty timerlist
timerId := 1
scanner := bufio.NewScanner(file)
for scanner.Scan() {
@ -202,12 +322,13 @@ func (timerlist *TimerList) LoadFromFile(file *os.File) error {
return err
}
timer.Id = timerId
*timerlist = append(*timerlist, timer)
timerId++
timerlist.Timers = append(timerlist.Timers, timer)
}
if err := scanner.Err(); err != nil {
return err
}
timerlist.refresh()
return nil
}
@ -216,10 +337,14 @@ func (timerlist *TimerList) WriteToFile(file *os.File) error {
writer := bufio.NewWriter(file)
_, err := writer.WriteString(timerlist.String())
writer.Flush()
return err
if err != nil {
return err
}
timerlist.ResetIds()
return nil
}
// WriteToFile writes a TimerList to *os.File.
// LoadFromFilename loads a TimerList from the filename.
func (timerlist *TimerList) LoadFromFilename(filename string) error {
file, err := os.Open(filename)
if err != nil {
@ -231,16 +356,20 @@ func (timerlist *TimerList) LoadFromFilename(filename string) error {
// WriteToFilename writes a TimerList to the specified file (most likely called "timer.txt").
func (timerlist *TimerList) WriteToFilename(filename string) error {
return ioutil.WriteFile(filename, []byte(timerlist.String()), 0640)
if err := ioutil.WriteFile(filename, []byte(timerlist.String()), 0640); err != nil {
return err
}
timerlist.ResetIds()
return nil
}
// LoadFromFile loads and returns a TimerList from *os.File.
func LoadFromFile(file *os.File) (TimerList, error) {
func LoadFromFile(file *os.File) (*TimerList, error) {
timerlist := TimerList{}
if err := timerlist.LoadFromFile(file); err != nil {
return nil, err
}
return timerlist, nil
return &timerlist, nil
}
// WriteToFile writes a TimerList to *os.File.
@ -249,12 +378,12 @@ func WriteToFile(timerlist *TimerList, file *os.File) error {
}
// LoadFromFilename loads and returns a TimerList from a file (most likely called "timer.txt")
func LoadFromFilename(filename string) (TimerList, error) {
func LoadFromFilename(filename string) (*TimerList, error) {
timerlist := TimerList{}
if err := timerlist.LoadFromFilename(filename); err != nil {
return nil, err
}
return timerlist, nil
return &timerlist, nil
}
// WriteToFilename write a TimerList to the specified file (most likely called "timer.txt")