Files
leistungsbilanz-ts/src/external-model/domain/external-model-matching.ts
T

148 lines
4.8 KiB
TypeScript

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<string, string>;
}
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<T extends { ifcGuid: string }>(
objects: readonly T[]
): Map<string, T> {
const index = new Map<string, T>();
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;
}