Zum Inhalt

kc, log, Date-Erweiterungen, $feld-Aliase

Das Sammelbecken für Kleinkram, der überall gebraucht wird: robuste Zahlenkonvertierung, Logging, Datumsfunktionen und die Feld-Aliase. Klein im Umfang, groß im Alltagsnutzen.

Zahlen aus Feldern: .num

Eingabefelder kommen aus dem Browser immer als Text, auch wenn das Schema type: number sagt. Rechnest du direkt damit, klebt JavaScript die Ziffern aneinander:

$wert1 + $wert2   // "3" + "4" → "34"  ❌

.num löst das — hänge es an den Feldwert:

log.info($wert1.num + $wert2.num);   // 3 + 4 → 7  ✅

Für ein deklariertes Feld gibt es zwei Schreibweisen — nimm die, die zum Rest deines Skripts passt:

$summe          = $wert1.num + $wert2.num;                // Alias, kurz
field.summe.num = field.wert1.num + field.wert2.num;      // Handle

.num liest den Wert als echte Zahl — auch aus "1.234,56" — und liefert 0, wenn nichts Lesbares drinsteht. Auch bei einem leeren Feld ist das Ergebnis 0, nicht etwa ein Fehler.

Nicht an ctx.form.<feld> hängen

ctx.form.betrag.num funktioniert nur, solange etwas im Feld steht. Ein nie ausgefülltes Feld ist dort null, und null.num bricht mit „Cannot read property 'num' of null" ab — ausgerechnet im häufigsten Fall.

Der $-Alias und das Handle fangen das ab (beide liefern 0). Wenn du ausgeschrieben arbeiten willst, nimm kc.num(ctx.form.betrag).

Warum nicht einfach rechnen

Ein integer kommt im Skript schon als Zahl an, ein number bewusst als Text (Begründung im ctx.form-Abschnitt). $menge + $menge2 rechnet deshalb, $preis + $preis2 verkettet — dieselbe Schreibweise, zwei Ergebnisse, abhängig von einem Schema-Detail, das man beim Skriptschreiben nicht vor Augen hat. .num macht beide Fälle gleich.

kc.num(v) — dieselbe Umwandlung als Funktion

.num gibt es nur an Feldwerten. Überall sonst macht kc.num(…) genau dasselbe — und dort ist es die einzige Möglichkeit:

Wo Beispiel
Werte aus der Datenbank kc.num(db.scalar("SELECT SUM(…)"))
Zeilen einer Detail-Tabelle kc.num(zeile.menge)
Zeilen aus CSV, XML, Web-Services kc.num(k.Umsatz)
Formular-Properties kc.num(ctx.params.grenze)
Bedingungen (kc:visibleIf, Regeln) und kc:calc kc.num(betrag) > 1000

Die letzte Zeile ist wichtig: Dort gibt es weder $feld noch field.<name> — in Bedingungen und Berechnungsfeldern bleibt kc.num(…) die Schreibweise.

.num delegiert intern an kc.num; gleiche Heuristik, gleiches Ergebnis.

Was kc.num akzeptiert:

Eingabe Ergebnis
null, undefined, "", " " 0
42 (Number) 42
"42" 42
"42.5" 42.5 (englischer Stil)
"42,5" 42.5 (deutscher Stil)
"1.234,56" 1234.56 (deutsch mit Tausender)
"1,234.56" 1234.56 (englisch mit Tausender)
"abc" 0 (kein Parse möglich)
Infinity, NaN 0

Die Heuristik hinter Tausender-Trenner: der zuletzt auftretende Trenner (Komma oder Punkt) ist der Dezimaltrenner; alles davor wird als Tausender weggestrippt. Damit funktionieren beide Konventionen gleichzeitig — ohne dass das Skript wissen muss, welche Locale der User gerade hat.

kc.num gibt nie eine Exception

Auch bei völlig kaputter Eingabe kommt 0 zurück. Skripte, die zwischen „leerer Eingabe" und „Eingabefehler" unterscheiden müssen, prüfen vorher mit kc.isEmpty(...) oder mit einem Format-Validator.

