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.