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 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 answerline> [<sub-answerline>]
13
+ <main section> [<sub-section>] [<sub-section>]? ...
14
14
  ```
15
15
 
16
- where the **sub-answerline** is a string of clauses separated by semicolons of the form:
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>; ...)?
@@ -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 parseAnswerline(answerline) {
87
- answerline = utils.removeItalics(answerline);
88
- answerline = utils.replaceSpecialCharacters(answerline);
89
- answerline = utils.replaceSpecialSubstrings(answerline);
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
- while (j < reference.length && matches === false) {
216
- let errors;
22
+ const isFormattedAnswerline = /<u>/.test(answerline);
217
23
 
218
- if (useStemmer) {
219
- errors = distance(stemmer(element), stemmer(reference[j]));
220
- } else {
221
- errors = distance(element, reference[j]);
222
- }
24
+ answerline = utils.replaceSpecialCharacters(answerline);
25
+ answerline = answerline.toLowerCase();
26
+ answerline = utils.replaceSpecialSubstrings(answerline);
27
+ answerline = utils.removeItalics(answerline);
223
28
 
224
- if (strictness > 0 && strictness * errors <= reference[j].length) {
225
- matches = true;
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
- if (acceptSubstring && reference[j].includes(element)) {
229
- matches = true;
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
- if (errors === 0) {
233
- matches = true;
234
- }
40
+ if (!isFormattedAnswerline && mainAnswer?.length > 1 && givenAnswer.length === 1 && isNaN(givenAnswer)) {
41
+ return { directive: 'reject' };
42
+ }
235
43
 
236
- j++;
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 (string.length === 0) {
264
- return false;
49
+ if (specialDirective === 'prompt on partial') {
50
+ parsedClauses.push({ directive: 'prompt', formattedAnswers: mainAnswer.split(' ') });
265
51
  }
52
+ }
266
53
 
267
- string = utils.removePunctuation(string).trim();
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
- if (/-/.test(reference)) {
281
- referenceTokenLists.push(generateTokens(reference.replace(/-/g, ' ')));
282
- referenceTokenLists.push(generateTokens(reference.replace(/-/g, '')));
283
- } else {
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
- if (!respectOrder) {
288
- stringTokenLists = stringTokenLists.map(tokenList => tokenList.sort());
289
- referenceTokenLists = referenceTokenLists.map(tokenList => tokenList.sort());
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
- // check if every token in the string is in the reference
293
- for (const stringTokenList of stringTokenLists) {
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
- return false;
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;
@@ -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
+ }