Nachschlagen: field.<name>.num

Das Handle-Schreibweise im Detail. field.summe.num = … legt die Zahl ab wie $summe = …; gelesen wird sie wie oben beschrieben.

field.summe.num = field.wert1.num + field.wert2.num;
log.info(field.summe.num);

Der $-Alias bleibt dabei der Wert selbst (typeof $preis ist string bzw. number) — .num hängt an Text und Zahl, nicht an einem Hilfsobjekt. Vergleiche wie $land === 'DE' funktionieren deshalb unverändert.

Zahlen anzeigen: .numFormat(…)

.num liefert die Zahl zum Rechnen. .numFormat(…) liefert sie als fertigen Text zum Anzeigen — Vorzeichen, Rundung, Tausendergruppen, Dezimaltrenner, Vor- und Nachsatz in einem Aufruf:

log.info($wert1.numFormat(null, "€", 12, 2, ",", ".", 3));  // 12.345,76 €
log.info($wert2.numFormat("kg", null, 4, 0));               // kg 1234

Alle sieben Angaben sind optional; null überspringt eine Position:

Nr. Parameter Vorgabe Bedeutung
1 prefix keiner Vorsatz, mit einem Leerzeichen abgesetzt
2 suffix keiner Nachsatz, mit einem Leerzeichen abgesetzt
3 stellenInsgesamt keine Grenze Obergrenze, siehe unten
4 anzNachkommastellen was der Wert mitbringt wird kaufmännisch gerundet
5 dezimaltrenner ,
6 separatorZeichen keins ohne Angabe wird nicht gruppiert
7 separatorBlockabstand 3 Ziffern je Gruppe

stellenInsgesamt ist eine Obergrenze

Gemeint sind die Ziffern vor und nach dem Komma zusammen, ohne Trennzeichen und ohne Vorzeichen — dieselbe Zählweise wie NUMERIC(12,2) in der Datenbank. Passt der Wert nicht hinein, kommen Rauten zurück:

log.info((12345.76).numFormat(null, "€", 4, 2));  // "#### €"
log.info((0.5).numFormat(null, null, 2, 2));      // "0,50" — führende Null zählt nicht

Das ist bewusst keine stille Kürzung: eine gekappte Summe sieht richtig aus und fällt niemandem auf. 0 oder weglassen heißt „keine Grenze".

Gerechnet wird genau

Gerundet wird kaufmännisch und auf dem Dezimalwert, nicht auf einer Gleitkommazahl:

log.info("12345.755".numFormat(null, null, 0, 2));   // "12345,76"

Als double wäre 12345.755 in Wirklichkeit 12345.7549…, und dasselbe Runden ergäbe 12345,75. Genau deshalb transportiert KCForms ein number-Feld als Text — .numFormat wirft diese Genauigkeit nicht weg.

Auch als Funktion: kc.numFormat(wert, …)

Gleiche Parameter, ein Argument davor. Nötig überall dort, wo der Wert nicht aus einem deklarierten Feld kommt — Datenbank-Zeilen, CSV-Zeilen, Detail-Zeilen, ctx.params:

var zeilen = [{ kunde: "Meyer", betrag: "1234.5" },
              { kunde: "Schulz", betrag: "89012.345" }];
zeilen.forEach(function (r) {
  log.info(r.kunde + ": " + kc.numFormat(r.betrag, null, "€", 0, 2, ",", ".", 3));
});
// Meyer: 1.234,50 €
// Schulz: 89.012,35 €

Leere Werte ergeben 0 — und Bedingungen kennen numFormat nicht

Ein leeres Feld liefert "0,00 €", nicht "" — dieselbe Lesart wie bei .num. Wer stattdessen nichts anzeigen will, fragt vorher mit kc.isEmpty(...). Und: numFormat gibt es im Skript, nicht in Bedingungen (kc:visibleIf, Regeln) und nicht in kc:calc — dort stehen kc.num, kc.text, kc.bool und kc.isEmpty zur Verfügung. Die Anzeige eines Feldes stellt man über die Feld-Eigenschaften ein (kc:numberPrefix/-Suffix, Nachkommastellen, Trennzeichen), nicht über eine Bedingung.

