193 lines
6.0 KiB
TypeScript
193 lines
6.0 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { execSync } from 'child_process';
|
|
|
|
import type { ProductData } from './types';
|
|
import { normalizeValue } from './utils';
|
|
|
|
type ExcelRow = Record<string, unknown>;
|
|
export type ExcelMatch = { rows: ExcelRow[]; units: Record<string, string> };
|
|
|
|
export type MediumVoltageCrossSectionExcelMatch = {
|
|
headerRow: ExcelRow;
|
|
rows: ExcelRow[];
|
|
units: Record<string, string>;
|
|
partNumberKey: string;
|
|
crossSectionKey: string;
|
|
ratedVoltageKey: string | null;
|
|
};
|
|
|
|
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'),
|
|
];
|
|
|
|
// Medium-voltage cross-section table (new format with multi-row header).
|
|
// IMPORTANT: this must NOT be used for the technical data table.
|
|
const MV_CROSS_SECTION_FILE = path.join(process.cwd(), 'data/excel/medium-voltage-KM 170126.xlsx');
|
|
|
|
type MediumVoltageCrossSectionIndex = {
|
|
headerRow: ExcelRow;
|
|
units: Record<string, string>;
|
|
partNumberKey: string;
|
|
crossSectionKey: string;
|
|
ratedVoltageKey: string | null;
|
|
rowsByDesignation: Map<string, ExcelRow[]>;
|
|
};
|
|
|
|
let EXCEL_INDEX: Map<string, ExcelMatch> | null = null;
|
|
let MV_CROSS_SECTION_INDEX: MediumVoltageCrossSectionIndex | null = null;
|
|
|
|
export 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 findKeyByHeaderValue(headerRow: ExcelRow, pattern: RegExp): string | null {
|
|
for (const [k, v] of Object.entries(headerRow || {})) {
|
|
const text = normalizeValue(String(v ?? ''));
|
|
if (!text) continue;
|
|
if (pattern.test(text)) return k;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getMediumVoltageCrossSectionIndex(): MediumVoltageCrossSectionIndex {
|
|
if (MV_CROSS_SECTION_INDEX) return MV_CROSS_SECTION_INDEX;
|
|
|
|
const rows = fs.existsSync(MV_CROSS_SECTION_FILE) ? loadExcelRows(MV_CROSS_SECTION_FILE) : [];
|
|
const headerRow = (rows[0] || {}) as ExcelRow;
|
|
|
|
const partNumberKey = findKeyByHeaderValue(headerRow, /^part\s*number$/i) || '__EMPTY';
|
|
const crossSectionKey = findKeyByHeaderValue(headerRow, /querschnitt|cross.?section/i) || '';
|
|
const ratedVoltageKey = findKeyByHeaderValue(headerRow, /rated voltage|voltage rating|nennspannung/i) || null;
|
|
|
|
const unitsRow = rows.find(r => normalizeValue(String((r as ExcelRow)?.[partNumberKey] ?? '')) === 'Units') || null;
|
|
const units: Record<string, string> = {};
|
|
if (unitsRow) {
|
|
for (const [k, v] of Object.entries(unitsRow)) {
|
|
if (k === partNumberKey) continue;
|
|
const unit = normalizeValue(String(v ?? ''));
|
|
if (unit) units[k] = unit;
|
|
}
|
|
}
|
|
|
|
const rowsByDesignation = new Map<string, ExcelRow[]>();
|
|
for (const r of rows) {
|
|
if (r === headerRow) continue;
|
|
const pn = normalizeValue(String((r as ExcelRow)?.[partNumberKey] ?? ''));
|
|
if (!pn || pn === 'Units' || pn === 'Part Number') continue;
|
|
|
|
const key = normalizeExcelKey(pn);
|
|
if (!key) continue;
|
|
|
|
const cur = rowsByDesignation.get(key) || [];
|
|
cur.push(r);
|
|
rowsByDesignation.set(key, cur);
|
|
}
|
|
|
|
MV_CROSS_SECTION_INDEX = { headerRow, units, partNumberKey, crossSectionKey, ratedVoltageKey, rowsByDesignation };
|
|
return MV_CROSS_SECTION_INDEX;
|
|
}
|
|
|
|
export function getExcelIndex(): Map<string, ExcelMatch> {
|
|
if (EXCEL_INDEX) return EXCEL_INDEX;
|
|
const idx = new Map<string, ExcelMatch>();
|
|
|
|
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<string, string> = {};
|
|
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;
|
|
}
|
|
|
|
export 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;
|
|
}
|
|
|
|
export function findMediumVoltageCrossSectionExcelForProduct(product: ProductData): MediumVoltageCrossSectionExcelMatch | null {
|
|
const idx = getMediumVoltageCrossSectionIndex();
|
|
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 rows = idx.rowsByDesignation.get(key) || [];
|
|
if (rows.length) {
|
|
return {
|
|
headerRow: idx.headerRow,
|
|
rows,
|
|
units: idx.units,
|
|
partNumberKey: idx.partNumberKey,
|
|
crossSectionKey: idx.crossSectionKey,
|
|
ratedVoltageKey: idx.ratedVoltageKey,
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|