qb-answer-checker 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/check-answer.js +54 -344
- package/lib/constants.js +11 -0
- package/lib/contains-tokens.js +41 -0
- package/lib/equivalent-answers.csv +144 -0
- package/lib/generate-unformatted-answers.js +50 -0
- package/lib/get-special-directives.js +18 -0
- package/lib/load-csv-as-dict.js +22 -0
- package/lib/split-into-sections.js +27 -0
- package/lib/split-section-into-clauses.js +60 -0
- package/lib/tokenize.js +187 -0
- package/lib/utils.js +45 -83
- package/package.json +4 -4
- package/test/formatted-tests.csv +184 -0
- package/test/script.js +39 -46
- package/test/unformatted-tests.csv +38 -0
- package/check-answer.d.ts +0 -4
- package/lib/equivalent-answers.js +0 -161
- package/lib/standardize-tokens.js +0 -202
- package/test/bcolors.js +0 -26
- package/test/tests.json +0 -655
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ This section specifies the kind of (quizbowl) answerlines that the program is de
|
|
|
10
10
|
**Answerlines** should be formatted as follows:
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
<main
|
|
13
|
+
<main section> [<sub-section>] [<sub-section>]? ...
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
where
|
|
16
|
+
where each **section** is a string of **clauses** separated by semicolons of the form:
|
|
17
17
|
|
|
18
18
|
```
|
|
19
19
|
(<special directives>;)? <clause> (; <clause>; ...)?
|
package/lib/check-answer.js
CHANGED
|
@@ -1,367 +1,77 @@
|
|
|
1
|
+
import referenceContainsTokens from './contains-tokens.js';
|
|
2
|
+
import generateUnformattedAnswers from './generate-unformatted-answers.js';
|
|
3
|
+
import getSpecialDirectives from './get-special-directives.js';
|
|
4
|
+
import splitIntoSections from './split-into-sections.js';
|
|
5
|
+
import splitSectionIntoParsedClauses from './split-section-into-clauses.js';
|
|
6
|
+
import tokenize from './tokenize.js';
|
|
1
7
|
import * as utils from './utils.js';
|
|
2
|
-
import getEquivalentAnswers from './equivalent-answers.js';
|
|
3
|
-
import standardizeTokens from './standardize-tokens.js';
|
|
4
|
-
|
|
5
|
-
import { distance } from 'damerau-levenshtein-js';
|
|
6
|
-
import numberToWords from 'number-to-words';
|
|
7
|
-
import { toArabic } from 'roman-numerals';
|
|
8
|
-
import { stemmer } from 'stemmer';
|
|
9
|
-
|
|
10
|
-
const { toWords } = numberToWords;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Splits a string into the main answer and the sub-answer (everything in brackets or parentheses),
|
|
14
|
-
* while intelligently detecting whether to keep the part in parentheses, if present.
|
|
15
|
-
* @param {string} string
|
|
16
|
-
* @returns {{ mainAnswer: string, subAnswer: string }}
|
|
17
|
-
*/
|
|
18
|
-
const splitMainAnswer = (string) => {
|
|
19
|
-
const bracketsSubAnswer = (string.match(/(?<=\[)[^\]]*(?=\])/g) ?? ['']).pop();
|
|
20
|
-
const parenthesesSubAnswer = (string.match(/(?<=\()[^)]*(?=\))/g) ?? ['']).pop();
|
|
21
|
-
|
|
22
|
-
const mainAnswer = utils.removeParentheses(string);
|
|
23
|
-
|
|
24
|
-
if (bracketsSubAnswer.length !== 0)
|
|
25
|
-
return { mainAnswer, subAnswer: bracketsSubAnswer };
|
|
26
|
-
|
|
27
|
-
for (const directive of ['or', 'prompt', 'antiprompt', 'anti-prompt', 'accept', 'reject', 'do not accept']) {
|
|
28
|
-
if (parenthesesSubAnswer.startsWith(directive))
|
|
29
|
-
return { mainAnswer, subAnswer: parenthesesSubAnswer };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return { mainAnswer, subAnswer: '' };
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Split either the main- or sub-answerline into clauses.
|
|
37
|
-
* @param {string} string
|
|
38
|
-
* @returns {string[]} the clauses in the string
|
|
39
|
-
*/
|
|
40
|
-
const splitAnswerlineIntoClauses = (string) => {
|
|
41
|
-
return string.split(';').map(token => token.trim());
|
|
42
|
-
};
|
|
43
8
|
|
|
44
9
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {string} clause
|
|
47
|
-
* @returns {{directive: "accept" | "reject" | "prompt", answers: string[], directedPrompt: string?}} the answers in the clause
|
|
48
|
-
*/
|
|
49
|
-
const splitClauseIntoAnswers = (clause) => {
|
|
50
|
-
let directive = 'accept'; // by default, this clause accepts answers that match to it
|
|
51
|
-
if (clause.startsWith('prompt')) {
|
|
52
|
-
directive = 'prompt';
|
|
53
|
-
} else if (clause.startsWith('antiprompt') || clause.startsWith('anti-prompt')) {
|
|
54
|
-
directive = 'accept';
|
|
55
|
-
} else if (clause.startsWith('reject') || clause.startsWith('do not accept')) {
|
|
56
|
-
directive = 'reject';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let directedPrompt = null;
|
|
60
|
-
if (directive === 'prompt') {
|
|
61
|
-
for (const key of ['by asking', 'with']) {
|
|
62
|
-
const index = clause.indexOf(key);
|
|
63
|
-
|
|
64
|
-
if (index < 0) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
directedPrompt = utils.extractQuotes(clause.slice(index + key.length));
|
|
69
|
-
clause = clause.slice(0, index);
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
clause = clause.replace(/^(or|prompt|prompt on|antiprompt|antiprompt on|anti-prompt|anti-prompt on|accept|reject|do not accept or prompt on|do not accept)/, '').trim();
|
|
75
|
-
|
|
76
|
-
const answers = clause.split(/,? or |, /).map(token => token.trim()).filter(token => token.length > 0);
|
|
77
|
-
|
|
78
|
-
return { directive, answers, directedPrompt };
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Parses the answerline, returning the acceptable, promptable, and rejectable answers.
|
|
10
|
+
* Check if the given answer matches the answerline.
|
|
84
11
|
* @param {string} answerline
|
|
12
|
+
* @param {string} givenAnswer
|
|
13
|
+
* @param {number} [strictness]
|
|
14
|
+
* @param {boolean} [verbose] - whether to print debug information
|
|
15
|
+
* @returns {{directive: "accept" | "prompt" | "reject", directedPrompt: string | undefined}}
|
|
85
16
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const { mainAnswer, subAnswer } = splitMainAnswer(answerline);
|
|
92
|
-
const mainAnswers = mainAnswer.split(' or ').map(token => token.trim()).filter(token => token.length > 0);
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @type {{ "accept": String[], "prompt": String[][2], "reject": String[] }}
|
|
96
|
-
*/
|
|
97
|
-
const parsedAnswerline = {
|
|
98
|
-
accept: [],
|
|
99
|
-
prompt: [],
|
|
100
|
-
reject: [],
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
for (const answer of mainAnswers) {
|
|
104
|
-
parsedAnswerline.accept.push(utils.extractUnderlining(answer), utils.extractKeyWords(answer), utils.extractQuotes(answer));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (utils.getAbbreviation(mainAnswer).length > 1) {
|
|
108
|
-
parsedAnswerline.accept.push(utils.getAbbreviation(mainAnswer));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (utils.getAbbreviation(utils.extractUnderlining(mainAnswer)).length > 1) {
|
|
112
|
-
parsedAnswerline.accept.push(utils.getAbbreviation(utils.extractUnderlining(mainAnswer)));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (/[[(]accept either/i.test(answerline) || /[[(]accept any/i.test(answerline)) {
|
|
116
|
-
for (const answer of parsedAnswerline.accept[0].split(' ')) {
|
|
117
|
-
parsedAnswerline.accept.push(answer);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (/prompt on (a )?partial/.test(answerline)) {
|
|
122
|
-
for (const answer of parsedAnswerline.accept[0].split(' ')) {
|
|
123
|
-
parsedAnswerline.prompt.push([answer, null]);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
for (const answer of parsedAnswerline.accept) {
|
|
128
|
-
const equivalentAnswers = getEquivalentAnswers(answer);
|
|
129
|
-
parsedAnswerline.accept = parsedAnswerline.accept.concat(equivalentAnswers);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const clauses = splitAnswerlineIntoClauses(subAnswer);
|
|
133
|
-
clauses.forEach(clause => {
|
|
134
|
-
if (clause.length === 0)
|
|
135
|
-
return;
|
|
136
|
-
|
|
137
|
-
const { directive, answers, directedPrompt } = splitClauseIntoAnswers(clause);
|
|
138
|
-
|
|
139
|
-
for (const answer of answers) {
|
|
140
|
-
switch (directive) {
|
|
141
|
-
case 'accept':
|
|
142
|
-
parsedAnswerline[directive].push(utils.extractUnderlining(answer), utils.extractKeyWords(answer), utils.extractQuotes(answer));
|
|
143
|
-
break;
|
|
144
|
-
case 'prompt': {
|
|
145
|
-
parsedAnswerline[directive].push([utils.extractUnderlining(answer), directedPrompt]);
|
|
146
|
-
parsedAnswerline[directive].push([utils.extractKeyWords(answer), directedPrompt]);
|
|
147
|
-
parsedAnswerline[directive].push([utils.extractQuotes(answer), directedPrompt]);
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
case 'reject':
|
|
151
|
-
parsedAnswerline[directive].push(utils.extractQuotes(answer));
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
return parsedAnswerline;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Generates standardized tokens from a string.
|
|
163
|
-
* @param {string} string
|
|
164
|
-
* @returns {string[]} the tokens generated from the string
|
|
165
|
-
*/
|
|
166
|
-
function generateTokens(string) {
|
|
167
|
-
const tokens = string.split(' ')
|
|
168
|
-
.filter(token => token.length > 0)
|
|
169
|
-
.map(string => standardizeTokens(string));
|
|
170
|
-
|
|
171
|
-
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
172
|
-
if (tokens[i].endsWith('s')) {
|
|
173
|
-
tokens[i] = tokens[i].slice(0, -1);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
tokens[i] = toArabic(tokens[i]);
|
|
178
|
-
} catch (e) {
|
|
179
|
-
if (e.message !== 'toArabic expects a valid roman number' && !(e instanceof TypeError)) {
|
|
180
|
-
throw e;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (isFinite(tokens[i])) {
|
|
185
|
-
tokens[i] = parseInt(tokens[i]);
|
|
186
|
-
} else {
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (tokens[i] <= 100) {
|
|
191
|
-
tokens[i] = toWords(tokens[i]);
|
|
192
|
-
} else {
|
|
193
|
-
tokens[i] = tokens[i].toString();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return tokens;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Helper method to check if every token in `given` is present in `reference`.
|
|
203
|
-
* @param {string[]} given
|
|
204
|
-
* @param {string[]} reference
|
|
205
|
-
* @param {boolean} acceptSubstring
|
|
206
|
-
* @param {number} strictness
|
|
207
|
-
* @param {boolean} useStemmer
|
|
208
|
-
* @returns {boolean}
|
|
209
|
-
*/
|
|
210
|
-
function tokenListsMatch(given, reference, acceptSubstring, strictness, useStemmer) {
|
|
211
|
-
let j = 0;
|
|
212
|
-
for (const element of given) {
|
|
213
|
-
let matches = false;
|
|
17
|
+
function checkAnswer (answerline, givenAnswer, strictness = 7, verbose = false) {
|
|
18
|
+
if (typeof answerline !== 'string' || typeof givenAnswer !== 'string') {
|
|
19
|
+
return { directive: 'reject', directedPrompt: undefined };
|
|
20
|
+
}
|
|
214
21
|
|
|
215
|
-
|
|
216
|
-
let errors;
|
|
22
|
+
const isFormattedAnswerline = /<u>/.test(answerline);
|
|
217
23
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
24
|
+
answerline = utils.replaceSpecialCharacters(answerline);
|
|
25
|
+
answerline = answerline.toLowerCase();
|
|
26
|
+
answerline = utils.replaceSpecialSubstrings(answerline);
|
|
27
|
+
answerline = utils.removeItalics(answerline);
|
|
223
28
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
29
|
+
givenAnswer = utils.replaceSpecialCharacters(givenAnswer);
|
|
30
|
+
givenAnswer = givenAnswer.toLowerCase();
|
|
31
|
+
givenAnswer = utils.replaceSpecialSubstrings(givenAnswer);
|
|
32
|
+
givenAnswer = utils.removeItalics(givenAnswer);
|
|
33
|
+
givenAnswer = utils.removePunctuation(givenAnswer);
|
|
34
|
+
const givenAnswerTokens = tokenize(givenAnswer, true);
|
|
227
35
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
36
|
+
const sections = splitIntoSections(answerline);
|
|
37
|
+
const parsedClauses = sections.flatMap((section, index) => splitSectionIntoParsedClauses(section, index === 0));
|
|
38
|
+
const mainAnswer = parsedClauses[0].formattedAnswers[0];
|
|
231
39
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
40
|
+
if (!isFormattedAnswerline && mainAnswer?.length > 1 && givenAnswer.length === 1 && isNaN(givenAnswer)) {
|
|
41
|
+
return { directive: 'reject' };
|
|
42
|
+
}
|
|
235
43
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!matches) {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Returns true if and only if every token in `string` is present in `reference`.
|
|
249
|
-
* @param {object} options
|
|
250
|
-
* @param {string} options.string
|
|
251
|
-
* @param {string} options.reference
|
|
252
|
-
* @param {number} [options.strictness=7] - the number of characters per error allowed for two tokens to match. If set to a negative number or 0, then no errors are allowed.
|
|
253
|
-
* @param {boolean} [options.acceptSubstring=false] - whether or not to accept substrings.
|
|
254
|
-
* @param {boolean} [options.useStemmer=true] - whether or not to use a stemmer.
|
|
255
|
-
* @param {boolean} [options.respectOrder=false] - whether or not to respect the order of the tokens (i.e. "a b" is not the same as "b a").
|
|
256
|
-
* @returns {boolean}
|
|
257
|
-
*/
|
|
258
|
-
function stringMatchesReference({ string, reference, strictness = 7, acceptSubstring = false, useStemmer = true, respectOrder = false }) {
|
|
259
|
-
if (string === null || string === undefined || reference === null || reference === undefined) {
|
|
260
|
-
return false;
|
|
44
|
+
for (const specialDirective of getSpecialDirectives(answerline)) {
|
|
45
|
+
if (specialDirective === 'accept either') {
|
|
46
|
+
parsedClauses.push({ directive: 'accept', formattedAnswers: mainAnswer.split(' ') });
|
|
261
47
|
}
|
|
262
48
|
|
|
263
|
-
if (
|
|
264
|
-
|
|
49
|
+
if (specialDirective === 'prompt on partial') {
|
|
50
|
+
parsedClauses.push({ directive: 'prompt', formattedAnswers: mainAnswer.split(' ') });
|
|
265
51
|
}
|
|
52
|
+
}
|
|
266
53
|
|
|
267
|
-
|
|
268
|
-
reference = utils.removePunctuation(reference).trim();
|
|
269
|
-
|
|
270
|
-
let stringTokenLists = [];
|
|
271
|
-
let referenceTokenLists = [];
|
|
272
|
-
|
|
273
|
-
if (/-/.test(string)) {
|
|
274
|
-
stringTokenLists.push(generateTokens(string.replace(/-/g, ' ')));
|
|
275
|
-
stringTokenLists.push(generateTokens(string.replace(/-/g, '')));
|
|
276
|
-
} else {
|
|
277
|
-
stringTokenLists.push(generateTokens(string));
|
|
278
|
-
}
|
|
54
|
+
parsedClauses.sort((a, b) => (a.directive === 'reject' ? -1 : 1) - (b.directive === 'reject' ? -1 : 1));
|
|
279
55
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
referenceTokenLists.push(generateTokens(reference));
|
|
285
|
-
}
|
|
56
|
+
for (const { directive, formattedAnswers, directedPrompt, isMainAnswer } of parsedClauses) {
|
|
57
|
+
for (const formattedAnswer of formattedAnswers) {
|
|
58
|
+
for (const unformattedAnswer of generateUnformattedAnswers(formattedAnswer, isMainAnswer)) {
|
|
59
|
+
const tokens = tokenize(unformattedAnswer, true);
|
|
286
60
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
61
|
+
const matches = referenceContainsTokens(
|
|
62
|
+
isFormattedAnswerline || directive === 'reject' ? tokens : givenAnswerTokens,
|
|
63
|
+
isFormattedAnswerline || directive === 'reject' ? givenAnswerTokens : tokens,
|
|
64
|
+
directive === 'reject' ? -1 : strictness,
|
|
65
|
+
!isFormattedAnswerline,
|
|
66
|
+
directive !== 'reject'
|
|
67
|
+
);
|
|
291
68
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
for (const referenceTokenList of referenceTokenLists) {
|
|
295
|
-
if (tokenListsMatch(stringTokenList, referenceTokenList, acceptSubstring, strictness, useStemmer)) {
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
69
|
+
if (matches) { return { directive, directedPrompt }; }
|
|
70
|
+
}
|
|
299
71
|
}
|
|
72
|
+
}
|
|
300
73
|
|
|
301
|
-
|
|
74
|
+
return { directive: 'reject' };
|
|
302
75
|
}
|
|
303
76
|
|
|
304
|
-
/**
|
|
305
|
-
* Check if the given answer matches the answerline.
|
|
306
|
-
* @param {String} answerline
|
|
307
|
-
* @param {String} givenAnswer
|
|
308
|
-
* @param {Number} [strictness]
|
|
309
|
-
* @returns {{
|
|
310
|
-
* directive: 'accept' | 'prompt' | 'reject',
|
|
311
|
-
* directedPrompt: String | null
|
|
312
|
-
* }}
|
|
313
|
-
*/
|
|
314
|
-
function checkAnswer(answerline, givenAnswer, strictness = 7) {
|
|
315
|
-
if (!answerline || !givenAnswer) {
|
|
316
|
-
return { directive: 'reject', directedPrompt: null };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (typeof answerline !== 'string' || typeof givenAnswer !== 'string') {
|
|
320
|
-
return { directive: 'reject', directedPrompt: null };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
answerline = answerline.toLowerCase();
|
|
324
|
-
givenAnswer = utils.replaceSpecialCharacters(givenAnswer.toLowerCase());
|
|
325
|
-
const answerlineIsFormatted = answerline.includes('<u>');
|
|
326
|
-
|
|
327
|
-
const answerWorks = (answerline, givenAnswer) => {
|
|
328
|
-
if (answerlineIsFormatted) {
|
|
329
|
-
return stringMatchesReference({ string: answerline, reference: givenAnswer, strictness: strictness });
|
|
330
|
-
} else {
|
|
331
|
-
return stringMatchesReference({ string: givenAnswer, reference: answerline, strictness: strictness, acceptSubstring: true });
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const parsedAnswerline = parseAnswerline(answerline);
|
|
336
|
-
|
|
337
|
-
if (!answerlineIsFormatted && parsedAnswerline.accept[0] && parsedAnswerline.accept[0].length > 1 && givenAnswer.length === 1 && isNaN(givenAnswer))
|
|
338
|
-
return { directive: 'reject', directedPrompt: null };
|
|
339
|
-
|
|
340
|
-
for (const answer of parsedAnswerline.reject) {
|
|
341
|
-
const useStemmer = (stemmer(answer) !== stemmer(parsedAnswerline.accept[0]));
|
|
342
|
-
|
|
343
|
-
if (!stringMatchesReference({ string: answer, reference: givenAnswer, strictness: -1, useStemmer }))
|
|
344
|
-
continue;
|
|
345
|
-
|
|
346
|
-
if (!stringMatchesReference({ string: givenAnswer, reference: answer, strictness: -1, useStemmer }))
|
|
347
|
-
continue;
|
|
348
|
-
|
|
349
|
-
return { directive: 'reject', directedPrompt: null };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (parsedAnswerline.accept.some(answer => answerWorks(answer, givenAnswer))) {
|
|
353
|
-
return { directive: 'accept', directedPrompt: null };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
for (const answer of parsedAnswerline.prompt) {
|
|
357
|
-
const directedPrompt = answer[1];
|
|
358
|
-
if (answerWorks(answer[0], givenAnswer)) {
|
|
359
|
-
return { directive: 'prompt', directedPrompt: directedPrompt };
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return { directive: 'reject', directedPrompt: null };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
77
|
export default checkAnswer;
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DIRECTIVES = {
|
|
2
|
+
accept: ['accept', 'or', 'antiprompt on', 'anti-prompt on', 'antiprompt', 'anti-prompt'],
|
|
3
|
+
prompt: ['prompt on', 'prompt'],
|
|
4
|
+
reject: ['reject', 'do not accept or prompt on', 'do not accept']
|
|
5
|
+
};
|
|
6
|
+
export const DIRECTIVES_FLATTENED = Object.values(DIRECTIVES).flat();
|
|
7
|
+
|
|
8
|
+
export const SPECIAL_DIRECTIVES = {
|
|
9
|
+
'accept either': ['accept either', 'accept any'],
|
|
10
|
+
'prompt on partial': ['prompt on partial', 'prompt on a partial']
|
|
11
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { distance } from 'damerau-levenshtein-js';
|
|
2
|
+
import { stemmer } from 'stemmer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if all elements of `tokens` are present in `reference`.
|
|
6
|
+
* @param {string[]} tokens
|
|
7
|
+
* @param {string[]} references
|
|
8
|
+
* @param {number} strictness
|
|
9
|
+
* @param {boolean} acceptSubstring
|
|
10
|
+
* @param {boolean} useStemmer
|
|
11
|
+
*/
|
|
12
|
+
export default function referenceContainsTokens (tokens, references, strictness, acceptSubstring, useStemmer) {
|
|
13
|
+
let index = 0;
|
|
14
|
+
for (const token of tokens) {
|
|
15
|
+
let containsToken = false;
|
|
16
|
+
while (index < references.length) {
|
|
17
|
+
const reference = references[index];
|
|
18
|
+
index++;
|
|
19
|
+
const errors = useStemmer ? distance(stemmer(token), stemmer(reference)) : distance(token, reference);
|
|
20
|
+
|
|
21
|
+
if (strictness > 0 && strictness * errors <= reference.length) {
|
|
22
|
+
containsToken = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (acceptSubstring && reference.includes(token)) {
|
|
27
|
+
containsToken = true;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (errors === 0) {
|
|
32
|
+
containsToken = true;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!containsToken) { return false; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
atomic bombs,atomic weapons,nuclear bombs,nuclear weapons,nukes,fission bombs,A-bombs
|
|
2
|
+
nuclear weapons,atomic bombs,atomic weapons,nuclear bombs,nukes,fission bombs,A-bombs
|
|
3
|
+
nukes,atomic bombs,atomic weapons,nuclear bombs,nuclear weapons,fission bombs,A-bombs
|
|
4
|
+
fairytales,fairy tales
|
|
5
|
+
fairy tales,fairytales
|
|
6
|
+
house,home,dwelling,residence
|
|
7
|
+
mouse,mice
|
|
8
|
+
rail,railroad
|
|
9
|
+
railroad,rail
|
|
10
|
+
nineteen eighty-four,1984,nineteen eighty four
|
|
11
|
+
nineteen eighty four,1984,nineteen eighty-four
|
|
12
|
+
oxidation number,oxidation state
|
|
13
|
+
oxidation state,oxidation number
|
|
14
|
+
ralph vaughan-williams,rvw
|
|
15
|
+
spacewalk,space walk
|
|
16
|
+
spacewalks,space walk
|
|
17
|
+
sugar cane,sugarcane
|
|
18
|
+
sugarcane,sugar cane
|
|
19
|
+
wavefunction,wave function
|
|
20
|
+
wave function,wavefunction
|
|
21
|
+
world war 1,first world war,great war,world war i,world war one
|
|
22
|
+
world war i,first world war,great war,world war 1,world war one
|
|
23
|
+
world war one,first world war,great war,world war 1,world war i
|
|
24
|
+
world war ii,ww2,wwii,world war 2,world war two,second world war
|
|
25
|
+
world war two,ww2,wwii,world war ii,world war 2,second world war
|
|
26
|
+
world war 2,ww2,wwii,world war ii,world war two,second world war
|
|
27
|
+
hydrogen,h
|
|
28
|
+
helium,he
|
|
29
|
+
lithium,li
|
|
30
|
+
beryllium,be
|
|
31
|
+
boron,b
|
|
32
|
+
carbon,c
|
|
33
|
+
nitrogen,n
|
|
34
|
+
oxygen,o
|
|
35
|
+
fluorine,f
|
|
36
|
+
neon,ne
|
|
37
|
+
sodium,na
|
|
38
|
+
magnesium,mg
|
|
39
|
+
aluminum,al
|
|
40
|
+
silicon,si
|
|
41
|
+
phosphorus,p
|
|
42
|
+
sulfur,s
|
|
43
|
+
chlorine,cl
|
|
44
|
+
argon,ar
|
|
45
|
+
potassium,k
|
|
46
|
+
calcium,ca
|
|
47
|
+
scandium,sc
|
|
48
|
+
titanium,ti
|
|
49
|
+
vanadium,v
|
|
50
|
+
chromium,cr
|
|
51
|
+
manganese,mn
|
|
52
|
+
iron,fe
|
|
53
|
+
cobalt,co
|
|
54
|
+
nickel,ni
|
|
55
|
+
copper,cu
|
|
56
|
+
zinc,zn
|
|
57
|
+
gallium,ga
|
|
58
|
+
germanium,ge
|
|
59
|
+
arsenic,as
|
|
60
|
+
selenium,se
|
|
61
|
+
bromine,br
|
|
62
|
+
krypton,kr
|
|
63
|
+
rubidium,rb
|
|
64
|
+
strontium,sr
|
|
65
|
+
yttrium,y
|
|
66
|
+
zirconium,zr
|
|
67
|
+
niobium,nb
|
|
68
|
+
molybdenum,mo
|
|
69
|
+
technetium,tc
|
|
70
|
+
ruthenium,ru
|
|
71
|
+
rhodium,rh
|
|
72
|
+
palladium,pd
|
|
73
|
+
silver,ag
|
|
74
|
+
cadmium,cd
|
|
75
|
+
indium,in
|
|
76
|
+
tin,sn
|
|
77
|
+
antimony,sb
|
|
78
|
+
tellurium,te
|
|
79
|
+
iodine,i
|
|
80
|
+
xenon,xe
|
|
81
|
+
cesium,cs
|
|
82
|
+
barium,ba
|
|
83
|
+
lanthanum,la
|
|
84
|
+
cerium,ce
|
|
85
|
+
praseodymium,pr
|
|
86
|
+
neodymium,nd
|
|
87
|
+
promethium,pm
|
|
88
|
+
samarium,sm
|
|
89
|
+
europium,eu
|
|
90
|
+
gadolinium,gd
|
|
91
|
+
terbium,tb
|
|
92
|
+
dysprosium,dy
|
|
93
|
+
holmium,ho
|
|
94
|
+
erbium,er
|
|
95
|
+
thulium,tm
|
|
96
|
+
ytterbium,yb
|
|
97
|
+
lutetium,lu
|
|
98
|
+
hafnium,hf
|
|
99
|
+
tantalum,ta
|
|
100
|
+
tungsten,w
|
|
101
|
+
rhenium,re
|
|
102
|
+
osmium,os
|
|
103
|
+
iridium,ir
|
|
104
|
+
platinum,pt
|
|
105
|
+
gold,au
|
|
106
|
+
mercury,hg
|
|
107
|
+
thallium,tl
|
|
108
|
+
lead,pb
|
|
109
|
+
bismuth,bi
|
|
110
|
+
polonium,po
|
|
111
|
+
astatine,at
|
|
112
|
+
radon,rn
|
|
113
|
+
francium,fr
|
|
114
|
+
radium,ra
|
|
115
|
+
actinium,ac
|
|
116
|
+
thorium,th
|
|
117
|
+
protactinium,pa
|
|
118
|
+
uranium,u
|
|
119
|
+
neptunium,np
|
|
120
|
+
plutonium,pu
|
|
121
|
+
americium,am
|
|
122
|
+
curium,cm
|
|
123
|
+
berkelium,bk
|
|
124
|
+
californium,cf
|
|
125
|
+
einsteinium,es
|
|
126
|
+
fermium,fm
|
|
127
|
+
mendelevium,md
|
|
128
|
+
nobelium,no
|
|
129
|
+
lawrencium,lr
|
|
130
|
+
rutherfordium,rf
|
|
131
|
+
dubnium,db
|
|
132
|
+
seaborgium,sg
|
|
133
|
+
bohrium,bh
|
|
134
|
+
hassium,hs
|
|
135
|
+
meitnerium,mt
|
|
136
|
+
darmstadtium,ds
|
|
137
|
+
roentgenium,rg
|
|
138
|
+
copernicium,cn
|
|
139
|
+
nihonium,nh
|
|
140
|
+
flerovium,fl
|
|
141
|
+
moscovium,mc
|
|
142
|
+
livermorium,lv
|
|
143
|
+
tennessine,ts
|
|
144
|
+
oganesson,og
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import loadCSVAsDict from './load-csv-as-dict.js';
|
|
2
|
+
import * as utils from './utils.js';
|
|
3
|
+
|
|
4
|
+
const equivalentAnswers = loadCSVAsDict('equivalent-answers.csv');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the abbreviation of a string by taking the first letter of each word.
|
|
8
|
+
* For example, "World Health Organization" becomes "WHO".
|
|
9
|
+
* @param {string} string
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function getAbbreviation (string) {
|
|
13
|
+
return string
|
|
14
|
+
.split(' ')
|
|
15
|
+
.filter(token => token.length > 0)
|
|
16
|
+
.map(token => utils.removeHTMLTags(token).charAt(0))
|
|
17
|
+
.reduce((a, b) => a + b, '')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} formattedAnswer
|
|
23
|
+
* @param {boolean} isMainAnswer
|
|
24
|
+
* @returns {string[]}
|
|
25
|
+
*/
|
|
26
|
+
export default function generateUnformattedAnswers (formattedAnswer, isMainAnswer) {
|
|
27
|
+
if (/-/.test(formattedAnswer)) {
|
|
28
|
+
const object1 = generateUnformattedAnswers(formattedAnswer.replace(/-/g, ' '), isMainAnswer);
|
|
29
|
+
const object2 = generateUnformattedAnswers(formattedAnswer.replace(/-/g, ''), isMainAnswer);
|
|
30
|
+
return [...object1, ...object2];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const answers = [
|
|
34
|
+
utils.removeHTMLTags(formattedAnswer),
|
|
35
|
+
utils.extractUnderlining(formattedAnswer),
|
|
36
|
+
utils.extractKeyWords(formattedAnswer),
|
|
37
|
+
utils.extractQuotes(formattedAnswer)
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (isMainAnswer) {
|
|
41
|
+
answers.push(getAbbreviation(formattedAnswer));
|
|
42
|
+
answers.push(getAbbreviation(utils.extractUnderlining(formattedAnswer)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (answers[0] in equivalentAnswers) {
|
|
46
|
+
answers.push(...equivalentAnswers[answers[0]]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return answers.map(answer => utils.removePunctuation(answer));
|
|
50
|
+
}
|