Im Formularfeld klebt das €-Zeichen, hier nicht

Die Feldanzeige setzt €, %, ° und $ bewusst ohne Leerzeichen an den Wert. .numFormat setzt immer eines. Der Text landet in Mails, PDFs und Protokollen, wo kein Feld danebensteht — dort ist Vorhersagbarkeit mehr wert als die Übereinstimmung mit der Feldanzeige. Wer es anders will, übergibt den Nachsatz ohne Leerzeichenwunsch und baut ihn selbst an: $betrag.numFormat(null, null, 0, 2, ",", ".", 3) + "€".

Steht da nichts? .isEmpty

Die Frage „ist das Feld ausgefüllt?" beantwortet .isEmpty — direkt am Wert, ohne Klammern:

log.info("leer: " + $suchbegriff.isEmpty + " / gefüllt: " + $wert1.isEmpty);
// leer: true / gefüllt: false

Wahr ist es bei null, bei "" und bei reinen Leerzeichen — also genau dann, wenn in der Formular-Welt „nichts eingetragen" gilt. Ein Feld, das noch nie ausgefüllt wurde, steht in der Datenbank auf NULL; auch dafür ist .isEmpty wahr — ein optionales Feld wie doktorgrad ist der typische Fall:

if ($email.isEmpty) {
  field.email.error("Bitte eine E-Mail-Adresse angeben.");
}

0 und false sind nicht leer — das ist der eigentliche Zweck der ganzen Familie. Eine eingetragene Menge von 0 ist ein gültiger Wert, und false ist eine bewusste Auswahl; if ($menge) wäre bei beiden falsch, $menge.isEmpty ist es nicht.

log.info("0 ist leer: " + $wert1.isEmpty);   // false

Ohne Klammern — und warum das wichtig ist

.isEmpty ist eine Eigenschaft, keine Methode. Schreibst du $email.isEmpty(), bekommst du sofort einen Fehler — das ist gewollt. Wäre es umgekehrt eine Methode, liefe if ($email.isEmpty) still ins Falsche: Ein Funktionsobjekt ist immer truthy, die Bedingung also immer wahr. Genau die Sorte Fehler, die .isEmpty vermeiden soll.

Am Handle: field.<name>.isEmpty

Dieselbe Prüfung, dieselbe Antwort — nur über das Feld-Handle. Praktisch, wenn direkt danach eine Meldung an dasselbe Feld geht:

if (field.email.isEmpty) field.email.error('Pflichtfeld.');

Überall: kc.isEmpty(v)

Die Kurzformen gibt es nur an deklarierten Feldern und nur in Skripten. Für alles andere ist kc.isEmpty(...) die Schreibweise, und dort ist sie die einzige Möglichkeit: Zwischenergebnisse, Werte aus db.query(…), Zeilen einer Detail-Tabelle oder CSV-Datei — und Bedingungen (kc:visibleIf, Regeln) sowie kc:calc.

if (kc.isEmpty(ctx.form.email)) {
  ctx.addError('email', 'Pflichtfeld.');
}

Dieselbe Grenze wie bei .num — und derselbe Grund, .isEmpty nicht an ctx.form.<feld> zu hängen: dort ist ein nie ausgefülltes Feld null, und null.isEmpty bricht ab. Ausgeschrieben also kc.isEmpty(ctx.form.doktorgrad).

kc.text(v) und kc.bool(v)

kc.text gibt den Wert als Text zurück — außen getrimmt und nie null, also sicher für Verkettung und Vergleich. kc.bool liest die üblichen Schreibweisen als Wahrheitswert: true, "true", "1", "yes", "ja", "on" und Zahlen ungleich 0 sind wahr, alles andere falsch — dieselbe Lesart, die auch beim Schreiben in die Datenbank gilt.

if (kc.bool(ctx.form.einverstanden)) log.info(kc.text(ctx.form.ort));

Kürzer: $feld.text und $feld.bool

An einem deklarierten Feld hängen beide direkt am Wert — und ebenso am Handle und mit berechnetem Namen:

log.info($land.text + " / " + $flag.bool);        // Schweden / true
log.info(field.land.text + " / " + $["flag"].bool);

$land.text ist außen getrimmt und nie null — ein nie ausgefülltes Feld ergibt "", kein undefined. Damit sind alle fünf Zugriffe des Feld-Handles auch an der Kurzschreibweise vorhanden:

Frage Kurzform Am Handle
der Wert $betrag field.betrag.value
als Zahl $betrag.num field.betrag.num
als Anzeigetext $betrag.numFormat(…) field.betrag.numFormat(…)
steht da nichts? $betrag.isEmpty field.betrag.isEmpty
als Text $ort.text field.ort.text
als Wahrheitswert $haken.bool field.haken.bool

Außerhalb deklarierter Felder — Zwischenwerte, Datenbankzeilen, Bedingungen — bleiben kc.text(...) und kc.bool(...) die Schreibweise.

kc.text fehlte im Skript

Vor dem Stand vom 26.09.2026 gab es kc.text nur in Bedingungen — der Bedingungs- Auswerter bringt sein eigenes kc-Objekt mit. Im Skript warf derselbe Aufruf Unknown identifier: text, obwohl die Doku beide als „die gleichen Helper" führte. Jetzt sind beide Seiten gleich ausgestattet, kc.bool eingeschlossen; ein Test hält das fest.

log.debug / log.info / log.warn / log.error

Schreibt ins Skript-Protokoll:

log.info("Kunde " + ctx.form.kundennr + " gespeichert");
log.warn("Veraltetes Skript-Pattern in onSubmit");
log.error("Mahnung-Service nicht erreichbar — fallback");

Jeder Wert darf direkt hinein

Zahl, Wahrheitswert, Objekt, Array, Datum — es braucht weder + "" noch .toString():

log.info($wert1.num);            // 1234.56
log.info(0.0);                   // 0       — nicht "0.0"
log.info(ctx.form.einverstanden) // true
log.info([1, 2, 3]);             // [1,2,3]
log.info({ nr: 4711, ort: "Kiel" });
// {"nr":4711,"ort":"Kiel"}

Die Regeln dahinter sind die von JavaScript, nicht die von Java:

Wert Im Protokoll
Zahl wie in JavaScript — 5, nicht 5.0; 1234.56 bleibt 1234.56
Text unverändert, ohne Anführungszeichen
Wahrheitswert true / false
Objekt, Array als JSON — kopierbar, ab 8000 Zeichen gekürzt
Datum ISO-Zeitstempel (2026-09-29T21:04:07.940Z)
null, undefined bleiben unterscheidbar

Zusammensetzen mit + bleibt selbstverständlich richtig und ist oft lesbarer — log.info(zeilen.length + " Zeilen") sagt mehr als eine nackte Zahl. Neu ist nur, dass es kein Muss mehr ist.

Vor dem Stand vom 29.09.2026 ging ausschließlich Text

log.info(5) brach vorher mit TypeError: … Cannot convert '5' … to Java type 'java.lang.String' ab — GraalVM wandelt einen Skript-Wert nicht von sich aus in Text um. Betroffen war alles außer dem String, also auch Objekte und Wahrheitswerte. Aufgefallen ist es an der Kurzschreibweise $feld.num aus vom 29.09.2026: die liefert eine Zahl, und die ging nicht durch. Deshalb steht in älteren Beispielen dieses Handbuchs durchgängig + " Zeilen" oder .toString() — das waren Umgehungen dieser Grenze. Sie funktionieren unverändert weiter.

Dasselbe gilt für job.log(…) und job.fail(…) im Zeitplaner: auch dort landet eine Zahl jetzt als 5 in der Lauf-Historie, nicht als 5.0.

