parley
answervalidator.cpp
Go to the documentation of this file.00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013 #include "answervalidator.h"
00014
00015 #include "prefs.h"
00016
00017 #include <keduvocexpression.h>
00018 #include <keduvocdocument.h>
00019 #include <keduvocidentifier.h>
00020
00021 #include <KLocalizedString>
00022 #include <sonnet/speller.h>
00023 #include <kdebug.h>
00024
00027 namespace ParleyStringHandler {
00028 QString stripAccents(const QString& original){
00029 QString noAccents;
00030 QString decomposed = original.normalized(QString::NormalizationForm_D);
00031 for (int i = 0; i < decomposed.length(); ++i) {
00032 if ( decomposed[i].category() != 1 ) {
00033 noAccents.append(decomposed[i]);
00034 }
00035 }
00036 kDebug() << original << " without accents: " << noAccents;
00037 return noAccents;
00038 }
00039 }
00040
00041
00042 const double AnswerValidator::LEVENSHTEIN_THRESHOLD = 0.2;
00043 const double AnswerValidator::UNRELATED_WORD_GRADE = 0.0;
00044 const double AnswerValidator::FALSE_FRIEND_GRADE = 0.0;
00045 const double AnswerValidator::SPELLING_MISTAKE_PER_LETTER_PUNISHMENT = 0.2;
00046 const double AnswerValidator::CAPITALIZATION_MISTAKE_PUNISHMENT = 0.1;
00047 const double AnswerValidator::ACCENT_MISTAKE_PUNISHMENT = 0.1;
00048 const double AnswerValidator::WRONG_ARTICLE_PUNISHMENT = 0.1;
00049
00050 AnswerValidator::AnswerValidator(KEduVocDocument* doc)
00051 {
00052 m_doc = doc;
00053 m_entry = 0;
00054 m_speller = 0;
00055 m_spellerAvailable = false;
00056 }
00057
00058 AnswerValidator::~AnswerValidator()
00059 {
00060 delete m_speller;
00061 }
00062
00063
00064 void AnswerValidator::setLanguage(int translation)
00065 {
00066 m_translation = translation;
00067
00068
00069 if ( !m_speller ) {
00070 m_speller = new Sonnet::Speller(m_doc->identifier(translation).locale());
00071 } else {
00072 m_speller->setLanguage(m_doc->identifier(translation).locale());
00073 }
00074
00075
00076 if ( !m_speller->isValid() ) {
00077 m_speller->setLanguage(m_doc->identifier(translation).name());
00078 }
00079
00080 if ( !m_speller->isValid() ) {
00081 kDebug() << "No spellchecker for current language found: " << m_doc->identifier(m_translation).locale();
00082 kDebug() << "Avaliable dictionaries: " << m_speller->availableLanguages()
00083 << "\n names: " << m_speller->availableLanguageNames()
00084 << "\n backends: " << m_speller->availableBackends();
00085 m_spellerAvailable = false;
00086 } else {
00087 m_spellerAvailable = true;
00088 }
00089 }
00090
00091
00092 void AnswerValidator::setTestEntry(TestEntry * entry)
00093 {
00094 m_entry = entry;
00095 if (m_entry) {
00096 m_solution = m_entry->exp->translation(m_translation).text();
00097 }
00098 }
00099
00100 int AnswerValidator::levenshteinDistance(QString s, QString t)
00101 {
00102 int m = s.length();
00103 int n = t.length();
00104
00105 int dWidth = m+1 ;
00106
00107
00108 if( m_d.size() < (m+1) * (n+1)) {
00109 m_d.resize( (m+1) * (n+1) );
00110 }
00111
00112 int i;
00113 int j;
00114
00115
00116 for ( i = 0; i <= m; i++ )
00117 {
00118 m_d[i + 0*dWidth] = i;
00119 }
00120 for ( j = 0; j <= n; j++ ) {
00121 m_d[0 + j*dWidth] = j;
00122 }
00123
00124 int cost;
00125 for (i = 1; i <= m; i++) {
00126 for (j = 1; j <= n; j++) {
00127 if ( s[i-1] == t[j-1] ) {
00128
00129 cost = 0;
00130 } else {
00131 cost = 1;
00132 }
00133 m_d[i + j*dWidth] = qMin( qMin (
00134 m_d[i-1 + (j )*dWidth] + 1,
00135 m_d[i + (j-1)*dWidth] + 1),
00136 m_d[i-1 + (j-1)*dWidth] + cost);
00137 }
00138 }
00139 return m_d[m + n*dWidth];
00140 }
00141
00142 bool AnswerValidator::spellcheckerMisspelled(QString userAnswer)
00143 {
00144 if (!m_spellerAvailable) {
00145 return true;
00146 }
00147 return m_speller->isMisspelled(userAnswer);
00148 }
00149
00150 bool AnswerValidator::spellcheckerInSuggestionList(QString solution, QString userAnswer)
00151 {
00152 if ( !m_spellerAvailable ) {
00153 return false;
00154 }
00155
00156 kDebug() << "entered: " << userAnswer << " misspelled: " << m_speller->isMisspelled(userAnswer) << " suggestions: " << m_speller->suggest(userAnswer);
00157
00158 if ( m_speller->suggest(userAnswer).contains(solution) ) {
00159 kDebug() << "I think this is a spelling error.";
00160 return true;
00161 } else {
00162 kDebug() << "No, this is a different word I think.";
00163 return false;
00164 }
00165 }
00166
00167
00168 void AnswerValidator::simpleCorrector()
00169 {
00170 kDebug() << "simpleCorrector";
00171 if ( m_entry == 0 ) {
00172 kError() << "No entry set, cannot verify answer.";
00173 return;
00174 }
00175
00177 if ( m_solution == m_userAnswer ) {
00178 m_entry->setLastErrors(TestEntry::Correct);
00179 m_entry->setLastPercentage(1.0);
00180
00181 kDebug() << "right";
00182 return;
00183 }
00184
00185 TestEntry::ErrorTypes errorTypes = TestEntry::UnknownMistake;
00186
00187 if ( m_entry ) {
00188
00189 if ( m_entry->exp->translation(m_translation).synonym() == m_userAnswer ) {
00190 m_entry->setLastErrors(TestEntry::Synonym);
00191 if ( Prefs::countSynonymsAsCorrect() ) {
00192 m_entry->setLastPercentage(1.0);
00193
00194 } else {
00195 m_entry->setLastPercentage(0.0);
00196
00197 }
00198 return;
00199 }
00200 }
00201
00202 int levensthein = levenshteinDistance( m_solution, m_userAnswer );
00203
00204 m_entry->setLastPercentage(1.0 - ((double)levensthein/ qMax(m_solution.length(), m_userAnswer.length())));
00205
00206 kDebug() << "simpleCorrector" << m_userAnswer << "-" << m_solution << "has levensthein distance: " << levensthein << " grade: " << m_entry->lastPercentage();
00207 }
00208
00209
00210 void AnswerValidator::defaultCorrector()
00211 {
00214 if ( m_solution == m_userAnswer ) {
00215 m_entry->setLastErrors(TestEntry::Correct);
00216 m_entry->setLastPercentage(1.0);
00217 return;
00218 }
00219
00220 if ( m_userAnswer.isEmpty() ) {
00221 m_entry->setLastErrors(TestEntry::Empty);
00222 m_entry->setLastPercentage(0.0);
00223 return;
00224 }
00225
00226
00227 if ( m_entry->exp->translation(m_translation).synonym() == m_userAnswer ) {
00228 m_entry->setLastErrors(TestEntry::Synonym);
00229 if ( Prefs::countSynonymsAsCorrect() ) {
00230
00231 m_entry->setLastPercentage(1.0);
00232 } else {
00233
00234 m_entry->setLastPercentage(0.0);
00235 }
00236 return;
00237 }
00238
00239 int numberSolutionWords = m_solution.simplified().split(" ").count();
00240 int numberAnswerWords = m_userAnswer.simplified().split(" ").count();
00241
00242 if ( numberSolutionWords == 1 ) {
00243 double grade;
00244 TestEntry::ErrorTypes errors;
00245 wordCompare(m_solution, m_userAnswer, grade, errors);
00246 m_entry->setLastPercentage(grade);
00247 m_entry->setLastErrors(errors);
00248 return;
00249 }
00250
00251 if ( numberSolutionWords == 2 ) {
00252
00253 QStringList solutionWords = m_solution.simplified().split(" ");
00254
00255 if ( m_translation >= 0 ) {
00256 if (m_doc->identifier(m_translation).article().isArticle(solutionWords.value(0)) ) {
00257
00258 if ( numberAnswerWords == 1 ) {
00259 double percent;
00260 TestEntry::ErrorTypes errors;
00261 wordCompare(solutionWords.value(1), m_userAnswer.simplified(), percent, errors);
00262 m_entry->setLastPercentage(qMax(percent-WRONG_ARTICLE_PUNISHMENT, 0.0));
00263 m_entry->setLastErrors(errors|TestEntry::ArticleMissing);
00264 return;
00265 }
00266 if ( numberAnswerWords == 2 ) {
00267 double percent;
00268 TestEntry::ErrorTypes errors;
00269 wordCompare(solutionWords.value(1), m_userAnswer.simplified().split(" ").value(1), percent, errors);
00270
00271 if ( m_userAnswer.simplified().split(" ").value(0) == solutionWords.value(0) ) {
00272 m_entry->setLastErrors(errors);
00273 } else {
00274 m_entry->setLastPercentage(qMax(percent-WRONG_ARTICLE_PUNISHMENT, 0.0));
00275 m_entry->setLastErrors(errors|TestEntry::ArticleWrong);
00276 }
00277 return;
00278 }
00279 }
00280 }
00281 }
00282
00283
00284 sentenceAnalysis();
00285 }
00286
00287
00288
00289
00290 void AnswerValidator::checkUserAnswer(const QString & userAnswer)
00291 {
00292 if ( m_entry == 0 ) {
00293 kError() << "Error: no entry set for validator.";
00294 return;
00295 }
00296
00297 m_userAnswer = userAnswer;
00298
00299
00300 defaultCorrector();
00301 }
00302
00303 void AnswerValidator::checkUserAnswer(const QString & solution, const QString & userAnswer, const QString& language)
00304 {
00305 kDebug() << "CheckUserAnswer with two strings. The one string version is prefered.";
00306 if ( !language.isEmpty() ) {
00307
00308 } else {
00309 m_spellerAvailable = false;
00310 }
00311
00312 m_solution = solution;
00313
00314 checkUserAnswer(userAnswer);
00315 }
00316
00317
00318 void AnswerValidator::wordCompare(const QString & solution, const QString & userWord, double& grade, TestEntry::ErrorTypes& errorTypes)
00319 {
00321
00322
00323 if ( solution == userWord ) {
00324 grade = 1.0;
00325 errorTypes = TestEntry::Correct;
00326 return;
00327 }
00328 if ( solution.toLower() == userWord.toLower() ) {
00329 grade = 1.0 - CAPITALIZATION_MISTAKE_PUNISHMENT;
00330 errorTypes = TestEntry::CapitalizationMistake;
00331 return ;
00332 }
00333 if ( ParleyStringHandler::stripAccents(solution) == ParleyStringHandler::stripAccents(userWord) ) {
00334 grade = 1.0 - ACCENT_MISTAKE_PUNISHMENT;
00335 errorTypes = TestEntry::AccentMistake;
00336 return ;
00337 }
00338
00339 int levenshtein = levenshteinDistance(solution, userWord);
00340
00341 if ( m_spellerAvailable ) {
00342 bool inSuggestions = false;
00343 bool isMisspelled = m_speller->isMisspelled( userWord );
00344 if ( m_speller->suggest(userWord).contains(solution) ) {
00345 inSuggestions = true;
00346 }
00347
00348 if ( isMisspelled && inSuggestions ) {
00349 grade = 1.0 - qMax (levenshtein * SPELLING_MISTAKE_PER_LETTER_PUNISHMENT, 1.0);
00350 errorTypes = TestEntry::SpellingMistake;
00351 return;
00352 }
00353
00354 if ( !isMisspelled && inSuggestions ) {
00355 grade = FALSE_FRIEND_GRADE;
00356
00357 errorTypes = errorTypes = TestEntry::FalseFriend;
00358 return ;
00359 }
00360
00361 if ( !isMisspelled && !inSuggestions ) {
00362 grade = UNRELATED_WORD_GRADE;
00363
00364 errorTypes = TestEntry::UnrelatedWord;
00365 return;
00366 }
00367
00368 if ( isMisspelled && !inSuggestions ) {
00369 if ( ((double)levenshtein/ qMax(solution.length(), userWord.length())) < LEVENSHTEIN_THRESHOLD ) {
00370
00371 errorTypes = TestEntry::SpellingMistake;
00372 return;
00373 } else {
00374
00375 errorTypes = TestEntry::UnknownMistake;
00376 return;
00377 }
00378 }
00379 } else {
00380 if ( ((double)levenshtein/ qMax(solution.length(), userWord.length())) < LEVENSHTEIN_THRESHOLD ) {
00381 grade = 1.0 - ((double)levenshtein/ qMax(solution.length(), userWord.length()));
00382
00383 errorTypes = TestEntry::SpellingMistake;
00384 return;
00385 } else {
00386 grade = 1.0 - ((double)levenshtein/ qMax(solution.length(), userWord.length()));
00387
00388 errorTypes = TestEntry::UnknownMistake;
00389 return;
00390 }
00391 }
00392
00393
00394
00395 errorTypes = TestEntry::UnknownMistake;
00396 return;
00397 }
00398
00399
00400 void AnswerValidator::sentenceAnalysis()
00401 {
00402 QStringList solutionWords;
00403 QStringList userAnswerWords;
00404
00405
00406 solutionWords = m_solution.simplified().split(QRegExp("\\W"), QString::SkipEmptyParts);
00407 userAnswerWords = m_userAnswer.simplified().split(QRegExp("\\W"), QString::SkipEmptyParts);
00408 kDebug() << "Solution words: " << solutionWords;
00409 kDebug() << "Answer: " << userAnswerWords;
00410
00411 QStringList correctWords;
00412 QStringList wrongWords;
00413
00414 for ( int i = 0; i < userAnswerWords.count(); i++ ) {
00415 int pos = solutionWords.indexOf( userAnswerWords.value(i) );
00416 if ( pos >= 0 ) {
00417 correctWords.append(userAnswerWords.value(i));
00418 solutionWords.removeAt(pos);
00419 userAnswerWords.removeAt(i);
00420 } else {
00421 wrongWords.append(userAnswerWords.value(i));
00422 }
00423 }
00424
00425 kDebug() << " remaining: solution: " << solutionWords.count()
00426 << "user: " << userAnswerWords.count();
00427
00428 QList< QPair<QString, QString> > pairs = bestPairs(solutionWords, userAnswerWords);
00429
00430 for ( int i = 0; i < pairs.count(); i++ ) {
00431 kDebug() << "Possible pair: " << pairs.value(i).first << " and " << pairs.value(i).second;
00432 }
00433
00434
00435 QString correction;
00436 correction.append("Correct: ");
00437 foreach (QString correctWord, correctWords) {
00438 correction.append(QString::fromLatin1("<font color=\"#188C18\">") + correctWord + QString::fromLatin1("</font> "));
00439 }
00440
00441 correction.append(" Wrong: ");
00442 foreach (QString wrongWord, wrongWords) {
00443 correction.append(QString::fromLatin1("<font color=\"#8C1818\">") + wrongWord + QString::fromLatin1("</font> "));
00444 }
00445
00446 int levenshtein = levenshteinDistance(m_solution, m_userAnswer);
00447
00448 kDebug() << correction;
00449 kDebug() << "IMPLEMENT ME TO ACTUALLY EVALUATE THE ABOVE AND GENERATE A GRADE!";
00450 m_entry->setLastPercentage(1.0 - ((double)levenshtein/ qMax(m_solution.length(), m_userAnswer.length())));
00451 m_entry->setLastErrors(TestEntry::UnknownMistake);
00452 }
00453
00454 QList< QPair < QString , QString > > AnswerValidator::bestPairs(const QStringList& solutionWords , const QStringList& userAnswerWords )
00455 {
00456 int nSol = solutionWords.count();
00457 int nUser = userAnswerWords.count();
00458
00459 QByteArray d;
00460 d.resize( nSol * nUser );
00461
00462 QList< QPair < QString , QString > > pairList;
00463
00464
00465 for ( int i = 0; i < nSol; i++) {
00466 for ( int j = 0; j < nUser; j++) {
00467 d[i + nSol*j] = levenshteinDistance(solutionWords.value(i), userAnswerWords.value(j));
00468 }
00469 }
00470
00471 int MAX_LEVENSHTEIN = 5;
00472
00473
00474 int min;
00475 int posSol;
00476 int posUser;
00477 do {
00478 min = MAX_LEVENSHTEIN;
00479 for ( int i = 0; i < nSol; i++ ) {
00480 for ( int j = 0; j < nUser; j++) {
00481 if ( d.at(i + j*nSol ) < min ) {
00482 min = d.at(i + j*nSol );
00483 posSol = i;
00484 posUser = j;
00485 }
00486 }
00487 }
00488 if ( min < MAX_LEVENSHTEIN ) {
00489 pairList.append( qMakePair(solutionWords.value(posSol), userAnswerWords.value(posUser)) );
00490
00491
00492 d[posSol + posUser*nSol] = MAX_LEVENSHTEIN;
00493 }
00494 } while ( min < MAX_LEVENSHTEIN );
00495
00496 return pairList;
00497 }
00498
00499
00500 bool AnswerValidator::spellcheckerAvailable()
00501 {
00502 return m_spellerAvailable;
00503 }