forked from jappel/leistungsbilanz-ts
266 lines
9.1 KiB
TypeScript
266 lines
9.1 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import {
|
|
circuitRefKey,
|
|
findRowConflicts,
|
|
newRowChoiceKey,
|
|
objectGroupKey,
|
|
resolveAssignmentPlan,
|
|
splitSocketsByRoom,
|
|
toAssignmentPlanRequestRows,
|
|
type AssignmentPlanCircuit,
|
|
type AssignmentPlanObject,
|
|
type AssignmentPlanRow,
|
|
} from "../src/shared/revit/assignment-plan.js";
|
|
|
|
function socket(
|
|
id: string,
|
|
overrides: Partial<AssignmentPlanObject> & { quantity?: number } = {}
|
|
): AssignmentPlanObject {
|
|
const { quantity = 2, ...rest } = overrides;
|
|
return {
|
|
id,
|
|
worksharingId: `ws-${id}`,
|
|
familyAndType: "Steckdose: Doppel",
|
|
selectionMarker: "SD",
|
|
deviceTypeId: "type-socket",
|
|
roomId: "room-1",
|
|
roomNumber: "1.01",
|
|
roomName: "Büro",
|
|
linkedProjectDeviceId: null,
|
|
isSocket: true,
|
|
planningValues: {
|
|
displayName: "Steckdose",
|
|
category: "single_phase",
|
|
connectionKind: null,
|
|
effectiveQuantity: quantity,
|
|
powerPerUnitW: 200,
|
|
simultaneityFactor: 1,
|
|
cosPhi: 0.95,
|
|
costGroup: "440",
|
|
remark: null,
|
|
},
|
|
...rest,
|
|
};
|
|
}
|
|
|
|
function row(overrides: Partial<AssignmentPlanRow> = {}): AssignmentPlanRow {
|
|
return {
|
|
id: "row-1",
|
|
circuitId: "circuit-1",
|
|
roomId: "room-1",
|
|
displayName: "Steckdose",
|
|
linkedProjectDeviceId: null,
|
|
category: "single_phase",
|
|
connectionKind: null,
|
|
powerPerUnit: 0.2,
|
|
simultaneityFactor: 1,
|
|
cosPhi: 0.95,
|
|
costGroup: "440",
|
|
remark: null,
|
|
linkedSelectionMarkers: ["SD"],
|
|
linkedObjectCount: 2,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function circuit(overrides: Partial<AssignmentPlanCircuit> = {}): AssignmentPlanCircuit {
|
|
return {
|
|
id: "circuit-1",
|
|
sectionId: "section-2",
|
|
category: "single_phase",
|
|
equipmentIdentifier: "-2F1",
|
|
displayName: "Steckdose",
|
|
rows: [row()],
|
|
linkedSocketQuantity: 4,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const sections = [
|
|
{ id: "section-1", category: "lighting" as const },
|
|
{ id: "section-2", category: "single_phase" as const },
|
|
];
|
|
|
|
describe("Revit assignment plan", () => {
|
|
it("splits 8 double sockets with limit 8 into two circuits of 4 objects", () => {
|
|
const objects = Array.from({ length: 8 }, (_, index) => socket(`s${index + 1}`));
|
|
const result = splitSocketsByRoom(objects, 8);
|
|
assert.deepEqual(result.circuits.map((ids) => ids.length), [4, 4]);
|
|
assert.deepEqual(result.oversizedObjectIds, []);
|
|
assert.deepEqual(result.circuits.flat().sort(), objects.map((object) => object.id).sort());
|
|
});
|
|
|
|
it("keeps rooms together and only splits a room when it exceeds the limit", () => {
|
|
const objects = [
|
|
...[1, 2].map((index) => socket(`a${index}`, { roomId: "room-a", roomNumber: "1.02" })),
|
|
...[1, 2, 3].map((index) => socket(`b${index}`, { roomId: "room-b", roomNumber: "1.10" })),
|
|
...[1, 2].map((index) => socket(`c${index}`, { roomId: "room-c", roomNumber: "1.3" })),
|
|
...[1, 2, 3, 4, 5].map((index) => socket(`d${index}`, { roomId: "room-d", roomNumber: "2.01" })),
|
|
];
|
|
// Natural order 1.02 (4) · 1.3 (4) · 1.10 (6) · 2.01 (10)
|
|
const result = splitSocketsByRoom(objects, 8);
|
|
assert.deepEqual(result.circuits, [
|
|
["a1", "a2", "c1", "c2"],
|
|
["b1", "b2", "b3"],
|
|
["d1", "d2", "d3", "d4"],
|
|
["d5"],
|
|
]);
|
|
});
|
|
|
|
it("gives an oversized object its own circuit and skips non-sockets", () => {
|
|
const result = splitSocketsByRoom(
|
|
[
|
|
socket("big", { quantity: 10 }),
|
|
socket("small", { roomId: "room-2", roomNumber: "1.02" }),
|
|
socket("light", { isSocket: false }),
|
|
],
|
|
8
|
|
);
|
|
assert.deepEqual(result.circuits, [["big"], ["small"]]);
|
|
assert.deepEqual(result.oversizedObjectIds, ["big"]);
|
|
assert.deepEqual(result.skippedObjectIds, ["light"]);
|
|
assert.throws(() => splitSocketsByRoom([], 0), /positive ganze Zahl/);
|
|
});
|
|
|
|
it("groups single and double sockets of a room into one row and separates rooms", () => {
|
|
const single = socket("single", { quantity: 1 });
|
|
assert.equal(objectGroupKey(single), objectGroupKey(socket("double")));
|
|
const plan = resolveAssignmentPlan({
|
|
objects: [single, socket("double"), socket("other-room", { roomId: "room-2" })],
|
|
circuits: [],
|
|
sections,
|
|
plan: {
|
|
newCircuits: [{ key: "n1", sectionId: "section-2" }],
|
|
assignments: ["single", "double", "other-room"].map((objectId) => ({
|
|
objectId,
|
|
target: { kind: "new-circuit" as const, key: "n1" },
|
|
})),
|
|
newRowChoices: [],
|
|
},
|
|
maxSocketsPerCircuit: 8,
|
|
});
|
|
assert.equal(plan.circuits.length, 1);
|
|
assert.deepEqual(plan.circuits[0]!.rows.map((entry) => entry.quantity), [3, 2]);
|
|
assert.equal(plan.circuits[0]!.socketQuantityAfter, 5);
|
|
assert.equal(plan.newCircuitCount, 1);
|
|
assert.deepEqual(plan.issues, []);
|
|
assert.deepEqual(toAssignmentPlanRequestRows(plan).map((entry) => entry.kind), [
|
|
"new-row-in-new-circuit",
|
|
"new-row-in-new-circuit",
|
|
]);
|
|
});
|
|
|
|
it("joins a matching linked row on circuit drop unless a new row is chosen", () => {
|
|
const input = {
|
|
objects: [socket("s1")],
|
|
circuits: [circuit()],
|
|
sections,
|
|
maxSocketsPerCircuit: 8,
|
|
};
|
|
const joined = resolveAssignmentPlan({
|
|
...input,
|
|
plan: {
|
|
newCircuits: [],
|
|
assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }],
|
|
newRowChoices: [],
|
|
},
|
|
});
|
|
assert.deepEqual(joined.circuits[0]!.rows[0]!.row, { kind: "existing", rowId: "row-1" });
|
|
assert.equal(joined.circuits[0]!.socketQuantityAfter, 6);
|
|
|
|
const separate = resolveAssignmentPlan({
|
|
...input,
|
|
plan: {
|
|
newCircuits: [],
|
|
assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }],
|
|
newRowChoices: [
|
|
newRowChoiceKey({ kind: "existing", circuitId: "circuit-1" }, objectGroupKey(socket("s1"))),
|
|
],
|
|
},
|
|
});
|
|
assert.equal(separate.circuits[0]!.rows[0]!.row.kind, "new");
|
|
assert.equal(separate.circuits[0]!.rows[0]!.matchingRowId, "row-1");
|
|
|
|
const manualOnly = resolveAssignmentPlan({
|
|
...input,
|
|
circuits: [circuit({ rows: [row({ linkedObjectCount: 0, linkedSelectionMarkers: [] })] })],
|
|
plan: {
|
|
newCircuits: [],
|
|
assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }],
|
|
newRowChoices: [],
|
|
},
|
|
});
|
|
assert.equal(manualOnly.circuits[0]!.rows[0]!.row.kind, "new");
|
|
});
|
|
|
|
it("lists planning conflicts and blocking issues for an explicit row drop", () => {
|
|
const differing = socket("s1", {
|
|
planningValues: { ...socket("x").planningValues, powerPerUnitW: 250, simultaneityFactor: null },
|
|
});
|
|
assert.deepEqual(
|
|
findRowConflicts(row({ simultaneityFactor: 0.5 }), differing).map((entry) => entry.field),
|
|
["powerPerUnit", "simultaneityFactor"]
|
|
);
|
|
const plan = resolveAssignmentPlan({
|
|
objects: [differing, socket("s2", { roomId: "room-2", selectionMarker: "X" })],
|
|
circuits: [circuit()],
|
|
sections,
|
|
plan: {
|
|
newCircuits: [],
|
|
assignments: [
|
|
{ objectId: "s1", target: { kind: "row", rowId: "row-1" } },
|
|
{ objectId: "s2", target: { kind: "row", rowId: "row-1" } },
|
|
],
|
|
newRowChoices: [],
|
|
},
|
|
maxSocketsPerCircuit: null,
|
|
});
|
|
assert.deepEqual(plan.conflictObjectIds, ["s1"]);
|
|
assert.ok(plan.issues.some((issue) => /Raum/.test(issue)));
|
|
assert.ok(plan.issues.some((issue) => /Auswahlkenner/.test(issue)));
|
|
assert.equal(plan.circuits[0]!.overLimit, false);
|
|
});
|
|
|
|
it("marks only circuits that gain sockets above the limit and rejects wrong categories", () => {
|
|
const plan = resolveAssignmentPlan({
|
|
objects: [socket("s1"), socket("s2"), socket("s3", { quantity: 4 })],
|
|
circuits: [circuit()],
|
|
sections,
|
|
plan: {
|
|
newCircuits: [{ key: "light", sectionId: "section-1" }],
|
|
assignments: [
|
|
{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } },
|
|
{ objectId: "s2", target: { kind: "circuit", circuitId: "circuit-1" } },
|
|
{ objectId: "s3", target: { kind: "new-circuit", key: "light" } },
|
|
],
|
|
newRowChoices: [],
|
|
},
|
|
maxSocketsPerCircuit: 6,
|
|
});
|
|
assert.deepEqual(plan.overLimitRefKeys, ["circuit-1"]);
|
|
assert.equal(plan.circuits[0]!.socketQuantityAfter, 8);
|
|
assert.equal(circuitRefKey(plan.circuits[1]!.circuit), "new:light");
|
|
assert.ok(plan.issues.some((issue) => /Kategorie/.test(issue)));
|
|
});
|
|
|
|
it("reports duplicate and unknown objects", () => {
|
|
const plan = resolveAssignmentPlan({
|
|
objects: [socket("s1")],
|
|
circuits: [],
|
|
sections,
|
|
plan: {
|
|
newCircuits: [{ key: "n1", sectionId: "section-2" }],
|
|
assignments: [
|
|
{ objectId: "s1", target: { kind: "new-circuit", key: "n1" } },
|
|
{ objectId: "s1", target: { kind: "new-circuit", key: "n1" } },
|
|
{ objectId: "gone", target: { kind: "new-circuit", key: "n1" } },
|
|
],
|
|
newRowChoices: [],
|
|
},
|
|
maxSocketsPerCircuit: 8,
|
|
});
|
|
assert.equal(plan.objectCount, 1);
|
|
assert.equal(plan.issues.length, 2);
|
|
});
|
|
});
|