Die Ausgabe landet in logs/script.log und je Benutzer in logs/by-user/<login>.log, jede Zeile mit [form:<id>] davor — sowie in der Skript-Konsole, wenn das Skript dort gestartet wurde. Ansehen lässt sie sich unter System-Administration → Betrieb & Überwachung → System-Logs (auch live). In der Docker-Konsole (docker compose logs backend) erscheint sie bewusst nicht.

Methode Sichtbarkeit Default
debug(msg) nur sichtbar bei aktiviertem DEBUG-Level
info(msg) immer sichtbar
warn(msg) immer sichtbar, gelb in der Konsole
error(msg) immer sichtbar, rot in der Konsole

Unter System-Administration → Betrieb & Überwachung → Log-Konfiguration lässt sich der Logger kcforms.script zur Laufzeit auf DEBUG schalten — ohne Neustart.

Date.prototype.format(pattern[, locale[, zone]])

Erweiterung am Date-Prototyp — formatiert ein JavaScript-Date mit einem Java-SimpleDateFormat-Pattern:

new Date().format("dd.MM.yyyy HH:mm:ss")
// "27.04.2026 14:23:11"

new Date().format("d. MMMM yyyy", "de_DE")
// "27. April 2026"

new Date().format("EEEE, d. MMMM yyyy", "de_DE")
// "Montag, 27. April 2026"
Pattern-Token Bedeutung
dd Tag, 2-stellig
MM Monat numerisch, 2-stellig
MMMM Monatsname ausgeschrieben (lokalisiert)
yyyy Jahr 4-stellig
HH Stunde 24h, 2-stellig
mm Minute, 2-stellig
ss Sekunde, 2-stellig
EEEE Wochentag ausgeschrieben
EEE Wochentag-Kurzform
zzz Zeitzone-Kurzform

Weitere Tokens: Date.format läuft serverseitig durch java.text.SimpleDateFormat; alle dort definierten Pattern-Buchstaben funktionieren.

Welche Uhr gilt?

Ein Skript läuft auf dem Server, nicht im Browser. Welche Uhrzeit format schreibt, hängt deshalb von einer Einstellung ab:

  • System-Administration → Betrieb & Überwachung → System-Einstellungen → Zeitzone für Datums- und Zeitangaben. Ist dort eine Zeitzone gewählt (z. B. Europe/Berlin), rechnen format, Date.fromString und die eingebauten Funktionen wie getHours() in dieser Zone.
  • Ohne Einstellung gilt die Uhr des Servers. Ein Server läuft oft in der Weltzeit (UTC) — dann steht im Formular „19:38", wenn es bei dir 21:38 ist.

Für einen einzelnen Aufruf nennst du die Zone als drittes Argument:

var t = new Date(Date.UTC(2026, 6, 15, 19, 38));

log.info(t.format("HH:mm zzz", null, "UTC"));              // "19:38 UTC"
log.info(t.format("dd.MM.yyyy HH:mm", "de", "Asia/Tokyo")); // "16.07.2026 04:38"

Ein unbekannter Zonenname ist ein Fehler — das Skript bricht mit einer Meldung ab, statt still eine falsche Uhrzeit zu schreiben.

Eine geschriebene Uhrzeit bleibt, wie sie ist

Schreibt ein Skript eine Uhrzeit als Text in ein Feld, steht sie dort fest. Stellst du die Zeitzone später um, ändern sich nur neue Einträge. Wer sichergehen will, schreibt die Zone dazu: format("dd.MM.yyyy HH:mm zzz").

Locales akzeptiert beide Schreibweisen — "de_DE" und "de-DE" (IETF-Tag). Ohne Locale-Argument gilt das Server-Default.

Date.fromString(s[, pattern[, locale[, zone]]])

Parst einen Datums-String zurück in ein JS-Date:

Date.fromString("27.04.2026", "dd.MM.yyyy");
// Date object für 2026-04-27 00:00:00 lokal

Date.fromString("27. April 2026", "d. MMMM yyyy", "de_DE");
// Date object — funktioniert nur mit passender Locale

