/** * Product Encoding/Decoding Helper * * Provides functions to encode and decode product configurations into compact * hex-based URL strings. Uses bit-packing for efficient, stable encoding. * * Format: v-T-C-M-HHHH-HHHH-HHHH * See /planning/ENCODING_SYSTEM.md for detailed documentation */ // ============================================================================ // LOOKUP TABLES // ============================================================================ const ENCODING_VERSION = 0; // Main Product Attributes (single hex character, 0-15) const PRODUCT_TYPE = { 'PATIO_DOOR': 1, 'STORM_DOOR': 3, 'STORM_WINDOW': 5, 'PRIMARY_WINDOW': 7 }; const PRODUCT_TYPE_REVERSE = { 1: 'PATIO_DOOR', 3: 'STORM_DOOR', 5: 'STORM_WINDOW', 7: 'PRIMARY_WINDOW' }; const PRODUCT_COLOR = { 'BLACK': 1, 'BRONZE': 2, 'SANDSTONE': 3, 'WHITE': 4, 'TAN': 5, 'MILL': 6 }; const PRODUCT_COLOR_REVERSE = { 1: 'BLACK', 2: 'BRONZE', 3: 'SANDSTONE', 4: 'WHITE', 5: 'TAN', 6: 'MILL' }; const PRODUCT_MATERIAL = { 'ALUMINUM': 1, 'VINYL': 2 }; const PRODUCT_MATERIAL_REVERSE = { 1: 'ALUMINUM', 2: 'VINYL' }; // Hardware Fields (5 bits each, 0-31) const HARDWARE_TYPE = { 'NONE': 0, 'LEVER': 1, 'PULL': 2, 'PULL_HANDLE': 3, 'DEADBOLT': 4, 'HINGE': 5 }; const HARDWARE_TYPE_REVERSE = { 0: 'NONE', 1: 'LEVER', 2: 'PULL', 3: 'PULL_HANDLE', 4: 'DEADBOLT', 5: 'HINGE' }; const HARDWARE_STYLE = { 'NONE': 0, 'STANDARD': 1, 'PUSH': 2, 'ALTERNATIVE': 3, 'CONTEMPORARY': 4, 'TRADITIONAL': 5 }; const HARDWARE_STYLE_REVERSE = { 0: 'NONE', 1: 'STANDARD', 2: 'PUSH', 3: 'ALTERNATIVE', 4: 'CONTEMPORARY', 5: 'TRADITIONAL' }; const HARDWARE_COLOR = { 'NONE': 0, 'BRASS': 1, 'WHITE': 2, 'BLACK': 3, 'SATIN': 4, 'NICKEL': 5, 'BRONZE': 6 }; const HARDWARE_COLOR_REVERSE = { 0: 'NONE', 1: 'BRASS', 2: 'WHITE', 3: 'BLACK', 4: 'SATIN', 5: 'NICKEL', 6: 'BRONZE' }; // ============================================================================ // ENCODING FUNCTIONS // ============================================================================ /** * Encode a hardware configuration into a 4-character hex string * Uses 5 bits per field (32 values each) * * @param {number} type - Hardware type (0-31) * @param {number} style - Hardware style (0-31) * @param {number} color - Hardware color (0-31) * @returns {string} 4-character hex string (e.g., "0421") */ function encodeHardware(type, style, color) { // Validate inputs if (type < 0 || type > 31) throw new Error(`Invalid hardware type: ${type} (must be 0-31)`); if (style < 0 || style > 31) throw new Error(`Invalid hardware style: ${style} (must be 0-31)`); if (color < 0 || color > 31) throw new Error(`Invalid hardware color: ${color} (must be 0-31)`); // Pack into 16 bits: [reserved(1)][type(5)][style(5)][color(5)] const value = ((type & 0x1F) << 10) | // Bits 10-14 ((style & 0x1F) << 5) | // Bits 5-9 (color & 0x1F); // Bits 0-4 return value.toString(16).toUpperCase().padStart(4, '0'); } /** * Encode a complete product configuration into a URL string * * @param {Object} config - Product configuration * @param {number} config.type - Product type (0-15) * @param {number} config.color - Product color (0-15) * @param {number} config.material - Product material (0-15) * @param {Array} config.hardware - Array of hardware items (optional) * @param {number} config.hardware[].type - Hardware type (0-31) * @param {number} config.hardware[].style - Hardware style (0-31) * @param {number} config.hardware[].color - Hardware color (0-31) * @returns {string} Encoded URL string (e.g., "0-1-4-1-0421") */ function encodeProduct(config) { // Validate main attributes if (config.type < 0 || config.type > 15) { throw new Error(`Invalid product type: ${config.type} (must be 0-15)`); } if (config.color < 0 || config.color > 15) { throw new Error(`Invalid product color: ${config.color} (must be 0-15)`); } if (config.material < 0 || config.material > 15) { throw new Error(`Invalid product material: ${config.material} (must be 0-15)`); } // Build base string: version-type-color-material const parts = [ ENCODING_VERSION.toString(16).toUpperCase(), config.type.toString(16).toUpperCase(), config.color.toString(16).toUpperCase(), config.material.toString(16).toUpperCase() ]; // Add hardware items if present if (config.hardware && config.hardware.length > 0) { config.hardware.forEach(hw => { parts.push(encodeHardware(hw.type, hw.style, hw.color)); }); } return parts.join('-'); } /** * Encode using named constants (convenience function) * * @param {Object} config - Product configuration with named values * @param {string} config.type - Product type name (e.g., 'PATIO_DOOR') * @param {string} config.color - Color name (e.g., 'WHITE') * @param {string} config.material - Material name (e.g., 'ALUMINUM') * @param {Array} config.hardware - Array of hardware items (optional) * @returns {string} Encoded URL string */ function encodeProductByName(config) { const numericConfig = { type: PRODUCT_TYPE[config.type], color: PRODUCT_COLOR[config.color], material: PRODUCT_MATERIAL[config.material], hardware: [] }; if (config.hardware && config.hardware.length > 0) { numericConfig.hardware = config.hardware.map(hw => ({ type: HARDWARE_TYPE[hw.type], style: HARDWARE_STYLE[hw.style], color: HARDWARE_COLOR[hw.color] })); } return encodeProduct(numericConfig); } // ============================================================================ // DECODING FUNCTIONS // ============================================================================ /** * Decode a hardware hex string back to its components * * @param {string} hex - 4-character hex string (e.g., "0421") * @returns {Object} Decoded hardware configuration */ function decodeHardware(hex) { if (typeof hex !== 'string' || hex.length !== 4) { throw new Error(`Invalid hardware hex string: ${hex} (must be 4 characters)`); } const value = parseInt(hex, 16); if (isNaN(value)) { throw new Error(`Invalid hex value: ${hex}`); } return { type: (value >> 10) & 0x1F, // Extract bits 10-14 style: (value >> 5) & 0x1F, // Extract bits 5-9 color: value & 0x1F // Extract bits 0-4 }; } /** * Decode a complete product URL string * * @param {string} encodedString - Encoded URL string (e.g., "0-1-4-1-0421") * @returns {Object} Decoded product configuration */ function decodeProduct(encodedString) { if (typeof encodedString !== 'string') { throw new Error('Invalid encoded string: must be a string'); } const parts = encodedString.split('-'); if (parts.length < 4) { throw new Error(`Invalid encoded string: ${encodedString} (must have at least 4 parts)`); } // Parse version const version = parseInt(parts[0], 16); if (version !== ENCODING_VERSION) { throw new Error(`Unsupported encoding version: ${version} (current: ${ENCODING_VERSION})`); } // Parse main attributes const config = { version: version, type: parseInt(parts[1], 16), color: parseInt(parts[2], 16), material: parseInt(parts[3], 16), hardware: [] }; // Parse hardware items (if present) for (let i = 4; i < parts.length; i++) { config.hardware.push(decodeHardware(parts[i])); } return config; } /** * Decode and return named values (convenience function) * * @param {string} encodedString - Encoded URL string * @returns {Object} Decoded product with named values */ function decodeProductToNames(encodedString) { const numeric = decodeProduct(encodedString); return { version: numeric.version, type: PRODUCT_TYPE_REVERSE[numeric.type] || `UNKNOWN_${numeric.type}`, color: PRODUCT_COLOR_REVERSE[numeric.color] || `UNKNOWN_${numeric.color}`, material: PRODUCT_MATERIAL_REVERSE[numeric.material] || `UNKNOWN_${numeric.material}`, hardware: numeric.hardware.map(hw => ({ type: HARDWARE_TYPE_REVERSE[hw.type] || `UNKNOWN_${hw.type}`, style: HARDWARE_STYLE_REVERSE[hw.style] || `UNKNOWN_${hw.style}`, color: HARDWARE_COLOR_REVERSE[hw.color] || `UNKNOWN_${hw.color}` })) }; } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ /** * Get the binary representation of a hex string (for debugging) * * @param {string} hex - Hex string * @returns {string} Binary string with spaces every 4 bits */ function hexToBinary(hex) { const value = parseInt(hex, 16); const binary = value.toString(2).padStart(16, '0'); return binary.match(/.{1,4}/g).join(' '); } /** * Validate that a configuration uses only defined values * * @param {Object} config - Product configuration * @returns {Object} Validation result with errors array */ function validateConfiguration(config) { const errors = []; if (!PRODUCT_TYPE_REVERSE[config.type]) { errors.push(`Invalid product type: ${config.type}`); } if (!PRODUCT_COLOR_REVERSE[config.color]) { errors.push(`Invalid product color: ${config.color}`); } if (!PRODUCT_MATERIAL_REVERSE[config.material]) { errors.push(`Invalid product material: ${config.material}`); } if (config.hardware) { config.hardware.forEach((hw, index) => { if (!HARDWARE_TYPE_REVERSE[hw.type]) { errors.push(`Invalid hardware ${index + 1} type: ${hw.type}`); } if (!HARDWARE_STYLE_REVERSE[hw.style]) { errors.push(`Invalid hardware ${index + 1} style: ${hw.style}`); } if (!HARDWARE_COLOR_REVERSE[hw.color]) { errors.push(`Invalid hardware ${index + 1} color: ${hw.color}`); } }); } return { valid: errors.length === 0, errors: errors }; } // ============================================================================ // EXPORTS (for Node.js / Module usage) // ============================================================================ if (typeof module !== 'undefined' && module.exports) { module.exports = { // Constants ENCODING_VERSION, PRODUCT_TYPE, PRODUCT_COLOR, PRODUCT_MATERIAL, HARDWARE_TYPE, HARDWARE_STYLE, HARDWARE_COLOR, // Encoding functions encodeHardware, encodeProduct, encodeProductByName, // Decoding functions decodeHardware, decodeProduct, decodeProductToNames, // Utilities hexToBinary, validateConfiguration }; } // ============================================================================ // EXAMPLE USAGE // ============================================================================ /* // Example 1: Encode by numeric values const encoded1 = encodeProduct({ type: 1, // Patio Door color: 4, // White material: 1, // Aluminum hardware: [ { type: 1, style: 1, color: 1 } // Lever, Standard, Brass ] }); console.log(encoded1); // "0-1-4-1-0421" // Example 2: Encode by named values const encoded2 = encodeProductByName({ type: 'STORM_DOOR', color: 'WHITE', material: 'ALUMINUM', hardware: [ { type: 'LEVER', style: 'STANDARD', color: 'BRASS' }, { type: 'PULL_HANDLE', style: 'ALTERNATIVE', color: 'SATIN' } ] }); console.log(encoded2); // "0-3-4-1-0421-0C64" // Example 3: Decode back to numeric values const decoded1 = decodeProduct("0-1-4-1-0421"); console.log(decoded1); // { version: 0, type: 1, color: 4, material: 1, hardware: [{ type: 1, style: 1, color: 1 }] } // Example 4: Decode to named values const decoded2 = decodeProductToNames("0-3-4-1-0421-0C64"); console.log(decoded2); // { // version: 0, // type: 'STORM_DOOR', // color: 'WHITE', // material: 'ALUMINUM', // hardware: [ // { type: 'LEVER', style: 'STANDARD', color: 'BRASS' }, // { type: 'PULL_HANDLE', style: 'ALTERNATIVE', color: 'SATIN' } // ] // } // Example 5: Debug hardware encoding const hwHex = encodeHardware(3, 12, 20); console.log(hwHex); // "0D94" console.log(hexToBinary(hwHex)); // "0000 1101 1001 0100" console.log(decodeHardware(hwHex)); // { type: 3, style: 12, color: 20 } // Example 6: Validate configuration const validation = validateConfiguration({ type: 1, color: 4, material: 1, hardware: [{ type: 1, style: 1, color: 1 }] }); console.log(validation); // { valid: true, errors: [] } */