Showing posts with label Android; SRS;. Show all posts
Showing posts with label Android; SRS;. Show all posts

Monday, August 1, 2011

Solving an SRS bug which wasn't an SRS bug

Well, for some reason on my last couple of run throughs, the SRS module hasn't been repeating the missed answer. To resolve this conundrum, the first order of business is to inspect the word schedule table on the db and see what's going on there.

Ah, here it is, still in shell history:


adb pull sdcard/data/com.kanjisoft.jlpt/jlpt .

Ok, it's actually under the new package name.

Ok, it looks like what you expect - I've got most of 200-400 in there. But a few are missing, and they should have come up in the select statement. If I reset the start and end numbers to go back to 300-400 - before it was incorrectly advanced - and run the quiz, questions are returned. So, the problem doesn't lay with the select statement. It lays with my counters - they are somehow getting fouled up. Well, this has happend before. I took a stab at a fix, but what ever I did didn't solve the problem, obviously. Now, the question is, why don't I just use the select statement as my criteria? Hmmm...I'd better be a bit careful, because browse mode relies on a little bit different selection criteria. Still, I might be able to convert that one as well.

Still, what's the problem with the numbers? Is it the app state thing? Ok, well, it works on a smaller scale, when I make the interval 4 questions.

It's probably something to do with correct answers not getting reset at some point. All the other numbers, like the number of questions, are taken from the question array.


if (appState.quizSequenceIndex < appState.quizSequence.size()) {

incrementQuestion();

QuestionActivity.this.setupDisplay();

} else { // finished

// Do this to keep highlight in reversed position from showing up
makeListTranparent(this.getListView());

int pct = CalcTotals.calcPctCorrect((AppState) this
.getApplicationContext());

if (pct < 100) {

text = "You correctly answered " + appState.correctAnswers
+ " out of " + appState.quizSequence.size()
+ " for a total score of " + pct + "%";

Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG)
.show();

} else {

// pass control to next level activity
Intent intent = new Intent(QuestionActivity.this,
NextLevelActivity.class);

startActivity(intent);
}


It *could* have something to do with re-running the app, but it happened twice in a row, without ever exiting the app.

I think it's probably easiest if I just log the correct answers, and appState.quizSequence.size, and appState.quizSequenceIndex

Ok, here it is:

Log.d(TAG, "**************************************************************");
Log.d(TAG, "appState.correctAnswers: " + appState.correctAnswers );
Log.d(TAG, "appState.quizSequenceIndex : " + appState.quizSequenceIndex );
Log.d(TAG, "appState.quizSequence.size() : " + appState.quizSequence.size() );
int pct2 = CalcTotals.calcPctCorrect((AppState) this
.getApplicationContext());
Log.d(TAG, "percent: : " + pct2 );
Log.d(TAG, "**************************************************************");

Well, I went through about 100, 96 to be exact, checked the score each time, and it was fine.

Ok, I exited the question activity, and when I came back in, the numbers had been correctly re-done, i.e., it went from this:

D/QuestionActivity(18396): **************************************************************
D/QuestionActivity(18396): appState.correctAnswers: 1
D/QuestionActivity(18396): appState.quizSequenceIndex : 1
D/QuestionActivity(18396): appState.quizSequence.size() : 9
D/QuestionActivity(18396): percent: : 100
D/QuestionActivity(18396): **************************************************************

To this:

/QuestionActivity(18396): **************************************************************
D/QuestionActivity(18396): appState.correctAnswers: 1
D/QuestionActivity(18396): appState.quizSequenceIndex : 2
D/QuestionActivity(18396): appState.quizSequence.size() : 9
D/QuestionActivity(18396): percent: : 50
D/QuestionActivity(18396): **************************************************************
D

So, it regenerates the list, the correct answers..wait. Ah. The index is 2 - it should only be 1. It's not getting reset. It should get reset along with the quizSequence.

Well, actually that would be a better place for correct answers to get reset - they should be in the same place. Let's find where it gets reset.


Ok, well, here's the issue. We have a couple of modes - srs mode and browse mode. The srs mode can rely on the word schedule to whittle down the list of available questions within a range, but the browse mode doesn't have that luxury. So, I've steadfastly refused to save the current random quiz sequence when an app the app gets paused, because I don't think it's worth dealing with the added complexity - I mean, this is a quiz-game-thing. Especially now with the SRS mode, which really should be the main way of quizzing, so you get the repetition.

So, essentially, I'm just going to restart the quiz, srs mode or not, if the question display gets existed for any reason. It's just a simpler, saner approach.

So, the implications for the app are, we no longer persist the number of questions correct nor the current question number if the question screen gets exited. Start for scratch. The takeaway is, prefer srs mode or use short browse quizzes.

