• Skip to content
  • Skip to link menu
KDE 4.0 API Reference
  • KDE API Reference
  • kdeedu
  • Sitemap
  • Contact Us
 

parley

answervalidator.cpp

Go to the documentation of this file.
00001 /***************************************************************************
00002     Copyright 2007 Frederik Gladhorn <frederik.gladhorn@kdemail.net>
00003  ***************************************************************************/
00004 
00005 /***************************************************************************
00006  *                                                                         *
00007  *   This program is free software; you can redistribute it and/or modify  *
00008  *   it under the terms of the GNU General Public License as published by  *
00009  *   the Free Software Foundation; either version 2 of the License, or     *
00010  *   (at your option) any later version.                                   *
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     // default: try locale
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     // we might succeed with language name instead.
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     // make sure the matrix is big enough
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     // init 0..m, 0..n as starting values - distance to ""
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                 // if current char is equal, no cost for substitution
00129                 cost = 0;
00130             } else {
00131                 cost = 1;
00132             }
00133             m_d[i + j*dWidth] = qMin( qMin ( // min of three possibilities
00134                       m_d[i-1 + (j  )*dWidth] + 1,     // deletion
00135                       m_d[i   + (j-1)*dWidth] + 1),     // insertion
00136                       m_d[i-1 + (j-1)*dWidth] + cost);   // substitution
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 //         m_htmlCorrection = i18n("Your answer is right!");
00181 kDebug() << "right";
00182         return;
00183     }
00184 
00185     TestEntry::ErrorTypes errorTypes = TestEntry::UnknownMistake;
00186 
00187     if ( m_entry ) {
00188         // check synonym
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 //                 m_htmlCorrection = i18n("You entered a synonym.");
00194             } else {
00195                 m_entry->setLastPercentage(0.0); // bit harsh maybe
00196 //                 m_htmlCorrection = i18n("You entered a synonym.");
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     // check synonym
00227     if ( m_entry->exp->translation(m_translation).synonym() == m_userAnswer ) {
00228         m_entry->setLastErrors(TestEntry::Synonym);
00229         if ( Prefs::countSynonymsAsCorrect() ) {
00230             // synonym, good for you
00231             m_entry->setLastPercentage(1.0);
00232         } else {
00233             // it is the synonym but we don't accept it
00234             m_entry->setLastPercentage(0.0); // bit harsh maybe
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         // could be noun + article
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                 // yes, the answer is an article + noun
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     // ok, more than one word (or one+article)
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 //     simpleCorrector();
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     // nothing to be done here if it's right
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         // probably misspelled
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         // this is a different word but sounds similiar!
00354         if ( !isMisspelled && inSuggestions ) {
00355             grade = FALSE_FRIEND_GRADE;
00356 //             htmlCorrection = QString::fromLatin1("<font color=\"#8C1818\">NOOOO! That was a false friend!</font> ");
00357             errorTypes = errorTypes = TestEntry::FalseFriend;
00358             return ;
00359         }
00360         // unrelated word
00361         if ( !isMisspelled && !inSuggestions ) {
00362             grade = UNRELATED_WORD_GRADE;
00363 //             htmlCorrection = QString::fromLatin1("<font color=\"#8C1818\">Do you have any idea what you are talking about? (Wrong word, you spelled it correct I guess.)</font> ");
00364             errorTypes = TestEntry::UnrelatedWord;
00365             return;
00366         }
00367         // complete nonsense, unless levenshtein comes to the rescue
00368         if ( isMisspelled && !inSuggestions ) {
00369             if ( ((double)levenshtein/ qMax(solution.length(), userWord.length())) < LEVENSHTEIN_THRESHOLD ) {
00370 //                 htmlCorrection = QString::fromLatin1("<font color=\"#8C1818\">Seems like you got the spellig wrong.</font> ");
00371                 errorTypes = TestEntry::SpellingMistake;
00372                 return;
00373             } else {
00374 //                 htmlCorrection = QString::fromLatin1("<font color=\"#8C1818\">I don't know that word and it is not similiar to the solution.</font> ");
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 //             htmlCorrection = QString::fromLatin1("<font color=\"#8C1818\">No spellchecker, but seems like a spelling error.</font> ");
00383             errorTypes = TestEntry::SpellingMistake;
00384             return;
00385         } else {
00386             grade = 1.0 - ((double)levenshtein/ qMax(solution.length(), userWord.length()));
00387 //             htmlCorrection = QString::fromLatin1("<font color=\"#8C1818\">No dictionary and no clue.</font> ");
00388             errorTypes = TestEntry::UnknownMistake;
00389             return;
00390         }
00391     }
00392 
00393     // cannot get here
00394 //     htmlCorrection = QString::fromLatin1("<font color=\"#8C1818\">No dictionary and no clue.</font> ");
00395     errorTypes = TestEntry::UnknownMistake;
00396     return;
00397 }
00398 
00399 
00400 void AnswerValidator::sentenceAnalysis()
00401 {
00402     QStringList solutionWords;
00403     QStringList userAnswerWords;
00404 
00405     // filter !?.,;:¿ etc and throw them away
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     // matrix of levenshteinDistances
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     // check if another pair is possible
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             // taken
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 }

parley

Skip menu "parley"
  • Main Page
  • Namespace List
  • Class Hierarchy
  • Alphabetical List
  • Class List
  • File List
  • Namespace Members
  • Class Members
  • Related Pages

kdeedu

Skip menu "kdeedu"
  • kalzium
  • kanagram
  • kig
  • klettres
  • kstars
  • libkdeedu
  •   keduvocdocument
  •   docs
  •   src
  • parley
Generated for kdeedu by doxygen 1.5.4
This website is maintained by Adriaan de Groot and Allen Winter.
KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal