Eksamen: REA3049-JS | Semester: Vår 2025 | Tema: Pseudokode, OOP, Caesar-chiffer, bibliotek, friluftsdatasett, skogbrann-simulering
sjekkTall(6)? Tall delelig på både 2 og 3 erstattes med "TrommeLom", bare 2 med "Tromm", bare 3 med "Lom".
| i | i % 2 | i % 3 | Hvilken gren? | Lagt til |
|---|---|---|---|---|
| 1 | 1 | 1 | else | "1 " |
| 2 | 0 | 2 | else if i%2==0 | "Tromm " |
| 3 | 1 | 0 | else if i%3==0 | "Lom " |
| 4 | 0 | 1 | else if i%2==0 | "Tromm " |
| 5 | 1 | 2 | else | "5 " |
| 6 | 0 | 0 | første if (begge) | "TrommeLom " |
En variant av FizzBuzz-problemet. Sammensetningen blir "1 Tromm Lom Tromm 5 TrommeLom".
Komposisjon er en sterk «inneholder»-relasjon der den indre delens livssyklus er bundet til den ytre helheten. Skiller seg fra aggregering (svakere — delene kan eksistere alene), assosiasjon (løs «vet om»), generalisering (arv) og avhengighet (midlertidig bruk).
Polymorfisme («mange former») betyr at samme metodenavn kan ha forskjellig oppførsel avhengig av objektets faktiske type. I JavaScript skjer dette via prototypekjeden: obj.metode() ser først etter metode på objektet selv, deretter oppover prototypekjeden.
Innkapsling skjuler implementasjonen og eksponerer kun et grensesnitt utad. I moderne JavaScript bruker man #-prefiks for private felter (#felt) eller closures for å oppnå reell innkapsling. Innkapsling gjør at implementasjonen kan endres uten å bryte koden som bruker objektet.
Algoritmen er et enkelt Caesar-chiffer tilpasset det norske alfabetet. Den forskyver hver bokstav n plasser framover i et alfabet på 29 tegn (a–å). Bokstaver som ikke er bokstaver — mellomrom, tall, tegnsetting — beholdes uendret. Manipuleringen finner posisjonen til hver bokstav og bruker modulo 29 ((posisjon + n) % 29) slik at vi pakker rundt når vi går forbi å. Med n = 3 blir a → d, og å → c.
// oppgave5b.mjs — Caesar-chiffer for norsk alfabet
const ALFABET_SMA = "abcdefghijklmnopqrstuvwxyzæøå";
const ALFABET_STORE = ALFABET_SMA.toUpperCase();
const ALFABET_LENGDE = ALFABET_SMA.length; // 29
function manipulerBokstav(bokstav, n) {
const idxSma = ALFABET_SMA.indexOf(bokstav);
if (idxSma !== -1) {
const nyPos = ((idxSma + n) % ALFABET_LENGDE + ALFABET_LENGDE) % ALFABET_LENGDE;
return ALFABET_SMA[nyPos];
}
const idxStor = ALFABET_STORE.indexOf(bokstav);
if (idxStor !== -1) {
const nyPos = ((idxStor + n) % ALFABET_LENGDE + ALFABET_LENGDE) % ALFABET_LENGDE;
return ALFABET_STORE[nyPos];
}
return bokstav; // tall, mellomrom, tegnsetting beholdes
}
export function behandleTekst(tekst, n) {
return [...tekst].map(b => manipulerBokstav(b, n)).join("");
}
// CLI-bruk: node oppgave5b.mjs "Hei på deg" 3
if (import.meta.url === `file://${process.argv[1]}`) {
const [, , tekst, nStr] = process.argv;
const n = parseInt(nStr, 10);
if (Number.isNaN(n)) {
console.error("Bruk: node oppgave5b.mjs <tekst> <n>");
process.exit(1);
}
const resultat = behandleTekst(tekst, n);
console.log(`Original: ${tekst}`);
console.log(`Kryptert: ${resultat}`);
console.log(`Verifisering: ${behandleTekst(resultat, -n)}`);
}
node oppgave5b.mjs "Hei på deg" 3 → "Khl rb gho". Den dobbelte modulo-trikset ((x % m) + m) % m er nødvendig for å håndtere negative n-verdier korrekt — JavaScripts % kan returnere negativt resultat.
Toveis assosiasjon: en Bok kan være utlånt til én Låner (0..1), og en Låner kan ha flere Bok-objekter (0..*). Begge eksisterer uavhengig av hverandre.
+----------------------+ +-------------------------+
| Bok | | Laner |
+----------------------+ +-------------------------+
| - tittel: string | 0..* | - lanerId: number |
| - forfatter: string |<-------------| - lanteBoker: Bok[] |
| - utlant: Laner|null | 0..1 +-------------------------+
+----------------------+ | + lanBok(b: Bok) |
| + visInfo() | | + leverTilbakeBok(b) |
+----------------------+ +-------------------------+
// bibliotek.mjs
export class BibliotekFeil extends Error {
constructor(melding) {
super(melding);
this.name = "BibliotekFeil";
}
}
export class Bok {
constructor(tittel, forfatter) {
this.tittel = tittel;
this.forfatter = forfatter;
this.utlant = null;
}
visInfo() {
const status = this.utlant
? `utlånt til ${this.utlant.navn} (ID ${this.utlant.lanerId})`
: "tilgjengelig";
console.log(`'${this.tittel}' av ${this.forfatter} — ${status}`);
}
}
export class Laner {
constructor(lanerId, navn) {
this.lanerId = lanerId;
this.navn = navn;
this.lanteBoker = [];
}
lanBok(bok) {
if (bok.utlant !== null) {
throw new BibliotekFeil(
`'${bok.tittel}' er allerede utlånt til ${bok.utlant.navn}.`
);
}
bok.utlant = this;
this.lanteBoker.push(bok);
}
leverTilbakeBok(bok) {
const idx = this.lanteBoker.indexOf(bok);
if (idx === -1) {
throw new BibliotekFeil(`${this.navn} har ikke lånt '${bok.tittel}'.`);
}
bok.utlant = null;
this.lanteBoker.splice(idx, 1);
}
}
// test_bibliotek.mjs
import { Bok, Laner, BibliotekFeil } from "./bibliotek.mjs";
const bok1 = new Bok("Sofies verden", "Jostein Gaarder");
const bok2 = new Bok("Naiv. Super.", "Erlend Loe");
const bok3 = new Bok("Beatles", "Lars Saabye Christensen");
const emma = new Laner(1, "Emma");
const ola = new Laner(2, "Ola");
// 1) Lån ut
emma.lanBok(bok1);
emma.lanBok(bok2);
bok1.visInfo();
console.assert(emma.lanteBoker.includes(bok1));
console.assert(bok1.utlant === emma);
// 2) Lever tilbake
emma.leverTilbakeBok(bok1);
bok1.visInfo();
console.assert(!emma.lanteBoker.includes(bok1));
console.assert(bok1.utlant === null);
// 3) Forsøk på å låne ut bok som allerede er utlånt
ola.lanBok(bok3);
try {
emma.lanBok(bok3);
throw new Error("Skulle kastet BibliotekFeil");
} catch (e) {
if (e instanceof BibliotekFeil) console.log(`OK — fanget: ${e.message}`);
else throw e;
}
// 4) Lever tilbake bok som ikke er lånt
try {
emma.leverTilbakeBok(bok3);
} catch (e) {
if (e instanceof BibliotekFeil) console.log(`OK — fanget: ${e.message}`);
}
console.log("Alle tester passert.");
Egendefinert BibliotekFeil-klasse arver fra Error. Kallende kode kan bruke instanceof BibliotekFeil for å skille bibliotek-feil fra andre feil. Begge feilstier er testet i 6c.
VR-baserte behandlingsverktøy gir nye muligheter for trygg eksponering, men reiser etiske spørsmål. Jeg vil drøfte to sentrale dilemmaer: (1) personvern og biometri, og (2) overføringsverdi versus teknologiavhengighet.
Dilemma 1: Personvern og biometri. VR-headset registrerer mer enn bilde og lyd: blikkretning, hodebevegelser, hudkonduktans, puls. For en ungdom med sosial angst innebærer dette innsamling av sensitive data om frykt-respons og kroppslige reaksjoner. Etter GDPR (artikkel 9) er dette spesielt beskyttede helseopplysninger. Hvem eier dataene — sykehuset, leverandøren, eller Emma? Brukes de bare til behandling, eller også til å trene maskinlæringsmodeller? At Emma er mindreårig (16) skjerper kravet til reelt informert samtykke. Et argument for bruk er at behandlingen kan hjelpe henne ut av en alvorlig lidelse; mot taler at innsamlede biometriske data kan følge henne resten av livet hvis de lekker.
Dilemma 2: Overføringsverdi versus teknologiavhengighet. Casen sier at Emma «føler seg tryggere i virtuelle situasjoner, men er usikker på hvordan hun vil håndtere virkelige presentasjoner». Forskning på eksponeringsterapi viser at habituering må generaliseres til reelle situasjoner. Hvis Emma blir avhengig av VR-rommet, kan teknologien forsterke unngåelsesatferd — det motsatte av målet. Etisk er det viktig at behandlingen designes med tydelig overgang til reell øvelse, og at framgang måles i hverdagslige situasjoner.
Konklusjon: VR er etisk forsvarlig dersom det kombineres med streng datakontroll, reell overføring til ekte situasjoner, og likeverdig tilgang. Uten disse rammene risikerer man både personverninngrep og at teknologien blir et komfortabelt unngåelsesrom.
// oppgave8.mjs — friluftsaktiviteter (Node.js + Chart.js for diagram)
// pip install ikke nødvendig; vi bruker innebygd fs og csv-parsing.
import { readFileSync } from "node:fs";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
// --- Enkel CSV-parser (semikolon-separert) ---
function lesCSV(filsti) {
const linjer = readFileSync(filsti, "utf-8").trim().split(/\r?\n/);
const [headerLinje, ...rest] = linjer;
const headers = headerLinje.split(";");
return rest.map(linje => {
const verdier = linje.split(";");
return Object.fromEntries(headers.map((h, i) => [h, verdier[i]]));
});
}
function aktivitetsnavn(data) {
return Object.keys(data[0]).filter(k => k !== "Fylke");
}
function totalerPerAktivitet(data) {
const aktiviteter = aktivitetsnavn(data);
const result = {};
for (const a of aktiviteter) {
result[a] = data.reduce((s, rad) => s + Number(rad[a]), 0);
}
return result;
}
function visTotaltabell(totaler) {
console.log(`\n${"Aktivitet".padEnd(28)}${"Total".padStart(10)}`);
console.log("-".repeat(38));
Object.entries(totaler)
.sort(([, a], [, b]) => b - a)
.forEach(([navn, total]) => {
console.log(navn.padEnd(28) + String(total).padStart(10));
});
}
function visFylke(data, fylke) {
const rad = data.find(r => r.Fylke.toLowerCase() === fylke.toLowerCase());
if (!rad) {
console.log(`Fylket '${fylke}' ble ikke funnet.`);
return null;
}
const aktiviteter = aktivitetsnavn(data);
const par = aktiviteter
.map(a => [a, Number(rad[a])])
.sort(([, a], [, b]) => a - b);
const total = par.reduce((s, [, v]) => s + v, 0) || 1;
console.log(`\nAktiviteter i ${rad.Fylke} (sortert stigende):`);
console.log(`${"Aktivitet".padEnd(28)}${"Antall".padStart(8)}${"Prosent".padStart(10)}`);
par.forEach(([a, v]) => {
const p = ((v / total) * 100).toFixed(1);
console.log(a.padEnd(28) + String(v).padStart(8) + `${p} %`.padStart(10));
});
return rad;
}
// 8c — Topp 3 som SVG-stolpediagram (uten eksterne bibliotek)
import { writeFileSync } from "node:fs";
function toppTreSvg(data, fylke) {
const rad = data.find(r => r.Fylke.toLowerCase() === fylke.toLowerCase());
if (!rad) return;
const aktiviteter = aktivitetsnavn(data);
const topp3 = aktiviteter
.map(a => [a, Number(rad[a])])
.sort(([, a], [, b]) => b - a)
.slice(0, 3);
const bredde = 600, hoyde = 400, padding = 60;
const maks = Math.max(...topp3.map(t => t[1]));
const stolpebredde = (bredde - padding * 2) / topp3.length - 10;
const farger = ["#2196F3", "#4CAF50", "#FFC107"];
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${bredde}" height="${hoyde}">`;
svg += `<text x="${bredde/2}" y="30" text-anchor="middle" font-size="18" font-weight="bold">Topp 3 friluftsaktiviteter i ${rad.Fylke}</text>`;
topp3.forEach(([navn, v], i) => {
const h = ((hoyde - padding * 2) * v) / maks;
const x = padding + i * (stolpebredde + 20);
const y = hoyde - padding - h;
svg += `<rect x="${x}" y="${y}" width="${stolpebredde}" height="${h}" fill="${farger[i]}"/>`;
svg += `<text x="${x + stolpebredde/2}" y="${hoyde - padding + 20}" text-anchor="middle">${navn}</text>`;
svg += `<text x="${x + stolpebredde/2}" y="${y - 6}" text-anchor="middle" font-weight="bold">${v}</text>`;
});
svg += `</svg>`;
writeFileSync(`topp3_${rad.Fylke.replace(/ /g, "_")}.svg`, svg);
console.log(`\nDiagram lagret som topp3_${rad.Fylke.replace(/ /g, "_")}.svg`);
}
async function main() {
const data = lesCSV("friluftsaktiviteter_2024.csv");
visTotaltabell(totalerPerAktivitet(data));
const rl = readline.createInterface({ input, output });
const fylke = (await rl.question("\nVelg et fylke (f.eks. Oslo): ")).trim();
rl.close();
visFylke(data, fylke);
toppTreSvg(data, fylke);
}
main();
// skogbrann.html — kjøres direkte i nettleseren
// Lagre koden under som skogbrann.html og åpne i nettleser.
<!DOCTYPE html>
<html lang="no">
<head><meta charset="utf-8"><title>Skogbrann</title></head>
<body style="background:#222;color:#eee;font-family:sans-serif;text-align:center">
<h2>Skogbrann-simulering</h2>
<canvas id="lerret" width="560" height="560"></canvas>
<script>
const RUTER = 40;
const CELLE = 14;
const P_VOKS = 0.003; // sannsynlighet for at tom celle blir tre
const P_LYN = 0.0003; // sannsynlighet for lynnedslag på tre
const TILSTAND = { TOM: 0, TRE: 1, BRANN: 2 };
const FARGER = ["#e8d7b9", "#2e7d32", "#e53935"];
class Skog {
constructor(rader, kolonner) {
this.rader = rader;
this.kolonner = kolonner;
this.celler = Array.from({length: rader},
() => Array.from({length: kolonner}, () => TILSTAND.TOM));
}
brennerNoe() {
return this.celler.flat().includes(TILSTAND.BRANN);
}
naboer(r, k) {
const result = [];
for (const [dr, dk] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
const nr = r + dr, nk = k + dk;
if (nr >= 0 && nr < this.rader && nk >= 0 && nk < this.kolonner) {
result.push([nr, nk]);
}
}
return result;
}
tick() {
const ny = this.celler.map(rad => [...rad]);
const brenner = this.brennerNoe();
for (let r = 0; r < this.rader; r++) {
for (let k = 0; k < this.kolonner; k++) {
const t = this.celler[r][k];
if (t === TILSTAND.BRANN) {
ny[r][k] = TILSTAND.TOM;
for (const [nr, nk] of this.naboer(r, k)) {
if (this.celler[nr][nk] === TILSTAND.TRE) ny[nr][nk] = TILSTAND.BRANN;
}
} else if (t === TILSTAND.TRE) {
if (Math.random() < P_LYN) ny[r][k] = TILSTAND.BRANN;
} else if (t === TILSTAND.TOM && !brenner) {
if (Math.random() < P_VOKS) ny[r][k] = TILSTAND.TRE;
}
}
}
this.celler = ny;
}
}
const ctx = document.getElementById("lerret").getContext("2d");
const skog = new Skog(RUTER, RUTER);
function tegn() {
for (let r = 0; r < skog.rader; r++) {
for (let k = 0; k < skog.kolonner; k++) {
ctx.fillStyle = FARGER[skog.celler[r][k]];
ctx.fillRect(k * CELLE, r * CELLE, CELLE, CELLE);
}
}
}
function loop() {
skog.tick();
tegn();
setTimeout(loop, skog.brennerNoe() ? 40 : 100);
}
tegn();
setTimeout(loop, 100);
</script>
</body></html>
Skog-klassen håndterer modell og logikk; tegning skjer i ren funksjon mot Canvas-konteksten — ren separasjon av modell og view.ny-rutenettet før vi skriver tilbake, slik at brannen ikke «løper» tvers over kartet på ett trinn.123456/
├── oppgave5b/oppgave5b.mjs
├── oppgave6/
│ ├── bibliotek.mjs
│ └── test_bibliotek.mjs
├── oppgave8/
│ ├── oppgave8.mjs
│ ├── friluftsaktiviteter_2024.csv
│ └── topp3_Oslo.svg
├── oppgave9/skogbrann.html
└── README.md (krev: Node.js 18+ for ESM/Top-level await)