diff --git a/site/_templates/_menu.html b/site/_templates/_menu.html index 396a14c..2ad5fc1 100644 --- a/site/_templates/_menu.html +++ b/site/_templates/_menu.html @@ -1,5 +1,5 @@
Suche Preisänderungen - Warenkorb + Warenkörbe
\ No newline at end of file diff --git a/site/changes-new.js b/site/changes-new.js index cbab5a8..e042841 100644 --- a/site/changes-new.js +++ b/site/changes-new.js @@ -5,6 +5,7 @@ require("./views"); await model.load(); const itemsFilter = document.querySelector("items-filter"); const itemsList = document.querySelector("items-list"); + itemsList.elements.sort.value = "chain-and-name"; itemsFilter.model = itemsList.model = model.items; itemsFilter.filter(); })(); diff --git a/site/knn.js b/site/knn.js new file mode 100644 index 0000000..9abde06 --- /dev/null +++ b/site/knn.js @@ -0,0 +1,156 @@ +const { stem, stopWords } = require("./stem"); + +function dotProduct(vector1, vector2) { + let product = 0; + for (const key in vector1) { + if (vector2.hasOwnProperty(key)) { + product += vector1[key] * vector2[key]; + } + } + return product; +} +exports.dotProduct = dotProduct; + +function addVector(vector1, vector2) { + for (const key in vector2) { + vector1[key] = (vector1[key] || 0) + vector2[key]; + } +} +exports.addVector = addVector; + +function scaleVector(vector, scalar) { + for (const key in vector) { + vector[key] *= scalar; + } +} +exports.scaleVector = scaleVector; + +function normalizeVector(vector) { + const len = magnitude(vector); + for (const key in vector) { + vector[key] /= len; + } +} +exports.normalizeVector = normalizeVector; + +function magnitude(vector) { + let sumOfSquares = 0; + for (const key in vector) { + sumOfSquares += vector[key] ** 2; + } + return Math.sqrt(sumOfSquares); +} +exports.magnitude = magnitude; + +function findMostSimilarItem(refItem, items) { + let maxSimilarity = -1; + let similarItem = null; + let similarItemIdx = -1; + items.forEach((item, idx) => { + let similarity = dotProduct(refItem.vector, item.vector); + if (similarity > maxSimilarity) { + maxSimilarity = similarity; + similarItem = item; + similarItemIdx = idx; + } + }); + return { + similarity: maxSimilarity, + item: similarItem, + index: similarItemIdx, + }; +} +exports.findMostSimilarItem = findMostSimilarItem; + +function findMostSimilarItems(refItem, items, k = 5, accept = (ref, item) => true) { + let topSimilarItems = []; + let topSimilarities = []; + + items.forEach((item, idx) => { + if (!accept(refItem, item)) return; + let similarity = dotProduct(refItem.vector, item.vector); + + if (topSimilarItems.length < k) { + topSimilarItems.push(item); + topSimilarities.push(similarity); + } else { + let minSimilarity = Math.min(...topSimilarities); + let minIndex = topSimilarities.indexOf(minSimilarity); + + if (similarity > minSimilarity) { + topSimilarItems[minIndex] = item; + topSimilarities[minIndex] = similarity; + } + } + }); + + let similarItemsWithIndices = topSimilarItems.map((item, index) => { + return { + similarity: topSimilarities[index], + item: item, + index: items.indexOf(item), + }; + }); + + return similarItemsWithIndices; +} +exports.findMostSimilarItems = findMostSimilarItems; + +function similaritySortItems(items) { + if (items.length == 0) return items; + sortedItems = [items.shift()]; + let refItem = sortedItems[0]; + while (items.length > 0) { + const similarItem = findMostSimilarItem(refItem, items); + sortedItems.push(similarItem.item); + items.splice(similarItem.index, 1); + refItem = similarItem.item; + } + return sortedItems; +} +exports.similaritySortItems = similaritySortItems; + +const NGRAM = 4; +function vectorizeTokens(tokens) { + const vector = {}; + for (token of tokens) { + if (token.length > NGRAM) { + for (let i = 0; i < token.length - NGRAM; i++) { + let trigram = token.substring(i, i + NGRAM); + vector[trigram] = (vector[trigram] || 0) + 1; + } + } else { + vector[token] = (vector[token] || 0) + 1; + } + } + normalizeVector(vector); + return vector; +} +exports.vectorizeTokens = vectorizeTokens; + +function vectorizeItem(item, useUnit = true, useStem = true) { + const isNumber = /^\d+\.\d+$/; + let name = item.name + .toLowerCase() + .replace(/[^\w\s]|_/g, "") + .replace("-", " ") + .replace(",", " "); + item.tokens = name + .split(/\s+/) + .filter((token) => !stopWords.includes(token)) + .filter((token) => !isNumber.test(token)) + .map((token) => (useStem ? stem(token) : token)); + if (useUnit) { + if (item.quantity) item.tokens.push("" + item.quantity); + if (item.unit) item.tokens.push(item.unit); + } + item.vector = vectorizeTokens(item.tokens); +} +exports.vectorizeItem = vectorizeItem; + +function vectorizeItems(items, useUnit = true) { + items.forEach((item) => { + if (!item.vector) vectorizeItem(item, useUnit); + }); +} +exports.vectorizeItems = vectorizeItems; diff --git a/site/misc.js b/site/misc.js index 265fe28..7198c3d 100644 --- a/site/misc.js +++ b/site/misc.js @@ -53,6 +53,10 @@ if (typeof window !== "undefined") { setupLiveEdit(); } +exports.isMobile = () => { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +}; + exports.today = () => { const currentDate = new Date(); const year = currentDate.getFullYear(); diff --git a/site/stem.js b/site/stem.js new file mode 100644 index 0000000..7e26553 --- /dev/null +++ b/site/stem.js @@ -0,0 +1,952 @@ +/* by Joder Illi, Snowball mailing list */ +function stem(word) { + /* + Put u and y between vowels into upper case + */ + word = word.replace(/([aeiouyäöü])u([aeiouyäöü])/g, "$1U$2"); + word = word.replace(/([aeiouyäöü])y([aeiouyäöü])/g, "$1Y$2"); + + /* + and then do the following mappings, + (a) replace ß with ss, + (a) replace ae with ä, Not doing these, + have trouble with diphtongs + (a) replace oe with ö, Not doing these, + have trouble with diphtongs + (a) replace ue with ü unless preceded by q. Not doing these, + have trouble with diphtongs + So in quelle, ue is not mapped to ü because it follows q, and in + feuer it is not mapped because the first part of the rule changes it to + feUer, so the u is not found. + */ + word = word.replace(/ß/g, "ss"); + //word = word.replace(/ae/g, 'ä'); + //word = word.replace(/oe/g, 'ö'); + //word = word.replace(/([^q])ue/g, '$1ü'); + + /* + R1 and R2 are first set up in the standard way (see the note on R1 + and R2), but then R1 is adjusted so that the region before it contains at + least 3 letters. + R1 is the region after the first non-vowel following a vowel, or is + the null region at the end of the word if there is no such non-vowel. + R2 is the region after the first non-vowel following a vowel in R1, + or is the null region at the end of the word if there is no such non-vowel. + */ + + var r1Index = word.search(/[aeiouyäöü][^aeiouyäöü]/); + var r1 = ""; + if (r1Index != -1) { + r1Index += 2; + r1 = word.substring(r1Index); + } + + var r2Index = -1; + var r2 = ""; + + if (r1Index != -1) { + var r2Index = r1.search(/[aeiouyäöü][^aeiouyäöü]/); + if (r2Index != -1) { + r2Index += 2; + r2 = r1.substring(r2Index); + r2Index += r1Index; + } else { + r2 = ""; + } + } + + if (r1Index != -1 && r1Index < 3) { + r1Index = 3; + r1 = word.substring(r1Index); + } + + /* + Define a valid s-ending as one of b, d, f, g, h, k, l, m, n, r or t. + Define a valid st-ending as the same list, excluding letter r. + */ + + /* + Do each of steps 1, 2 and 3. + */ + + /* + Step 1: + Search for the longest among the following suffixes, + (a) em ern er + (b) e en es + (c) s (preceded by a valid s-ending) + */ + var a1Index = word.search(/(em|ern|er)$/g); + var b1Index = word.search(/(e|en|es)$/g); + var c1Index = word.search(/([bdfghklmnrt]s)$/g); + if (c1Index != -1) { + c1Index++; + } + var index1 = 10000; + var optionUsed1 = ""; + if (a1Index != -1 && a1Index < index1) { + optionUsed1 = "a"; + index1 = a1Index; + } + if (b1Index != -1 && b1Index < index1) { + optionUsed1 = "b"; + index1 = b1Index; + } + if (c1Index != -1 && c1Index < index1) { + optionUsed1 = "c"; + index1 = c1Index; + } + + /* + and delete if in R1. (Of course the letter of the valid s-ending is + not necessarily in R1.) If an ending of group (b) is deleted, and the ending + is preceded by niss, delete the final s. + (For example, äckern -> äck, ackers -> acker, armes -> arm, + bedürfnissen -> bedürfnis) + */ + + if (index1 != 10000 && r1Index != -1) { + if (index1 >= r1Index) { + word = word.substring(0, index1); + if (optionUsed1 == "b") { + if (word.search(/niss$/) != -1) { + word = word.substring(0, word.length - 1); + } + } + } + } + /* + Step 2: + Search for the longest among the following suffixes, + (a) en er est + (b) st (preceded by a valid st-ending, itself preceded by at least 3 + letters) + */ + + var a2Index = word.search(/(en|er|est)$/g); + var b2Index = word.search(/(.{3}[bdfghklmnt]st)$/g); + if (b2Index != -1) { + b2Index += 4; + } + + var index2 = 10000; + var optionUsed2 = ""; + if (a2Index != -1 && a2Index < index2) { + optionUsed2 = "a"; + index2 = a2Index; + } + if (b2Index != -1 && b2Index < index2) { + optionUsed2 = "b"; + index2 = b2Index; + } + + /* + and delete if in R1. + (For example, derbsten -> derbst by step 1, and derbst -> derb by + step 2, since b is a valid st-ending, and is preceded by just 3 letters) + */ + + if (index2 != 10000 && r1Index != -1) { + if (index2 >= r1Index) { + word = word.substring(0, index2); + } + } + + /* + Step 3: d-suffixes (*) + Search for the longest among the following suffixes, and perform the + action indicated. + end ung + delete if in R2 + if preceded by ig, delete if in R2 and not preceded by e + ig ik isch + delete if in R2 and not preceded by e + lich heit + delete if in R2 + if preceded by er or en, delete if in R1 + keit + delete if in R2 + if preceded by lich or ig, delete if in R2 + */ + + var a3Index = word.search(/(end|ung)$/g); + var b3Index = word.search(/[^e](ig|ik|isch)$/g); + var c3Index = word.search(/(lich|heit)$/g); + var d3Index = word.search(/(keit)$/g); + if (b3Index != -1) { + b3Index++; + } + + var index3 = 10000; + var optionUsed3 = ""; + if (a3Index != -1 && a3Index < index3) { + optionUsed3 = "a"; + index3 = a3Index; + } + if (b3Index != -1 && b3Index < index3) { + optionUsed3 = "b"; + index3 = b3Index; + } + if (c3Index != -1 && c3Index < index3) { + optionUsed3 = "c"; + index3 = c3Index; + } + if (d3Index != -1 && d3Index < index3) { + optionUsed3 = "d"; + index3 = d3Index; + } + + if (index3 != 10000 && r2Index != -1) { + if (index3 >= r2Index) { + word = word.substring(0, index3); + var optionIndex = -1; + var optionSubsrt = ""; + if (optionUsed3 == "a") { + optionIndex = word.search(/[^e](ig)$/); + if (optionIndex != -1) { + optionIndex++; + if (optionIndex >= r2Index) { + word = word.substring(0, optionIndex); + } + } + } else if (optionUsed3 == "c") { + optionIndex = word.search(/(er|en)$/); + if (optionIndex != -1) { + if (optionIndex >= r1Index) { + word = word.substring(0, optionIndex); + } + } + } else if (optionUsed3 == "d") { + optionIndex = word.search(/(lich|ig)$/); + if (optionIndex != -1) { + if (optionIndex >= r2Index) { + word = word.substring(0, optionIndex); + } + } + } + } + } + + /* + Finally, + turn U and Y back into lower case, and remove the umlaut accent from + a, o and u. + */ + word = word.replace(/U/g, "u"); + word = word.replace(/Y/g, "y"); + word = word.replace(/ä/g, "a"); + word = word.replace(/ö/g, "o"); + word = word.replace(/ü/g, "u"); + + return word; +} +exports.stem = stem; + +exports.stopWords = [ + "ab", + "aber", + "alle", + "allein", + "allem", + "allen", + "aller", + "allerdings", + "allerlei", + "alles", + "allmählich", + "allzu", + "als", + "alsbald", + "also", + "am", + "an", + "and", + "ander", + "andere", + "anderem", + "anderen", + "anderer", + "andererseits", + "anderes", + "anderm", + "andern", + "andernfalls", + "anders", + "anstatt", + "auch", + "auf", + "aus", + "ausgenommen", + "ausser", + "ausserdem", + "außer", + "außerdem", + "außerhalb", + "bald", + "bei", + "beide", + "beiden", + "beiderlei", + "beides", + "beim", + "beinahe", + "bereits", + "besonders", + "besser", + "beträchtlich", + "bevor", + "bezüglich", + "bin", + "bis", + "bisher", + "bislang", + "bist", + "bloß", + "bsp.", + "bzw", + "ca", + "ca.", + "content", + "da", + "dabei", + "dadurch", + "dafür", + "dagegen", + "daher", + "dahin", + "damals", + "damit", + "danach", + "daneben", + "dann", + "daran", + "darauf", + "daraus", + "darin", + "darum", + "darunter", + "darüber", + "darüberhinaus", + "das", + "dass", + "dasselbe", + "davon", + "davor", + "dazu", + "daß", + "dein", + "deine", + "deinem", + "deinen", + "deiner", + "deines", + "dem", + "demnach", + "demselben", + "den", + "denen", + "denn", + "dennoch", + "denselben", + "der", + "derart", + "derartig", + "derem", + "deren", + "derer", + "derjenige", + "derjenigen", + "derselbe", + "derselben", + "derzeit", + "des", + "deshalb", + "desselben", + "dessen", + "desto", + "deswegen", + "dich", + "die", + "diejenige", + "dies", + "diese", + "dieselbe", + "dieselben", + "diesem", + "diesen", + "dieser", + "dieses", + "diesseits", + "dir", + "direkt", + "direkte", + "direkten", + "direkter", + "doch", + "dort", + "dorther", + "dorthin", + "drauf", + "drin", + "drunter", + "drüber", + "du", + "dunklen", + "durch", + "durchaus", + "eben", + "ebenfalls", + "ebenso", + "eher", + "eigenen", + "eigenes", + "eigentlich", + "ein", + "eine", + "einem", + "einen", + "einer", + "einerseits", + "eines", + "einfach", + "einführen", + "einführte", + "einführten", + "eingesetzt", + "einig", + "einige", + "einigem", + "einigen", + "einiger", + "einigermaßen", + "einiges", + "einmal", + "eins", + "einseitig", + "einseitige", + "einseitigen", + "einseitiger", + "einst", + "einstmals", + "einzig", + "entsprechend", + "entweder", + "er", + "erst", + "es", + "etc", + "etliche", + "etwa", + "etwas", + "euch", + "euer", + "eure", + "eurem", + "euren", + "eurer", + "eures", + "falls", + "fast", + "ferner", + "folgende", + "folgenden", + "folgender", + "folgendes", + "folglich", + "fuer", + "für", + "gab", + "ganze", + "ganzem", + "ganzen", + "ganzer", + "ganzes", + "gar", + "gegen", + "gemäss", + "ggf", + "gleich", + "gleichwohl", + "gleichzeitig", + "glücklicherweise", + "gänzlich", + "hab", + "habe", + "haben", + "haette", + "hast", + "hat", + "hatte", + "hatten", + "hattest", + "hattet", + "heraus", + "herein", + "hier", + "hier", + "hinter", + "hiermit", + "hiesige", + "hin", + "hinein", + "hinten", + "hinter", + "hinterher", + "http", + "hätt", + "hätte", + "hätten", + "höchstens", + "ich", + "igitt", + "ihm", + "ihn", + "ihnen", + "ihr", + "ihre", + "ihrem", + "ihren", + "ihrer", + "ihres", + "im", + "immer", + "immerhin", + "in", + "indem", + "indessen", + "infolge", + "innen", + "innerhalb", + "ins", + "insofern", + "inzwischen", + "irgend", + "irgendeine", + "irgendwas", + "irgendwen", + "irgendwer", + "irgendwie", + "irgendwo", + "ist", + "ja", + "je", + "jed", + "jede", + "jedem", + "jeden", + "jedenfalls", + "jeder", + "jederlei", + "jedes", + "jedoch", + "jemand", + "jene", + "jenem", + "jenen", + "jener", + "jenes", + "jenseits", + "jetzt", + "jährig", + "jährige", + "jährigen", + "jähriges", + "kam", + "kann", + "kannst", + "kaum", + "kein", + "keine", + "keinem", + "keinen", + "keiner", + "keinerlei", + "keines", + "keineswegs", + "klar", + "klare", + "klaren", + "klares", + "klein", + "kleinen", + "kleiner", + "kleines", + "koennen", + "koennt", + "koennte", + "koennten", + "komme", + "kommen", + "kommt", + "konkret", + "konkrete", + "konkreten", + "konkreter", + "konkretes", + "können", + "könnt", + "künftig", + "leider", + "machen", + "man", + "manche", + "manchem", + "manchen", + "mancher", + "mancherorts", + "manches", + "manchmal", + "mehr", + "mehrere", + "mein", + "meine", + "meinem", + "meinen", + "meiner", + "meines", + "mich", + "mir", + "mit", + "mithin", + "muessen", + "muesst", + "muesste", + "muss", + "musst", + "musste", + "mussten", + "muß", + "mußt", + "müssen", + "müsste", + "müssten", + "müßt", + "müßte", + "nach", + "nachdem", + "nachher", + "nachhinein", + "nahm", + "natürlich", + "neben", + "nebenan", + "nehmen", + "nein", + "nicht", + "nichts", + "nie", + "niemals", + "niemand", + "nirgends", + "nirgendwo", + "noch", + "nun", + "nur", + "nächste", + "nämlich", + "nötigenfalls", + "ob", + "oben", + "oberhalb", + "obgleich", + "obschon", + "obwohl", + "oder", + "oft", + "per", + "plötzlich", + "schließlich", + "schon", + "sehr", + "sehrwohl", + "seid", + "sein", + "seine", + "seinem", + "seinen", + "seiner", + "seines", + "seit", + "seitdem", + "seither", + "selber", + "selbst", + "sich", + "sicher", + "sicherlich", + "sie", + "sind", + "so", + "sobald", + "sodass", + "sofort", + "sofern", + "sog", + "sogar", + "solange", + "solch", + "solche", + "solchem", + "solchen", + "solcher", + "solches", + "soll", + "sollen", + "sollst", + "sollt", + "sollte", + "sollten", + "somit", + "sondern", + "sonst", + "sonstige", + "sonstigen", + "sonstiger", + "sonstiges", + "sooft", + "soviel", + "soweit", + "sowie", + "sowieso", + "sowohl", + "später", + "statt", + "stattfinden", + "stattfand", + "stattgefunden", + "steht", + "stets", + "such", + "suche", + "suchen", + "tatsächlich", + "tatsächlichen", + "tatsächlicher", + "tatsächliches", + "tatsächlich", + "tatsächlichen", + "tatsächlicher", + "tatsächliches", + "tief", + "tiefer", + "trotz", + "trotzdem", + "tun", + "über", + "überall", + "überallhin", + "überdies", + "überhaupt", + "übrig", + "übrigens", + "um", + "umso", + "umsoweniger", + "unbedingt", + "und", + "unmöglich", + "unnötig", + "unser", + "unsere", + "unserem", + "unseren", + "unserer", + "unseres", + "unserseits", + "unter", + "unterhalb", + "unterhalb", + "untereinander", + "untergebracht", + "unterhalb", + "unterhalb", + "unterhalb", + "unterhalb", + "unterhalb", + "unterhalb", + "unterschiedlich", + "unterschiedliche", + "unterschiedlichen", + "unterschiedlicher", + "unterschiedliches", + "unterschiedlich", + "unterschiedliche", + "unterschiedlichen", + "unterschiedlicher", + "unterschiedliches", + "unzwar", + "usw", + "usw.", + "vermag", + "vermögen", + "vermutlich", + "verrate", + "verraten", + "verrätst", + "verschieden", + "verschiedene", + "verschiedenen", + "verschiedener", + "verschiedenes", + "versorgen", + "versorgt", + "versorgte", + "versorgten", + "viel", + "viele", + "vielem", + "vielen", + "vieler", + "vieles", + "vielleicht", + "vielmals", + "vier", + "vierte", + "viertel", + "vierten", + "vierter", + "viertes", + "vom", + "von", + "vor", + "vorbei", + "vorgestern", + "vorher", + "vorüber", + "wach", + "wachen", + "wahrend", + "wann", + "war", + "warauf", + "ward", + "waren", + "warst", + "wart", + "warum", + "was", + "weder", + "weil", + "weiter", + "weitere", + "weiterem", + "weiteren", + "weiterer", + "weiteres", + "weiterhin", + "weitgehend", + "welche", + "welchem", + "welchen", + "welcher", + "welches", + "wem", + "wen", + "wenig", + "wenige", + "wenigem", + "wenigen", + "weniger", + "wenigstens", + "wenn", + "wenngleich", + "wer", + "werde", + "werden", + "werdet", + "weshalb", + "wessen", + "wichtig", + "wie", + "wieder", + "wiederum", + "wieso", + "will", + "willst", + "wir", + "wird", + "wirklich", + "wirst", + "wissen", + "wo", + "woanders", + "wohl", + "woher", + "wohin", + "wohingegen", + "wohl", + "wohlweislich", + "wollen", + "wollt", + "wollte", + "wollten", + "womit", + "woraufhin", + "woraus", + "woraussichtlich", + "worauf", + "woraus", + "worin", + "worüber", + "wovon", + "wovor", + "wozu", + "während", + "währenddessen", + "wär", + "wäre", + "wären", + "wärst", + "wäre", + "wären", + "wärst", + "würde", + "würden", + "würdest", + "würdet", + "zB", + "z.b.", + "zehn", + "zeigen", + "zeitweise", + "zu", + "zufolge", + "zugleich", + "zuletzt", + "zum", + "zumal", + "zumeist", + "zunächst", + "zur", + "zurück", + "zurückgehend", + "zurückgehen", + "zurückgegangen", + "zurückgekommen", + "zurückgekommen", + "zurückgekommen", + "zurückgekommen", + "zurückgezogen", + "zusammen", + "zusätzlich", + "zusammen", + "zuvor", + "zuviel", + "zuweilen", + "zwanzig", + "zwar", + "zwei", + "zweite", + "zweiten", + "zweiter", + "zweites", + "zwischen", + "zwischendurch", + "zwölf", + "überall", + "überallhin", + "überdies", + "überhaupt", + "übrig", + "übrigens", +]; diff --git a/site/views/items-filter.js b/site/views/items-filter.js index 57d1e08..cba9117 100644 --- a/site/views/items-filter.js +++ b/site/views/items-filter.js @@ -150,8 +150,22 @@ class ItemsFilter extends View { const decreased = elements.priceDecreased.checked; filteredItems = filteredItems.filter((item) => { if (item.priceHistory.length == 1) return false; - if (increased && item.priceHistory[0].price > item.priceHistory[1].price) return true; - if (decreased && item.priceHistory[0].price < item.priceHistory[1].price) return true; + if (this._filterByPriceChanges && elements.priceChangesToday.checked) { + const today = elements.priceChangesDate.value; + for (let i = 0; i < item.priceHistory.length; i++) { + if (item.priceHistory[i].date == today && i + 1 < item.priceHistory.length) { + if (increased && item.priceHistory[i].price > item.priceHistory[i + 1].price) { + return true; + } + if (decreased && item.priceHistory[i].price < item.priceHistory[i + 1].price) { + return true; + } + } + } + } else { + if (increased && item.priceHistory[0].price > item.priceHistory[1].price) return true; + if (decreased && item.priceHistory[0].price < item.priceHistory[1].price) return true; + } return false; }); } @@ -182,11 +196,11 @@ class ItemsFilter extends View { filteredItems = queryItems(query, filteredItems); } + console.log("Filtering items took " + (performance.now() - now) / 1000 + " secs"); + this.model.removeListener(this._listener); this.model.filteredItems = filteredItems; this.model.addListener(this._listener); - - console.log("Filtering items took " + (performance.now() - now) / 1000 + " secs"); } render() { diff --git a/site/views/items-list.js b/site/views/items-list.js index b800860..af0e80d 100644 --- a/site/views/items-list.js +++ b/site/views/items-list.js @@ -1,4 +1,5 @@ -const { downloadJSON, dom, getDynamicElements, onVisibleOnce } = require("../misc"); +const { downloadJSON, dom, getDynamicElements, onVisibleOnce, isMobile } = require("../misc"); +const { vectorizeItems, similaritySortItems } = require("../knn"); const { stores } = require("../model/stores"); const { View } = require("./view"); @@ -20,12 +21,13 @@ class ItemsList extends View { @@ -34,7 +36,7 @@ class ItemsList extends View { Kette Name - Preis + + Preis + @@ -63,6 +65,65 @@ class ItemsList extends View { ` ); + + const elements = this.elements; + + elements.json.addEventListener("click", (event) => { + event.preventDefault(); + if (!this.model) return; + downloadJSON("items.json", this.model.filteredItems); + }); + + // Cache in a field, so we don't have to call this.elements in each renderItem() call. + this._showAllPriceHistories = false; + elements.expandPriceHistories.addEventListener("click", () => { + const showAll = (this._showAllPriceHistories = !this._showAllPriceHistories); + elements.expandPriceHistories.innerText = showAll ? "Preis -" : "Preis +"; + elements.tableBody.querySelectorAll(".priceinfo").forEach((el) => (showAll ? el.classList.remove("hidden") : el.classList.add("hidden"))); + }); + + this.addEventListener("change", () => { + this.render(); + }); + } + + sort(items) { + const sortType = this.elements.sort.value; + if (sortType == "price-asc") { + items.sort((a, b) => a.price - b.price); + } else if (sortType == "price-desc") { + items.sort((a, b) => b.price - a.price); + } else if (sortType == "quantity-asc") { + items.sort((a, b) => { + if (a.unit != b.unit) return a.unit.localeCompare(b.unit); + return a.quantity - b.quantity; + }); + } else if (sortType == "quantity-desc") { + items.sort((a, b) => { + if (a.unit != b.unit) return a.unit.localeCompare(b.unit); + return b.quantity - a.quantity; + }); + } else if (sortType == "chain-and-name") { + items.sort((a, b) => { + if (a.store < b.store) { + return -1; + } else if (a.store > b.store) { + return 1; + } + + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } + + return 0; + }); + } else { + vectorizeItems(items); + items = similaritySortItems(items); + } + return items; } renderItem(item) { @@ -139,12 +200,20 @@ class ItemsList extends View { } }); }); + if (this._showAllPriceHistories) elements.priceHistory.classList.remove("hidden"); return itemDom; } render() { - const items = this.model.filteredItems; const elements = this.elements; + if (this.model.filteredItems.length != 0 && this.model.filteredItems.length <= (isMobile() ? 200 : 1000)) { + elements.nameSimilarity.removeAttribute("disabled"); + } else { + elements.nameSimilarity.setAttribute("disabled", "true"); + if (elements.sort.value == "name-similarity") elements.sort.value = "price-asc"; + } + + const items = this.sort([...this.model.filteredItems]); elements.numItems.innerHTML = "Resultate: " + items.length + (this.model.totalItems > items.length ? " / " + this.model.totalItems : ""); const tableBody = elements.tableBody;