Ok, let's take those out of the persistent logic, and make the setting of them consistent.

Ok, give it another try:

/QuestionActivity(18861): **************************************************************
D/QuestionActivity(18861): appState.correctAnswers: 1
D/QuestionActivity(18861): appState.quizSequenceIndex : 2
D/QuestionActivity(18861): appState.quizSequence.size() : 7
D/QuestionActivity(18861): percent: : 50
D/QuestionActivity(18861): **************************************************************


And go back to StartActivity, then back into questions:

D/QuestionActivity(18861): **************************************************************
D/QuestionActivity(18861): appState.correctAnswers: 1
D/QuestionActivity(18861): appState.quizSequenceIndex : 1
D/QuestionActivity(18861): appState.quizSequence.size() : 5
D/QuestionActivity(18861): percent: : 100
D/QuestionActivity(18861): **************************************************************


Ok, and it got rest. That's the best solution.

Kill it click the icon, it goes right back into question, and we get this:

D/QuestionActivity(18919): **************************************************************
D/QuestionActivity(18919): appState.correctAnswers: 1
D/QuestionActivity(18919): appState.quizSequenceIndex : 1
D/QuestionActivity(18919): appState.quizSequence.size() : 2
D/QuestionActivity(18919): percent: : 100
D/QuestionActivity(18919): **************************************************************

Then let it time out for an incorrect answer:

D/QuestionActivity(18919): **************************************************************
D/QuestionActivity(18919): appState.correctAnswers: 1
D/QuestionActivity(18919): appState.quizSequenceIndex : 2
D/QuestionActivity(18919): appState.quizSequence.size() : 2
D/QuestionActivity(18919): percent: : 50
D/QuestionActivity(18919): **************************************************************
I/WindowManager

And we've advance to the next level. Sweet. *That's* what I should've done in the first place.

Ok. That's a wrap for now.

Monday, July 18, 2011

SRS - implementation part 2

Ok, yesterday, we implemented the *input* part of our SRS algorithm, and it seemed to test out ok. Today, we do the output part - that is:

1. For correct answers
a. calculated the time since last the last review,
b. Calculate a new review date
c. create or update a row in the data base with the new scheduled date
c. If there was never an answer set the new scheduled date according to a fixed schedule

2. For incorrect answers:
update or write the scheduled record for the same day, meaning it must be reviewed again before the level is advanced.

Let's look in the old blog entry for how I had come up with a rough schedule of how to set the next date.


Before I do that, I'm going to get rid of level and number from the schedule table. They are joining on _id, and that makes them superfluous.

Ok, I have to drop the table and recreate it to drop the columns. I'm also going to drop the rating. It's superfluous.

CREATE TABLE word_review_schedule
("_id" INTEGER,
last_review_date TEXT(25),
next_review_date TEXT(25))

Drop then:

CREATE TABLE word_review_schedule("_id" INTEGER, last_review_date TEXT(25), next_review_date TEXT(25))


Drop the view then; then create an index on the date.

Now, re-add a row:


insert into word_review_schedule (_id, last_review_date,next_review_date) values (1098, "2001-01-01", "2011-07-19");

Test it's showing up when selected by next date parameter:

select * from v_words_schedule_join where next_review_date > 1

Good! Ok, let's test it.

Copy jlpt to the eclipse project.

And get rid of the database on the device.

Btw, what a great idea it was to root the phone. I can get at whatever I want now.

Ok, now let's go ahead an run it.

Ok. It's hard to tell, but I think the index picked up the pace a little bit.

A couple of quick adjustments to the timing on the question display and get rid of some of the debug statements....

Ok. So, we're now at the point where I'm ready to get going on the srs row creation. Waht was that url again?

Here's where the enum is:

http://gettingintomobile.blogspot.com/2011/06/srs-coding.html


public enum Rating {
WRONG, SLOW, MEDIUM, FAST
};


And the values I was going to assign?

http://gettingintomobile.blogspot.com/2011/06/spaced-repetition-learning-systems-srs.html

There, I thought:

Let's say they get it right in under 2 seconds. That's "by heart". That will have the longest repeat, and will be scheduled in say, seven days.

The next one is between 2 and five. That's we'll say, repeat in 2 days.

If it's 5-10, that's one day.

Ok, let's just say we have Wrong, Slow, and Fast. That's simpler.

Fast is 5 seconds or under. Slow is anything over that.

Now, what were the formulae?

If it's wrong, you reschedule for today. That's easy.

Ah here it is:

Good answer = 2 * (today - last scheduled date)

So, if the difference is 5 days, it will become 10.

If it's 30 days, it will become 60.


A medium answer, over 5 seconds, will be 3 days initially, and times 1.5 the review interval.

