/** * @file JSC_checker.cpp * @brief JSCChecker class implementation * * This file is part of JS8Call. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * (C) 2018 Jordan Sherer - All Rights Reserved */ #include "JSC_checker.h" #include "JS8_Main/Varicode.h" #include "JSC.h" #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(jsc_checker_js8) const int CORRECT = QTextFormat::UserProperty + 10; const QString ALPHABET = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; /** * @brief Construct a new JSCChecker::JSCChecker object * * @param parent */ JSCChecker::JSCChecker(QObject *parent) : QObject(parent) {} /** * @brief Check if the cursor has the specified property * * @param cursor * @param property * @return true * @return false */ bool cursorHasProperty(const QTextCursor &cursor, int property) { if (property < QTextFormat::UserProperty) { return false; } if (cursor.charFormat().intProperty(property) == 1) { return true; } auto const &formats = cursor.block().layout()->formats(); int pos = cursor.positionInBlock(); foreach (auto const &range, formats) { if (pos > range.start && pos <= range.start + range.length && range.format.intProperty(property) == 1) { return true; } } return false; } /** * @brief Get the next character from the cursor * * @param c * @return QString */ QString nextChar(QTextCursor c) { QTextCursor cur(c); cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); return cur.selectedText().toUpper(); } /** * @brief Check if the string is numeric * * @param s * @return true * @return false */ bool isNumeric(QString s) { return s.indexOf(QRegularExpression("^\\d+$")) == 0; } /** * @brief Check if the character is a word character * * @param ch * @return true * @return false */ bool isWordChar(QString ch) { return ch.contains(QRegularExpression("^\\w$")); } /** * @brief Check the range of text in the QTextEdit for valid JSC callsigns * * @param edit * @param start * @param end */ void JSCChecker::checkRange(QTextEdit *edit, int start, int end) { if (end == -1) { QTextCursor tmpCursor(edit->textCursor()); tmpCursor.movePosition(QTextCursor::End); end = tmpCursor.position(); } // stop contentsChange signals from being emitted due to changed charFormats edit->document()->blockSignals(true); // qCDebug(jsc_checker_js8) << "checking range " << start << " - " << end; QTextCharFormat errorFmt; errorFmt.setFontUnderline(true); errorFmt.setUnderlineColor(Qt::red); errorFmt.setUnderlineStyle(QTextCharFormat::WaveUnderline); QTextCharFormat defaultFormat = QTextCharFormat(); auto cursor = edit->textCursor(); cursor.beginEditBlock(); { cursor.setPosition(start); while (cursor.position() < end) { bool correct = false; cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); if (cursor.selectedText() /*.toUpper()*/ == "@") { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); } if (cursorHasProperty(cursor, CORRECT)) { correct = true; } else { QString word = cursor.selectedText().toUpper(); // three or less is always "correct" if (word.length() < 4 || isNumeric(word)) { correct = true; } else { bool found = false; quint32 index = JSC::lookup(word, &found); if (found) { correct = JSC::map[index].size == word.length(); } if (!correct) { correct = Varicode::isValidCallsign(word, nullptr); } } // qCDebug(jsc_checker_js8) << "word" << word << "correct" << // correct; } if (correct) { QTextCharFormat fmt = cursor.charFormat(); fmt.setFontUnderline(defaultFormat.fontUnderline()); fmt.setUnderlineColor(defaultFormat.underlineColor()); fmt.setUnderlineStyle(defaultFormat.underlineStyle()); cursor.setCharFormat(fmt); } else { cursor.mergeCharFormat(errorFmt); } // Go to next word start // while(cursor.position() < end && !isWordChar(nextChar(cursor))){ // cursor.movePosition(QTextCursor::NextCharacter); //} cursor.movePosition(QTextCursor::NextCharacter); } } cursor.endEditBlock(); edit->document()->blockSignals(false); } /** * @brief Generate all candidate words that are one edit distance away * * @param word * @param includeAdditions * @param includeDeletions * @return QSet */ QSet oneEdit(QString word, bool includeAdditions, bool includeDeletions) { QSet all; // 1-edit distance words (i.e., prefixed/suffixed/edited characters) for (int i = 0; i < 26; i++) { if (includeAdditions) { auto prefixed = ALPHABET.mid(i, 1) + word; all.insert(prefixed); auto suffixed = word + ALPHABET.mid(i, 1); all.insert(suffixed); } for (int j = 0; j < word.length(); j++) { auto edited = word.mid(0, j) + ALPHABET.mid(i, 1) + word.mid(j + 1, word.length() - j); all.insert(edited); } } // 1-edit distance words (i.e., removed characters) if (includeDeletions) { for (int j = 0; j < word.length(); j++) { auto deleted = word.mid(0, j) + word.mid(j + 1, word.length() - j); all.insert(deleted); } } return all; } /** * @brief Generate candidate words that are one or two edit distances away * * @param word * @param includeTwoEdits * @return QMultiMap */ QMultiMap candidates(QString word, bool includeTwoEdits) { // one edit QSet one = oneEdit(word, true, true); // two edits QSet two; if (includeTwoEdits) { foreach (auto w, one) { two |= oneEdit(w, false, false); } } // existence check QMultiMap m; quint32 index; foreach (auto w, one | two) { if (JSC::exists(w, &index)) { m.insert(index, w); } } return m; } /** * @brief Generate suggestions for the given word * * @param word * @param n * @param pFound * @return QStringList */ QStringList JSCChecker::suggestions(QString word, int n, bool *pFound) { QStringList s; // qCDebug(jsc_checker_js8) << "computing suggestions for word" << word; QMultiMap m; bool prefixFound = false; // lookup actual word prefix that is not a single character quint32 index = JSC::lookup(word, &prefixFound); if (prefixFound) { auto t = JSC::map[index]; if (t.size > 1) { m.insert(index, QString::fromLatin1(t.str, t.size)); } } // compute suggestion candidates m.unite(candidates(word, false)); // return in order of probability (i.e., index rank) int i = 0; foreach (auto key, m.uniqueKeys()) { if (i >= n) { break; } // qCDebug(jsc_checker_js8) << "suggest" << m[key] << key; s.append(m.values(key)); i++; } if (pFound) *pFound = prefixFound; return s; } Q_LOGGING_CATEGORY(jsc_checker_js8, "jsc_checker.js8", QtWarningMsg)