import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; const EXCEL_SOURCE_FILES = [ path.join(process.cwd(), 'data/excel/high-voltage.xlsx'), path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'), path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'), path.join(process.cwd(), 'data/excel/solar-cables.xlsx'), ]; export interface ProductData { id?: number; name: string; slug?: string; sku: string; translationKey?: string; locale?: 'en' | 'de'; } export type ExcelRow = Record; export type ExcelMatch = { rows: ExcelRow[]; units: Record }; let EXCEL_INDEX: Map | null = null; export type KeyValueItem = { label: string; value: string; unit?: string }; export type VoltageTableModel = { voltageLabel: string; metaItems: KeyValueItem[]; crossSections: string[]; columns: Array<{ key: string; label: string; get: (rowIndex: number) => string }>; }; function stripHtml(html: string): string { if (!html) return ''; return html.replace(/<[^>]*>/g, '').trim(); } function normalizeValue(value: string): string { return stripHtml(value).replace(/\s+/g, ' ').trim(); } function formatNumber(n: number): string { const s = Number.isInteger(n) ? String(n) : String(n); return s.replace(/\.0+$/, ''); } function parseNumericOption(value: string): number | null { const v = normalizeValue(value).replace(/,/g, '.'); const m = v.match(/-?\d+(?:\.\d+)?/); if (!m) return null; const n = Number(m[0]); return Number.isFinite(n) ? n : null; } function summarizeNumericRange(options: string[] | undefined): { ok: boolean; text: string } { const vals = (options || []).map(parseNumericOption).filter((n): n is number => n !== null); if (vals.length < 3) return { ok: false, text: '' }; const uniq = Array.from(new Set(vals)); if (uniq.length < 4) return { ok: false, text: '' }; uniq.sort((a, b) => a - b); const min = uniq[0]; const max = uniq[uniq.length - 1]; return { ok: true, text: `${formatNumber(min)}–${formatNumber(max)}` }; } function summarizeOptions(options: string[] | undefined, maxItems: number = 3): string { const vals = (options || []).map(normalizeValue).filter(Boolean); if (vals.length === 0) return ''; const uniq = Array.from(new Set(vals)); if (uniq.length === 1) return uniq[0]; if (uniq.length <= maxItems) return uniq.join(' / '); return `${uniq.slice(0, maxItems).join(' / ')} / ...`; } function summarizeSmartOptions(label: string, options: string[] | undefined): string { const range = summarizeNumericRange(options); if (range.ok) return range.text; return summarizeOptions(options, 3); } function looksNumeric(value: string): boolean { const v = normalizeValue(value).replace(/,/g, '.'); return /^-?\d+(?:\.\d+)?$/.test(v); } function normalizeUnit(unitRaw: string): string { const u = normalizeValue(unitRaw); if (!u) return ''; if (/^c$/i.test(u) || /^°c$/i.test(u)) return '°C'; return u.replace(/Ω/gi, 'Ohm').replace(/[\u00B5\u03BC]/g, 'u'); } function denseAbbrevLabel(args: { key: string; locale: 'en' | 'de'; unit?: string; withUnit?: boolean; }): string { const u = normalizeUnit(args.unit || ''); const withUnit = args.withUnit ?? true; const unitSafe = u.replace(/Ω/gi, 'Ohm').replace(/[\u00B5\u03BC]/g, 'u'); const suffix = withUnit && unitSafe ? ` [${unitSafe}]` : ''; switch (args.key) { case 'DI': case 'RI': case 'Wi': case 'Ibl': case 'Ibe': case 'Wm': case 'Rbv': case 'Fzv': case 'G': return `${args.key}${suffix}`; case 'Ik_cond': return `Ik${suffix}`; case 'Ik_screen': return `Ik_s${suffix}`; case 'Ø': return `Ø${suffix}`; case 'Cond': return args.locale === 'de' ? 'Leiter' : 'Cond.'; case 'shape': return args.locale === 'de' ? 'Form' : 'Shape'; case 'cap': return `C${suffix}`; case 'X': return `X${suffix}`; case 'temp_range': return `T${suffix}`; case 'max_op_temp': return `T_op${suffix}`; case 'max_sc_temp': return `T_sc${suffix}`; case 'min_store_temp': return `T_st${suffix}`; case 'min_lay_temp': return `T_lay${suffix}`; case 'cpr': return `CPR${suffix}`; case 'flame': return `FR${suffix}`; case 'test_volt': return `U_test${suffix}`; case 'rated_volt': return `U0/U${suffix}`; default: return args.key || ''; } } function formatExcelHeaderLabel(key: string, unit?: string): string { const k = normalizeValue(key); if (!k) return ''; const u = normalizeValue(unit || ''); const compact = k .replace(/\s*\(approx\.?\)\s*/gi, ' (approx.) ') .replace(/\s+/g, ' ') .trim(); if (!u) return compact; if (new RegExp(`\\(${u.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\)`, 'i').test(compact)) return compact; return `${compact} (${u})`; } function metaFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string { const key = normalizeValue(args.key); if (args.locale === 'de') { switch (key) { case 'test_volt': return 'Prüfspannung'; case 'temp_range': return 'Temperaturbereich'; case 'max_op_temp': return 'Leitertemperatur (max.)'; case 'max_sc_temp': return 'Kurzschlusstemperatur (max.)'; case 'min_lay_temp': return 'Minimale Verlegetemperatur'; case 'min_store_temp': return 'Minimale Lagertemperatur'; case 'cpr': return 'CPR-Klasse'; case 'flame': return 'Flammhemmend'; default: return formatExcelHeaderLabel(args.excelKey); } } switch (key) { case 'test_volt': return 'Test voltage'; case 'temp_range': return 'Operating temperature range'; case 'max_op_temp': return 'Conductor temperature (max.)'; case 'max_sc_temp': return 'Short-circuit temperature (max.)'; case 'min_lay_temp': return 'Minimum laying temperature'; case 'min_store_temp': return 'Minimum storage temperature'; case 'cpr': return 'CPR class'; case 'flame': return 'Flame retardant'; default: return formatExcelHeaderLabel(args.excelKey); } } function technicalFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string { const k = normalizeValue(args.key); if (args.locale === 'de') { switch (k) { case 'DI': return 'Durchmesser über Isolierung'; case 'RI': return 'DC-Leiterwiderstand (20 °C)'; case 'Wi': return 'Isolationsdicke'; case 'Ibl': return 'Strombelastbarkeit in Luft (trefoil)'; case 'Ibe': return 'Strombelastbarkeit im Erdreich (trefoil)'; case 'Ik_cond': return 'Kurzschlussstrom Leiter'; case 'Ik_screen': return 'Kurzschlussstrom Schirm'; case 'Wm': return 'Manteldicke'; case 'Rbv': return 'Biegeradius (min.)'; case 'Ø': return 'Außen-Ø'; case 'Fzv': return 'Zugkraft (max.)'; case 'G': return 'Gewicht'; case 'Cond': case 'conductor': return 'Leiter'; case 'shape': return 'Leiterform'; case 'insulation': return 'Isolierung'; case 'sheath': return 'Mantel'; case 'cap': return 'Kapazität'; case 'ind_trefoil': return 'Induktivität (trefoil)'; case 'ind_air_flat': return 'Induktivität (Luft, flach)'; case 'ind_ground_flat': return 'Induktivität (Erdreich, flach)'; case 'X': return 'Reaktanz'; case 'test_volt': return 'Prüfspannung'; case 'rated_volt': return 'Nennspannung'; case 'temp_range': return 'Temperaturbereich'; case 'max_op_temp': return 'Leitertemperatur (max.)'; case 'max_sc_temp': return 'Kurzschlusstemperatur (max.)'; case 'min_store_temp': return 'Minimale Lagertemperatur'; case 'min_lay_temp': return 'Minimale Verlegetemperatur'; case 'cpr': return 'CPR-Klasse'; case 'flame': return 'Flammhemmend'; case 'packaging': return 'Verpackung'; case 'ce': return 'CE-Konformität'; case 'norm': return 'Norm'; case 'standard': return 'Standard'; case 'D_screen': return 'Durchmesser über Schirm'; case 'S_screen': return 'Metallischer Schirm'; default: break; } const raw = normalizeValue(args.excelKey); if (!raw) return ''; return raw .replace(/\(approx\.?\)/gi, '(ca.)') .replace(/\bcapacitance\b/gi, 'Kapazität') .replace(/\binductance\b/gi, 'Induktivität') .replace(/\breactance\b/gi, 'Reaktanz') .replace(/\btest voltage\b/gi, 'Prüfspannung') .replace(/\brated voltage\b/gi, 'Nennspannung') .replace(/\boperating temperature range\b/gi, 'Temperaturbereich') .replace(/\bminimum sheath thickness\b/gi, 'Manteldicke (min.)') .replace(/\bsheath thickness\b/gi, 'Manteldicke') .replace(/\bnominal insulation thickness\b/gi, 'Isolationsdicke (nom.)') .replace(/\binsulation thickness\b/gi, 'Isolationsdicke') .replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC-Leiterwiderstand (20 °C)') .replace(/\bouter diameter(?: of cable)?\b/gi, 'Außen-Ø') .replace(/\bbending radius\b/gi, 'Biegeradius') .replace(/\bpackaging\b/gi, 'Verpackung') .replace(/\bce\s*-?conformity\b/gi, 'CE-Konformität'); } return normalizeValue(args.excelKey); } function compactNumericForLocale(value: string, locale: 'en' | 'de'): string { const v = normalizeValue(value); if (!v) return ''; if (/\d+xD/.test(v)) { const numbers = []; const matches = Array.from(v.matchAll(/(\d+)xD/g)); for (let i = 0; i < matches.length; i++) numbers.push(matches[i][1]); if (numbers.length > 0) { const unique: string[] = []; for (const num of numbers) { if (!unique.includes(num)) { unique.push(num); } } return unique.join('/') + 'xD'; } } const hasDigit = /\d/.test(v); if (!hasDigit) return v; const trimmed = v.replace(/\s+/g, ' ').trim(); const parts = trimmed.split(/(–|-)/); const out = parts.map((p) => { if (p === '–' || p === '-') return p; const s = p.trim(); if (!/^-?\d+(?:[\.,]\d+)?$/.test(s)) return p; const n = s.replace(/,/g, '.'); const compact = n .replace(/\.0+$/, '') .replace(/(\.\d*?)0+$/, '$1') .replace(/\.$/, ''); const hadPlus = /^\+/.test(s); const withPlus = hadPlus && !/^\+/.test(compact) ? `+${compact}` : compact; return locale === 'de' ? withPlus.replace(/\./g, ',') : withPlus; }); return out.join(''); } function compactCellForDenseTable( value: string, unit: string | undefined, locale: 'en' | 'de', ): string { let v = normalizeValue(value); if (!v) return ''; const u = normalizeValue(unit || ''); if (u) { const esc = u.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); v = v.replace(new RegExp(`\\s*${esc}\\b`, 'ig'), '').trim(); v = v .replace(/\bkg\s*\/\s*km\b/gi, '') .replace(/\bohm\s*\/\s*km\b/gi, '') .replace(/\bΩ\s*\/\s*km\b/gi, '') .replace(/\bu\s*f\s*\/\s*km\b/gi, '') .replace(/\bmh\s*\/\s*km\b/gi, '') .replace(/\bkA\b/gi, '') .replace(/\bmm\b/gi, '') .replace(/\bkv\b/gi, '') .replace(/\b°?c\b/gi, '') .replace(/\s+/g, ' ') .trim(); } v = v .replace(/\s*–\s*/g, '-') .replace(/\s*-\s*/g, '-') .replace(/\s*\/\s*/g, '/') .replace(/\s+/g, ' ') .trim(); return compactNumericForLocale(v, locale); } function normalizeVoltageLabel(raw: string): string { const v = normalizeValue(raw); if (!v) return ''; const cleaned = v.replace(/\s+/g, ' '); if (/\bkv\b/i.test(cleaned)) return cleaned.replace(/\bkv\b/i, 'kV'); const num = cleaned.match(/\d+(?:[\.,]\d+)?(?:\s*\/\s*\d+(?:[\.,]\d+)?)?/); if (!num) return cleaned; if (/[a-z]/i.test(cleaned)) return cleaned; return `${cleaned} kV`; } function parseVoltageSortKey(voltageLabel: string): number { const v = normalizeVoltageLabel(voltageLabel); const nums = v .replace(/,/g, '.') .match(/\d+(?:\.\d+)?/g) ?.map((n) => Number(n)) .filter((n) => Number.isFinite(n)); if (!nums || nums.length === 0) return Number.POSITIVE_INFINITY; return nums[nums.length - 1]; } function normalizeExcelKey(value: string): string { return String(value || '') .toUpperCase() .replace(/-\d+$/g, '') .replace(/[^A-Z0-9]+/g, ''); } function loadExcelRows(filePath: string): ExcelRow[] { const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); const trimmed = out.trim(); const jsonStart = trimmed.indexOf('['); if (jsonStart < 0) return []; const jsonText = trimmed.slice(jsonStart); try { return JSON.parse(jsonText) as ExcelRow[]; } catch { return []; } } function getExcelIndex(): Map { if (EXCEL_INDEX) return EXCEL_INDEX; const idx = new Map(); for (const file of EXCEL_SOURCE_FILES) { if (!fs.existsSync(file)) continue; const rows = loadExcelRows(file); const unitsRow = rows.find((r) => r && r['Part Number'] === 'Units') || null; const units: Record = {}; if (unitsRow) { for (const [k, v] of Object.entries(unitsRow)) { if (k === 'Part Number') continue; const unit = normalizeValue(String(v ?? '')); if (unit) units[k] = unit; } } for (const r of rows) { const pn = r?.['Part Number']; if (!pn || pn === 'Units') continue; const key = normalizeExcelKey(String(pn)); if (!key) continue; const cur = idx.get(key); if (!cur) { idx.set(key, { rows: [r], units }); } else { cur.rows.push(r); if (Object.keys(cur.units).length < Object.keys(units).length) cur.units = units; } } } EXCEL_INDEX = idx; return idx; } function findExcelForProduct(product: ProductData): ExcelMatch | null { const idx = getExcelIndex(); const candidates = [ product.name, product.slug ? product.slug.replace(/-\d+$/g, '') : '', product.sku, product.translationKey, ].filter(Boolean) as string[]; for (const c of candidates) { const key = normalizeExcelKey(c); const match = idx.get(key); if (match && match.rows.length) return match; } return null; } function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null { const keys = Object.keys(row || {}); for (const re of patterns) { const k = keys.find((x) => { const key = String(x); if (re.test('conductor') && /ross section conductor/i.test(key)) return false; if (re.test('insulation thickness') && /Diameter over insulation/i.test(key)) return false; if (re.test('conductor') && !/^conductor$/i.test(key)) return false; if (re.test('insulation') && !/^insulation$/i.test(key)) return false; if (re.test('sheath') && !/^sheath$/i.test(key)) return false; if (re.test('norm') && !/^norm$/i.test(key)) return false; return re.test(key); }); if (k) return k; } return null; } export function buildExcelModel(args: { product: ProductData; locale: 'en' | 'de' }): { ok: boolean; technicalItems: KeyValueItem[]; voltageTables: VoltageTableModel[]; } { const match = findExcelForProduct(args.product); if (!match || match.rows.length === 0) return { ok: false, technicalItems: [], voltageTables: [] }; const units = match.units || {}; const rows = match.rows; let sample = rows.find((r) => r && Object.keys(r).length > 0) || {}; let maxColumns = Object.keys(sample).filter( (k) => k && k !== 'Part Number' && k !== 'Units', ).length; for (const r of rows) { const cols = Object.keys(r).filter((k) => k && k !== 'Part Number' && k !== 'Units').length; if (cols > maxColumns) { sample = r; maxColumns = cols; } } const columnMapping: Record = { 'number of cores and cross-section': { header: 'Cross-section', unit: '', key: 'cross_section', }, 'ross section conductor': { header: 'Cross-section', unit: '', key: 'cross_section' }, 'diameter over insulation': { header: 'DI', unit: 'mm', key: 'DI' }, 'diameter over insulation (approx.)': { header: 'DI', unit: 'mm', key: 'DI' }, 'dc resistance at 20 °C': { header: 'RI', unit: 'Ohm/km', key: 'RI' }, 'dc resistance at 20°C': { header: 'RI', unit: 'Ohm/km', key: 'RI' }, 'resistance conductor': { header: 'RI', unit: 'Ohm/km', key: 'RI' }, 'maximum resistance of conductor': { header: 'RI', unit: 'Ohm/km', key: 'RI' }, 'insulation thickness': { header: 'Wi', unit: 'mm', key: 'Wi' }, 'nominal insulation thickness': { header: 'Wi', unit: 'mm', key: 'Wi' }, 'current ratings in air, trefoil': { header: 'Ibl', unit: 'A', key: 'Ibl' }, 'current ratings in air, trefoil*': { header: 'Ibl', unit: 'A', key: 'Ibl' }, 'current ratings in ground, trefoil': { header: 'Ibe', unit: 'A', key: 'Ibe' }, 'current ratings in ground, trefoil*': { header: 'Ibe', unit: 'A', key: 'Ibe' }, 'conductor shortcircuit current': { header: 'Ik', unit: 'kA', key: 'Ik_cond' }, 'screen shortcircuit current': { header: 'Ik', unit: 'kA', key: 'Ik_screen' }, 'sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' }, 'minimum sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' }, 'nominal sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' }, 'bending radius': { header: 'Rbv', unit: 'mm', key: 'Rbv' }, 'bending radius (min.)': { header: 'Rbv', unit: 'mm', key: 'Rbv' }, 'outer diameter': { header: 'Ø', unit: 'mm', key: 'Ø' }, 'outer diameter (approx.)': { header: 'Ø', unit: 'mm', key: 'Ø' }, 'outer diameter of cable': { header: 'Ø', unit: 'mm', key: 'Ø' }, 'pulling force': { header: 'Fzv', unit: 'N', key: 'Fzv' }, 'max. pulling force': { header: 'Fzv', unit: 'N', key: 'Fzv' }, 'conductor aluminum': { header: 'Cond.', unit: '', key: 'Cond' }, 'conductor copper': { header: 'Cond.', unit: '', key: 'Cond' }, weight: { header: 'G', unit: 'kg/km', key: 'G' }, 'weight (approx.)': { header: 'G', unit: 'kg/km', key: 'G' }, 'cable weight': { header: 'G', unit: 'kg/km', key: 'G' }, 'conductor diameter (approx.)': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' }, 'conductor diameter': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' }, 'diameter conductor': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' }, 'diameter over screen': { header: 'Diameter over screen', unit: 'mm', key: 'D_screen' }, 'metallic screen mm2': { header: 'Metallic screen', unit: 'mm2', key: 'S_screen' }, 'metallic screen': { header: 'Metallic screen', unit: 'mm2', key: 'S_screen' }, reactance: { header: 'Reactance', unit: 'Ohm/km', key: 'X' }, 'capacitance (approx.)': { header: 'Capacitance', unit: 'uF/km', key: 'cap' }, capacitance: { header: 'Capacitance', unit: 'uF/km', key: 'cap' }, 'inductance, trefoil (approx.)': { header: 'Inductance trefoil', unit: 'mH/km', key: 'ind_trefoil', }, 'inductance, trefoil': { header: 'Inductance trefoil', unit: 'mH/km', key: 'ind_trefoil' }, 'inductance in air, flat (approx.)': { header: 'Inductance air flat', unit: 'mH/km', key: 'ind_air_flat', }, 'inductance in air, flat': { header: 'Inductance air flat', unit: 'mH/km', key: 'ind_air_flat', }, 'inductance in ground, flat (approx.)': { header: 'Inductance ground flat', unit: 'mH/km', key: 'ind_ground_flat', }, 'inductance in ground, flat': { header: 'Inductance ground flat', unit: 'mH/km', key: 'ind_ground_flat', }, 'current ratings in air, flat': { header: 'Current air flat', unit: 'A', key: 'cur_air_flat' }, 'current ratings in air, flat*': { header: 'Current air flat', unit: 'A', key: 'cur_air_flat' }, 'current ratings in ground, flat': { header: 'Current ground flat', unit: 'A', key: 'cur_ground_flat', }, 'current ratings in ground, flat*': { header: 'Current ground flat', unit: 'A', key: 'cur_ground_flat', }, 'heating time constant, trefoil*': { header: 'Heating time trefoil', unit: 's', key: 'heat_trefoil', }, 'heating time constant, trefoil': { header: 'Heating time trefoil', unit: 's', key: 'heat_trefoil', }, 'heating time constant, flat*': { header: 'Heating time flat', unit: 's', key: 'heat_flat' }, 'heating time constant, flat': { header: 'Heating time flat', unit: 's', key: 'heat_flat' }, 'maximal operating conductor temperature': { header: 'Max operating temp', unit: '°C', key: 'max_op_temp', }, 'maximal short-circuit temperature': { header: 'Max short-circuit temp', unit: '°C', key: 'max_sc_temp', }, 'operating temperature range': { header: 'Operating temp range', unit: '°C', key: 'temp_range', }, 'minimal storage temperature': { header: 'Min storage temp', unit: '°C', key: 'min_store_temp', }, 'minimal temperature for laying': { header: 'Min laying temp', unit: '°C', key: 'min_lay_temp', }, 'test voltage': { header: 'Test voltage', unit: 'kV', key: 'test_volt' }, 'rated voltage': { header: 'Rated voltage', unit: 'kV', key: 'rated_volt' }, conductor: { header: 'Conductor', unit: '', key: 'conductor' }, 'copper wire screen and tape': { header: 'Copper screen', unit: '', key: 'copper_screen' }, CUScreen: { header: 'Copper screen', unit: '', key: 'copper_screen' }, 'conductive tape below screen': { header: 'Conductive tape below', unit: '', key: 'tape_below', }, 'non conducting tape above screen': { header: 'Non-conductive tape above', unit: '', key: 'tape_above', }, 'al foil': { header: 'Al foil', unit: '', key: 'al_foil' }, 'shape of conductor': { header: 'Conductor shape', unit: '', key: 'shape' }, 'colour of insulation': { header: 'Insulation color', unit: '', key: 'color_ins' }, 'colour of sheath': { header: 'Sheath color', unit: '', key: 'color_sheath' }, insulation: { header: 'Insulation', unit: '', key: 'insulation' }, sheath: { header: 'Sheath', unit: '', key: 'sheath' }, norm: { header: 'Norm', unit: '', key: 'norm' }, standard: { header: 'Standard', unit: '', key: 'standard' }, 'cpr class': { header: 'CPR class', unit: '', key: 'cpr' }, 'flame retardant': { header: 'Flame retardant', unit: '', key: 'flame' }, 'self-extinguishing of single cable': { header: 'Flame retardant', unit: '', key: 'flame' }, packaging: { header: 'Packaging', unit: '', key: 'packaging' }, 'ce-conformity': { header: 'CE conformity', unit: '', key: 'ce' }, 'rohs/reach': { header: 'RoHS/REACH', unit: '', key: 'rohs_reach' }, }; const excelKeys = Object.keys(sample).filter((k) => k && k !== 'Part Number' && k !== 'Units'); const matchedColumns: Array<{ excelKey: string; mapping: { header: string; unit: string; key: string }; }> = []; for (const excelKey of excelKeys) { const normalized = normalizeValue(excelKey).toLowerCase(); for (const [pattern, mapping] of Object.entries(columnMapping)) { if (normalized === pattern.toLowerCase() || new RegExp(pattern, 'i').test(normalized)) { matchedColumns.push({ excelKey, mapping }); break; } } } const seenKeys = new Set(); const deduplicated: typeof matchedColumns = []; for (const item of matchedColumns) { if (!seenKeys.has(item.mapping.key)) { seenKeys.add(item.mapping.key); deduplicated.push(item); } } matchedColumns.length = 0; matchedColumns.push(...deduplicated); const sampleKeys = Object.keys(sample) .filter((k) => k && k !== 'Part Number' && k !== 'Units') .sort(); const compatibleRows = rows.filter((r) => { const rKeys = Object.keys(r) .filter((k) => k && k !== 'Part Number' && k !== 'Units') .sort(); return JSON.stringify(rKeys) === JSON.stringify(sampleKeys); }); if (compatibleRows.length === 0) return { ok: false, technicalItems: [], voltageTables: [] }; const csKey = guessColumnKey(sample, [ /number of cores and cross-section/i, /cross.?section/i, /ross section conductor/i, ]) || null; const voltageKey = guessColumnKey(sample, [/rated voltage/i, /voltage rating/i, /nennspannung/i, /spannungs/i]) || null; if (!csKey) return { ok: false, technicalItems: [], voltageTables: [] }; const byVoltage = new Map(); for (let i = 0; i < compatibleRows.length; i++) { const cs = normalizeValue(String(compatibleRows[i]?.[csKey] ?? '')); if (!cs) continue; const rawV = voltageKey ? normalizeValue(String(compatibleRows[i]?.[voltageKey] ?? '')) : ''; const voltageLabel = normalizeVoltageLabel(rawV || ''); const key = voltageLabel || (args.locale === 'de' ? 'Spannung unbekannt' : 'Voltage unknown'); const arr = byVoltage.get(key) ?? []; arr.push(i); byVoltage.set(key, arr); } const voltageTables: VoltageTableModel[] = []; const technicalItems: KeyValueItem[] = []; const voltageKeysSorted = Array.from(byVoltage.keys()).sort((a, b) => { const na = parseVoltageSortKey(a); const nb = parseVoltageSortKey(b); if (na !== nb) return na - nb; return a.localeCompare(b); }); const globalConstantColumns = new Set(); for (const { excelKey, mapping } of matchedColumns) { const values = compatibleRows .map((r) => normalizeValue(String(r?.[excelKey] ?? ''))) .filter(Boolean); const unique = Array.from(new Set(values.map((v) => v.toLowerCase()))); if (unique.length === 1 && values.length > 0) { globalConstantColumns.add(excelKey); const unit = normalizeUnit(units[excelKey] || mapping.unit || ''); const labelBase = technicalFullLabel({ key: mapping.key, excelKey, locale: args.locale }); const label = formatExcelHeaderLabel(labelBase, unit); const value = compactCellForDenseTable(values[0], unit, args.locale); const existing = technicalItems.find((t) => t.label === label); if (!existing) technicalItems.push({ label, value, unit }); } } const metaKeyPriority = [ 'test_volt', 'temp_range', 'max_op_temp', 'max_sc_temp', 'min_lay_temp', 'min_store_temp', 'cpr', 'flame', ]; const metaKeyPrioritySet = new Set(metaKeyPriority); for (const vKey of voltageKeysSorted) { const indices = byVoltage.get(vKey) || []; if (!indices.length) continue; const crossSections = indices.map((idx) => normalizeValue(String(compatibleRows[idx]?.[csKey] ?? '')), ); const metaItems: KeyValueItem[] = []; const metaCandidates = new Map(); if (voltageKey) { const rawV = normalizeValue(String(compatibleRows[indices[0]]?.[voltageKey] ?? '')); metaItems.push({ label: args.locale === 'de' ? 'Spannung' : 'Voltage', value: normalizeVoltageLabel(rawV || ''), }); } const tableColumns: Array<{ excelKey: string; mapping: { header: string; unit: string; key: string }; }> = []; const denseTableKeyOrder = [ 'Cond', 'shape', 'cap', 'X', 'DI', 'RI', 'Wi', 'Ibl', 'Ibe', 'Ik_cond', 'Wm', 'Rbv', 'Ø', 'D_screen', 'S_screen', 'Fzv', 'G', ] as const; const denseTableKeys = new Set(denseTableKeyOrder); const bendingRadiusKey = matchedColumns.find((c) => c.mapping.key === 'Rbv')?.excelKey || null; let bendUnitOverride = ''; if (bendingRadiusKey) { const bendVals = indices .map((idx) => normalizeValue(String(compatibleRows[idx]?.[bendingRadiusKey] ?? ''))) .filter(Boolean); if (bendVals.some((v) => /\bxD\b/i.test(v))) bendUnitOverride = 'xD'; } for (const { excelKey, mapping } of matchedColumns) { if (excelKey === csKey || excelKey === voltageKey) continue; const values = indices .map((idx) => normalizeValue(String(compatibleRows[idx]?.[excelKey] ?? ''))) .filter(Boolean); if (values.length > 0) { const unique = Array.from(new Set(values.map((v) => v.toLowerCase()))); let unit = normalizeUnit(units[excelKey] || mapping.unit || ''); if (mapping.key === 'Rbv' && bendUnitOverride) unit = bendUnitOverride; if (denseTableKeys.has(mapping.key)) { tableColumns.push({ excelKey, mapping }); continue; } if (globalConstantColumns.has(excelKey) && !metaKeyPrioritySet.has(mapping.key)) { continue; } const value = unique.length === 1 ? compactCellForDenseTable(values[0], unit, args.locale) : summarizeSmartOptions(mapping.key, values); const label = metaFullLabel({ key: mapping.key, excelKey, locale: args.locale }); metaCandidates.set(mapping.key, { label, value, unit }); } } for (const k of metaKeyPriority) { const item = metaCandidates.get(k); if (item && item.label && item.value) metaItems.push(item); } const mappedByKey = new Map< string, { excelKey: string; mapping: { header: string; unit: string; key: string } } >(); for (const c of tableColumns) { if (!mappedByKey.has(c.mapping.key)) mappedByKey.set(c.mapping.key, c); } const outerDiameterKey = mappedByKey.get('Ø')?.excelKey || '' || null; const sheathThicknessKey = mappedByKey.get('Wm')?.excelKey || '' || null; const canDeriveDenseKey = (k: (typeof denseTableKeyOrder)[number]): boolean => { if (k === 'DI') return Boolean(outerDiameterKey && sheathThicknessKey); if (k === 'Cond') return true; return false; }; const orderedTableColumns = denseTableKeyOrder .filter((k) => mappedByKey.has(k) || canDeriveDenseKey(k)) .map((k) => { const existing = mappedByKey.get(k); if (existing) return existing; return { excelKey: '', mapping: { header: k, unit: '', key: k }, }; }); const columns = orderedTableColumns.map(({ excelKey, mapping }) => { const defaultUnitByKey: Record = { DI: 'mm', RI: 'Ohm/km', Wi: 'mm', Ibl: 'A', Ibe: 'A', Ik_cond: 'kA', Wm: 'mm', Rbv: 'mm', Ø: 'mm', Fzv: 'N', G: 'kg/km', }; let unit = normalizeUnit( (excelKey ? units[excelKey] : '') || mapping.unit || defaultUnitByKey[mapping.key] || '', ); if (mapping.key === 'Rbv' && bendUnitOverride) unit = bendUnitOverride; return { key: mapping.key, label: denseAbbrevLabel({ key: mapping.key, locale: args.locale, unit, withUnit: true }) || formatExcelHeaderLabel(excelKey, unit), get: (rowIndex: number) => { const srcRowIndex = indices[rowIndex]; const raw = excelKey ? normalizeValue(String(compatibleRows[srcRowIndex]?.[excelKey] ?? '')) : ''; const unitLocal = unit; if (mapping.key === 'DI' && !raw && outerDiameterKey && sheathThicknessKey) { const odRaw = normalizeValue( String(compatibleRows[srcRowIndex]?.[outerDiameterKey] ?? ''), ); const wmRaw = normalizeValue( String(compatibleRows[srcRowIndex]?.[sheathThicknessKey] ?? ''), ); const od = parseNumericOption(odRaw); const wm = parseNumericOption(wmRaw); if (od !== null && wm !== null) { const di = od - 2 * wm; if (Number.isFinite(di) && di > 0) return `~${compactNumericForLocale(String(di), args.locale)}`; } } if (mapping.key === 'Cond' && !raw) { const pn = normalizeExcelKey( args.product.name || args.product.slug || args.product.sku || '', ); if (/^NA/.test(pn)) return 'Al'; if (/^N/.test(pn)) return 'Cu'; } if (mapping.key === 'Rbv' && /\bxD\b/i.test(raw)) return compactNumericForLocale(raw, args.locale); if (mapping.key === 'Rbv' && unitLocal.toLowerCase() === 'mm') { const n = parseNumericOption(raw); const looksLikeMeters = n !== null && n > 0 && n < 50 && /[\.,]\d{1,3}/.test(raw) && !/\dxD/i.test(raw); if (looksLikeMeters) return compactNumericForLocale(String(Math.round(n * 1000)), args.locale); } if (mapping.key === 'Fzv' && unitLocal.toLowerCase() === 'n') { const n = parseNumericOption(raw); const looksLikeKN = n !== null && n > 0 && n < 100 && !/\bN\b/i.test(raw) && !/\bkN\b/i.test(raw); if (looksLikeKN) return compactNumericForLocale(String(Math.round(n * 1000)), args.locale); } return compactCellForDenseTable(raw, unitLocal, args.locale); }, }; }); voltageTables.push({ voltageLabel: vKey, metaItems, crossSections, columns }); } technicalItems.sort((a, b) => a.label.localeCompare(b.label)); return { ok: true, technicalItems, voltageTables }; }