import type { ExternalCsvConfiguration } from "../csv/external-csv-contracts.js"; import type { ExternalModelObjectPlanningValues, ExternalModelObjectSourceValues, } from "./external-model-contracts.js"; export type InitialExternalObjectIssue = | "unknown-family-and-type" | "invalid-power" | "invalid-quantity" | "missing-room"; export interface InitialExternalObjectProjection { ifcGuid: string; sourceValues: ExternalModelObjectSourceValues; planningValues: ExternalModelObjectPlanningValues; issues: InitialExternalObjectIssue[]; } export interface ExternalObjectImportCandidate { rowNumber: number; ifcGuid: string; roomNumber: string; roomName: string; familyAndType: string; selectionMarker: string; circuitIdentifier: string; power: string; quantity: string | null; additionalSourceValues: Record; } export function normalizeExternalRoomPart(value: string): string { return value.trim().normalize("NFKC").toLocaleUpperCase("de-DE"); } export function createExternalRoomKey( roomNumber: string, roomName: string ): string | null { const normalizedNumber = normalizeExternalRoomPart(roomNumber); if (normalizedNumber) return `number:${normalizedNumber}`; const normalizedName = normalizeExternalRoomPart(roomName); return normalizedName ? `name:${normalizedName}` : null; } export function indexExternalObjectsByIfcGuid( objects: readonly T[] ): Map { const index = new Map(); for (const object of objects) { if (!object.ifcGuid || object.ifcGuid !== object.ifcGuid.trim()) { throw new Error("IfcGUID must be non-empty and must not contain outer whitespace."); } if (index.has(object.ifcGuid)) { throw new Error(`Duplicate IfcGUID: ${object.ifcGuid}`); } index.set(object.ifcGuid, object); } return index; } export function projectInitialExternalObject( object: ExternalObjectImportCandidate, configuration: ExternalCsvConfiguration ): InitialExternalObjectProjection { const rule = configuration.familyTypeRules.find( (candidate) => candidate.exactFamilyAndType === object.familyAndType ); const issues: InitialExternalObjectIssue[] = []; if (!rule) issues.push("unknown-family-and-type"); if (!createExternalRoomKey(object.roomNumber, object.roomName)) { issues.push("missing-room"); } const parsedPower = parseConfiguredNumber(object.power, configuration); if (object.power.trim() && parsedPower === null) issues.push("invalid-power"); let effectiveQuantity = 1; if (rule?.quantityRule.kind === "fixed") { effectiveQuantity = rule.quantityRule.quantity; } else if (rule?.quantityRule.kind === "mapped-column") { const parsedQuantity = parseConfiguredNumber(object.quantity ?? "", configuration); if (parsedQuantity === null || !Number.isInteger(parsedQuantity) || parsedQuantity <= 0) { issues.push("invalid-quantity"); } else { effectiveQuantity = parsedQuantity; } } return { ifcGuid: object.ifcGuid, sourceValues: { rowNumber: object.rowNumber, roomNumber: object.roomNumber, roomName: object.roomName, familyAndType: object.familyAndType, selectionMarker: object.selectionMarker, circuitIdentifier: object.circuitIdentifier, power: object.power, quantity: object.quantity, additionalSourceValues: { ...object.additionalSourceValues }, }, planningValues: { displayName: rule ? resolveDisplayName(rule.displayNameSuggestion, object) : null, internalDeviceType: rule?.internalDeviceType ?? null, category: rule?.category ?? null, connectionKind: rule?.connectionKind ?? null, effectiveQuantity, powerPerUnitW: parsedPower === null ? null : parsedPower * configuration.wattsPerSourceUnit, simultaneityFactor: 1, cosPhi: null, costGroup: null, remark: null, }, issues, }; } function parseConfiguredNumber( value: string, configuration: ExternalCsvConfiguration ): number | null { const trimmed = value.trim(); if (!trimmed) return null; const normalized = configuration.decimalSeparator === "," ? trimmed.replace(",", ".") : trimmed; if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalized)) return null; const parsed = Number(normalized); return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; } function resolveDisplayName( suggestion: ExternalCsvConfiguration["familyTypeRules"][number]["displayNameSuggestion"], object: ExternalObjectImportCandidate ): string | null { if (suggestion === null) return null; if (suggestion.kind === "fixed") return suggestion.value; if (suggestion.kind === "selection-marker") { return object.selectionMarker.trim() || null; } return object.familyAndType.trim() || null; }