-
Notifications
You must be signed in to change notification settings - Fork 0
/
repetition.go
240 lines (227 loc) · 6.88 KB
/
repetition.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"database/sql"
"fmt"
"log"
"strings"
"time"
)
type Repetition struct {
db *sql.DB
// FIXME: Probably not needed here. Maybe only the number of stages.
stages []time.Duration
}
func NewRepetition(dbPath string, stages []time.Duration) (*Repetition, error) {
// this is arbitrary big number
const maxStages = 1_000_000
if len(stages) == 0 {
panic("stages == 0")
}
if len(stages) >= maxStages {
panic(fmt.Sprintf("too many stages; should be less than %d", maxStages))
}
var sv []string
for k, s := range stages {
sv = append(sv, fmt.Sprintf("(%d, %d)", k, int64(s.Seconds())))
}
// insert large last id so that words with stages > len(stages) can still
// be queried (This can happen if number of stages shrinks)
sv = append(sv, fmt.Sprintf("(%d, %d)", maxStages, int64(stages[len(stages)-1].Seconds())))
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS Repetition (
chat_id INTEGER,
word STRING,
definition STRING,
stage INTEGER,
last_updated_seconds INTEGER -- seconds since UNIX epoch
);
CREATE TEMP TABLE IF NOT EXISTS Stages (
id INTEGER,
duration INTEGER
);
INSERT INTO Stages(id, duration)
VALUES ` +
// Usually not escaping sql parts can lead to sql injection. In
// this case it's more convenient, and only numbers are put inside.
strings.Join(sv, ","),
); err != nil {
return nil, err
}
row := db.QueryRow(`
SELECT COUNT(*)
FROM Repetition;`)
var d int
if err := row.Scan(&d); err != nil {
return nil, err
}
log.Printf("DEBUG: Repetition database initially contains %d rows!", d)
return &Repetition{db, stages}, nil
}
func (r *Repetition) Save(chatID int64, word, definition string) error {
// FIXME: Don't insert duplicates!!!
_, err := r.db.Exec(`
INSERT INTO Repetition(chat_id, word, definition, stage, last_updated_seconds)
VALUES($0, $1, $2, $3, $4)`,
chatID, word, definition, 0, time.Now().Unix())
return err
}
// Repeat retrieves a definitions of the word ready for repetition.
func (r *Repetition) Repeat(chatID int64) (string, error) {
// TODO: Can consider ordering by oldest
// TODO: Add a test for this somehow to make sure that correct amount of
// time is waited. (can modify last_updated_seconds inside the test to
// simulate time)
row := r.db.QueryRow(`
SELECT word, definition
FROM Repetition
INNER JOIN Stages ON Repetition.stage <= Stages.id
WHERE Repetition.last_updated_seconds + Stages.duration <= $0
AND Repetition.chat_id = $1;`,
time.Now().Unix(), chatID)
var w, d string
err := row.Scan(&w, &d)
if err != nil {
return "", err
}
// strip first paragraph which corresponds to the word in question.
if s := strings.Split(d, "\n\n"); len(s) > 1 {
d = strings.Join(s[1:], "\n\n")
}
// Make sure that the word is not in the question.
return strings.ReplaceAll(d, w, "********"), nil
}
// Repeat retrieves a word ready for repetition.
// TODO: Deduplicate with Repeat?
func (r *Repetition) RepeatWord(chatID int64) (string, error) {
row := r.db.QueryRow(`
SELECT word
FROM Repetition
INNER JOIN Stages ON Repetition.stage <= Stages.id
WHERE Repetition.last_updated_seconds + Stages.duration <= $0
AND Repetition.chat_id = $1;`,
time.Now().Unix(), chatID)
var w string
err := row.Scan(&w)
return w, err
}
// looks up definition and compares it to the word
// FIXME: FIXME: FIXME: FIXME: This doesn't work!!!!!!!!
// cannot save obfuscated - cannot check.
// cannot save clear - cannot extract raw from obfuscated.
// this need fixing - make sure repetition_test passes.
// a way to fix is to move obfuscation into commander, save into Asking
// not-obfuscated message, but send to user obfuscated one.
// Maybe this is already fixed, just not tested?
func (r *Repetition) Answer(chatID int64, definition, word string) (string, error) {
panic("This logic is broken, fix it!")
row := r.db.QueryRow(`
SELECT word, stage
FROM Repetition
WHERE definition = $0
AND chat_id = $1`,
definition, chatID)
var correct string
var stage int
if err := row.Scan(&correct, &stage); err != nil {
return "", fmt.Errorf("INTERNAL: Did not find definition %q: %w", definition, err)
}
if correct != word {
stage = 0
} else {
stage += 1
if stage >= len(r.stages) {
stage = len(r.stages) - 1
}
}
_, err := r.db.Exec(`
UPDATE Repetition
SET stage = $0, last_updated_seconds = $1
WHERE definition = $2
AND chat_id = $3;`,
stage, time.Now().Unix(), definition, chatID)
if err != nil {
return "", fmt.Errorf("INTERNAL: Failed updating stage: %w", err)
}
return correct, nil
}
func (r *Repetition) AnswerKnow(chatID int64, word string) error {
_, err := r.db.Exec(`
UPDATE Repetition
SET stage = MIN(stage + 1, $0), last_updated_seconds = $1
WHERE word = $2
AND chat_id = $3;`,
len(r.stages)-1, time.Now().Unix(), word, chatID)
if err != nil {
return fmt.Errorf("INTERNAL: Failed updating stage: %w", err)
}
return nil
}
func (r *Repetition) AnswerDontKnow(chatID int64, word string) error {
_, err := r.db.Exec(`
UPDATE Repetition
SET stage = 0, last_updated_seconds = $0
WHERE word = $1
AND chat_id = $2;`,
time.Now().Unix(), word, chatID)
if err != nil {
return fmt.Errorf("INTERNAL: Failed updating stage: %w", err)
}
return nil
}
func (r *Repetition) GetDefinition(chatID int64, word string) (string, error) {
row := r.db.QueryRow(`
SELECT definition
FROM Repetition
WHERE word = $0
AND chat_id = $1`,
word, chatID)
var d string
if err := row.Scan(&d); err != nil {
return "", fmt.Errorf("INTERNAL: Did not find definition: %w", err)
}
return d, nil
}
func (r *Repetition) Exists(chatID int64, word string) (bool, error) {
row := r.db.QueryRow(`
SELECT COUNT(*) FROM Repetition
WHERE chat_id = $1
AND word = $2`,
chatID, word)
var d int32
if err := row.Scan(&d); err != nil {
return false, fmt.Errorf("INTERNAL: Counting %q for chat %d: %w", word, chatID, err)
}
return d > 0, nil
}
func (r *Repetition) Delete(chatID int64, word string) error {
_, err := r.db.Exec(`
DELETE
FROM Repetition
WHERE word = $0
AND chat_id = $1`,
word, chatID)
if err != nil {
return fmt.Errorf("Failed deleting %q: %w", word, err)
}
return nil
}
// TODO later editing should be helpful.
// func (r *Repetition) Edit(chatID int64, word, newDefinition string) {
// }