2 * Copyright (c) 2021, S. Gilles <sgilles@sgilles.net>
4 * Permission to use, copy, modify, and/or distribute this software
5 * for any purpose with or without fee is hereby granted, provided
6 * that the above copyright notice and this permission notice appear
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
13 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
14 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
15 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
16 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
27 std::string
Session::salt
= "will be overwritten";
30 * Start up a session attached to a directory like "~/.data/sispare".
31 * Read in cards, set up state so that the UI can ask what's going on,
34 Session::Session(const std::filesystem::path
& data_dir
) :
37 session_start(std::chrono::system_clock::now()),
38 current_action(Currently_doing::Trivial
),
41 schedule_files_to_clean(),
46 * Read all the cards in the schedule directory, figure out what
47 * we need to review today.
49 const std::regex
ymd_matcher("^(\\d+)-(\\d+)-(\\d+)$");
50 const std::time_t now
= std::chrono::system_clock::to_time_t(session_start
);
51 std::ostringstream gcc_still_doesnt_have_std_format
;
53 gcc_still_doesnt_have_std_format
<< now
;
54 Session::salt
= gcc_still_doesnt_have_std_format
.str();
55 std::filesystem::create_directories(data_dir
/ "schedule");
56 std::filesystem::create_directories(data_dir
/ "cards");
58 for (const auto& entry
: std::filesystem::directory_iterator(data_dir
/ "schedule")) {
59 const std::filesystem::path
& p
= entry
.path();
63 * "~/.data/sispare/schedule/2099-10-27. Is it that day
66 const std::string ymd
= p
.stem().string();
67 std::smatch ymd_matches
;
68 struct tm this_tm
= {};
70 if (std::regex_search(ymd
, ymd_matches
, ymd_matcher
)) {
71 this_tm
.tm_year
= std::stoi(ymd_matches
[1]) - 1900;
72 this_tm
.tm_mon
= std::stoi(ymd_matches
[2]) - 1;
73 this_tm
.tm_mday
= std::stoi(ymd_matches
[3]);
74 const std::time_t sched_time
= mktime(&this_tm
);
76 if (sched_time
< now
) {
77 schedule_files_to_clean
.push_back(p
);
78 std::ifstream
sched_is(p
);
82 * The schedule file just contains a
83 * number of lines, and
84 * "~/.data/sispare/cards/<line>" is the
87 while (getline(sched_is
, line
)) {
89 cards
.insert(std::move(Card::mk(data_dir
/ "cards" / line
)));
90 } catch (const std::exception
& ex
) {
91 cards_to_delete
.insert(data_dir
/ "cards" / line
);
99 current_action
= Currently_doing::Viewing_A
;
100 cards_it
= cards
.cbegin();
106 Session::have_cards_to_review() const
108 return !cards
.empty();
112 Session::get_current_action() const
114 return current_action
;
117 std::optional
<const std::string
>
118 Session::get_current_A_side() const
121 cards_it
== cards
.end()) {
128 std::optional
<const std::string
>
129 Session::get_current_B_side() const
132 cards_it
== cards
.end()) {
140 Session::get_progress_string() const
142 std::ostringstream out
;
144 out
<< it_index
<< "/" << cards
.size();
150 Session::get_statistics() const
152 std::ostringstream out
;
153 std::size_t num_passed
= 0;
154 std::size_t num_failed
= 0;
156 for (auto& c
: cards
) {
157 auto ret
= card_status
.find(c
.path
);
159 if (ret
!= card_status
.end()) {
160 switch (ret
->second
) {
161 case Review_status::Pass_easy
:
162 case Review_status::Pass_hard
:
165 case Review_status::Fail
:
168 case Review_status::Unreviewed
:
174 out
<< "Finished." << std::endl
<< std::endl
;
175 out
<< "Passed: " << num_passed
<< std::endl
;
176 out
<< "Failed: " << num_failed
<< std::endl
;
182 Session::get_previously_seen_string() const
185 cards_it
== cards
.end()) {
189 auto ret
= card_status
.find(cards_it
->path
);
191 if (ret
== card_status
.end()) {
195 switch (ret
->second
) {
196 case Review_status::Pass_easy
:
197 return "Marked as passed";
198 case Review_status::Pass_hard
:
199 return "Marked as passed (hard)";
200 case Review_status::Fail
:
201 return "Marked as failed";
202 case Review_status::Unreviewed
:
210 Session::have_next_card() const
212 return !cards
.empty() &&
213 cards_it
!= cards
.end() &&
214 std::next(cards_it
) != cards
.end();
218 Session::have_prev_card() const
220 return !cards
.empty() &&
221 cards_it
!= cards
.begin();
225 Session::flip_current_card()
227 switch (current_action
) {
228 case Currently_doing::Viewing_A
:
229 current_action
= Currently_doing::Viewing_B
;
231 case Currently_doing::Viewing_B
:
232 current_action
= Currently_doing::Viewing_A
;
240 Session::mark_current_card_as(Review_status review_status
)
243 cards_it
== cards
.end()) {
247 card_status
[cards_it
->path
] = review_status
;
252 Session::move_next_card()
258 if (have_next_card()) {
259 current_action
= Currently_doing::Viewing_A
;
261 current_action
= Currently_doing::Finished
;
269 Session::move_prev_card()
275 if (have_prev_card()) {
278 current_action
= Currently_doing::Viewing_A
;
283 Session::update_cards_on_disk()
286 * First, go through all the cards and update their levels
287 * according to whether they were passed or failed today. Write
288 * out their new level (to the card's own directory) and the
289 * next time they should be reviewed (to somewhere in the
290 * schedule directory).
292 int cutoff_level
= (std::end(timing_data
) - std::begin(timing_data
)) - 1;
293 std::random_device rand_dev
;
294 std::mt19937
gen(rand_dev());
296 for (auto& c
: cards
) {
297 int new_level
= c
.level
;
298 auto ret
= card_status
.find(c
.path
);
300 if (ret
== card_status
.end()) {
304 /* Calculate and write new level. */
305 switch (ret
->second
) {
306 case Review_status::Unreviewed
:
308 case Review_status::Pass_easy
:
309 new_level
= std::min(new_level
+ 1, cutoff_level
);
311 case Review_status::Pass_hard
:
312 new_level
= std::max(new_level
/ 2, 1);
314 case Review_status::Fail
:
319 std::ofstream
out_level(c
.path
/ "level");
321 out_level
<< new_level
<< std::endl
;
323 if (new_level
> cutoff_level
) {
324 /* Don't reschedule this card */
328 /* Choose when it'll get reviewed. Note uniform_int_distribution is inclusive of endpoints. */
329 std::pair
<int, int> delay_range
= timing_data
[new_level
];
330 std::uniform_int_distribution
<> possible_waits(delay_range
.first
, delay_range
.second
);
331 int days_to_wait
= possible_waits(gen
);
332 std::time_t then
= std::chrono::system_clock::to_time_t(session_start
+ std::chrono::days(days_to_wait
));
333 struct tm then_tm
= *localtime(&then
);
335 /* Write to the proper schedule file */
336 std::ostringstream date
;
338 date
<< (then_tm
.tm_year
+ 1900);
340 date
<< std::setfill('0') << std::setw(2) << (then_tm
.tm_mon
+ 1);
342 date
<< std::setfill('0') << std::setw(2) << then_tm
.tm_mday
;
343 const std::string sched_day
= date
.str();
344 std::ofstream
out_sched_day(data_dir
/ "schedule" / sched_day
, std::ios_base::app
);
346 out_sched_day
<< c
.path
.stem().string() << std::endl
;
350 * Next, go through all the schedule files that we read from on
351 * startup. Every line in those files that corresponds to a card
352 * we reviewed needs to be wiped. If we quit the session early,
353 * there might be some lines left over. Schedule files that end
354 * up completely empty should just be deleted from disk.
356 for (auto& f
: schedule_files_to_clean
) {
358 std::ostringstream filtered
;
365 * The filter should only keep the cards we didn't
366 * review in this session. The cards that we did were
367 * re-scheduled above.
369 * Additionally, cards that caused errors on session
370 * load should be erased.
374 while (std::getline(ii
, line
)) {
375 /* Don't output deleted cards */
376 auto ret1
= cards_to_delete
.find(data_dir
/ "cards" / line
);
378 if (ret1
!= cards_to_delete
.end()) {
382 /* Don't output cards that were passed/failed this session */
383 auto ret2
= card_status
.find(data_dir
/ "cards" / line
);
385 if (ret2
== card_status
.end()) {
386 filtered
<< line
<< std::endl
;
390 switch (ret2
->second
) {
391 case Review_status::Pass_easy
:
392 case Review_status::Pass_hard
:
393 case Review_status::Fail
:
395 case Review_status::Unreviewed
:
396 filtered
<< line
<< std::endl
;
402 std::string filtered_str
= filtered
.str();
405 * Only keep the schedule file around if it has some
406 * cards in it. Otherwise, get rid of it.
408 if (filtered_str
.size() > 0) {
411 oo
<< filtered
.str();
413 std::filesystem::remove(f
);