diff --git a/.eslintrc.js b/.eslintrc.js index c4b3222..34be489 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,6 @@ module.exports = { eqeqeq: ['warn', 'smart'], 'no-unused-vars': 'warn', 'no-prototype-builtins': 'off', - 'id-length': ['warn', { exceptions: ['i', 'j'] }], + 'id-length': ['warn', { exceptions: ['i', 'j', 't', 'Q', 'A'] }], }, } diff --git a/src/utils/classes.js b/src/utils/classes.js index 044b790..040a5e8 100755 --- a/src/utils/classes.js +++ b/src/utils/classes.js @@ -31,510 +31,430 @@ const assert = (val) => { } } -class StringUtils { - GetSubjNameWithoutYear(subjName) { - let t = subjName.split(' - ') - if (t[0].match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{1}$/i)) { - return t[1] || subjName +// --------------------------------------------------------------------------------------------------------- +// String Utils +// --------------------------------------------------------------------------------------------------------- + +// Exported +// --------------------------------------------------------------------------------------------------------- +function getSubjNameWithoutYear(subjName) { + let t = subjName.split(' - ') + if (t[0].match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{1}$/i)) { + return t[1] || subjName + } else { + return subjName + } +} + +// Not exported +// --------------------------------------------------------------------------------------------------------- +function removeStuff(value, removableStrings, toReplace) { + removableStrings.forEach((removableString) => { + var regex = new RegExp(removableString, 'g') + value = value.replace(regex, toReplace || '') + }) + return value +} + +// removes whitespace from begining and and, and replaces multiple spaces with one space +function removeUnnecesarySpaces(toremove) { + assert(toremove) + + toremove = normalizeSpaces(toremove) + while (toremove.includes(' ')) { + toremove = toremove.replace(/ {2}/g, ' ') + } + return toremove.trim() +} + +// simplifies a string for easier comparison +function simplifyStringForComparison(value) { + assert(value) + + value = removeUnnecesarySpaces(value).toLowerCase() + return removeStuff(value, commonUselessStringParts) +} + +function removeSpecialChars(value) { + assert(value) + + return removeStuff(value, specialChars, ' ') +} + +// damn nonbreaking space +function normalizeSpaces(input) { + assert(input) + + return input.replace(/\s/g, ' ') +} + +function compareString(s1, s2) { + if (!s1 || !s2) { + if (!s1 && !s2) { + return 100 } else { - return subjName + return 0 } } - RemoveStuff(value, removableStrings, toReplace) { - removableStrings.forEach((removableString) => { - var regex = new RegExp(removableString, 'g') - value = value.replace(regex, toReplace || '') - }) + s1 = simplifyStringForComparison(s1).split(' ') + s2 = simplifyStringForComparison(s2).split(' ') + var match = 0 + for (var i = 0; i < s1.length; i++) { + if (s2.includes(s1[i])) { + match++ + } + } + var percent = Math.round(((match / s1.length) * 100).toFixed(2)) // matched words percent + var lengthDifference = Math.abs(s2.length - s1.length) + percent -= lengthDifference * lengthDiffMultiplier + if (percent < 0) { + percent = 0 + } + return percent +} + +function answerPreProcessor(value) { + assert(value) + + return removeStuff(value, commonUselessAnswerParts) +} + +// 'a. pécsi sör' -> 'pécsi sör' +function removeAnswerLetters(value) { + assert(value) + + let val = value.split('. ') + if (val[0].length < 2 && val.length > 1) { + val.shift() + return val.join(' ') + } else { return value } +} - SimplifyQuery(question) { - assert(question) - - var result = question.replace(/\n/g, ' ').replace(/\s/g, ' ') - return this.RemoveUnnecesarySpaces(result) +function simplifyQA(value, mods) { + if (!value) { + return } - ShortenString(toShorten, ammount) { - assert(toShorten) - - var result = '' - var i = 0 - while (i < toShorten.length && i < ammount) { - result += toShorten[i] - i++ - } - return result + const reducer = (res, fn) => { + return fn(res) } - ReplaceCharsWithSpace(val, char) { - assert(val) - assert(char) + return mods.reduce(reducer, value) +} - var toremove = this.NormalizeSpaces(val) +// TODO: simplify answer before setting +function simplifyAnswer(value) { + return simplifyQA(value, [ + removeSpecialChars.bind(this), + removeUnnecesarySpaces.bind(this), + answerPreProcessor.bind(this), + removeAnswerLetters.bind(this), + ]) +} - var regex = new RegExp(char, 'g') - toremove = toremove.replace(regex, ' ') - - return this.RemoveUnnecesarySpaces(toremove) - } - - // removes whitespace from begining and and, and replaces multiple spaces with one space - RemoveUnnecesarySpaces(toremove) { - assert(toremove) - - toremove = this.NormalizeSpaces(toremove) - while (toremove.includes(' ')) { - toremove = toremove.replace(/ {2}/g, ' ') - } - return toremove.trim() - } - - // simplifies a string for easier comparison - SimplifyStringForComparison(value) { - assert(value) - - value = this.RemoveUnnecesarySpaces(value).toLowerCase() - return this.RemoveStuff(value, commonUselessStringParts) - } - - RemoveSpecialChars(value) { - assert(value) - - return this.RemoveStuff(value, specialChars, ' ') - } - - // if the value is empty, or whitespace - EmptyOrWhiteSpace(value) { - // replaces /n-s with "". then replaces spaces with "". if it equals "", then its empty, or only consists of white space - if (value === undefined) { - return true - } - return ( - value - .replace(/\n/g, '') - .replace(/ /g, '') - .replace(/\s/g, ' ') === '' - ) - } - - // damn nonbreaking space - NormalizeSpaces(input) { - assert(input) - - return input.replace(/\s/g, ' ') - } - - CompareString(s1, s2) { - if (!s1 || !s2) { - if (!s1 && !s2) { - return 100 - } else { - return 0 - } - } - - s1 = this.SimplifyStringForComparison(s1).split(' ') - s2 = this.SimplifyStringForComparison(s2).split(' ') - var match = 0 - for (var i = 0; i < s1.length; i++) { - if (s2.includes(s1[i])) { - match++ - } - } - var percent = Math.round(((match / s1.length) * 100).toFixed(2)) // matched words percent - var lengthDifference = Math.abs(s2.length - s1.length) - percent -= lengthDifference * lengthDiffMultiplier - if (percent < 0) { - percent = 0 - } - return percent - } - - AnswerPreProcessor(value) { - assert(value) - - return this.RemoveStuff(value, commonUselessAnswerParts) - } - - // 'a. pécsi sör' -> 'pécsi sör' - RemoveAnswerLetters(value) { - assert(value) - - let val = value.split('. ') - if (val[0].length < 2 && val.length > 1) { - val.shift() - return val.join(' ') - } else { - return value - } - } - - SimplifyQA(value, mods) { - if (!value) { - return - } - - const reducer = (res, fn) => { - return fn(res) - } - - return mods.reduce(reducer, value) - } - - SimplifyAnswer(value) { - return this.SimplifyQA(value, [ - this.RemoveSpecialChars.bind(this), - this.RemoveUnnecesarySpaces.bind(this), - this.AnswerPreProcessor.bind(this), - this.RemoveAnswerLetters.bind(this), +function simplifyQuestion(question) { + if (typeof q === 'string') { + return simplifyQA(question, [ + removeSpecialChars.bind(this), + removeUnnecesarySpaces.bind(this), ]) - } - - SimplifyQuestion(value) { - return this.SimplifyQA(value, [ - this.RemoveSpecialChars.bind(this), - this.RemoveUnnecesarySpaces.bind(this), + } else { + question.Q = simplifyQA(question.Q, [ + removeSpecialChars.bind(this), + removeUnnecesarySpaces.bind(this), ]) - } - - SimplifyStack(stack) { - return this.SimplifyQuery(stack) + question.A = simplifyQA(question.A, [ + removeSpecialChars.bind(this), + removeUnnecesarySpaces.bind(this), + ]) + return question } } -const SUtils = new StringUtils() +// --------------------------------------------------------------------------------------------------------- +// Question +// --------------------------------------------------------------------------------------------------------- -class Question { - constructor(question, answer, data) { - this.Q = SUtils.SimplifyQuestion(question) - this.A = SUtils.SimplifyAnswer(answer) - this.data = { ...data } - } - - toString() { - if (this.data.type !== 'simple') { - return '?' + this.Q + '\n!' + this.A + '\n>' + JSON.stringify(this.data) - } else { - return '?' + this.Q + '\n!' + this.A - } - } - - HasQuestion() { - return this.Q !== undefined - } - - HasAnswer() { - return this.A !== undefined - } - - HasImage() { - return this.data.type === 'image' - } - - IsComplete() { - return this.HasQuestion() && this.HasAnswer() - } - - CompareImage(data2) { - return SUtils.CompareString( - this.data.images.join(' '), - data2.images.join(' ') - ) - } - - // returns -1 if botth is simple - CompareData(qObj) { - try { - if (qObj.data.type === this.data.type) { - let dataType = qObj.data.type - if (dataType === 'simple') { - return -1 - } else if (dataType === 'image') { - return this.CompareImage(qObj.data) - } else { - debugLog( - `Unhandled data type ${dataType}`, - 'Compare question data', - 1 - ) - debugLog(qObj, 'Compare question data', 2) - } - } else { - return 0 - } - } catch (error) { - debugLog('Error comparing data', 'Compare question data', 1) - debugLog(error.message, 'Compare question data', 1) - debugLog(error, 'Compare question data', 2) - } - return 0 - } - - CompareQuestion(qObj) { - return SUtils.CompareString(this.Q, qObj.Q) - } - - CompareAnswer(qObj) { - return SUtils.CompareString(this.A, qObj.A) - } - - Compare(q2, data) { - assert(q2) - let qObj - - if (typeof q2 === 'string') { - qObj = { - Q: q2, - data: data, - } - } else { - qObj = q2 - } - - const qMatch = this.CompareQuestion(qObj) - const aMatch = this.CompareAnswer(qObj) - // -1 if botth questions are simple - const dMatch = this.CompareData(qObj) - - let avg = -1 - if (qObj.A) { - if (dMatch === -1) { - avg = (qMatch + aMatch) / 2 - } else { - avg = (qMatch + aMatch + dMatch) / 3 - } - } else { - if (dMatch === -1) { - avg = qMatch - } else { - avg = (qMatch + dMatch) / 2 - } - } - - return { - qMatch: qMatch, - aMatch: aMatch, - dMatch: dMatch, - avg: avg, - } +function createQuestion(question, answer, data) { + return { + Q: simplifyQuestion(question), + A: simplifyAnswer(answer), + data, } } -class Subject { - constructor(name) { - assert(name) +function compareImage(data, data2) { + return compareString(data.images.join(' '), data2.images.join(' ')) +} - this.Name = name - this.Questions = [] - } - - setIndex(i) { - this.index = i - } - - getIndex() { - return this.index - } - - get length() { - return this.Questions.length - } - - AddQuestion(question) { - assert(question) - - this.Questions.push(question) - } - - getSubjNameWithoutYear() { - return SUtils.GetSubjNameWithoutYear(this.Name) - } - - getYear() { - let t = this.Name.split(' - ')[0] - if (t.match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{1}$/i)) { - return t +function compareData(data, qObj) { + try { + if (qObj.data.type === data.type) { + let dataType = qObj.data.type + if (dataType === 'simple') { + return -1 + } else if (dataType === 'image') { + return compareImage(qObj.data) + } else { + debugLog(`Unhandled data type ${dataType}`, 'Compare question data', 1) + debugLog(qObj, 'Compare question data', 2) + } } else { - return '' + return 0 + } + } catch (error) { + debugLog('Error comparing data', 'Compare question data', 1) + debugLog(error.message, 'Compare question data', 1) + debugLog(error, 'Compare question data', 2) + } + return 0 +} + +function compareQuestion(q1, q2) { + return compareString(q1.Q, q2.Q) +} + +function compareAnswer(q1, q2) { + return compareString(q1.A, q2.A) +} + +function compareQuestionObj(q1, q2, data) { + assert(data) + assert(q1) + assert(q2) + assert(typeof q2 === 'object') + let qObj + + if (typeof q2 === 'string') { + qObj = { + Q: q2, + data: data, + } + } else { + qObj = q2 + } + + const qMatch = compareQuestion(q1, qObj.Q) + const aMatch = compareAnswer(q1.A, qObj.A) + // -1 if botth questions are simple + const dMatch = compareData(q1.data, qObj.data) + + let avg = -1 + if (qObj.A) { + if (dMatch === -1) { + avg = (qMatch + aMatch) / 2 + } else { + avg = (qMatch + aMatch + dMatch) / 3 + } + } else { + if (dMatch === -1) { + avg = qMatch + } else { + avg = (qMatch + dMatch) / 2 } } - Search(question, data) { - assert(question) - - var result = [] - for (let i = 0; i < this.length; i++) { - let percent = this.Questions[i].Compare(question, data) - if (percent.avg > minMatchAmmount) { - result.push({ - question: this.Questions[i], - match: percent.avg, - detailedMatch: percent, - }) - } - } - - for (let i = 0; i < result.length; i++) { - for (var j = i; j < result.length; j++) { - if (result[i].match < result[j].match) { - var tmp = result[i] - result[i] = result[j] - result[j] = tmp - } - } - } - - return result - } - - toString() { - var result = [] - for (var i = 0; i < this.Questions.length; i++) { - result.push(this.Questions[i].toString()) - } - return '+' + this.Name + '\n' + result.join('\n') + return { + qMatch: qMatch, + aMatch: aMatch, + dMatch: dMatch, + avg: avg, } } -class QuestionDB { - constructor() { - this.Subjects = [] +function questionToString(question) { + const { Q, A, data } = question + + if (data.type !== 'simple') { + return '?' + Q + '\n!' + A + '\n>' + JSON.stringify(data) + } else { + return '?' + Q + '\n!' + A + } +} + +// --------------------------------------------------------------------------------------------------------- +// Subject +// --------------------------------------------------------------------------------------------------------- +function searchQuestion(questions, question, questionData) { + assert(question) + + var result = [] + questions.forEach((currentQuestion) => { + let percent = compareQuestionObj(currentQuestion, question, questionData) + if (percent.avg > minMatchAmmount) { + result.push({ + question: currentQuestion, + match: percent.avg, + detailedMatch: percent, + }) + } + }) + + // TODO: check if sorting is correct! + result.sort((q1, q2) => { + return q1.match < q2.match + }) + + return result +} + +function subjectToString(subj) { + const { Questions, Name } = subj + + var result = [] + Questions.forEach((question) => { + result.push(questionToString(question)) + }) + + return '+' + Name + '\n' + result.join('\n') +} + +// --------------------------------------------------------------------------------------------------------- +// QuestionDB +// --------------------------------------------------------------------------------------------------------- +function addQuestion(data, subj, question) { + debugLog('Adding new question with subjName: ' + subj, 'qdb add', 1) + debugLog(question, 'qdb add', 3) + assert(data) + assert(subj) + assert(question) + assert(typeof question === 'object') + let result = [] + + var i = 0 + while ( + i < data.length && + !subj.toLowerCase().includes(getSubjNameWithoutYear(data[i]).toLowerCase()) + ) { + i++ } - get length() { - return this.Subjects.length + if (i < data.length) { + debugLog('Adding new question to existing subject', 'qdb add', 1) + result = [...data] + result[i].Questions = { + ...data[i].Questions, + question, + } + } else { + debugLog('Creating new subject for question', 'qdb add', 1) + result = [ + ...data, + { + name: subj, + Questions: [question], + }, + ] } - AddQuestion(subj, question) { - debugLog('Adding new question with subjName: ' + subj, 'qdb add', 1) - debugLog(question, 'qdb add', 3) - assert(subj) + return result +} - var i = 0 - while ( - i < this.Subjects.length && - !subj +function searchData(data, question, subjName, questionData) { + assert(data) + assert(question) + debugLog('Searching for question', 'qdb search', 1) + debugLog('Question:', 'qdb search', 2) + debugLog(question, 'qdb search', 2) + debugLog(`Subject name: ${subjName}`, 'qdb search', 2) + debugLog('Data:', 'qdb search', 2) + debugLog(questionData || question.data, 'qdb search', 2) + + if (!questionData) { + questionData = question.data || { type: 'simple' } + } + if (!subjName) { + subjName = '' + debugLog('No subject name as param!', 'qdb search', 1) + } + question = simplifyQuestion(question) + + let result = [] + data.forEach((subj) => { + if ( + subjName .toLowerCase() - .includes(this.Subjects[i].getSubjNameWithoutYear().toLowerCase()) + .includes(getSubjNameWithoutYear(subj.Name).toLowerCase()) ) { - i++ + debugLog(`Searching in ${subj.Name} `, 2) + result = result.concat(subj.Search(question, questionData)) } + }) - if (i < this.Subjects.length) { - debugLog('Adding new question to existing subject', 'qdb add', 1) - this.Subjects[i].AddQuestion(question) - } else { - debugLog('Creating new subject for question', 'qdb add', 1) - const newSubject = new Subject(subj) - newSubject.AddQuestion(question) - this.Subjects.push(newSubject) - } - } - - SimplifyQuestion(question) { - if (typeof q === 'string') { - return SUtils.SimplifyQuestion(question) - } else { - question.Q = SUtils.SimplifyQuestion(question.Q) - question.A = SUtils.SimplifyQuestion(question.A) - return question - } - } - - Search(question, subjName, data) { - assert(question) - debugLog('Searching for question', 'qdb search', 1) - debugLog('Question:', 'qdb search', 2) - debugLog(question, 'qdb search', 2) - debugLog(`Subject name: ${subjName}`, 'qdb search', 2) - debugLog('Data:', 'qdb search', 2) - debugLog(data || question.data, 'qdb search', 2) - - if (!data) { - data = question.data || { type: 'simple' } - } - if (!subjName) { - subjName = '' - debugLog('No subject name as param!', 'qdb search', 1) - } - question = this.SimplifyQuestion(question) - - var result = [] - this.Subjects.forEach((subj) => { - if ( - subjName - .toLowerCase() - .includes(subj.getSubjNameWithoutYear().toLowerCase()) - ) { - debugLog(`Searching in ${subj.Name} `, 2) - result = result.concat(subj.Search(question, data)) - } + // FIXME: try to remove this? but this is also a good backup plan so idk + if (result.length === 0) { + debugLog( + 'Reqults length is zero when comparing names, trying all subjects', + 'qdb search', + 1 + ) + data.forEach((subj) => { + result = result.concat( + searchQuestion(subj.Questions, question, questionData) + ) }) - - // FIXME: try to remove this? but this is also a good backup plan so idk - if (result.length === 0) { + if (result.length > 0) { debugLog( - 'Reqults length is zero when comparing names, trying all subjects', + `FIXME: '${subjName}' gave no result but '' did!`, 'qdb search', 1 ) - this.Subjects.forEach((subj) => { - result = result.concat(subj.Search(question, data)) - }) - if (result.length > 0) { - debugLog( - `FIXME: '${subjName}' gave no result but '' did!`, - 'qdb search', - 1 - ) - console.error(`FIXME: '${subjName}' gave no result but '' did!`) - } + console.error(`FIXME: '${subjName}' gave no result but '' did!`) } + } - for (let i = 0; i < result.length; i++) { - for (var j = i; j < result.length; j++) { - if (result[i].match < result[j].match) { - var tmp = result[i] - result[i] = result[j] - result[j] = tmp + // TODO: check if sorting is correct! + result.sort((q1, q2) => { + return q1.match < q2.match + }) + + debugLog(`QDB search result length: ${result.length}`, 'qdb search', 1) + return result +} + +function addSubject(data, subj) { + assert(data) + assert(subj) + + var i = 0 + while (i < length && subj.Name !== data[i].Name) { + i++ + } + + if (i < length) { + return data.map((currSubj, j) => { + if (j === i) { + return { + ...currSubj, + Questions: [...currSubj.Questions, ...subj.Questions], } + } else { + return currSubj } - } - - debugLog(`QDB search result length: ${result.length}`, 'qdb search', 1) - return result - } - - AddSubject(subj) { - assert(subj) - - var i = 0 - while (i < this.length && subj.Name !== this.Subjects[i].Name) { - i++ - } - - if (i < this.length) { - this.Subjects.concat(subj.Questions) - } else { - this.Subjects.push(subj) - } - } - - toString() { - var result = [] - for (var i = 0; i < this.Subjects.length; i++) { - result.push(this.Subjects[i].toString()) - } - return result.join('\n\n') + }) + } else { + return [...data, subj] } } -module.exports.StringUtils = StringUtils // TODO: export singleton string utils, remove nea StringUtils from other files -module.exports.SUtils = SUtils -module.exports.Question = Question -module.exports.Subject = Subject -module.exports.QuestionDB = QuestionDB -module.exports.minMatchAmmount = minMatchAmmount -module.exports.initLogger = initLogger +function dataToString(data) { + var result = [] + data.forEach((subj) => { + result.push(subjectToString(subj)) + }) + return result.join('\n\n') +} + +module.exports = { + minMatchAmmount, + initLogger, + getSubjNameWithoutYear, + createQuestion, + addQuestion, + addSubject, + searchData, + dataToString, +}