Ohne pattern erwartet Date.fromString das Format "EEE MMM dd HH:mm:ss zzz yyyy" (so wie Date.toString() ausgibt). Für eigenen Code immer ausdrücklich ein Muster angeben.

Falsches Format → klare Fehlermeldung:

IllegalArgumentException: Date.fromString: cannot parse '27/04/2026'
with pattern 'dd.MM.yyyy'

$feld — Form-Field-Aliase

Vor jedem Skript-Lauf installiert KCForms automatisch Getter/Setter-Aliase auf globalThis, damit man ctx.form.feld als $feld schreiben kann:

// Lang
ctx.form.summe = kc.num(ctx.form.wert1) + kc.num(ctx.form.wert2);

// Kurz — funktional identisch
$summe = kc.num($wert1) + kc.num($wert2);

Was bekommt einen Alias?

  • Alle Felder des Formulars (aus dem Schema) — auch solche, die gerade leer sind — sowie alle Schlüssel, die beim Start in ctx.form stehen.
  • Vier Testwerte für die Skript-Konsole: $text, $number, $date, $flag.

Was nicht aliased ist:

  • Schlüssel, die weder im Schema stehen noch beim Start in ctx.form waren (etwa ein Hilfswert, den das Skript selbst anlegt): Dafür gibt es keinen Alias — ctx.form.hilfswert verwenden.
  • Feldnamen mit Leerzeichen, Punkten, Bindestrichen — die sind keine gültigen JS-Identifier. Du erreichst sie über $['weird name'] (siehe gleich) oder ctx.form['weird name'].

Berechneter Name: $["feld"]

$beitrag1 ist ein fest getippter Bezeichner — aus einer Schleifenvariablen lässt sich so ein Name nicht zusammensetzen. Dafür gibt es die eckigen Klammern:

var summe = 0;
for (var i = 1; i <= 2; i++) {
  summe += $["wert" + i].num;
}
$["summe"] = summe;
log.info($["summe"].numFormat(null, "€", 0, 2, ",", ".", 3));   // 300,50 €

Gelesen und geschrieben wird dasselbe wie bei $feld: $["wert1"] und $wert1 sind derselbe Wert, null kommt als leerer Text an, und .num, .numFormat und .isEmpty hängen genauso daran. Dieselbe Schreibweise gibt es am Handle — field["wert" + i].value — und ausgeschrieben als ctx.form["wert" + i].

Ein Name, den es nicht gibt, bricht ab

$["beitrga1"] meldet $["beitrga1"]: Es gibt kein Datenelement mit diesem Namen. Das ist Absicht und dasselbe Verhalten wie bei der festen Schreibweise, wo $beitrga1 ein ReferenceError ist. Die dynamische Form darf nicht stiller sein als die feste: Eine Schleife, die einen Schritt zu weit läuft, würde sonst ab da mit 0 weiterrechnen.

Tolerant fragen geht trotzdem — "beitrag1" in $ ist wahr oder falsch, ohne Abbruch, und ctx.form["beitrga1"] liefert undefined.

Komfort, kein Pflichtgebrauch

Die $feld-Schreibweise ist eine Kurzform. Wer alles über ctx.form.x schreibt, macht sofort klar, woher der Wert kommt. Bei kurzen Berechnungen sind die Aliase aber angenehm.

field.<name> — Fehler an ein Feld hängen

Für jedes Feld gibt es außerdem ein Handle field.<name>: field.menge.error('…') (wie ctx.addError('menge', '…')), field.menge.hardError('…') und field.menge.value. Details in der ctx-Referenz.

Wann ist welcher Helper richtig?

Situation Helper
Eingabewerte rechnen kc.num
Pflicht-Check schreiben kc.isEmpty
Text fürs PDF erzeugen Date.format
Datum aus User-Input parsen Date.fromString
Diagnose/Audit log.info
Kurzform für ctx.form.x $x

Diese Vier sind die Bausteine, mit denen 90 % aller Form-Hooks auskommen. Alles darüber hinaus ist Spezial-API (mail, view, pdf, delivery, fs, xml).