diff --git a/käyttöliittymä.js b/käyttöliittymä.js new file mode 100644 index 0000000..8c1dfd8 --- /dev/null +++ b/käyttöliittymä.js @@ -0,0 +1,108 @@ +'use strict'; +document.getElementById('kumoa').addEventListener('click', () => { + kumoa(); + piirräKaikki(); +}); +document.getElementById('tee-uudelleen').addEventListener('click', () => { + teeUudelleen(); + piirräKaikki(); +}); + +document.getElementById('lisää-luokka-aste').addEventListener('click', () => { + suorita(tapahtumaTyypit.lisääAste); + piirräLuokat(); +}); + +function piirräKaikki() { + piirräLuokat(); +} + +function piirräLuokat() { + let vanhaLuokkaAsteLista = document.getElementById('luokka-asteet'); + let luokkaAsteLista = luoLuokkaAsteLista(); + vanhaLuokkaAsteLista.parentNode.replaceChild( + luokkaAsteLista, + vanhaLuokkaAsteLista + ); +} + +function luoLuokkaAsteLista() { + let luokkaAsteLista = document.createElement('ul'); + luokkaAsteLista.id = 'luokka-asteet'; + + for (let aste = 0; aste < luokkaAsteet.asteet.length; aste++) { + if (luokkaAsteet.asteet[aste] !== undefined) { + let luokkaAsteListassa = luoLuokkaAsteListassa(aste); + luokkaAsteLista.appendChild(luokkaAsteListassa); + } + } + + return luokkaAsteLista; +} + +function luoLuokkaAsteListassa(aste) { + let luokkaAsteListassa = document.createElement('li'); + luokkaAsteListassa.classList.add('luokka-aste'); + + luokkaAsteListassa.appendChild(luoPainike('+', () => { + suorita(tapahtumaTyypit.lisääLuokka, aste); + piirräLuokat(); + })); + luokkaAsteListassa.appendChild(luoPainike('-', () => { + if (luokkaAsteet.asteet[aste].luokat().length > 1) { + suorita(tapahtumaTyypit.poistaLuokka, aste); + } else { + suorita(tapahtumaTyypit.poistaAste, aste); + } + piirräLuokat(); + })); + + luokkaAsteListassa.appendChild(luoLuokkaLista(aste)); + + luokkaAsteListassa.appendChild(luoPainike('✎', () => { + // TODO: Älä käytä prompt():ia ja alert():ia + let vastaus = prompt('Uusi luokka-aste'); + if (vastaus === null) { + // Käytääjä painoi 'Peruuta' + return; + } + vastaus = parseInt(vastaus, 10); + if (vastaus === NaN) { + // Ei numero + alert('Luokka-asteen pitää olla numero'); + return; + } + if (vastaus < 1) { + // Luokka-aste alkaa 1:stä + alert('Luokka-asteeen pitää olla vähintään 1'); + return; + } + suorita(tapahtumaTyypit.muutaAste, aste, vastaus); + piirräLuokat(); + })); + + return luokkaAsteListassa; +} + +function luoLuokkaLista(aste) { + let luokkaLista = document.createElement('ul'); + luokkaLista.classList.add('luokat'); + + for (let luokka of luokkaAsteet.asteet[aste].luokat()) { + let luokkaListassa = document.createElement('li'); + luokkaListassa.appendChild(document.createTextNode(`${aste}${luokka}`)); + luokkaLista.appendChild(luokkaListassa); + } + + return luokkaLista; +} + +function luoPainike(teksti, funktio) { + let painike = document.createElement('input'); + painike.type = 'button'; + painike.value = teksti; + painike.addEventListener('click', funktio); + return painike; +} + +// TOOD: Käyttöliittymätestit diff --git a/lukujärjestäjä.html b/lukujärjestäjä.html index 2359524..598eb49 100644 --- a/lukujärjestäjä.html +++ b/lukujärjestäjä.html @@ -4,427 +4,35 @@ Lukujärjestäjä 0.1 -
+ + +
Luokat - -
- - -
+ +
-
- Opettajat - -
- - -
-
- -
- Tunnit - -
- - - - -
-
- -
- Lukujärjestykset - -
-
-
    -
    -
    - - + + + + + diff --git a/testit.js b/testit.js new file mode 100644 index 0000000..0bb4b09 --- /dev/null +++ b/testit.js @@ -0,0 +1,81 @@ +'use strict'; +let testit = []; + +function testi(nimi, funktio) { + testit.push([nimi, funktio]); +} + +function assertEq(konteksti, a, b) { + if (a !== b) { + throw new Error(`${konteksti}: ${a}, pitäisi olla ${b}`); + } +} + +function assertNe(konteksti, a, b) { + if (a === b) { + throw new Error(`${konteksti}: ei tulisi olla ${b}`); + } +} + +function assertRange(konteksti, a, min, max) { + if (a < min || a > max) { + throw new Error(`${konteksti}: ${a}, pitäisi olla välillä [${min}, ${max}]`); + } +} + +function assertThrow(konteksti, viesti, funktio) { + try { + funktio(); + } catch(poikkeus) { + if (poikkeus.message !== viesti) { + throw new Error(`${konteksti}: poikkeus "${poikkeus}", pitäisi olla "${viesti}"`); + } + return; + } + throw new Error(`${konteksti}: pitäisi heittää "${viesti}"`); +} + +function ajaTestit() { + // Luo otsikko ja lista testeistä sivulle + let otsikkoTeksti = document.createTextNode('Testit käynnissä…'); + let otsikko = document.createElement('h1'); + otsikko.id = 'testit-otsikko'; + otsikko.appendChild(otsikkoTeksti); + + let testilista = document.createElement('ul'); + testilista.id = 'testit-lista'; + + let body = document.getElementsByTagName('body')[0]; + body.appendChild(otsikko); + body.appendChild(testilista); + + // Aja testit + let epäonnistuneitaTestejä = 0; + for (let [nimi, funktio] of testit) { + // Lisää testi listaan + let testiListassa = document.createElement('li'); + let testiTeksti = document.createTextNode(`${nimi} …`); + testiListassa.appendChild(testiTeksti); + testilista.appendChild(testiListassa); + + try { + funktio(); + } catch (poikkeus) { + // Virhe testiä suoritettaessa — kerro virhe käyttäjälle ja päivitä + // epäonnistuneiden testien laskuria + testiTeksti.textContent = `${nimi}: ${poikkeus}`; + testiListassa.classList.add('testit-epäonnistunut'); + epäonnistuneitaTestejä++; + continue; + } + + testiTeksti.textContent = `${nimi}`; + } + + // Päivitä otsikko – onnistuivatko testit? + if (epäonnistuneitaTestejä === 0) { + otsikko.textContent = 'Testit suoritettu onnistuneesti'; + } else { + otsikko.textContent = `${epäonnistuneitaTestejä} epäonnistui`; + } +} diff --git a/tietomalli.js b/tietomalli.js new file mode 100644 index 0000000..c6d822e --- /dev/null +++ b/tietomalli.js @@ -0,0 +1,186 @@ +'use strict'; +const tapahtumaTyypit = { + lisääAste: 'lisääAste', + poistaAste: 'poistaAste', + muutaAste: 'muutaAste', + + lisääLuokka: 'lisääLuokka', + poistaLuokka: 'poistaLuokka', +}; + +class Tapahtuma { + constructor(tyyppi, argumentit) { + this.tyyppi = tyyppi; + this.argumentit = argumentit; + } +} + +let historia, tulevaisuus; +let luokkaAsteet; +alustaMalli(); + +function alustaMalli() { + historia = []; + tulevaisuus = []; + luokkaAsteet = new LuokkaAsteet(); +} + +function suorita(tyyppi, ...argumentit) { + let paluuarvo = undefined; + switch (tyyppi) { + case tapahtumaTyypit.lisääAste: + assertRange('lisääAste argumentit määrä', argumentit.length, 0, 1); + paluuarvo = luokkaAsteet.lisää(...argumentit) + break; + case tapahtumaTyypit.poistaAste: + assertEq('poistaAste argumentit määrä', argumentit.length, 1); + paluuarvo = luokkaAsteet.poista(...argumentit) + break; + case tapahtumaTyypit.muutaAste: + assertEq('muutaAste argumentit määrä', argumentit.length, 2); + luokkaAsteet.muuta(...argumentit) + break; + case tapahtumaTyypit.lisääLuokka: + assertEq('lisääLuokka argumentit määrä', argumentit.length, 1); + luokkaAsteet.asteet[argumentit[0]].lisää(); + break; + case tapahtumaTyypit.poistaLuokka: + assertEq('poistaLuokka argumentit määrä', argumentit.length, 1); + luokkaAsteet.asteet[argumentit[0]].poista(); + break; + default: + throw new Error(`tuntematon tapahtumatyyppi ${tyyppi}`); + } + historia.push(new Tapahtuma(tyyppi, argumentit)); + tulevaisuus = []; + return paluuarvo; +} + +function kumoa() { + if (historia.length === 0) { + return; + } + + // Kumoaminen tapahtuu ottamalla historia uusinta tapahtumaa lukuun + // ottamatta ja suorittamalla se siihen asti uudestaan tyhjältä mallilta + let kumottu = historia.pop(); + let uusi_tulevaisuus = tulevaisuus.concat(kumottu); + let vanha_historia = historia; + alustaMalli(); + for (let {tyyppi, argumentit} of vanha_historia) { + suorita(tyyppi, ...argumentit); + } + tulevaisuus = uusi_tulevaisuus; +} + +function teeUudelleen() { + if (tulevaisuus.length === 0) { + return; + } + + let {tyyppi, argumentit} = tulevaisuus.pop(); + // Tulevaisuus tulee tallentaa, sillä suorita() tuhoaa sen + let uusi_tulevaisuus = tulevaisuus; + suorita(tyyppi, ...argumentit); + tulevaisuus = uusi_tulevaisuus; +} + +testi('mallin alustaminen', () => { + historia = undefined; + tulevaisuus = undefined; + luokkaAsteet = undefined; + alustaMalli(); + assertNe('historia', historia, undefined); + assertNe('tulevaisuus', tulevaisuus, undefined); + assertNe('luokkaAsteet', luokkaAsteet, undefined); +}); + +testi('tapahtumahistoria', () => { + alustaMalli(); + suorita(tapahtumaTyypit.lisääAste); + suorita(tapahtumaTyypit.poistaAste, 1); + assertEq('historia.length', historia.length, 2); + assertEq('historia[0].tyyppi', historia[0].tyyppi, tapahtumaTyypit.lisääAste); + assertEq('historia[0].argumentit.length', historia[0].argumentit.length, 0); + assertEq('historia[1].tyyppi', historia[1].tyyppi, tapahtumaTyypit.poistaAste); + assertEq('historia[1].argumentit.length', historia[1].argumentit.length, 1); + assertEq('historia[1].argumentit[0]', historia[1].argumentit[0], 1); + assertThrow('poistaAste 1', 'luokka-astetta 1 ei ole olemassa', () => { + suorita(tapahtumaTyypit.poistaAste, 1); + }); + assertEq('historia.length poikkeuksen jälkeen', historia.length, 2); + alustaMalli(); +}); + +testi('kumoamiminen', () => { + alustaMalli(); + kumoa(); + suorita(tapahtumaTyypit.lisääAste); + suorita(tapahtumaTyypit.lisääLuokka, 1); + suorita(tapahtumaTyypit.lisääAste); + suorita(tapahtumaTyypit.lisääLuokka, 2); + suorita(tapahtumaTyypit.lisääLuokka, 1); + assertEq('aluksi 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'ABC'); + assertEq('aluksi 2. aste', luokkaAsteet.asteet[2].luokat().join(''), 'AB'); + kumoa(); + assertEq('kerran 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'AB'); + assertEq('kerran 2. aste', luokkaAsteet.asteet[2].luokat().join(''), 'AB'); + kumoa(); + assertEq('kahdesti 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'AB'); + assertEq('kahdesti 2. aste', luokkaAsteet.asteet[2].luokat().join(''), 'A'); + kumoa(); + assertEq('kolmesti 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'AB'); + assertEq('kolmesti 2. aste', luokkaAsteet.asteet[2], undefined); + kumoa(); + assertEq('neljästi 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'A'); + kumoa(); + assertEq('viidesti 1. aste', luokkaAsteet.asteet[1], undefined); + alustaMalli(); +}); + +testi('uudelleen tekeminen', () => { + alustaMalli(); + teeUudelleen(); + suorita(tapahtumaTyypit.lisääAste); + suorita(tapahtumaTyypit.lisääAste); + suorita(tapahtumaTyypit.lisääLuokka, 1); + suorita(tapahtumaTyypit.lisääLuokka, 2); + suorita(tapahtumaTyypit.lisääLuokka, 2); + kumoa(); + kumoa(); + kumoa(); + assertEq('aluksi 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'A'); + assertEq('aluksi 2. aste', luokkaAsteet.asteet[2].luokat().join(''), 'A'); + teeUudelleen(); + assertEq('kerran 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'AB'); + assertEq('kerran 2. aste', luokkaAsteet.asteet[2].luokat().join(''), 'A'); + teeUudelleen(); + assertEq('kahdesti 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'AB'); + assertEq('kahdesti 2. aste', luokkaAsteet.asteet[2].luokat().join(''), 'AB'); + suorita(tapahtumaTyypit.lisääLuokka, 1); + teeUudelleen(); + assertEq('kolmesti 1. aste', luokkaAsteet.asteet[1].luokat().join(''), 'ABC'); + assertEq('kolmesti 2. aste', luokkaAsteet.asteet[2].luokat().join(''), 'AB'); + alustaMalli(); +}); + +testi('asteiden käsittely', () => { + alustaMalli(); + assertEq('lisää', suorita(tapahtumaTyypit.lisääAste), 1); + suorita(tapahtumaTyypit.muutaAste, 1, 2); + assertEq('muutettua seuraava aste', luokkaAsteet.seuraavaAste(), 3); + suorita(tapahtumaTyypit.poistaAste, 2); + assertEq('lopuksi seuraava aste', luokkaAsteet.seuraavaAste(), 1); + alustaMalli(); +}); + +testi('luokkien käsittely', () => { + alustaMalli(); + suorita(tapahtumaTyypit.lisääAste); + assertEq('aluksi', luokkaAsteet.asteet[1].luokat().join(''), 'A'); + suorita(tapahtumaTyypit.lisääLuokka, 1); + assertEq('lisättyä', luokkaAsteet.asteet[1].luokat().join(''), 'AB'); + suorita(tapahtumaTyypit.poistaLuokka, 1); + assertEq('poistettua', luokkaAsteet.asteet[1].luokat().join(''), 'A'); + alustaMalli(); +}); diff --git a/tietotyypit.js b/tietotyypit.js new file mode 100644 index 0000000..6e66e42 --- /dev/null +++ b/tietotyypit.js @@ -0,0 +1,145 @@ +'use strict'; +class LuokkaAste { + #luokat = 1; + + luokat() { + // Tee lista, jossa on #luokat ensimmäistä isoa kirjainta + let luokat = []; + for (let i = 0; i < this.#luokat; i++) { + luokat.push(String.fromCharCode(65 + i)); + } + return luokat; + } + + lisää() { + this.#luokat++; + } + + poista() { + if (this.#luokat <= 1) { + throw new Error('viimeistä luokkaa ei voi poistaa asteelta'); + } + this.#luokat--; + } +} + +class LuokkaAsteet { + asteet = []; + + seuraavaAste() { + // Seuraava aste on yksi suurinta listassa jo olevaa astetta isompi, tai + // 1 jos listassa ei vielä ole asteita + let aste = 1; + for (let i = 0; i < this.asteet.length; i++) { + if (this.asteet[i] !== undefined) { + aste = i + 1; + } + } + return aste; + } + + lisää(aste) { + aste ??= this.seuraavaAste() + if (this.asteet[aste] !== undefined) { + throw new Error(`luokka-aste ${aste} on jo olemassa`); + } + let asteObjekti = new LuokkaAste(); + this.asteet[aste] = asteObjekti; + return aste; + } + + poista(aste) { + if (this.asteet[aste] === undefined) { + throw new Error(`luokka-astetta ${aste} ei ole olemassa`); + } + this.asteet[aste] = undefined; + } + + muuta(vanha, uusi) { + if (vanha === uusi) { + return; + } + // Heitä poikkeus jo ennen poistoa, jotta asteet-taulukkoon ei tule + // mitään muutoksia poikkeustilanteessa + if (this.asteet[uusi] !== undefined) { + throw new Error(`luokka-aste ${uusi} on jo olemassa`); + } + let aste = this.asteet[vanha]; + this.poista(vanha); + this.asteet[uusi] = aste; + } +} + +testi('seuraava aste', () => { + let luokkaAsteet = new LuokkaAsteet(); + assertEq('aluksi', luokkaAsteet.seuraavaAste(), 1); + luokkaAsteet.lisää(); + assertEq('asteen 1 jälkeen', luokkaAsteet.seuraavaAste(), 2); + luokkaAsteet.lisää(3); + assertEq('asteiden 1 ja 3 jälkeen', luokkaAsteet.seuraavaAste(), 4); + luokkaAsteet.lisää(); + assertEq('asteiden 1, 3, 4 jälkeen', luokkaAsteet.seuraavaAste(), 5); + luokkaAsteet.poista(4); + luokkaAsteet.poista(3); + assertEq('asteet 3 ja 4 poistettua', luokkaAsteet.seuraavaAste(), 2); + luokkaAsteet.muuta(1, 5); + assertEq('aste 1 muutettua 5:ksi', luokkaAsteet.seuraavaAste(), 6); +}); + +testi('asteiden lisääminen', () => { + let luokkaAsteet = new LuokkaAsteet(); + assertEq('1. aste', luokkaAsteet.lisää(), 1); + assertEq('2. aste', luokkaAsteet.lisää(), 2); + assertEq('3. aste', luokkaAsteet.lisää(3), 3); + assertThrow('3. aste uudelleen', 'luokka-aste 3 on jo olemassa', () => { + luokkaAsteet.lisää(3); + }); +}); + +testi('asteiden poistaminen', () => { + let luokkaAsteet = new LuokkaAsteet(); + luokkaAsteet.lisää(); + luokkaAsteet.lisää(); + luokkaAsteet.poista(1); + luokkaAsteet.poista(2); + assertThrow('3. asteen poisto', 'luokka-astetta 3 ei ole olemassa', () => { + luokkaAsteet.poista(3); + }); +}); + +testi('asteiden muuttaminen', () => { + let luokkaAsteet = new LuokkaAsteet(); + assertEq('lisääminen', luokkaAsteet.lisää(), 1); + luokkaAsteet.muuta(1, 5); + assertThrow('poisto', 'luokka-astetta 1 ei ole olemassa', () => { + luokkaAsteet.poista(1); + }); + luokkaAsteet.poista(5); +}); + +testi('luokkien lisääminen', () => { + let luokkaAsteet = new LuokkaAsteet(); + let aste = luokkaAsteet.asteet[luokkaAsteet.lisää()]; + assertEq('aluksi pituus', aste.luokat().length, 1); + assertEq('aluksi', aste.luokat()[0], 'A'); + aste.lisää(); + aste.lisää(); + assertEq('lisättyä pituus', aste.luokat().length, 3); + assertEq('lisättyä[0]', aste.luokat()[0], 'A'); + assertEq('lisättyä[1]', aste.luokat()[1], 'B'); + assertEq('lisättyä[2]', aste.luokat()[2], 'C'); +}); + +testi('luokkien poistaminen', () => { + let luokkaAsteet = new LuokkaAsteet(); + let aste = luokkaAsteet.asteet[luokkaAsteet.lisää()]; + aste.lisää(); + aste.lisää(); + aste.poista(); + aste.poista(); + assertThrow('viimeisen poisto', 'viimeistä luokkaa ei voi poistaa asteelta', () => { + aste.poista(); + }); + assertEq('poistettua pituus', aste.luokat().length, 1); + assertEq('poistettua', aste.luokat()[0], 'A'); +});