grading
Version:
Grading of student submissions, in particular programming tests.
394 lines • 13.8 kB
JavaScript
"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