Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is loosely inspired by Keep a Changelog.

### Changed

- **Web 0.14.21, array output like in Java:** `System.out.println(zahlen)` now prints a reference such as `[I@1e3779b1` in Simulation and Programmieren, as real Java does (`[D@`, `[Z@`, `[Ljava.lang.String;@`, `[[I@`; the same array keeps its id). `Arrays.toString(…)` and `Arrays.deepToString(…)` show the content; `Arrays` needs `import java.util.Arrays;` like javac, and the Java export adds it. The variable table still shows the content. Lesson 15 „Jetzt in Java“ explains reference and `Arrays.toString` (de/en/pt).
- **Web 0.14.18, upper and lower case, exercises „Warum läuft das nicht?“:** lesson 4 gets the new step „Groß- und Kleinschreibung“ (case-sensitive: `zahl`, `Zahl` and `ZAHL` are three names) with a structure chart exercise (`zahl` and `Zahl` both declared, `ZAHL` used), a Kompetenz item and a quiz question; glossary entry case-sensitive. Lessons 4–17 and 19 each get a Java exercise „Warum läuft das nicht?“: the start code doesn't run (misspelled variable such as `Alter`/`Grenze`/`properson`, variable used before it is created or never created, `system.out`, `.Length`, `charat`), the student reads the error message, fixes it and clicks Check (de/en/pt). The Code area now reports wrong case in Java words (`system.out.println`, `System.Out`, `Println`, `math.sqrt`, `text.Length()`, `name.charat(0)`) with the right spelling, blocks Run like javac and offers the 💡 fix; the simulation says „Unbekannte Variable: Zahl. Java unterscheidet Groß- und Kleinschreibung: Meintest du zahl?“ when the name exists in another case (all 21 languages).
- **Web 0.14.17, hint on a missing line break:** when a Java exercise's output is right except that lines run together (e.g. `System.out.print` twice instead of `println`), the check now says „Fast! Es fehlt nur ein Zeilenumbruch …“ and explains print vs. println, instead of only showing a mismatch. `\n` inside an output now starts a new line, so `System.out.print("…\n")` counts like `println` (de/en/pt; all exercises).
- **Web 0.14.16, „Im Editor öffnen“ under the Java examples in lessons 4–19:** every runnable Java example in the lesson text of lessons 4–19 now has the button „Im Editor öffnen“ (like the Mia example in lesson 4), so students can run and change it in Programmieren. Not in lesson 18, whose examples are a method and its call on their own, and not in cloze texts or games (de/en/pt).
Expand Down
2 changes: 2 additions & 0 deletions web/lessons/de/15-arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ for (int i = 0; i < zahlen.length; i++) {
- Ein leeres Array mit 5 Fächern legst du mit `new int[5]` an.
- `zahlen[i]` ist das Fach mit dem Index `i`, `zahlen.length` die Länge.

Gibst du das ganze Array aus, also `System.out.println(zahlen);`, zeigt Java nicht die Zahlen. Du siehst nur einen Text wie `[I@1b6d3586`. Das ist die **Referenz**, also die Stelle, an der das Array im Speicher liegt. Den Inhalt zeigt erst `Arrays.toString(zahlen)`, das ergibt `[2, 4, 6, 8, 10]`. Dafür schreibst du ganz oben `import java.util.Arrays;`.

::: übung l15-java-noten
titel: Notensumme in Java
code: java
Expand Down
2 changes: 2 additions & 0 deletions web/lessons/en/15-arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ for (int i = 0; i < numbers.length; i++) {
- You create an empty array with 5 slots with `new int[5]`.
- `numbers[i]` is the slot with index `i`, `numbers.length` is the length.

If you output the whole array, that is `System.out.println(numbers);`, Java does not show the numbers. You only see a text like `[I@1b6d3586`. This is the **reference**, the place where the array is stored in memory. `Arrays.toString(numbers)` shows the content: `[2, 4, 6, 8, 10]`. For this you write `import java.util.Arrays;` at the very top.

::: übung l15-java-noten
titel: Sum of grades in Java
code: java
Expand Down
2 changes: 2 additions & 0 deletions web/lessons/pt/15-arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ for (int i = 0; i < numeros.length; i++) {
- Um array vazio com 5 posições cria-se com `new int[5]`.
- `numeros[i]` é a posição com o índice `i`, `numeros.length` é o comprimento.

Se mostrares o array inteiro, ou seja `System.out.println(numeros);`, o Java não mostra os números. Só vês um texto como `[I@1b6d3586`. É a **referência**, o sítio onde o array está guardado na memória. O conteúdo só aparece com `Arrays.toString(numeros)`, que dá `[2, 4, 6, 8, 10]`. Para isso escreves `import java.util.Arrays;` logo no início.

::: übung l15-java-noten
titel: Soma das notas em Java
code: java
Expand Down
4 changes: 2 additions & 2 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "visustruct-web",
"private": true,
"version": "0.14.20",
"version": "0.14.21",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
12 changes: 8 additions & 4 deletions web/src/core/codeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,27 +1095,31 @@ function documentContainsJavaInput(doc: VSDocument): boolean {
return found
}

const arraysImport = 'import java.util.Arrays;'

/**
* Imports für Dateien: `Files`/`Path` aus `java.nio.file`, `File`, `PrintWriter`, `FileReader` und `BufferedReader` aus
* `java.io`, dazu `java.util.Scanner` für einen Scanner auf einer Datei.
* `java.io`, dazu `java.util.Scanner` für einen Scanner auf einer Datei und `java.util.Arrays` für `Arrays.toString(…)`.
*/
export function fileImports(doc: VSDocument): string[] {
let nio = false
let io = false
let scanner = false
let arrays = false
walkElements(doc.elements, (e) => {
for (const l of e.textLines) {
if (/\bArrays\s*\./.test(l)) arrays = true
if (/\bFiles\s*\./.test(l)) nio = true
if (/\bnew\s+(?:File|PrintWriter|FileReader|FileWriter|BufferedReader)\s*\(/.test(l)) io = true
if (/\bnew\s+Scanner\s*\(\s*new\s+File\b/.test(l)) scanner = true
}
})
return [...(io ? ['import java.io.*;'] : []), ...(nio ? ['import java.nio.file.*;'] : []), ...(scanner ? ['import java.util.Scanner;'] : [])]
return [...(io ? ['import java.io.*;'] : []), ...(nio ? ['import java.nio.file.*;'] : []), ...(scanner ? ['import java.util.Scanner;'] : []), ...(arrays ? [arraysImport] : [])]
}

/** Arbeitet das Programm mit Dateien? Dann kann eine IOException kommen, und Java verlangt `throws` an der Methode. */
export function documentReadsFiles(doc: VSDocument): boolean {
return fileImports(doc).length > 0
return fileImports(doc).some((i) => i !== arraysImport)
}

function documentContainsJavaMathRandom(doc: VSDocument): boolean {
Expand Down Expand Up @@ -1804,7 +1808,7 @@ export function generateJavaClassFile(
const scannerField = methods.length > 0 && (hasInput || methodInput)
// `Files.readString` kann eine IOException werfen; Java verlangt dann `throws` an der Methode.
const imports = [...new Set([document, ...methodDocs.map((m) => m.d)].flatMap(fileImports))]
const files = imports.length > 0
const files = imports.some((i) => i !== arraysImport)
if ((hasInput || methodInput) && !imports.includes('import java.util.Scanner;')) imports.push('import java.util.Scanner;')
for (const line of imports) out += line + '\n'
if (imports.length) out += '\n'
Expand Down
7 changes: 4 additions & 3 deletions web/src/core/javaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,15 @@ class Parser {

/**
* Wie javac: `Scanner` ohne `import java.util.Scanner;` (oder `java.util.*`) ist ein Fehler, ebenso `Files` und `Path` ohne
* `java.nio.file` und `File`, `PrintWriter`, `FileReader`, `FileWriter`, `BufferedReader` ohne `java.io`.
* `java.nio.file`, `File`, `PrintWriter`, `FileReader`, `FileWriter`, `BufferedReader` ohne `java.io` und `Arrays` ohne
* `java.util.Arrays`.
*/
checkImports() {
const io = ['File', 'PrintWriter', 'FileReader', 'FileWriter', 'BufferedReader', 'IOException', 'FileNotFoundException'].map((n) => [n, 'java.io'])
for (const [name, pkg] of [['Scanner', 'java.util'], ['Files', 'java.nio.file'], ['Path', 'java.nio.file'], ['Paths', 'java.nio.file'], ...io]) {
for (const [name, pkg] of [['Scanner', 'java.util'], ['Arrays', 'java.util'], ['Files', 'java.nio.file'], ['Path', 'java.nio.file'], ['Paths', 'java.nio.file'], ...io]) {
const known = this.imports.some((i) => i === `import${pkg}.${name};` || i === `import${pkg}.*;`)
// `Files.readString(…)`: nur als Klasse vor einem Punkt, damit eine Variable `path` o. Ä. nicht zählt.
const use = this.toks.find((t, k) => t.kind === 'id' && t.text === name && (pkg !== 'java.nio.file' || this.toks[k + 1]?.text === '.'))
const use = this.toks.find((t, k) => t.kind === 'id' && t.text === name && (name !== 'Arrays' && pkg !== 'java.nio.file' || this.toks[k + 1]?.text === '.'))
if (!known && use) {
this.problem('web_parse_missing_import', use, pkg === 'java.util' ? `import ${pkg}.${name};` : `import ${pkg}.*;`)
return
Expand Down
53 changes: 51 additions & 2 deletions web/src/core/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,38 @@ function elementDefault(type: SimValueType): number | string | boolean {
return type === 'string' ? '' : type === 'boolean' ? false : 0
}

/**
* Was `System.out.println(zahlen)` in Java ausgibt: nicht den Inhalt, sondern Typ und Kennung der Referenz,
* z. B. `[I@1b6d3586`. Den Inhalt zeigt erst `Arrays.toString(zahlen)`. Die Kennung bleibt für dasselbe Array gleich.
*/
const arrayIds = new WeakMap<object, string>()
let nextArrayId = 0

export function javaReference(v: SimValue): string {
const code = (t: SimValueType) => ({ int: 'I', double: 'D', boolean: 'Z', string: 'Ljava.lang.String;' })[t]
const prefix =
v.kind === 'grid' ? '[[' + code(v.elementType)
: v.kind === 'intArray' ? '[I'
: v.kind === 'doubleArray' ? '[D'
: v.kind === 'booleanArray' ? '[Z'
: v.kind === 'stringArray' ? '[Ljava.lang.String;'
: undefined
if (!prefix || !('value' in v)) return describeValue(v)
const key = v.value as object
let id = arrayIds.get(key)
if (!id) {
id = ((Math.imul(++nextArrayId, 0x9e3779b1) >>> 0) & 0x7fffffff).toString(16)
arrayIds.set(key, id)
}
return prefix + '@' + id
}

/** Ein Fach wurde geändert und das Array dabei kopiert: Es ist in Java aber dasselbe Array, also dieselbe Kennung. */
function keepArrayId(from: object, to: object): void {
const id = arrayIds.get(from)
if (id) arrayIds.set(to, id)
}

function isArrayValue(v: SimValue): boolean {
return v.kind === 'intArray' || v.kind === 'doubleArray' || v.kind === 'stringArray' || v.kind === 'booleanArray' || v.kind === 'grid'
}
Expand Down Expand Up @@ -1938,7 +1970,10 @@ export class SimulationEngine {
}
}

/** `Integer.parseInt(…)`, `Double.parseDouble(…)`, `String.valueOf(…)`, `Files.readString(Path.of(…))`. */
/**
* `Integer.parseInt(…)`, `Double.parseDouble(…)`, `String.valueOf(…)`, `Files.readString(Path.of(…))`,
* `Arrays.toString(…)` und `Arrays.deepToString(…)`.
*/
private evaluateStaticCall(text: string, call: { target: string; method: string; args: string[] }): SimValue | undefined {
const one = () => {
if (call.args.length !== 1) throw invalid(text)
Expand All @@ -1964,6 +1999,17 @@ export class SimulationEngine {
}
case 'String.valueOf':
return { kind: 'string', value: this.evaluateOutputExpression(one()) }
case 'Arrays.toString':
case 'Arrays.deepToString': {
const array = this.state.variables.get(one().trim())
if (!array || !isArrayValue(array)) throw invalid(text)
// `Arrays.toString` zeigt bei einem 2D-Array nur die Referenzen der Zeilen, `deepToString` alles.
if (array.kind === 'grid' && call.method === 'toString') {
const rows = array.value.map((row) => javaReference({ kind: `${array.elementType}Array`, value: row } as SimValue))
return { kind: 'string', value: '[' + rows.join(', ') + ']' }
}
return { kind: 'string', value: describeValue(array) }
}
case 'Files.readString': {
const path = /^(?:Path\s*\.\s*of|Paths\s*\.\s*get)\s*\(\s*("(?:[^"\\]|\\.)*")\s*\)$/u.exec(one().trim())
if (!path) throw invalid(text)
Expand Down Expand Up @@ -2076,6 +2122,7 @@ export class SimulationEngine {
if (raw === undefined || isArrayValue(value)) throw invalid(trimmed)
const rows = old.value.map((row) => [...row])
rows[indices[0]][indices[1]] = raw as number | string | boolean
keepArrayId(old.value, rows)
this.state.variables.set(access.arrayName, { ...old, value: rows })
return
}
Expand All @@ -2095,6 +2142,7 @@ export class SimulationEngine {
}
if (index >= (updated.value as unknown[]).length) throw invalid(trimmed)
;(updated.value as unknown[])[index] = value.value
keepArrayId(old.value, updated.value)
this.state.variables.set(access.arrayName, updated)
}

Expand Down Expand Up @@ -2122,7 +2170,8 @@ export class SimulationEngine {
else sum = sum.kind === 'int' && value.kind === 'int' ? int(sum.value + value.value) : dbl((sum as { value: number }).value + (value as { value: number }).value)
continue
}
text = (text ?? (sum ? describeValue(sum) : '')) + describeValue(value)
// Ein ganzes Array gibt Java nur als Referenz aus, z. B. `[I@1b6d3586`.
text = (text ?? (sum ? describeValue(sum) : '')) + javaReference(value)
sum = undefined
}
return text ?? (sum ? describeValue(sum) : '')
Expand Down
7 changes: 7 additions & 0 deletions web/test/javaParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ describe('Java tippen → Struktogramm', () => {
expect(ask(fach + 'note[i] = scanner.nextInt();').request?.prompt).toBe('note[i]:')
})

it('Arrays ohne import ist ein Fehler, wie bei javac', () => {
const keys = (src: string) => parse(src).problems.map((p) => p.key)
expect(keys('int[] a = {1, 2};\nSystem.out.println(Arrays.toString(a));')).toEqual(['web_parse_missing_import'])
expect(keys('import java.util.Arrays;\nint[] a = {1, 2};\nSystem.out.println(Arrays.toString(a));')).toEqual([])
expect(keys('int[] a = {1, 2};\nSystem.out.println(a);')).toEqual([])
})

it('Aufruf eines Unterprogramms aus einem anderen Reiter', () => {
expect(parse('int y = quadrat(4);', new Set(['quadrat'])).elements[0].type).toBe(ElementType.aufruf)
expect(parse('int y = quadrat(4);').elements[0].type).toBe(ElementType.anweisung)
Expand Down
30 changes: 29 additions & 1 deletion web/test/simulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,42 @@ describe('Simulation', () => {
expect(engine.message).toBe('Simulation finished.')
})

it('gibt ein ganzes Array wie Java als Referenz aus, den Inhalt erst mit Arrays.toString', () => {
const state = runSimulation(
doc([
stmt('int[] zahlenreihe = {1, 2, 3}'),
stmt('String[] namen = {"Ada", "Bob"}'),
stmt('double[][] t = new double[2][2]'),
stmt('output: zahlenreihe'),
stmt('zahlenreihe[0] = 9'),
stmt('output: "Reihe: " + zahlenreihe'),
stmt('output: namen'),
stmt('output: t'),
stmt('output: Arrays.toString(zahlenreihe)'),
stmt('output: Arrays.toString(namen)'),
stmt('output: Arrays.deepToString(t)'),
stmt('output: Arrays.toString(t)'),
]),
)
const [ref, refAgain, names, grid, ...rest] = state.outputLines
expect(ref).toMatch(/^\[I@[0-9a-f]+$/u)
expect(refAgain).toBe('Reihe: ' + ref)
expect(names).toMatch(/^\[Ljava\.lang\.String;@[0-9a-f]+$/u)
expect(grid).toMatch(/^\[\[D@[0-9a-f]+$/u)
expect(rest.slice(0, 3)).toEqual(['[9, 2, 3]', '[Ada, Bob]', '[[0.0, 0.0], [0.0, 0.0]]'])
expect(rest[3]).toMatch(/^\[\[D@[0-9a-f]+, \[D@[0-9a-f]+\]$/u)
// Die Variablentabelle zeigt weiter den Inhalt.
expect(state.variables.get('zahlenreihe')).toEqual({ kind: 'intArray', value: [9, 2, 3] })
})

it('Werte und Arrays', () => {
const state = runSimulation(
doc([
stmt('double d = 7.0 / 2'),
stmt('int[] a = {3, 4, 5}'),
stmt('a[1] = a[0] * 10'),
stmt('String s = "x"'),
stmt('print: "len=" + a.length + " a=" + a + " d=" + d'),
stmt('print: "len=" + a.length + " a=" + Arrays.toString(a) + " d=" + d'),
stmt('d += 1'),
]),
)
Expand Down
Loading
Loading