Initial Commit
This commit is contained in:
parent
275fbc205c
commit
fa79088d41
83
sort.go
Normal file
83
sort.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package timertxt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flags for defining sort element and order.
|
||||||
|
const (
|
||||||
|
SORT_START_DATE_ASC = iota
|
||||||
|
SORT_START_DATE_DESC
|
||||||
|
SORT_FINISH_DATE_ASC
|
||||||
|
SORT_FINISH_DATE_DESC
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort allows a TimerList to be sorted by certain predefined fields.
|
||||||
|
// See constants SORT_* for fields and sort order.
|
||||||
|
func (timerlist *TimerList) Sort(sortFlag int) error {
|
||||||
|
switch sortFlag {
|
||||||
|
case SORT_START_DATE_ASC, SORT_START_DATE_DESC:
|
||||||
|
timerlist.sortByStartDate(sortFlag)
|
||||||
|
case SORT_FINISH_DATE_ASC, SORT_FINISH_DATE_DESC:
|
||||||
|
timerlist.sortByFinishDate(sortFlag)
|
||||||
|
default:
|
||||||
|
return errors.New("Unrecognized sort option")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type timerlistSort struct {
|
||||||
|
timerlists TimerList
|
||||||
|
by func(t1, t2 *Timer) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *timerlistSort) Len() int {
|
||||||
|
return len(ts.timerlists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *timerlistSort) Swap(l, r int) {
|
||||||
|
ts.timerlists[l], ts.timerlists[r] = ts.timerlists[r], ts.timerlists[l]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *timerlistSort) Less(l, r int) bool {
|
||||||
|
return ts.by(&ts.timerlists[l], &ts.timerlists[r])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (timerlist *TimerList) sortBy(by func(t1, t2 *Timer) bool) *TimerList {
|
||||||
|
ts := &timerlistSort{
|
||||||
|
timerlists: *timerlist,
|
||||||
|
by: by,
|
||||||
|
}
|
||||||
|
sort.Sort(ts)
|
||||||
|
return timerlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortByDate(asc bool, date1, date2 time.Time) bool {
|
||||||
|
if asc { // ASC
|
||||||
|
if !date1.IsZero() && !date2.IsZero() {
|
||||||
|
return date1.Before(date2)
|
||||||
|
}
|
||||||
|
return !date2.IsZero()
|
||||||
|
}
|
||||||
|
// DESC
|
||||||
|
if !date1.IsZero() && !date2.IsZero() {
|
||||||
|
return date1.After(date2)
|
||||||
|
}
|
||||||
|
return date2.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 timerlist
|
||||||
|
}
|
150
timer.go
Normal file
150
timer.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
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 // 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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-15T10:00:00-0600 2019-02-15T06: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 "
|
||||||
|
if timer.HasFinishDate() {
|
||||||
|
text += fmt.Sprintf("%s ", timer.FinishDate.Format(DateLayout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text += fmt.Sprintf("%s ", timer.StartDate.Format(DateLayout))
|
||||||
|
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()
|
||||||
|
return timer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTimer parses the input text string into a Timer struct
|
||||||
|
func ParseTimer(text string) (*Timer, error) {
|
||||||
|
var err error
|
||||||
|
timer := Timer{}
|
||||||
|
timer.Original = strings.Trim(text, "\t\n\r ")
|
||||||
|
originalParts := strings.Fields(timer.Original)
|
||||||
|
|
||||||
|
// Check for finished
|
||||||
|
if originalParts[0] == "x" {
|
||||||
|
timer.Finished = true
|
||||||
|
// If it's finished, there _must_ be a finished date
|
||||||
|
if timer.FinishDate, err = time.Parse(DateLayout, originalParts[1]); err != nil {
|
||||||
|
return nil, errors.New("Timer marked finished, but failed to parse FinishDate: " + err.Error())
|
||||||
|
}
|
||||||
|
originalParts = originalParts[2:]
|
||||||
|
}
|
||||||
|
if timer.StartDate, err = time.Parse(DateLayout, originalParts[0]); err != nil {
|
||||||
|
return nil, errors.New("Unable to parse StartDate: " + err.Error())
|
||||||
|
}
|
||||||
|
originalParts = originalParts[1:]
|
||||||
|
var notes []string
|
||||||
|
for _, v := range originalParts {
|
||||||
|
if strings.HasPrefix("@", v) {
|
||||||
|
// Contexts
|
||||||
|
timer.Contexts = append(timer.Contexts, v)
|
||||||
|
} else if strings.HasPrefix("+", v) {
|
||||||
|
// Projects
|
||||||
|
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() bool {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
170
timerlist.go
Normal file
170
timerlist.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package timertxt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimerList represents a list of timer.txt timer entries.
|
||||||
|
// It is usually loasded from a whole timer.txt file.
|
||||||
|
type TimerList []Timer
|
||||||
|
|
||||||
|
// NewTimerList creates a new empty TimerList.
|
||||||
|
func NewTimerList() TimerList {
|
||||||
|
timerlist := TimerList{}
|
||||||
|
return timerlist
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a complete list of timers in timer.txt format.
|
||||||
|
func (timerlist TimerList) String() string {
|
||||||
|
var ret string
|
||||||
|
for _, timer := range timerlist {
|
||||||
|
ret += fmt.Sprintf("%s\n", timer.String())
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTimer appends a Timer to the current TimerList and takes care to set the Timer.Id correctly
|
||||||
|
func (timerlist *TimerList) AddTimer(timer *Timer) {
|
||||||
|
timer.Id = 0
|
||||||
|
for _, t := range *timerlist {
|
||||||
|
if t.Id > timer.Id {
|
||||||
|
timer.Id = t.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.Id += 1
|
||||||
|
*timerlist = append(*timerlist, *timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.new("timer not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return errors.new("timer not found")
|
||||||
|
}
|
||||||
|
*timerlist = newList
|
||||||
|
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 {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return errors.New("timer not found")
|
||||||
|
}
|
||||||
|
*timerlist = newList
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter filters the current TimerList for the given predicate (a function that takes a timer as input and returns a
|
||||||
|
// 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 {
|
||||||
|
if predicate(t) {
|
||||||
|
newList = append(newList, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &newList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
timerId := 1
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := strings.Trim(scanner.Text(), "\t\n\r") // Read Line
|
||||||
|
// Ignore blank lines
|
||||||
|
if text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
timer, err := ParseTimer(text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
timer.Id = timerId
|
||||||
|
*timerlist = append(*timerlist, *timer)
|
||||||
|
timerId++
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToFile writes a TimerList to *os.File.
|
||||||
|
func (timerlist *TimerList) LoadFromFilename(filename string) error {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
return timerlist.LoadFromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromFile loads and returns a TimerList from *os.File.
|
||||||
|
func LoadFromFile(file *os.File) (TimerList, error) {
|
||||||
|
timerlist := TimerList{}
|
||||||
|
if err := timerlist.LoadFromFile(file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return timerlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToFile writes a TimerList to *os.File.
|
||||||
|
func WriteToFile(timerlist *TimerList, file *os.File) error {
|
||||||
|
return timerlist.WriteToFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromFilename loads and returns a TimerList from a file (most likely called "timer.txt")
|
||||||
|
func LoadFromFilename(filename string) (TimerList, error) {
|
||||||
|
timerlist := TimerList{}
|
||||||
|
if err := timerlist.LoadFromFilename(filename); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return timerlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToFilename write a TimerList to the specified file (most likely called "timer.txt")
|
||||||
|
func WriteToFilename(timerlist *TimerList, filename string) error {
|
||||||
|
return timerlist.WriteToFilename(filename)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user