3243b19ed5c2f52ef019868a32136f253bf41481
[sheet.git] / word / quiz.js
1 Array.prototype.shuffle = function () {
2         for (let i = this.length - 1; i > 0; i--) {
3                 const j = Math.floor(Math.random() * (i + 1)); // random index 0..i
4                 [this[i], this[j]] = [this[j], this[i]]; // swap elements
5         }
6         return this;
7 };
8
9 Set.prototype.filter = function (f) {
10         return new Set([...this].filter(f));
11 };
12
13 class WordQuiz {
14         dataselect(json) {
15                 this.data = this.datafilter(json);
16                 let rows = Object.values(this.data);
17                 rows = rows.filter(row => !row[3].length); // remove referenced categories
18                 return rows.shuffle();
19         }
20
21         datafilter(json) {
22                 // find viable rows from json data
23                 let ids = new Set(Object.keys(json));
24                 const selection = {...json}; // clone
25
26                 for (let cat of selection[''][3]) {
27                         if (selection[cat])
28                         selection[cat][1] = 0; // keep root categories
29                 }
30
31                 if (this.preset.cat !== undefined) {
32                         ids.clear();
33                         let children = this.preset.cat;
34                         for (let loop = 0; children.length && loop < 20; loop++) {
35                                 for (let child of children) ids.add(child.toString());
36                                 children = children.map(cat => json[cat][3]).filter(is => is).flat()
37                         }
38                 }
39                 if (this.preset.images) {
40                         ids = ids.filter(id => json[id][2]);
41                 }
42                 if (this.preset.level !== undefined) {
43                         ids = ids.filter(id => json[id][1] <= this.preset.level);
44                 }
45
46                 // keep only wanted ids
47                 for (let id in selection) {
48                         if (id && !ids.has(id)) {
49                                 delete selection[id];
50                         }
51                 }
52
53                 // retain orphaned references in grandparent categories
54                 for (let id in selection) {
55                         selection[id][3] = function subresolve(subs) {
56                                 //console.log(subs);
57                                 return (subs || []).flatMap(sub =>
58                                         sub in selection ? [sub] : json[sub] ? subresolve(json[sub][3]) : []
59                                 );
60                         }(selection[id][3]);
61                 }
62                 return selection;
63         }
64
65         configure(params = window.location.hash.split('#')) {
66                 const opts = new Map(params.map(arg => arg.split(/[:=](.*)/)));
67                 for (let [query, val] of opts) {
68                         if (query.match(/^\d+$/)) {
69                                 this.preset.cat = [parseInt(query)];
70                         }
71                         else if (query === 'level') {
72                                 this.preset.level = parseInt(val);
73                         }
74                         else {
75                                 this.preset[query] = val;
76                         }
77                 }
78                 this.preset.dataurl = `/data/wordlist.${this.preset.lang}.json`
79         }
80
81         setup() {
82                 this.form = document.getElementById('quiz');
83         }
84
85         load() {
86                 this.configure();
87                 fetch(this.preset.dataurl).then(res => res.json()).then(json => {
88                         this.words = this.dataselect(json)
89                         this.setup();
90                 });
91         }
92
93         log(...args) {
94                 this.history.push([new Date().toISOString(), ...args]);
95         }
96
97         stop(...args) {
98                 this.log(...args);
99                 window.onbeforeunload = null;
100                 fetch('/word/report', {method: 'POST', body: JSON.stringify(this.history)});
101         }
102
103         constructor() {
104                 this.preset = {images: true, lang: 'en'};
105                 this.load();
106                 this.history = [];
107                 window.onbeforeunload = e => {
108                         this.stop('abort');
109                 };
110                 window.onhashchange = e => {
111                         this.load();
112                 };
113         }
114 }