This commit is contained in:
Erik Winter 2021-07-14 07:17:53 +02:00
parent d57a0a8162
commit ebe2c401ef
9 changed files with 273 additions and 6 deletions

View File

@ -1,6 +1,8 @@
package command
import (
"fmt"
"git.ewintr.nl/gte/cmd/cli/format"
"git.ewintr.nl/gte/internal/configuration"
"git.ewintr.nl/gte/internal/process"
@ -21,8 +23,22 @@ func NewDone(conf *configuration.Configuration, cmdArgs []string) (*Done, error)
disp := storage.NewDispatcher(msend.NewSSLSMTP(conf.SMTP()))
fields := process.UpdateFields{"done": "true"}
localIds, err := local.LocalIds()
if err != nil {
return &Done{}, err
}
var tId string
for id, localId := range localIds {
if fmt.Sprintf("%d", localId) == cmdArgs[0] {
tId = id
break
}
}
if tId == "" {
return &Done{}, fmt.Errorf("could not find task")
}
updater := process.NewUpdate(local, disp, cmdArgs[0], fields)
updater := process.NewUpdate(local, disp, tId, fields)
return &Done{
doner: updater,

View File

@ -10,6 +10,7 @@ import (
// Today lists all task that are due today or past their due date
type Today struct {
local storage.LocalRepository
todayer *process.List
}
@ -25,6 +26,7 @@ func NewToday(conf *configuration.Configuration) (*Today, error) {
todayer := process.NewList(local, reqs)
return &Today{
local: local,
todayer: todayer,
}, nil
}
@ -38,5 +40,5 @@ func (t *Today) Do() string {
return "nothing left\n"
}
return format.FormatTaskTable(res.Tasks)
return format.FormatTaskTable(t.local, res.Tasks)
}

View File

@ -10,6 +10,7 @@ import (
// Tomorrow lists all tasks that are due tomorrow
type Tomorrow struct {
local storage.LocalRepository
tomorrower *process.List
}
@ -25,6 +26,7 @@ func NewTomorrow(conf *configuration.Configuration) (*Tomorrow, error) {
tomorrower := process.NewList(local, reqs)
return &Tomorrow{
local: local,
tomorrower: tomorrower,
}, nil
}
@ -39,5 +41,5 @@ func (t *Tomorrow) Do() string {
return "nothing to do tomorrow\n"
}
return format.FormatTaskTable(res.Tasks)
return format.FormatTaskTable(t.local, res.Tasks)
}

View File

@ -3,6 +3,7 @@ package format
import (
"fmt"
"git.ewintr.nl/gte/internal/storage"
"git.ewintr.nl/gte/internal/task"
)
@ -10,10 +11,19 @@ func FormatError(err error) string {
return fmt.Sprintf("could not perform command.\n\nerror: %s\n", err.Error())
}
func FormatTaskTable(tasks []*task.Task) string {
func FormatTaskTable(local storage.LocalRepository, tasks []*task.Task) string {
if len(tasks) == 0 {
return "no tasks to display\n"
}
localIds, err := local.LocalIds()
if err != nil {
return FormatError(err)
}
var output string
for _, t := range tasks {
output += fmt.Sprintf("%s\t%s\t%s\n", t.Id, t.Due.String(), t.Action)
output += fmt.Sprintf("%d\t%s\t%s\n", localIds[t.Id], t.Due.String(), t.Action)
}
return output

View File

@ -17,4 +17,41 @@ type LocalRepository interface {
FindAllInFolder(folder string) ([]*task.Task, error)
FindAllInProject(project string) ([]*task.Task, error)
FindById(id string) (*task.Task, error)
FindByLocalId(id int) (*task.Task, error)
LocalIds() (map[string]int, error)
}
func NextLocalId(used []int) int {
if len(used) == 0 {
return 1
}
usedMax := 1
for _, u := range used {
if u > usedMax {
usedMax = u
}
}
var limit int
for limit = 1; limit <= len(used) || limit < usedMax; limit *= 10 {
}
newId := used[len(used)-1] + 1
if newId < limit {
return newId
}
usedMap := map[int]bool{}
for _, u := range used {
usedMap[u] = true
}
for i := 1; i < limit; i++ {
if _, ok := usedMap[i]; !ok {
return i
}
}
return limit
}

View File

@ -0,0 +1,66 @@
package storage_test
import (
"testing"
"git.ewintr.nl/go-kit/test"
"git.ewintr.nl/gte/internal/storage"
)
func TestNextLocalId(t *testing.T) {
for _, tc := range []struct {
name string
used []int
exp int
}{
{
name: "empty",
used: []int{},
exp: 1,
},
{
name: "not empty",
used: []int{5},
exp: 6,
},
{
name: "multiple",
used: []int{2, 3, 4},
exp: 5,
},
{
name: "holes",
used: []int{1, 5, 8},
exp: 9,
},
{
name: "expand limit",
used: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
exp: 11,
},
{
name: "wrap if possible",
used: []int{8, 9},
exp: 1,
},
{
name: "find hole",
used: []int{1, 2, 3, 4, 5, 7, 8, 9},
exp: 6,
},
{
name: "dont wrap if expanded before",
used: []int{15, 16},
exp: 17,
},
{
name: "do wrap if expanded limit is reached",
used: []int{99},
exp: 1,
},
} {
t.Run(tc.name, func(t *testing.T) {
test.Equals(t, tc.exp, storage.NextLocalId(tc.used))
})
}
}

View File

@ -10,11 +10,13 @@ import (
type Memory struct {
tasks []*task.Task
latestSync time.Time
localIds map[string]int
}
func NewMemory() *Memory {
return &Memory{
tasks: []*task.Task{},
tasks: []*task.Task{},
localIds: map[string]int{},
}
}
@ -28,6 +30,7 @@ func (m *Memory) SetTasks(tasks []*task.Task) error {
nt := *t
nt.Message = nil
nTasks = append(nTasks, &nt)
m.setLocalId(t.Id)
}
m.tasks = nTasks
m.latestSync = time.Now()
@ -35,6 +38,16 @@ func (m *Memory) SetTasks(tasks []*task.Task) error {
return nil
}
func (m *Memory) setLocalId(id string) {
used := []int{}
for _, id := range m.localIds {
used = append(used, id)
}
next := NextLocalId(used)
m.localIds[id] = next
}
func (m *Memory) FindAllInFolder(folder string) ([]*task.Task, error) {
tasks := []*task.Task{}
for _, t := range m.tasks {
@ -67,3 +80,17 @@ func (m *Memory) FindById(id string) (*task.Task, error) {
return &task.Task{}, ErrTaskNotFound
}
func (m *Memory) FindByLocalId(localId int) (*task.Task, error) {
for _, t := range m.tasks {
if m.localIds[t.Id] == localId {
return t, nil
}
}
return &task.Task{}, ErrTaskNotFound
}
func (m *Memory) LocalIds() (map[string]int, error) {
return m.localIds, nil
}

View File

@ -86,4 +86,25 @@ func TestMemory(t *testing.T) {
test.OK(t, err)
test.Equals(t, task2, act)
})
t.Run("findbylocalid", func(t *testing.T) {
mem := storage.NewMemory()
test.OK(t, mem.SetTasks(tasks))
act, err := mem.FindByLocalId(2)
test.OK(t, err)
test.Equals(t, task2, act)
})
t.Run("localids", func(t *testing.T) {
mem := storage.NewMemory()
test.OK(t, mem.SetTasks(tasks))
act, err := mem.LocalIds()
test.OK(t, err)
exp := map[string]int{
"id-1": 1,
"id-2": 2,
"id-3": 3,
}
test.Equals(t, exp, act)
})
}

View File

@ -16,6 +16,7 @@ var sqliteMigrations = []sqliteMigration{
`CREATE TABLE task ("id" TEXT, "version" INTEGER, "folder" TEXT, "action" TEXT, "project" TEXT, "due" TEXT, "recur" TEXT)`,
`CREATE TABLE system ("latest_sync" INTEGER)`,
`INSERT INTO system (latest_sync) VALUES (0)`,
`CREATE TABLE local_id ("id" TEXT, "local_id" INTEGER)`,
}
var (
@ -68,10 +69,12 @@ func (s *Sqlite) LatestSync() (time.Time, error) {
}
func (s *Sqlite) SetTasks(tasks []*task.Task) error {
// set tasks
if _, err := s.db.Exec(`DELETE FROM task`); err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
newIds := []string{}
for _, t := range tasks {
var recurStr string
if t.Recur != nil {
@ -86,8 +89,56 @@ VALUES
if err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
newIds = append(newIds, t.Id)
}
// set local_ids
oldIds := map[string]int{}
rows, err := s.db.Query(`SELECT id, local_id FROM local_id`)
if err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
defer rows.Close()
for rows.Next() {
var id string
var local_id int
if err := rows.Scan(&id, &local_id); err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
oldIds[id] = local_id
}
usedLocalIds := []int{}
newLocalIds := map[string]int{}
for _, n := range newIds {
if localId, ok := oldIds[n]; ok {
newLocalIds[n] = localId
usedLocalIds = append(usedLocalIds, localId)
continue
}
localId := NextLocalId(usedLocalIds)
newLocalIds[n] = localId
usedLocalIds = append(usedLocalIds, localId)
}
if _, err := s.db.Exec(`DELETE FROM local_id`); err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
for id, localId := range newLocalIds {
_, err := s.db.Exec(`
INSERT INTO local_id
(id, local_id)
VALUES
(?, ?)`, id, localId)
if err != nil {
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
}
// update system
if _, err := s.db.Exec(`
UPDATE system
SET latest_sync = ?`,
@ -145,6 +196,41 @@ LIMIT 1`, id)
}, nil
}
func (s *Sqlite) FindByLocalId(localId int) (*task.Task, error) {
var id string
row := s.db.QueryRow(`SELECT id FROM local_id WHERE local_id = ?`, localId)
if err := row.Scan(&id); err != nil {
return &task.Task{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
t, err := s.FindById(id)
if err != nil {
return &task.Task{}, nil
}
return t, nil
}
func (s *Sqlite) LocalIds() (map[string]int, error) {
rows, err := s.db.Query(`SELECT id, local_id FROM local_id`)
if err != nil {
return map[string]int{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
idMap := map[string]int{}
defer rows.Close()
for rows.Next() {
var id string
var local_id int
if err := rows.Scan(&id, &local_id); err != nil {
return map[string]int{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
idMap[id] = local_id
}
return idMap, nil
}
func tasksFromRows(rows *sql.Rows) ([]*task.Task, error) {
tasks := []*task.Task{}