So if review interval was 6 days, you'll see it in 1.5 * 5 = 7 days. The again 7 * 1.5 = 10.5, then if it's 10, 1.5 * 10 = 15.

Ok. I'll take it. I can always fine tune it later.

Ok, let's get to coding this. Let's make it a separate module, because Question Activity is bloated already. We'll call it HandleSrsUpdate, and just pass it a method with the parameters it's going to need. It's going to need the id, and the time timer count when the question was answered. Anything else?


In the code, it well determine which of the three intervals it falls into. For that, it will access the row from the schedule, using a new method in database helper. If it's not found,it will calculate the next review data based on the wrong, slow and fast "initial" values - 0, 3 and 5. it will get the difference between the last reviewed data and todays date, and do the multiplication. Or if it was wrong, just reassign today's date.

Ok, I have no more excuses. To the code, gentlemen!

Ok, here's what I have so far:




public class UpdateSRSUtil {

enum Rating {
WRONG, SLOW, FAST
}

Rating rating;

/***********************************************************************************
*
*
*/

public void updateSRSschedule(int _id, int secRemaining) {

// calculate the difference
if (0 == secRemaining ) {
rating = Rating.WRONG;
} else if (secRemaining > 5 ){
rating = Rating.SLOW;
}
else {
rating = Rating.FAST;
}

// retrieve data

}

}


Ok, the retrive should be easy. I can model on the retrive of the word row selection. Wait, I need the id. Actually, a lot of the information will, or can be already int the Question. No, I have them in a separate object, wordSchedule. Hmmm. I wonder of I could just make it an attribute of the question? Actually, although I had previously coded a select of word, I'm not using it because of the join read, and just added next review date to question. All I need to do is add id and last review date to question, and then I can just use that.

Ok, I've added them to Question. Now, I just have to add Database helper select statement.

Wait, wait wait wait wait. There are a couple of other places where I'm selecting question, where I don't need next review date. But, I was already thinking of unifying on the select from the view.

I think my best bet would be not to make any assumptions about whether effective date is set or not. And I'm not sure I should be accessing questions through the scheduled table join if I don't need to. All I really need is the ID, then I can just grab the info from the database.

I'll compromise. I'll add the id, leave the next review date, and leave out last review date. This means no one will have any illusions that Question has the last_reviewed date. But I will be able to use id, because I've just changes the tow other question access routines to add id to question (and set the nextReviewDate to null).

That way, I don't have any entangling alliances with the schedule join. I can just get the last reviewed date from the database when I need it. It's slightly less efficient, but it reduces complexity.

Ok, now I can get back to coding the select statement for the schedule. And I actually have one out there, which I can just change to select on id instead of level / number. Wait I can select on level number. Should I? It would have to be on the join table/view - which really isn't a problem. But I'll just go with id.

Ok, here is the database select:


public WordSchedule selectScheduleById(int id) {


Cursor cursor = this.myDataBase.query(SCHEDULE_TABLE_NAME,
new String[] { "_id", "last_review_date",
"next_review_date" }, "_id = ?",
new String[] { Integer.toString(id) }, null, null, null);
ArrayList rows = new ArrayList();

if (cursor.moveToFirst()) {
do {

int read_id = cursor.getInt(0);
String lastReviewDateStr = cursor.getString(1);
String nextReviewDateStr = cursor.getString(2);
Date lastReviewDate = Utils.stringToDate(lastReviewDateStr);
Date nextReviewDate = Utils.stringToDate(nextReviewDateStr);

WordSchedule wordSchedule = new WordSchedule(read_id, lastReviewDate, nextReviewDate);

rows.add(wordSchedule);
} while (cursor.moveToNext());
}

if (cursor != null && !cursor.isClosed()) {
cursor.close();
}

// close();

return rows.get(0);

}



And to access it:

DataBaseHelper dbHelper = DataBaseHelper.createDB(context);

WordSchedule ws = dbHelper.selectScheduleById(id);

dbHelper.close();

Ok. Let's see if what we have so far is working. Let's add a call to this from QuestionActivity and print to the logcat to make sure it's working.

Oky dokey. After a fair a amount of tweaking and adjusting, I've gotten to the point of accessing the correct row in the schedule. Yipee-yi-yay, cowboy.

On thing I did, which I should do more often, is give the code a looking over before running the test. I found that I'd forgotten to code the call to the method I was supposed to be testing, for one thing. I also neatened up the code that I checked. Eyeballing the code and catching a bug there is *exponentially* better than running and debugging. Debugging takes time, and so often, I've traced it back to a careless mistake. Plus, it gets you more familiar with what your code is doing, as well. It makes you calmer, mor in control, less stressed. This is a skill I'm really going to cultivate.

Ok, this post is getting kind of long. I'm going to break it off here, and resume the final part - creating the output and seeing if it works - in my next post.