I'm trying to build a format that can represent the user's cart on my website in the most compact way. The website is related to computer parts and there are 23 product categories. Each product has an unsigned integer as the product id. The product ids can range from 0 to a couple of million maybe.
Here is my attempt at it (Typescript):
const categories = [ 'motherboard', 'cpu', 'cpu-cooler', 'ram', 'internal-ssd-hdd', 'graphics-card', 'power-supply', 'case', 'case-fan', 'fan-controller', 'thermal-paste', 'optical-drive', 'sound-card', 'wired-network-card', 'wifi-card', 'monitor', 'external-ssd-hdd', 'headphones', 'keyboard', 'mouse', 'speakers', 'ups', 'laptop', ]; // Serialize build data const buildData = [ { category: 'ups', product_id: 231973 }, { category: 'keyboard', product_id: 92153 }, { category: 'monitor', product_id: 98231 }, { category: 'mouse', product_id: 92752 }, { category: 'wired-network-card', product_id: 36789 }, { category: 'speakers', product_id: 59871 }, { category: 'laptop', product_id: 84963 }, ]; function encodeBuildData(categories: string[], build: { category: string, product_id: number }[]) { const bitsPerProduct = 32; // 8 bits for category index + 24 bits for product_id const bufferSize = Math.ceil((build.length * bitsPerProduct) / 8) + 1; const buffer = Buffer.alloc(bufferSize); // write version number buffer.writeUInt8(1, 0); let bitOffset = 8; build.forEach(product => { const categoryIndex = categories.indexOf(product.category); const productID = product.product_id; // Write the 8 bits of the category index buffer.writeUInt8(categoryIndex, Math.floor(bitOffset / 8)); bitOffset += 8; // Write the first 16 bits of the productID buffer.writeUInt16BE(productID & 0xFFFF, Math.floor(bitOffset / 8)); bitOffset += 16; // Write the remaining 8 bits of the productID buffer.writeUInt8((productID >> 16) & 0xFF, Math.floor(bitOffset / 8)); bitOffset += 8; }); return buffer; } function decodeBuildData(categories: string[], buffer: Buffer): { Version: number, Build: { [key: string]: number[] } } { const bitsPerProduct = 32; // 8 bits for category index + 24 bits for product_id const numProducts = Math.floor((buffer.length * 8) / bitsPerProduct); const build: ReturnType<typeof decodeBuildData>['Build'] = {}; let bitOffset = 8; const version = buffer.readUInt8(0); for (let i = 0; i < numProducts; i++) { // Read the 8 bits of categoryIndex from the buffer const categoryIndex = buffer.readUInt8(Math.floor(bitOffset / 8)); bitOffset += 8; // Read the first 16 bits of productID const productIDLower16Bits = buffer.readUInt16BE(Math.floor(bitOffset / 8)); bitOffset += 16; // Read the remaining 8 bits of productID const productIDUpper8Bits = buffer.readUInt8(Math.floor(bitOffset / 8)); bitOffset += 8; // Combine the 16 lower bits and 8 upper bits of productID const productID = (productIDUpper8Bits << 16) | productIDLower16Bits; const category = categories[categoryIndex]; build[category] = [...(build[category] || []), productID]; } return { Version: version, Build: build }; } const encoded = encodeBuildData(categories, buildData); console.log(decodeBuildData(categories, Buffer.from(encoded.toString('base64url'), 'base64url')), encoded.toString('base64url')); This approach has the benefit of allowing more than one product for each category, which I would prefer to retain. I tried to use 5 bits for category id but managing bit offset between different bytes was too much for me.
Can you improve upon this or maybe come up with a different format? Remember, the goal is to have the cart config represented in the shortest URL-compatible string.