From 6569b17da2a9fe30f22397156547f814babcc0e2 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 18 Jun 2023 23:23:02 +0200 Subject: [PATCH] Remove binary encoding, web worker, clean-up. --- README.md | 2 +- analysis.js | 111 --- bundle.js | 1 - server.js | 1 - site/_templates/_footer.html | 2 +- site/js/misc.js | 12 - site/model/items-loader.js | 289 -------- site/model/items.js | 192 +++++- site/model/settings.js | 3 - stores/spar.js | 1241 ++++++++++++++++++++++++++++++++++ 10 files changed, 1419 insertions(+), 435 deletions(-) delete mode 100644 site/model/items-loader.js diff --git a/README.md b/README.md index 120844e..45a4f5f 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Merged price history App listening on port 3000 ``` -Once the app is listening per default on port 3000, open in your browser.\ +Once the app is listening per default on port 3000, open in your browser. Subsequent starts will fetch the data asynchronously, so you can start working immediately. diff --git a/analysis.js b/analysis.js index c6a7e98..a304f20 100644 --- a/analysis.js +++ b/analysis.js @@ -149,117 +149,6 @@ function sortItems(items) { }); } -function compressBinary(items) { - const buffer = []; - buffer.push(STORE_KEYS.length); - for (const key of STORE_KEYS) { - const nameBuffer = Buffer.from(key, "utf8"); - const nameLengthBuffer = Buffer.allocUnsafe(2); - nameLengthBuffer.writeUInt16LE(nameBuffer.length, 0); - buffer.push(...nameLengthBuffer, ...nameBuffer); - } - - const dictionary = {}; - const words = []; - let id = 0; - for (const item of items) { - const tokens = item.name.split(/\s+/); - for (const token of tokens) { - if (!dictionary[token]) { - dictionary[token] = id++; - words.push(token); - if (token.length > 256) { - console.log("Dictionary word > 256 characters: " + token); - } - } - } - } - - const numWordsBuffer = Buffer.allocUnsafe(4); - numWordsBuffer.writeUint32LE(id, 0); - buffer.push(...numWordsBuffer); - for (const word of words) { - const wordBuffer = Buffer.from(word, "utf8"); - buffer.push(wordBuffer.length); - buffer.push(...wordBuffer); - } - - for (const item of items) { - const idBuffer = Buffer.from("" + item.id, "utf8"); - buffer.push(idBuffer.length); - buffer.push(...idBuffer); - - let flagsByte = 0; - if (item.bio) flagsByte |= 1; - if (item.isWeighted) flagsByte |= 2; - if (item.unit === "ml") flagsByte |= 4; - if (item.unit === "g") flagsByte |= 8; - if (item.unit === "stk") flagsByte |= 16; - if (item.unit === "cm") flagsByte |= 32; - if (item.unit === "wg") flagsByte |= 64; - buffer.push(flagsByte); - - const quantityBuffer = Buffer.allocUnsafe(2); - let quantity = Math.min(64000, item.quantity); - if (quantity > 64000) { - console.log(`Item quantity > 64000 ${item.id} - ${item.store} - ${item.name}`); - } - quantityBuffer.writeUint16LE(quantity, 0); - buffer.push(...quantityBuffer); - - const storeByte = STORE_KEYS.findIndex((store) => store == item.store); - buffer.push(storeByte); - - const tokenIds = item.name.split(/\s+/).map((token) => { - const id = dictionary[token]; - if (id === undefined) { - console.log(`Undefined token ${token} ${item.id} - ${item.store} - ${item.name}`); - } - return id; - }); - - buffer.push(tokenIds.length); - for (const tokenId of tokenIds) { - const tokenIdBuffer = Buffer.allocUnsafe(4); - tokenIdBuffer.writeUint32LE(tokenId, 0); - buffer.push(tokenIdBuffer[0], tokenIdBuffer[1], tokenIdBuffer[2]); - } - - if (item.url !== undefined) { - const urlBuffer = Buffer.from(item.url, "utf8"); - const urlLengthBuffer = Buffer.allocUnsafe(2); - urlLengthBuffer.writeUInt16LE(urlBuffer.length, 0); - buffer.push(...urlLengthBuffer, ...urlBuffer); - } else { - buffer.push(0); - buffer.push(0); - } - - const priceHistoryLengthBuffer = Buffer.allocUnsafe(2); - priceHistoryLengthBuffer.writeUInt16LE(item.priceHistory.length, 0); - buffer.push(...priceHistoryLengthBuffer); - - for (const priceEntry of item.priceHistory) { - const priceEntryBuffer = Buffer.allocUnsafe(2); - if (priceEntry.price == 999) priceEntry.price = 9.99; - let price = Math.round(priceEntry.price * 100); - if (price > 64000) { - console.log(`Item price > 64000 ${item.id} - ${item.store} - ${item.name}`); - price = 64000; - } - priceEntryBuffer.writeUint16LE(price, 0); - buffer.push(...priceEntryBuffer); - - const dateBuffer = Buffer.allocUnsafe(2); - dateBuffer.writeUint16LE(dateToUint16(priceEntry.date), 0); - buffer.push(...dateBuffer); - } - } - - return Buffer.from(buffer); -} -exports.compressBinary = compressBinary; - // Keep this in sync with utils.js:decompress function compress(items) { const compressed = { diff --git a/bundle.js b/bundle.js index 0177903..5a7d0a9 100755 --- a/bundle.js +++ b/bundle.js @@ -108,7 +108,6 @@ async function bundleJS(inputDir, outputDir, watch) { changes: `${inputDir}/changes.js`, settings: `${inputDir}/settings.js`, index: `${inputDir}/index.js`, - "items-loader": `${inputDir}/model/items-loader.js`, }, bundle: true, sourcemap: true, diff --git a/server.js b/server.js index a5ecd1a..cf09f8e 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,6 @@ function copyItemsToSite(dataDir) { for (const store of analysis.STORE_KEYS) { const storeItems = items.filter((item) => item.store === store); analysis.writeJSON(`site/output/data/latest-canonical.${store}.compressed.json`, storeItems, false, 0, true); - fs.writeFileSync(`site/output/data/latest-canonical.${store}.bin.json`, analysis.compressBinary(storeItems)); } } diff --git a/site/_templates/_footer.html b/site/_templates/_footer.html index baa6701..d15773d 100644 --- a/site/_templates/_footer.html +++ b/site/_templates/_footer.html @@ -6,7 +6,7 @@
- Historische Daten von @h43z & Dossier + Historische Daten von @h43z & Dossier

Alle Angaben ohne Gewähr, Irrtümer vorbehalten.
diff --git a/site/js/misc.js b/site/js/misc.js index bd9f6a7..1948601 100644 --- a/site/js/misc.js +++ b/site/js/misc.js @@ -63,18 +63,6 @@ exports.today = () => { return `${year}-${month}-${day}`; }; -exports.dateToUint16 = (dateString) => { - const [year, month, day] = dateString.split("-").map(Number); - return (((year - 2000) << 9) | (month << 5) | day) & 0xffff; -}; - -exports.uint16ToDate = (encodedDate) => { - const year = (encodedDate >> 9) + 2000; - const month = (encodedDate >> 5) & 0xf; - const day = encodedDate & 0x1f; - return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`; -}; - exports.fetchJSON = async (url) => { const response = await fetch(url); return await response.json(); diff --git a/site/model/items-loader.js b/site/model/items-loader.js deleted file mode 100644 index 8182bcd..0000000 --- a/site/model/items-loader.js +++ /dev/null @@ -1,289 +0,0 @@ -const { deltaTime, log, uint16ToDate } = require("../js/misc"); -const { stores, STORE_KEYS } = require("./stores"); - -function decompressBinary(buffer) { - const objects = []; - let offset = 0; - const view = new DataView(buffer); - const baseDate = new Date("2000-01-01"); - const textDecoder = new TextDecoder("utf-8"); - - const numStores = view.getUint8(offset++); - const stores = []; - for (let i = 0; i < numStores; i++) { - const nameLength = view.getUint16(offset, true); - offset += 2; - const nameBuffer = new Uint8Array(buffer, offset, nameLength); - stores.push(textDecoder.decode(nameBuffer)); - offset += nameLength; - } - - const numWords = view.getUint32(offset, true); - offset += 4; - const words = new Array(numWords); - for (let i = 0; i < numWords; i++) { - const nameLength = view.getUint8(offset++); - const nameBuffer = new Uint8Array(buffer, offset, nameLength); - words[i] = textDecoder.decode(nameBuffer); - offset += nameLength; - } - - while (offset < buffer.byteLength) { - const obj = {}; - const idLength = view.getUint8(offset++); - const idBuffer = new Uint8Array(buffer, offset, idLength); - obj.id = textDecoder.decode(idBuffer); - offset += idLength; - - const flagsByte = view.getUint8(offset++); - obj.bio = (flagsByte & 1) !== 0; - obj.isWeighted = (flagsByte & 2) !== 0; - if (flagsByte & 4) obj.unit = "ml"; - if (flagsByte & 8) obj.unit = "g"; - if (flagsByte & 16) obj.unit = "stk"; - if (flagsByte & 32) obj.unit = "cm"; - if (flagsByte & 64) obj.unit = "wg"; - - obj.quantity = view.getUint16(offset, true); - offset += 2; - - obj.store = stores[view.getUint8(offset++)]; - - let name = ""; - const numTokens = view.getUint8(offset++); - for (let i = 0; i < numTokens; i++) { - const b1 = view.getUint8(offset++); - const b2 = view.getUint8(offset++); - const b3 = view.getUint8(offset++); - const tokenId = (b3 << 16) | (b2 << 8) | b1; - name += words[tokenId]; - if (i < numTokens - 1) name += " "; - } - obj.name = name; - - const urlLength = view.getUint16(offset, true); - offset += 2; - if (urlLength !== 0) { - const urlBuffer = new Uint8Array(buffer, offset, urlLength); - obj.url = textDecoder.decode(urlBuffer); - } else { - obj.url = undefined; - } - offset += urlLength; - - const priceHistoryLength = view.getUint16(offset, true); - offset += 2; - obj.priceHistory = new Array(priceHistoryLength); - - for (let i = 0; i < priceHistoryLength; i++) { - const price = view.getUint16(offset, true) / 100; - offset += 2; - - const date = uint16ToDate(view.getUint16(offset, true)); - offset += 2; - - obj.priceHistory[i] = { date, price }; - } - - obj.price = obj.priceHistory[0].price; - - objects.push(obj); - } - - return objects; -} - -function decompress(compressedItems) { - const storeLookup = compressedItems.stores; - const data = compressedItems.data; - const dates = compressedItems.dates; - const numItems = compressedItems.n; - const items = new Array(numItems); - let i = 0; - for (let l = 0; l < numItems; l++) { - const store = storeLookup[data[i++]]; - const id = data[i++]; - const name = data[i++]; - const numPrices = data[i++]; - const prices = new Array(numPrices); - for (let j = 0; j < numPrices; j++) { - const date = dates[data[i++]]; - const price = data[i++]; - prices[j] = { - date: date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6, 8), - price, - }; - } - const unit = data[i++]; - const quantity = data[i++]; - const isWeighted = data[i++] == 1; - const bio = data[i++] == 1; - const url = data[i++]; - - items[l] = { - store, - id, - name, - price: prices[0].price, - priceHistory: prices, - isWeighted, - unit, - quantity, - bio, - url, - }; - } - return items; -} - -function processItems(items) { - const lookup = {}; - const start = performance.now(); - const interns = new Map(); - const intern = (value) => { - if (interns.has(value)) { - return interns.get(value); - } else { - interns.set(value, value); - return value; - } - }; - - const getters = { - unitPrice: { - get() { - const unitPriceFactor = this.unit == "g" || this.unit == "ml" ? 1000 : 1; - return (this.price / this.quantity) * unitPriceFactor; - }, - }, - numPrices: { - get() { - return this.priceHistory.length; - }, - }, - date: { - get() { - return this.priceHistory[0].date; - }, - }, - priceOldest: { - get() { - return this.priceHistory[this.priceHistory.length - 1].price; - }, - }, - dateOldest: { - get() { - return this.priceHistory[this.priceHistory.length - 1].date; - }, - }, - }; - - for (let i = 1; i < 3; i++) { - (getters[`price${i}`] = { - get() { - return this.priceHistory[i] ? this.priceHistory[i].price : 0; - }, - }), - (getters[`date${i}`] = { - get() { - return this.priceHistory[i] ? this.priceHistory[i].date : null; - }, - }); - } - - items.forEach((item) => { - lookup[item.store + item.id] = item; - for (const getter in getters) { - Object.defineProperty(item, getter, getters[getter]); - } - - item.store = intern(item.store); - item.id = intern(item.id); - item.name = intern(item.name); - item.category = intern(item.category); - item.price = intern(item.price); - for (const price of item.priceHistory) { - price.date = intern(price.date); - price.price = intern(price.price); - } - item.unit = intern(item.unit); - item.quantity = intern(item.quantity); - - item.search = item.name + " " + item.quantity + " " + item.unit; - item.search = intern(item.search.toLowerCase().replace(",", ".")); - - const unitPriceFactor = item.unit == "g" || item.unit == "ml" ? 1000 : 1; - for (let i = 0; i < item.priceHistory.length; i++) { - const price = item.priceHistory[i]; - price.unitPrice = (price.price / item.quantity) * unitPriceFactor; - } - }); - - 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; - }); - - log(`Loader - processing ${items.length} items took ${deltaTime(start).toFixed(4)} secs`); - return { items, lookup }; -} - -exports.loadItems = async (settings) => { - let start = performance.now(); - const compressedItemsPerStore = []; - log(`Loader - load using JSON: ${settings.useJson}`); - for (const store of STORE_KEYS) { - compressedItemsPerStore.push( - new Promise(async (resolve) => { - let start = performance.now(); - try { - const useJSON = true; // settings.useJson; - if (useJSON) { - const response = await fetch(`data/latest-canonical.${store}.compressed.json`); - const json = await response.json(); - log(`Loader - loading compressed items for ${store} took ${deltaTime(start)} secs`); - start = performance.now(); - let items = decompress(json); - log(`Loader - Decompressing items for ${store} took ${deltaTime(start)} secs`); - resolve(items); - } else { - const response = await fetch(`data/latest-canonical.${store}.bin.json`); - const binary = await response.arrayBuffer(); - log(`Loader - loading compressed binary items for ${store} took ${deltaTime(start)} secs`); - start = performance.now(); - let items = decompressBinary(binary); - log(`Loader - Decompressing items for ${store} took ${deltaTime(start)} secs`); - resolve(items); - } - } catch (e) { - log(`Loader - error while loading compressed items for ${store} ${e.message}`); - resolve([]); - } - }) - ); - } - const items = [].concat(...(await Promise.all(compressedItemsPerStore))); - log(`Loader - loaded ${items.length} items took ${deltaTime(start).toFixed(4)} secs`); - - const result = processItems(items); - log(`Loader - total loading took ${deltaTime(start).toFixed(4)} secs`); - - return result; -}; - -onmessage = async (event) => { - const settings = event.data.settings; - const result = await exports.loadItems(settings); - postMessage(result); -}; diff --git a/site/model/items.js b/site/model/items.js index 46014d2..08d67ce 100644 --- a/site/model/items.js +++ b/site/model/items.js @@ -1,6 +1,7 @@ const { Model } = require("./model"); +const { STORE_KEYS } = require("./stores"); const { Settings } = require("./settings"); -const { loadItems } = require("./items-loader"); +const { log, deltaTime } = require("../js/misc"); class Items extends Model { constructor() { @@ -29,22 +30,181 @@ class Items extends Model { async load() { const settings = new Settings(); - if (window.Worker && false) { - const self = this; - return new Promise((resolve, reject) => { - const loader = new Worker("items-loader.js"); - loader.onmessage = (event) => { - self._items = event.data.items; - self._lookup = event.data.lookup; - resolve(); - }; - loader.postMessage({ settings }); - }); - } else { - const { items, lookup } = await loadItems(settings); - this._items = items; - this._lookup = lookup; + let start = performance.now(); + const compressedItemsPerStore = []; + for (const store of STORE_KEYS) { + compressedItemsPerStore.push( + new Promise(async (resolve) => { + let start = performance.now(); + try { + const response = await fetch(`data/latest-canonical.${store}.compressed.json`); + const json = await response.json(); + log(`Loader - loading compressed items for ${store} took ${deltaTime(start)} secs`); + start = performance.now(); + let items = this.decompress(json); + log(`Loader - Decompressing items for ${store} took ${deltaTime(start)} secs`); + resolve(items); + } catch (e) { + log(`Loader - error while loading compressed items for ${store} ${e.message}`); + resolve([]); + } + }) + ); } + let items = [].concat(...(await Promise.all(compressedItemsPerStore))); + log(`Loader - loaded ${items.length} items took ${deltaTime(start).toFixed(4)} secs`); + + const result = this.processItems(items); + log(`Loader - total loading took ${deltaTime(start).toFixed(4)} secs`); + + this._items = result.items; + this._lookup = result.lookup; + } + + processItems(items) { + const lookup = {}; + const start = performance.now(); + const interns = new Map(); + const intern = (value) => { + if (interns.has(value)) { + return interns.get(value); + } else { + interns.set(value, value); + return value; + } + }; + + const getters = { + unitPrice: { + get() { + const unitPriceFactor = this.unit == "g" || this.unit == "ml" ? 1000 : 1; + return (this.price / this.quantity) * unitPriceFactor; + }, + }, + numPrices: { + get() { + return this.priceHistory.length; + }, + }, + date: { + get() { + return this.priceHistory[0].date; + }, + }, + priceOldest: { + get() { + return this.priceHistory[this.priceHistory.length - 1].price; + }, + }, + dateOldest: { + get() { + return this.priceHistory[this.priceHistory.length - 1].date; + }, + }, + }; + + for (let i = 1; i < 3; i++) { + (getters[`price${i}`] = { + get() { + return this.priceHistory[i] ? this.priceHistory[i].price : 0; + }, + }), + (getters[`date${i}`] = { + get() { + return this.priceHistory[i] ? this.priceHistory[i].date : null; + }, + }); + } + + items.forEach((item) => { + lookup[item.store + item.id] = item; + for (const getter in getters) { + Object.defineProperty(item, getter, getters[getter]); + } + + item.store = intern(item.store); + item.id = intern(item.id); + item.name = intern(item.name); + item.category = intern(item.category); + item.price = intern(item.price); + for (const price of item.priceHistory) { + price.date = intern(price.date); + price.price = intern(price.price); + } + item.unit = intern(item.unit); + item.quantity = intern(item.quantity); + + item.search = item.name + " " + item.quantity + " " + item.unit; + item.search = intern(item.search.toLowerCase().replace(",", ".")); + + const unitPriceFactor = item.unit == "g" || item.unit == "ml" ? 1000 : 1; + for (let i = 0; i < item.priceHistory.length; i++) { + const price = item.priceHistory[i]; + price.unitPrice = (price.price / item.quantity) * unitPriceFactor; + } + }); + + 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; + }); + + log(`Loader - processing ${items.length} items took ${deltaTime(start).toFixed(4)} secs`); + return { items, lookup }; + } + + decompress(compressedItems) { + const storeLookup = compressedItems.stores; + const data = compressedItems.data; + const dates = compressedItems.dates; + const numItems = compressedItems.n; + const items = new Array(numItems); + let i = 0; + for (let l = 0; l < numItems; l++) { + const store = storeLookup[data[i++]]; + const id = data[i++]; + const name = data[i++]; + const numPrices = data[i++]; + const prices = new Array(numPrices); + for (let j = 0; j < numPrices; j++) { + const date = dates[data[i++]]; + const price = data[i++]; + prices[j] = { + date: date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6, 8), + price, + }; + } + const unit = data[i++]; + const quantity = data[i++]; + const isWeighted = data[i++] == 1; + const bio = data[i++] == 1; + const url = data[i++]; + + items[l] = { + store, + id, + name, + price: prices[0].price, + priceHistory: prices, + isWeighted, + unit, + quantity, + bio, + url, + }; + } + return items; } } diff --git a/site/model/settings.js b/site/model/settings.js index 4f03919..959947a 100644 --- a/site/model/settings.js +++ b/site/model/settings.js @@ -10,7 +10,6 @@ class Settings extends Model { STORE_KEYS.forEach((store) => { this[store] = stores[store].defaultChecked; }); - this.useJson = true; let settings = localStorage.getItem("settings"); if (settings) { @@ -19,8 +18,6 @@ class Settings extends Model { this[prop] = settings[prop]; } } - this.useJson = true; - log(`Settings - using JSON: ${this.useJson}`); } save() { diff --git a/stores/spar.js b/stores/spar.js index f81b474..ceb93e3 100644 --- a/stores/spar.js +++ b/stores/spar.js @@ -1,5 +1,6 @@ const axios = require("axios"); const utils = require("./utils"); +const { UNKNOWN_CATEGORY } = require("../site/model/categories"); const HITS = Math.floor(30000 + Math.random() * 2000); const units = { @@ -8,6 +9,23 @@ const units = { "100g": { unit: "g", factor: 100 }, }; +function getCategory(item) { + if (!item.masterValues["category-path"]) return null; + const regex = /F(\d+)-(\d+)/g; + const categoryPath = item.masterValues["category-path"] + .filter((p) => { + const match = regex.exec(p); + if (match == null) return false; + if (Number.parseInt(match[1]) > 13) return false; + return true; + }) + .map((p) => p[0]); + if (categoryPath.length == 0) return null; + const sparCategory = categoryPath[0]; + // console.log(sparCategory); + return UNKNOWN_CATEGORY; +} + exports.getCanonical = function (item, today) { let price, unit, quantity; if (item.masterValues["quantity-selector"]) { @@ -51,11 +69,14 @@ exports.getCanonical = function (item, today) { quantity = fallback.quantity; } + const category = getCategory(item); + return utils.convertUnit( { id: item.masterValues["product-number"], name: item.masterValues.title + " " + (item.masterValues["short-description"] ?? item.masterValues.name), description: item.masterValues["marketing-text"] ?? "", + category, price, priceHistory: [{ date: today, price }], unit, @@ -76,3 +97,1223 @@ exports.fetchData = async function () { }; exports.urlBase = "https://www.interspar.at/shop/lebensmittel"; + +// Generated in Chrome dev tools at https://www.interspar.at/shop/lebensmittel/ via: +// +// Array.from(document.querySelectorAll(`.flyout-categories__link`)).filter(el => !(el.innerText.toLowerCase().includes("übersicht") || el.innerText.toLowerCase().includes("zurück"))).map(el => { +// const paths = el.href.split("/"); +// return { category: paths[paths.length - 2], name: el.innerText.trim(), code: "" }; +// }) +// +const categoryMapping = [ + { + category: "F1-1", + name: "Frischgemüse", + code: "01", + }, + { + category: "F1-2", + name: "Frischobst", + code: "00", + }, + { + category: "F1-3", + name: "Obst-, Gemüse- & Salat-Zubereitungen", + code: "01", + }, + { + category: "F1-4", + name: "Saisonartikel", + code: "01", + }, + { + category: "F1-5", + name: "Obst- & Gemüsekisten", + code: "00", + }, + { + category: "F2", + name: "KÜHLREGAL", + code: "", + }, + { + category: "F2-1", + name: "Molkerei & Eier", + code: "", + }, + { + category: "F2-1-1", + name: "Milch", + code: "34", + }, + { + category: "F2-1-2", + name: "Milchgetränk", + code: "34", + }, + { + category: "F2-1-3", + name: "Eiskaffee", + code: "20", + }, + { + category: "F2-1-4", + name: "Produkte auf Pflanzenbasis", + code: "5D", + }, + { + category: "F2-1-5", + name: "Rahm, Schlagobers & Topfen", + code: "34", + }, + { + category: "F2-1-7", + name: "Butter, Margarine & Fette", + code: "34", + }, + { + category: "F2-1-8", + name: "Eier", + code: "31", + }, + { + category: "F2-2", + name: "Joghurt & Desserts", + code: "34", + }, + { + category: "F2-3", + name: "Käse", + code: "33", + }, + { + category: "F2-4", + name: "Aufstriche & Salate", + code: "33", + }, + { + category: "F2-5", + name: "Fertiggerichte, Snacks & Teige", + code: "30", + }, + { + category: "F2-6", + name: "Vegetarisch, Tofu, Soja & Co", + code: "3B", + }, + { + category: "F3-1", + name: "Wurst & Selchwaren", + code: "37", + }, + { + category: "F3-2", + name: "Frischfleisch & -Geflügel", + code: "32", + }, + { + category: "F3-3", + name: "Fisch", + code: "39", + }, + { + category: "F4-1", + name: "Feinkost & Konserven", + code: "57", + }, + { + category: "F4-2-1", + name: "Konfitüre", + code: "56", + }, + { + category: "F4-2-2", + name: "Honig", + code: "56", + }, + { + category: "F4-2-3", + name: "Nuss- & Schokoaufstriche", + code: "56", + }, + { + category: "F4-2-4", + name: "Cerealien & Müsli", + code: "5A", + }, + { + category: "F4-3-1", + name: "Gewürze", + code: "55", + }, + { + category: "F4-3-2", + name: "Saucen & Würze", + code: "5C", + }, + { + category: "F4-3-3", + name: "Saucen Süß", + code: "5C", + }, + { + category: "F4-3-4", + name: "Senf & Kren", + code: "5C", + }, + { + category: "F4-3-5", + name: "Ketchup & Mayonnaise", + code: "5C", + }, + { + category: "F4-3-6", + name: "Essig", + code: "53", + }, + { + category: "F4-3-7", + name: "Öl", + code: "53", + }, + { + category: "F4-3-8", + name: "Dressing & Croutons", + code: "5C", + }, + { + category: "F4-3-9", + name: "Zucker", + code: "5E", + }, + { + category: "F4-3-10", + name: "Salz", + code: "", + }, + { + category: "F4-4-1", + name: "Teigwaren", + code: "5B", + }, + { + category: "F4-4-2", + name: "Reis", + code: "5B", + }, + { + category: "F4-4-3", + name: "Kartoffelprodukte", + code: "5F", + }, + { + category: "F4-4-4", + name: "Samen & Hülsenfrüchte", + code: "57", + }, + { + category: "F4-4-5", + name: "Fix- & Basisprodukte", + code: "5F", + }, + { + category: "F4-4-7", + name: "Saucen & Würze", + code: "5C", + }, + { + category: "F4-4-8", + name: "Einkochen", + code: "52", + }, + { + category: "F4-4-10", + name: "Desserts", + code: "58", + }, + { + category: "F4-4-6", + name: "Suppen & Bouillons", + code: "54", + }, + { + category: "F4-5-1", + name: "Mehl", + code: "59", + }, + { + category: "F4-5-2", + name: "Grieß & Co", + code: "59", + }, + { + category: "F4-5-3", + name: "Getreideprodukte", + code: "59", + }, + { + category: "F4-5-4", + name: "Backmischungen", + code: "52", + }, + { + category: "F4-5-5", + name: "Backzutaten & Hilfsmittel", + code: "52", + }, + { + category: "F4-5-6", + name: "Verfeinerungen", + code: "52", + }, + { + category: "F4-6", + name: "Trockenfrüchte, Nüsse & Kerne", + code: "03", + }, + { + category: "F4-7", + name: "Reform & Nahrungsergänzung", + code: "5D", + }, + { + category: "F4-8", + name: "Asien & Mexiko", + code: "50", + }, + { + category: "F5-1", + name: "Süßwaren", + code: "64", + }, + { + category: "F5-2", + name: "Knabbergebäck", + code: "63", + }, + { + category: "F6-1", + name: "Aufbackware Brot & Gebäck", + code: "10", + }, + { + category: "F6-2", + name: "Brot", + code: "11", + }, + { + category: "F6-3", + name: "Gebäck", + code: "11", + }, + { + category: "F6-4", + name: "Feinbackwaren", + code: "12", + }, + { + category: "F6-5", + name: "Knäckebrot & Zwieback", + code: "12", + }, + { + category: "F6-6", + name: "Brösel & Semmelwürfel", + code: "14", + }, + { + category: "F7-1", + name: "Softdrinks & Säfte", + code: "20", + }, + { + category: "F7-2", + name: "Mineral- & Tafelwasser", + code: "26", + }, + { + category: "F7-3", + name: "Kaffee", + code: "22", + }, + { + category: "F7-4", + name: "Tee", + code: "22", + }, + { + category: "F7-5", + name: "Kakao", + code: "22", + }, + { + category: "F7-6", + name: "Bier", + code: "21", + }, + { + category: "F7-7-1", + name: "Rotwein", + code: "25", + }, + { + category: "F7-7-2", + name: "Weißwein", + code: "25", + }, + { + category: "F7-7-3", + name: "Rosewein", + code: "25", + }, + { + category: "F7-7-4", + name: "Dessertwein, Sherry & Port", + code: "25", + }, + { + category: "F7-7-5", + name: "Sekt & Champagner", + code: "23", + }, + { + category: "F7-7-6", + name: "Frizzante & Prosecco", + code: "23", + }, + { + category: "F7-7-7", + name: "Cider & Fruchtschaumwein", + code: "25", + }, + { + category: "F7-7-8", + name: "Weinhaltige Getränke", + code: "25", + }, + { + category: "F7-7-9", + name: "Alkoholfreier Wein & Schaumwein", + code: "25", + }, + { + category: "F7-8", + name: "Spirituosen", + code: "24", + }, + { + category: "F8-1-1", + name: "Schwein", + code: "42", + }, + { + category: "F8-1-2", + name: "Rind & Wild", + code: "42", + }, + { + category: "F8-1-3", + name: "Gans & Ente", + code: "42", + }, + { + category: "F8-1-4", + name: "Huhn & Pute", + code: "42", + }, + { + category: "F8-1-5", + name: "Fisch", + code: "43", + }, + { + category: "F8-1-6", + name: "Meeresfrüchte", + code: "43", + }, + { + category: "F8-2-1", + name: "Gemüse", + code: "44", + }, + { + category: "F8-2-2", + name: "Kräuter & Pilze", + code: "44", + }, + { + category: "F8-2-3", + name: "Obst", + code: "47", + }, + { + category: "F8-2-4", + name: "Pommes Frites", + code: "45", + }, + { + category: "F8-2-5", + name: "Kroketten & Co", + code: "45", + }, + { + category: "F8-3", + name: "Fertiggerichte & Teige", + code: "42", + }, + { + category: "F8-4-1", + name: "Baguette", + code: "46", + }, + { + category: "F8-4-2", + name: "Pizzasnacks", + code: "46", + }, + { + category: "F8-4-3", + name: "American Style Pizza", + code: "46", + }, + { + category: "F8-4-4", + name: "Italian Style Pizza", + code: "46", + }, + { + category: "F8-4-5", + name: "Tiefkühl Gebäck", + code: "46", + }, + { + category: "F8-5-1", + name: "Süße Knödel", + code: "47", + }, + { + category: "F8-5-2", + name: "Strudel", + code: "47", + }, + { + category: "F8-5-3", + name: "Nudeln", + code: "47", + }, + { + category: "F8-5-5", + name: "Sonstige klassische Mehlspeisen", + code: "47", + }, + { + category: "F8-5-6", + name: "Torten, Kuchen & Desserts", + code: "47", + }, + { + category: "F8-5-7", + name: "Eisbecher", + code: "40", + }, + { + category: "F8-5-8", + name: "Eis am Stiel & Stanitzel", + code: "40", + }, + { + category: "F8-5-10", + name: "Eis", + code: "40", + }, + { + category: "F9-1", + name: "Babynahrung & Getränke", + code: "51", + }, + { + category: "F9-1-1", + name: "Milchfertignahrung", + code: "51", + }, + { + category: "F9-1-2", + name: "Breie", + code: "51", + }, + { + category: "F9-1-3", + name: "Menüs", + code: "51", + }, + { + category: "F9-1-4", + name: "Gemüse & Gemüsemischungen", + code: "51", + }, + { + category: "F9-1-5", + name: "Früchte, Mischungen & Desserts", + code: "51", + }, + { + category: "F9-1-6", + name: "Snacks", + code: "51", + }, + { + category: "F9-1-7", + name: "Getränke", + code: "51", + }, + { + category: "F9-2", + name: "Flaschen & Sauger", + code: "70", + }, + { + category: "F9-2-2", + name: "Sauger", + code: "70", + }, + { + category: "F9-3", + name: "Pflege & Windeln", + code: "60", + }, + { + category: "F10-1-1", + name: "Hund", + code: "90", + }, + { + category: "F10-1-2", + name: "Katze", + code: "91", + }, + { + category: "F10-1-3", + name: "Nager", + code: "92", + }, + { + category: "F10-1-4", + name: "Vögel", + code: "93", + }, + { + category: "F10-2-1", + name: "Hund", + code: "90", + }, + { + category: "F10-2-2", + name: "Katze", + code: "91", + }, + { + category: "F10-3-1", + name: "Heimtier Heu", + code: "92", + }, + { + category: "F10-3-2", + name: "Heimtier Streu", + code: "91", + }, + { + category: "F10-3-3", + name: "Heimtier Sand", + code: "91", + }, + { + category: "F10-3-4", + name: "Heimtier sonstige Verbrauchsstoffe", + code: "90", + }, + { + category: "F11-1-1", + name: "Nagellackentferner & -härter", + code: "75", + }, + { + category: "F11-1-2", + name: "Düfte", + code: "72", + }, + { + category: "F11-2", + name: "Haare", + code: "73", + }, + { + category: "F11-3", + name: "Mund & Zahn", + code: "76", + }, + { + category: "F11-4-1", + name: "Baden & Duschen", + code: "78", + }, + { + category: "F11-4-2", + name: "Seifen", + code: "78", + }, + { + category: "F11-4-3", + name: "Händedesinfektion", + code: "74", + }, + { + category: "F11-4-4", + name: "Deodorants", + code: "72", + }, + { + category: "F11-4-5", + name: "Hautpflege", + code: "75", + }, + { + category: "F11-4-6", + name: "Fußpflege & Zubehör", + code: "7B", + }, + { + category: "F11-4-7", + name: "Rasur & Haarentfernung", + code: "77", + }, + { + category: "F11-4-8", + name: "Sonnen- & Insektenschutz", + code: "79", + }, + { + category: "F11-5-1", + name: "Desinfektionsmittel & Gesichtsmasken", + code: "74", + }, + { + category: "F11-5-2", + name: "Kondome & Gleitmittel", + code: "7A", + }, + { + category: "F11-5-3", + name: "Wundversorgung", + code: "74", + }, + { + category: "F11-5-4", + name: "Massage- & Einreibemittel", + code: "75", + }, + { + category: "F12-1", + name: "Haushaltspapier & Hygiene", + code: "", + }, + { + category: "F12-1-1", + name: "Taschentücher", + code: "", + }, + { + category: "F12-1-3", + name: "Watte", + code: "", + }, + { + category: "F12-1-4", + name: "Damenhygiene", + code: "", + }, + { + category: "F12-1-5", + name: "Küchenrollen", + code: "", + }, + { + category: "F12-1-6", + name: "Toilettenpapier", + code: "", + }, + { + category: "F12-1-7", + name: "Inkontinenz", + code: "", + }, + { + category: "F12-2", + name: "Putzen & Reinigen", + code: "", + }, + { + category: "F12-2-1", + name: "Geschirrreiniger", + code: "", + }, + { + category: "F12-2-2", + name: "Allzweckreiniger", + code: "", + }, + { + category: "F12-2-3", + name: "WC Reiniger", + code: "", + }, + { + category: "F12-2-4", + name: "Bodenpflege", + code: "", + }, + { + category: "F12-2-5", + name: "Glasreiniger", + code: "", + }, + { + category: "F12-2-6", + name: "Küchenreiniger", + code: "", + }, + { + category: "F12-2-7", + name: "Metallpflege & Entkalker", + code: "", + }, + { + category: "F12-2-8", + name: "Badreiniger", + code: "", + }, + { + category: "F12-2-9", + name: "Hygienereiniger & Desinfektion", + code: "", + }, + { + category: "F12-2-10", + name: "Abflussreiniger", + code: "", + }, + { + category: "F12-2-11", + name: "Möbelpflege", + code: "", + }, + { + category: "F12-2-12", + name: "Schuhpflege", + code: "", + }, + { + category: "F12-2-13", + name: "Putzutensilien", + code: "", + }, + { + category: "F12-2-14", + name: "Lufterfrischer", + code: "", + }, + { + category: "F12-3", + name: "Elektrische Putzgeräte", + code: "", + }, + { + category: "F12-3-1", + name: "Staubsauger & Reinigungsgeräte", + code: "", + }, + { + category: "F12-3-2", + name: "Staubbeutel & Zubehör", + code: "", + }, + { + category: "F12-4", + name: "Waschen, Trocknen & Bügeln", + code: "", + }, + { + category: "F12-4-1", + name: "Waschmittel", + code: "", + }, + { + category: "F12-4-2", + name: "Weichspüler", + code: "", + }, + { + category: "F12-4-3", + name: "Fleckenentferner & Textilfarben", + code: "", + }, + { + category: "F12-4-4", + name: "Wäschestärke, -desinfektion & Imprägnieren", + code: "", + }, + { + category: "F12-4-5", + name: "Wasserenthärter", + code: "", + }, + { + category: "F12-4-6", + name: "Textilerfrischer", + code: "", + }, + { + category: "F12-4-7", + name: "Waschzubehör", + code: "", + }, + { + category: "F12-4-8", + name: "Bügeln", + code: "", + }, + { + category: "F12-5", + name: "Haushaltszubehör", + code: "", + }, + { + category: "F12-5-1", + name: "Kleiderbügel & Türhaken", + code: "", + }, + { + category: "F12-5-2", + name: "Putzutensilien", + code: "", + }, + { + category: "F12-5-3", + name: "Aufbewahrung & Abfalleimer", + code: "", + }, + { + category: "F12-5-4", + name: "Akkus, Batterien & Ladegeräte", + code: "", + }, + { + category: "F12-5-5", + name: "Beleuchtung & Taschenlampen", + code: "", + }, + { + category: "F12-5-6", + name: "Technik & Elektronik", + code: "", + }, + { + category: "F12-5-7", + name: "Nähen & Kurzware", + code: "", + }, + { + category: "F12-5-8", + name: "Grillen & Zubehör", + code: "", + }, + { + category: "F12-5-9", + name: "Pflanzenpflege & Insektenschutz", + code: "", + }, + { + category: "F12-5-10", + name: "Kerzen, Raumdüfte & Anzündhilfe", + code: "", + }, + { + category: "F12-5-11", + name: "Papier, Schule & Büro", + code: "", + }, + { + category: "F12-5-12", + name: "Party- & Festtagsartikel", + code: "", + }, + { + category: "F12-5-13", + name: "Autopflege", + code: "", + }, + { + category: "F12-6", + name: "Spielware", + code: "", + }, + { + category: "F12-7", + name: "Regenschirme", + code: "", + }, + { + category: "F13", + name: "KÜCHE & TISCH", + code: "", + }, + { + category: "F13-1", + name: "Kochgeschirr", + code: "", + }, + { + category: "F13-1-1", + name: "Töpfe & Deckel", + code: "", + }, + { + category: "F13-1-2", + name: "Pfannen & Deckel", + code: "", + }, + { + category: "F13-1-3", + name: "Sets & Garnituren", + code: "", + }, + { + category: "F13-1-4", + name: "Fondue-Sets, Plattengrill, Raclette & Co", + code: "", + }, + { + category: "F13-1-5", + name: "Bräter & Auflaufformen", + code: "", + }, + { + category: "F13-2", + name: "Gedeckter Tisch", + code: "", + }, + { + category: "F13-2-1", + name: "Essbesteck", + code: "", + }, + { + category: "F13-2-2", + name: "Gläser & Glaswaren", + code: "", + }, + { + category: "F13-2-3", + name: "Porzellan", + code: "", + }, + { + category: "F13-2-4", + name: "Servietten", + code: "", + }, + { + category: "F13-2-5", + name: "Tischdecken & Läufer", + code: "", + }, + { + category: "F13-3", + name: "Backen", + code: "", + }, + { + category: "F13-3-1", + name: "Backblech & Formen", + code: "", + }, + { + category: "F13-3-2", + name: "Backzubehör", + code: "", + }, + { + category: "F13-4", + name: "Küchenhelfer", + code: "", + }, + { + category: "F13-4-1", + name: "Rühren", + code: "", + }, + { + category: "F13-4-3", + name: "Küchenwaagen", + code: "", + }, + { + category: "F13-4-4", + name: "Messbecher", + code: "", + }, + { + category: "F13-4-5", + name: "Schälen & Zerteilen", + code: "", + }, + { + category: "F13-4-6", + name: "Siebe & Trichter", + code: "", + }, + { + category: "F13-4-7", + name: "Weinzubehör", + code: "", + }, + { + category: "F13-4-8", + name: "Reiben", + code: "", + }, + { + category: "F13-4-9", + name: "Bratenwender & Schaumlöffel", + code: "", + }, + { + category: "F13-4-10", + name: "Suppenkelle & Löffel", + code: "", + }, + { + category: "F13-4-11", + name: "Sonstige Küchenhelfer", + code: "", + }, + { + category: "F13-4-12", + name: "Schneidebretter, Untersetzer & Tablett", + code: "", + }, + { + category: "F13-4-13", + name: "Messer", + code: "", + }, + { + category: "F13-4-14", + name: "Salz- & Pfeffermühlen", + code: "", + }, + { + category: "F13-4-15", + name: "Kunststoffgeschirr", + code: "", + }, + { + category: "F13-4-16", + name: "Einkochen", + code: "", + }, + { + category: "F13-5", + name: "Folien, Säcke & Filter", + code: "", + }, + { + category: "F13-6", + name: "Aufbewahrung", + code: "", + }, + { + category: "F13-7", + name: "Sodaprodukte & Sahneaufbereitung", + code: "", + }, + { + category: "F13-7-1", + name: "Sodamaker & Sodaprodukte", + code: "", + }, + { + category: "F13-7-2", + name: "Sahneaufbereitung", + code: "", + }, + { + category: "F13-8", + name: "Küchenwäsche", + code: "", + }, + { + category: "F13-9", + name: "Trink- & Isolierflaschen", + code: "", + }, + { + category: "F13-10", + name: "Wasseraufbereitung", + code: "", + }, + { + category: "F13-11", + name: "Einweggeschirr & Strohhalme", + code: "", + }, + { + category: "F13-12", + name: "Party- & Festtagsartikel", + code: "", + }, + { + category: "F13-13", + name: "Elektrische Küchengeräte", + code: "", + }, + { + category: "F13-13-1", + name: "Kaffeemaschinen", + code: "", + }, + { + category: "F13-13-2", + name: "Fondue-Sets, Plattengrill, Raclette & Co", + code: "", + }, + { + category: "F13-13-3", + name: "Mikrowelle & Kleinküchen", + code: "", + }, + { + category: "F13-13-4", + name: "Mixer & Küchenmaschinen", + code: "", + }, + { + category: "F13-13-6", + name: "Wasserkocher", + code: "", + }, + { + category: "F13-13-8", + name: "Entsafter & Zitruspressen", + code: "", + }, + { + category: "F13-13-9", + name: "Elektrische Schneidegeräte", + code: "", + }, + { + category: "F13-13-10", + name: "Toaster", + code: "", + }, +];