heissepreise/site/model/items-loader.js
2023-06-18 20:29:03 +02:00

250 lines
8.8 KiB
JavaScript

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;
const strings = new Map();
const internString = (string) => {
if (strings.has(string)) {
return strings.get(string);
} else {
strings.set(string, string);
return string;
}
};
for (let l = 0; l < numItems; l++) {
const store = storeLookup[data[i++]];
const id = internString(data[i++]);
const name = internString(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: internString(date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6, 8)),
price,
};
}
const unit = internString(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();
for (const item of items) {
lookup[item.store + item.id] = item;
item.search = item.name + " " + item.quantity + " " + item.unit;
item.search = item.search.toLowerCase().replace(",", ".");
const unitPriceFactor = item.unit == "g" || item.unit == "ml" ? 1000 : 1;
item.unitPrice = (item.price / item.quantity) * unitPriceFactor;
item.numPrices = item.priceHistory.length;
item.priceOldest = item.priceHistory[item.priceHistory.length - 1].price;
item.dateOldest = item.priceHistory[item.priceHistory.length - 1].date;
item.date = item.priceHistory[0].date;
let highestPriceBefore = -1;
let lowestPriceBefore = 100000;
for (let i = 0; i < item.priceHistory.length; i++) {
const price = item.priceHistory[i];
price.unitPrice = (price.price / item.quantity) * unitPriceFactor;
if (i == 0) continue;
if (i < 10) {
item["price" + i] = price.price;
item["unitPrice" + i] = price.unitPrice;
item["date" + i] = price.date;
}
highestPriceBefore = Math.max(highestPriceBefore, price.price);
lowestPriceBefore = Math.min(lowestPriceBefore, price.price);
}
if (highestPriceBefore == -1) highestPriceBefore = item.price;
if (lowestPriceBefore == 100000) lowestPriceBefore = item.price;
item.highestBefore = highestPriceBefore;
item.lowestBefore = lowestPriceBefore;
}
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 = 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);
};