Skip to content

Commit

Permalink
chore: tact: improve logbook calculation logic and add more edge cases
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilsk committed Nov 20, 2023
1 parent 55e091f commit 74fa524
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 45 deletions.
28 changes: 28 additions & 0 deletions internal/pkg/assert/assert.go
@@ -0,0 +1,28 @@
package xassert

import (
"strings"

"github.com/pkg/errors"
)

var disabled bool

func True(check func() bool, messages ...string) {
if disabled {
return
}
if check() {
return
}

message := "assertion failed"
if len(messages) > 0 {
message = messages[0]
if len(messages) > 1 {
message = strings.Join(messages, " ")
}
}
// I need stack trace here
panic(errors.New(message))
}
1 change: 1 addition & 0 deletions internal/pkg/time/duration.go
@@ -0,0 +1 @@
package xtime
24 changes: 24 additions & 0 deletions internal/pkg/time/range.go
@@ -0,0 +1,24 @@
package xtime

import (
"time"

xassert "go.octolab.org/ecosystem/sparkle/internal/pkg/assert"
)

func IsLinear(past, future time.Time, threshold time.Duration) (is bool, shift bool) {
// hard invariant: timestamps within a day
xassert.True(func() bool {
delta := future.Sub(past)
return (delta >= 0 && delta <= Day) ||
(delta < 0 && delta >= -Day)
})

// invariant: past <= future
if !past.After(future) {
return true, false
}

// invariant: breaks and work time are less than a threshold
return future.Add(Day).Sub(past) < threshold, true
}
11 changes: 11 additions & 0 deletions internal/pkg/time/time.go
@@ -0,0 +1,11 @@
package xtime

import "time"

const (
Kitchen = "15:04" // mix of the time.Kitchen and time.TimeOnly

HalfDay = 12 * time.Hour
Day = 24 * time.Hour
Week = 7 * Day
)
84 changes: 53 additions & 31 deletions internal/service/tact/logbook.go
Expand Up @@ -3,15 +3,13 @@ package tact
import (
"fmt"
"regexp"
"sync"
"time"

xtime "go.octolab.org/time"
xtime "go.octolab.org/ecosystem/sparkle/internal/pkg/time"
)

