diff --git a/anki.d b/anki.d new file mode 100755 index 0000000..1efa3f5 Binary files /dev/null and b/anki.d differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..735bf00 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module gogs.inhome.blapointe.com/anki.d + +go 1.20 + +require ( + github.com/google/uuid v1.3.0 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..00b3916 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..737441e --- /dev/null +++ b/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/google/uuid" + "gopkg.in/yaml.v2" +) + +type ( + DB struct { + Knowledge Knowledge + Users map[string]User + Cadence []Duration + } + Knowledge struct { + Questions map[string]Question + Answers map[string]Answer + } + Question struct { + Q string + Tags []string + } + Answer struct { + Q string + A string + TS int64 + Author string + } + User struct { + History map[string][]History + } + History struct { + A string + TS int64 + Pass bool + } + Duration time.Duration +) + +func main() { + if err := Main(); err != nil { + panic(err) + } +} + +func Main() error { + var db DB + if b, err := os.ReadFile(os.Getenv("DB")); err != nil { + return err + } else if err := yaml.Unmarshal(b, &db); err != nil { + return err + } + user := os.Getenv("USER") + for q, history := range db.Users[user].History { + log.Printf("%s/%s/%+v", user, q, history) + if time.Until(db.Next(user, q)) > 0 { + continue + } + question := db.Question(q) + fmt.Printf("> Q: %s\n", question.Q) + fmt.Printf("> %+v\n\t", question.Tags) + response := readline() + if lastAnswer := db.Answer(db.LastAnswer(user, q).A); lastAnswer.A != "" { + fmt.Printf("> Last time, you responded:\n\t%s\n", lastAnswer.A) + } + fmt.Printf("> Did you pass this time? [Yn] ") + pass := readline() == "n" + if err := db.PushAnswer(user, q, response, pass); err != nil { + return err + } + fmt.Println() + } + if b, _ := yaml.Marshal(db); len(b) > 0 { + log.Printf("%s\n", b) + } + return nil +} + +func readline() string { + reader := bufio.NewReader(os.Stdin) + text, _ := reader.ReadString('\n') + return text +} + +func (db DB) Question(q string) Question { + return db.Knowledge.Questions[q] +} + +func (db DB) Answer(a string) Answer { + return db.Knowledge.Answers[a] +} + +func (db DB) LastAnswer(user, q string) Answer { + for _, v := range db.Knowledge.Answers { + if v.Q == q && v.Author == user { + return v + } + } + return Answer{} +} + +func (db DB) PushAnswer(user, q, a string, pass bool) error { + uuid := uuid.New().String() + db.Knowledge.Answers[uuid] = Answer{ + Q: q, + A: a, + TS: time.Now().UnixNano(), + Author: user, + } + db.Users[user].History[q] = append(db.Users[user].History[q], History{ + A: uuid, + Pass: pass, + }) + return nil +} + +func (db DB) Next(user, q string) time.Time { + history := db.Users[user].History[q] + progress := 0 + for i := range history { + if history[i].Pass { + progress += 1 + } else { + progress -= 1 + } + } + if progress < 0 { + progress = 0 + } else if progress > len(db.Cadence) { + return time.Now().Add(time.Hour * 24 * 365 * 10) + } + return db.LastTS(user, q).Add(time.Duration(db.Cadence[progress])) +} + +func (db DB) LastTS(user, q string) time.Time { + max := int64(0) + for _, v := range db.Users[user].History[q] { + if v.TS > max { + max = v.TS + } + } + return time.Unix(0, max) +} + +func (d Duration) MarshalYAML() (interface{}, error) { + return time.Duration(d).String(), nil +} + +func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + count := "" + var ttl time.Duration + for i := range s { + if s[i] < '0' || s[i] > '9' { + n, err := strconv.Atoi(count) + if err != nil { + return err + } + count += s[i : i+1] + switch s[i] { + case 'w': + count = fmt.Sprintf("%dh", n*24*7) + case 'd': + count = fmt.Sprintf("%dh", n*24) + } + d, err := time.ParseDuration(count) + if err != nil { + return err + } + ttl += d + count = "" + } else { + count += s[i : i+1] + } + } + if count != "" { + return errors.New(count) + } + return nil +} diff --git a/testdata/sample.yaml b/testdata/sample.yaml new file mode 100644 index 0000000..1a9ad67 --- /dev/null +++ b/testdata/sample.yaml @@ -0,0 +1,25 @@ +knowledge: + questions: + uuid1: + q: whats a fieldset + tags: + - ops + - data-platform + answers: + uuid2: + q: uuid1 + a: a schema + ts: 123 + author: breel +users: + breel: + history: + uuid1: + - {a: uuid2, pass: true} +cadence: +- 1d +- 3d +- 1w +- 3w +- 1m +- 3m