UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

394 lines 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Table = void 0; /** * Eine einfache, automatisch wachsende Tabelle mit Strings. * * Begriffe: * - Spalte (Column): vertikal angeordnete Zellen * - Zeile (Row): horizontal angeordnete Zellen * - Zelle (Cell): ein Feld mit einem Wert (Value) * * Eine Zelle ist eindeutig über Spalten- und Zeilennummer bestimmt. Die Indizes sind beginnen bei 1! * * Nach außen sieht die Tabelle immer komplett gefüllt aus. Intern werden nur bei Bedarf * echte Zellen erzeugt. * * @author Jens von Pilgrim */ class Table extends Object { /** * Erzeugt eine Tabelle. * Spalten und Zeilen werden beim Setzen von Text automatisch erzeugt. * * @param values Initiale Werte (äußeres Array enthält die Zeilen) oder Quell-Tabelle */ constructor(values) { super(); this._columnsCount = 0; this.rows = []; const src = values instanceof Array ? values : (values instanceof Table ? values.rows : undefined); if (src) { for (let row = 0; row < src.length; row++) { for (let col = 0; col < src[row].length; col++) { this.setText(col + 1, row + 1, src[row][col]); } } } } /** * Der Spaltenindex kann als Zahl oder als Buchstaben eingeben werden. * Die Buchstaben beginnen bei a,bis z, dann aa bis zz, usw. Groß-/Kleinschreibung wird ignoriert. * * Diese Methode wird intern von vielen Methoden verwendet, um die Spaltenangabe in eine Zahl umzuwandeln. * * @param col Spaltenindex als Zahl oder String. * @returns Den Spaltenindex als Zahl, beginnend bei 1. */ static columnIndexAsNumber(col) { if (typeof col === "number") { return col; } else { let colNo = 0; for (let i = 0; i < col.length; i++) { let c = col[i].toLowerCase(); if (c < 'a' || c > 'z') { throw new RangeError("invalid column index '" + c + "'"); } colNo *= 26; colNo += c.charCodeAt(0) - 'a'.charCodeAt(0) + 1; } return colNo; } } getColForTitle(title) { if (this.rows.length < 1) { throw new Error("Not enough rows to use title access"); } const titles = this.rows[0]; let col = 0; while (col < titles.length) { if (titles[col] === title) { break; } col++; } if (col >= titles.length) { throw new Error("No columns with title '" + title + "' found, titles are: " + titles); } return col + 1; // 1-based } getColForTitles(...titles) { let lastError; for (const title of titles) { try { return this.getColForTitle(title); } catch (err) { lastError = err; } } throw lastError; } /** * Gibt den Text in gegebener Spalte und Zeile zurück. * Falls die Zeile oder Spalte nicht existiert, wird ein leerer String zurückgegeben. * * @param col Spaltenindex, 1-basiert oder beginnend bei 'a' * @param row Zeilenindex, beginnend bei 1 * @return der Text, evtl. leer aber nie null oder undefined */ getText(col, row) { const colNo = Table.columnIndexAsNumber(col); if (colNo < 1 || row < 1) { throw new RangeError("col (" + col + ") and row (" + row + ") must be greater 0."); } if (row > this.rowsCount || colNo > this.columnsCount) { return ""; } return this.rows[row - 1]?.[colNo - 1] ?? ""; } at(title, row) { const col = this.getColForTitle(title); return this.getText(col, row); } setAt(title, row, value) { const col = this.getColForTitle(title); return this.setText(col, row, "" + value); } concatAt(title, row, value) { const col = this.getColForTitle(title); let prev = this.getText(col, row); if (prev.length > 0) { prev += " "; } return this.setText(col, row, prev + value); } addTextToRow(row, text) { const col = this.columnsCount + 1; return this.setText(col, row, text); } addRow(...values) { const row = this.rowsCount + 1; for (let col = 1; col <= values.length; col++) { const val = values[col - 1]; this.setText(col, row, String(val)); } } /** * Setzt den Text in der Zelle mit gegebener Spalte und Zeile. * * Falls col gleich oder größer der aktuellen Spaltenzahl ist, werden entsprechend leere Spalten angefügt. * Falls row gleich oder größer der aktuellen Zeilenzahl ist, werden entsprechend leere Zeilen angefügt. * * @param col Spaltenindex, 1-basiert oder beginnend bei 'a' * @param row Zeilenindex, 1-basiert * @param text der zu setzende Text * @return Der Text, der vorher in der Zelle war (evtl. "", aber nie undefined) */ setText(col, row, text) { const colNo = Table.columnIndexAsNumber(col); if (colNo < 1 || row < 1) { throw new RangeError("col (" + col + ") and row (" + row + ") must be greater 0."); } if (!this.rows[row - 1]) { this.rows[row - 1] = []; } const oldVal = this.rows[row - 1][colNo - 1] ?? ""; this.rows[row - 1][colNo - 1] = text; if (colNo > this.columnsCount) { this._columnsCount = colNo; } return oldVal; } /** * Fügt eine Spalte links von der angegebenen Stelle ein * und verschiebt die anderen Spalten nach rechts. D.h. danach * ist die neue Spalte gerade an dem gegebenen Spaltenindex. * * Falls der Spaltenindex größer-gleich als die aktuelle Anzahl an Spalten ist, * wird nichts verändert. * * @param col Spaltenindex, an der neue Spalte eingefügt werden soll. * @return true, wenn tatsächlich eine Spalte eingefügt wurde. */ insertColumnLeft(col) { const colNo = Table.columnIndexAsNumber(col); if (colNo < 1) { throw new RangeError("col (" + col + ") must be greater 0."); } if (colNo > this.columnsCount) { return false; } this.rows.forEach(r => r.splice(colNo - 1, 0, "")); this._columnsCount++; return true; } /** * Löscht die angegebene Spalte, verschiebt also alle Spalten rechts davon eins nach links. * * Falls der Spaltenindex größer-gleich der aktuellen Anzahl an Spalten ist, * wird nichts verändert. * * @param col Spaltenindex der zu löschenden Spalte * @returns true, wenn tatsächlich etwas verändert wurde */ deleteColumn(col) { const colNo = Table.columnIndexAsNumber(col); if (colNo < 1) { throw new RangeError("col (" + col + ") must be greater 0."); } if (colNo > this.columnsCount) { return false; } this.rows.forEach(r => r.splice(colNo - 1, 1)); this._columnsCount--; return true; } /** * Fügt eine Zeile oberhalb von der angegebenen Zeile ein * und verschiebt die anderen Zeilen nach unten. D.h. danach * ist die neue Zeile gerade an dem gegebenen Zeilenindex. * * Falls der Zeilenindex größer-gleich als die aktuelle Anzahl an Zeilen ist, * wird nichts verändert. * * @param row Zeilenindex, an der neue Zeile eingefügt werden soll. * @return true, wenn tatsächlich eine Zeile eingefügt wurde. */ insertRowBefore(row) { if (row < 1) { throw new RangeError("row (" + row + ") must be greater 0."); } if (row > this.rowsCount) { return false; } this.rows.splice(row - 1, 0, []); return true; } /** * Löscht die angegebene Spalte, verschiebt also alle Spalten rechts davon eins nach links. * * Falls der Spaltenindex größer-gleich der aktuellen Anzahl an Spalten ist, * wird nichts verändert. * * @param col Spaltenindex der zu löschenden Spalte * @returns true, wenn tatsächlich etwas verändert wurde */ deleteRow(row) { if (row < 1) { throw new RangeError("row (" + row + ") must be greater 0."); } if (row > this.rowsCount) { return false; } this.rows.splice(row - 1, 1); return true; } /** * Summiert alle Werte einer Zeile. Die einzelnen Werte werden in Zahlen umgewandelt (mittels parseInt). * * @param row Die Zeile, deren Inhalte summiert werden sollen * @returns die Summe, ggf. 0 falls die Zeile nicht existiert */ sumOfRow(row) { if (row < 1) { throw new RangeError("row (" + row + ") must be greater 0."); } const line = this.rows[row - 1]; if (!line) { return 0; } const sum = line.reduce((acc, val) => acc + parseInt(val), 0); return sum; } /** * Summiert alle Werte einer Reihe. Die einzelnen Werte werden in Zahlen umgewandelt (mittels parseInt). * * @param col Die Reihe, deren Inhalte summiert werden sollen, als Zahl oder Buchstaben * @returns die Summe, ggf. 0 falls die Reihe nicht existiert */ sumOfCol(col) { const colNum = Table.columnIndexAsNumber(col); if (colNum < 1) { throw new RangeError("col (" + col + ") must be greater 0."); } const sum = this.rows.map(row => row[colNum - 1]).reduce((acc, val, ci, arr) => { let tab = this; let valAsNumber = parseFloat(val); if (Number.isNaN(valAsNumber)) { return acc; } return acc + valAsNumber; }, 0); return sum; } /** * Setzt alle Zellen zwischen den angegebenen Spalten/Zeilen auf den angegebenen Wert. * * @param text Der zu setzende Text * @param colFrom Spaltenindex (inklusive), ab dem Zellen gesetzt werden * @param rowFrom Zeilenindex (inklusive), ab dem Zellen gesetzt werden * @param colTo Spaltenindex (inklusive), bis zu dem Zellen gesetzt werden * @param rowTo Zeilenindex (inklusive) , bis zu dem Zellen gesetzt werden */ fill(text, colFrom, rowFrom, colTo, rowTo) { const colFromNum = Table.columnIndexAsNumber(colFrom); const colToNum = Table.columnIndexAsNumber(colTo); if (colFromNum < 1 || rowFrom < 1 || colToNum < colFrom || colToNum < colFromNum) { throw new RangeError(); } for (let row = rowFrom; row <= rowTo; row++) { if (!this.rows[row - 1] || this.rows[row - 1].length === 0) { this.rows[row - 1] = []; } for (let col = colFromNum; col <= colTo; col++) { this.rows[row - 1][col - 1] = text; } } if (colToNum > this.columnsCount) { this._columnsCount = colToNum; } } /** * Gibt die Anzahl der Spalten zurück. */ get columnsCount() { return this._columnsCount; } /** * Gibt die Anzahl der Zeilen der Tabelle zurück. */ get rowsCount() { return this.rows.length; } findRowWhere(condition) { for (let row = 1; row <= this.rowsCount; row++) { if (condition(row)) { return row; } } return -1; } /** * Convenience (Bequemlichkeits-) Methode zur Anzeige der Tabelle im Debugger */ toString() { let s = ""; for (let row = 1; row <= this.rowsCount; row++) { s += "|"; for (let col = 1; col <= this.columnsCount; col++) { let val = this.getText(col, row); if (val.length > 4) { val = val.substring(0, 3) + "…"; } else if (val.length < 4) { val = val + " ".substring(val.length); } s += val + "|"; } s += "\n"; } return s.toString(); } /** * Convenience Methode um Tests zu vereinfachen, gibt true zurück, wenn * die Tabelle die gleichen Inhalte hat wie das übergebene Array. * * @param arr Array mit Vergleichswerten, das äußere Array enthält die Zeilen. * In dem Array müssen zumindest alle Zeile existieren, auch wenn diese sparse sein können. */ equalsArray(arr) { if (arr.length === this.rowsCount) { for (let row = 0; row < this.rowsCount; row++) { const arrRow = arr[row]; if (!arrRow) { return false; } for (let col = 0; col < this.columnsCount; col++) { if (arrRow[col] !== (this.getText(col + 1, row + 1))) { return false; } } } return true; } return false; } /** * Only for testing */ get rawArray() { return this.rows; } addRowFromTable(srcTable, rowToCopy) { const row = this.rowsCount + 1; const colCount = srcTable.columnsCount; for (let col = 1; col <= colCount; col++) { this.setText(col, row, srcTable.getText(col, rowToCopy)); } } } exports.Table = Table; //# sourceMappingURL=table.js.map