const (
threshold = 12 * time.Hour
watch = "15:04"

_ = iota
activated
deactivated
Expand All @@ -20,33 +18,44 @@ const (
var (
rec = regexp.MustCompile(`^- (\d{2}:\d{2}) /(?: ((?:\d+[hms])+) /)? (\d{2}:\d{2}) - .*$`)
end = regexp.MustCompile(`^\w+ total / \w+ break \d+% / \w+ work \d+%`)
zero = regexp.MustCompile(`(\D)0[m,s]`)
zero = regexp.MustCompile(`(\D)0[m,s]`) // 12H
)

func NewLogbook(format string, threshold time.Duration) *Logbook {
return &Logbook{format: format, threshold: threshold}
}

type Logbook struct {
// config
init sync.Once
format string
threshold time.Duration

// log state
state int
shift time.Duration
start time.Time
end time.Time
breaks time.Duration
}

func (log *Logbook) isActivated() bool {
return log.state == activated
}

func (log *Logbook) isDeactivated() bool {
return log.state == deactivated
}

func (log *Logbook) Log(record string) error {
log.init.Do(func() {
if log.format == "" {
log.format = xtime.Kitchen
}
if log.threshold == 0 {
log.threshold = xtime.HalfDay
}
})

if log.isDeactivated() || record == "" {
return nil
}

// expected: {record, from, breaks, to} or {exit}
marks := rec.FindStringSubmatch(record)
if len(marks) != 4 {
markers := rec.FindStringSubmatch(record)
if len(markers) != 4 {
if !log.isActivated() {
return nil
}
Expand All @@ -68,45 +77,50 @@ func (log *Logbook) Log(record string) error {
log.state = activated

var shift time.Duration
from, err := time.Parse(watch, marks[1])
from, err := time.Parse(log.format, markers[1])
if err != nil {
return err
}
from = from.Add(log.shift)
if log.start.IsZero() {
log.start = from
} else if from.Before(log.end) {
if log.end.Sub(from) < threshold {
} else {
is, shifted := xtime.IsLinear(log.end, from, log.threshold)
if !is {
return errTimeTravel
}
shift = xtime.Day
from = from.Add(shift)
if shifted {
shift = xtime.Day
from = from.Add(shift)
}
}

var breaks time.Duration
if marks[2] != "" {
breaks, err = time.ParseDuration(marks[2])
if markers[2] != "" {
breaks, err = time.ParseDuration(markers[2])
if err != nil {
return err
}
}
if !log.end.IsZero() {
breaks += from.Sub(log.end)
}

to, err := time.Parse(watch, marks[3])
to, err := time.Parse(log.format, markers[3])
if err != nil {
return err
}
to = to.Add(log.shift)
if from.After(to) {
if from.Sub(to) < threshold {
return errTimeTravel
}
// invariant: work time < threshold, work time = duration(from, to) - breaks
is, shifted := xtime.IsLinear(from, to, log.threshold+breaks)
if !is {
return errTimeTravel
}
if shifted {
shift = xtime.Day
to = to.Add(shift)
}

if !log.end.IsZero() {
breaks += from.Sub(log.end)
}
log.breaks += breaks
log.shift += shift
log.end = to
Expand All @@ -133,7 +147,7 @@ func (log *Logbook) String() string {
)
}

func (log *Logbook) clean(d time.Duration) string {
func (*Logbook) clean(d time.Duration) string {
base := d.String()
iter := zero.ReplaceAllString(base, "$1")
for iter != base {
Expand All @@ -142,3 +156,11 @@ func (log *Logbook) clean(d time.Duration) string {
}
return iter
}

func (log *Logbook) isActivated() bool {
return log.state == activated
}

func (log *Logbook) isDeactivated() bool {
return log.state == deactivated
}
53 changes: 39 additions & 14 deletions internal/service/tact/logbook_test.go
Expand Up @@ -10,15 +10,40 @@ func TestLogbook_Log(t *testing.T) {
tests := map[string]struct {
logs []string
report string
desc string
}{
"long internal interval": {
"threshold challenge": {
logs: []string{
"- 12:15 / 13:00 - routine solving / 🥱",
"- 13:15 / 14:00 - day planning / 🤔",
"- 14:00 / 15:00 - micro-tasking / 🥱",
"- 15:00 / 2h15m / 22:15 - focusing on the goal / 😤",
"- 09:30 / 10:00 - day planning and reflection / 🤔",
"- 10:00 / 5h / 00:00 - focused work on tasks / 🫠",
},
report: "10h total / 2h30m break 25% / 7h30m work 75%",
report: "14h30m total / 5h break 35% / 9h30m work 65%",
desc: `
There is a threshold to prevent incorrect inputs, e.g., from the past.
An example:
- 09:30 / 10:00 - day planning / 🤔
- 09:50 / 12:00 - hard work / 😤
A primitive solution for checking linearity has a disadvantage:
- 23:00 / 01:00 - hard work / 😤
From "23:00" >> To "01:00" because they are parsed for the same day.
To handle this case, we must define a work time threshold.
`,
},
"long breaks between actions": {
logs: []string{
"- 09:15 / 10:00 - day planning / 🤔",
"- 13:00 / 15:00 - routine solving / 🥱",
"- 16:00 / 19:15 - goal achieving / 😤",
},
report: "10h total / 4h break 40% / 6h work 60%",
},
"long breaks inside actions": {
logs: []string{
"- 09:15 / 10:00 - day planning / 🤔",
"- 11:00 / 2h / 15:00 - routine solving / 🥱",
"- 16:00 / 1h / 19:15 - goal achieving / 😤",
},
report: "10h total / 5h break 50% / 5h work 50%",
},
"long working day": {
logs: []string{
Expand All @@ -27,17 +52,17 @@ func TestLogbook_Log(t *testing.T) {
"- 13:45 / 45m / 16:30 - reading the book / 😤",
"- 17:00 / 1h / 21:00 - focusing on the goal / 😬",
"- 21:00 / 22:00 - solve critical issue / 😬",
"- 23:00 / 01:15 - write tests / 🫠",
"- 23:15 / 01:15 - write tests / 🫠",
},
report: "14h total / 3h45m break 27% / 10h15m work 73%",
report: "14h total / 4h break 29% / 10h work 71%",
},
"late start": {
logs: []string{
"- 21:00 / 22:00 - solve critical issue / 😬",
"- 23:00 / 01:15 - write tests / 🫠",
"- 01:30 / 03:00 - focusing on the goal / 😤",
"- 01:30 / 45m / 07:00 - focusing on the goal / 😤",
},
report: "6h total / 1h15m break 21% / 4h45m work 79%",
report: "10h total / 2h break 20% / 8h work 80%",
},
"two days run": {
logs: []string{
Expand All @@ -46,16 +71,16 @@ func TestLogbook_Log(t *testing.T) {
"- 13:45 / 45m / 16:30 - reading the book / 😤",
"- 17:00 / 1h / 21:00 - focusing on the goal / 😬",
"- 21:00 / 22:00 - solve critical issue / 😬",
"- 23:00 / 01:15 - write tests / 🫠",
"- 01:30 / 08:00 - focusing on the goal / 😤",
"- 23:15 / 01:15 - write tests / 🫠",
"- 01:30 / 45m / 07:00 - focusing on the goal / 😤",
"- 11:15 / 12:15 - day planning / 🤔",
"- 12:15 / 13:15 - task solving / 😤",
"- 13:45 / 45m / 16:30 - reading the book / 😤",
"- 17:00 / 1h / 21:00 - focusing on the goal / 😬",
"- 21:00 / 22:00 - solve critical issue / 😬",
"- 23:00 / 01:15 - write tests / 🫠",
"- 23:15 / 01:15 - write tests / 🫠",
},
report: "38h total / 11h break 29% / 27h work 71%",
report: "38h total / 13h15m break 35% / 24h45m work 65%",
},
}

Expand Down

0 comments on commit 74fa524

Please sign in to comment.