Initial
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* 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<Object>} 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<Object>} 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: [] }
|
||||
*/
|
||||
+1444
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user