275 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			275 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const taulut = {
 | |
| 	luokat: 'luokat',
 | |
| 	opettajat: 'opettajat',
 | |
| 	tilat: 'tilat',
 | |
| 	tunnit: 'tunnit',
 | |
| };
 | |
| 
 | |
| class Transaktio {
 | |
| 	peruttu = false;
 | |
| 
 | |
| 	muutokset = [];
 | |
| 
 | |
| 	constructor(tietokanta) {
 | |
| 		this.tietokanta = tietokanta;
 | |
| 		this.seuraavaId = tietokanta.seuraavaId;
 | |
| 		this.taulut = new Map;
 | |
| 		for (const taulu of tietokanta.taulut.keys()) {
 | |
| 			this.taulut.set(taulu, new Map);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	peru() {
 | |
| 		this.peruttu = true;
 | |
| 	}
 | |
| 
 | |
| 	hae(taulu, id) {
 | |
| 		if (!this.taulut.has(taulu)) {
 | |
| 			throw new Error(`ei taulua ${taulu}`);
 | |
| 		}
 | |
| 		if (this.taulut.get(taulu).has(id)) {
 | |
| 			return this.taulut.get(taulu).get(id);
 | |
| 		} else {
 | |
| 			return this.tietokanta.taulut.get(taulu).get(id);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	lisää(taulu, sisältö) {
 | |
| 		if (this.peruttu) {
 | |
| 			throw new Error(`yritys lisätä rivi perutussa transaktiossa`);
 | |
| 		}
 | |
| 		if (!this.taulut.has(taulu)) {
 | |
| 			throw new Error(`ei taulua ${taulu}`);
 | |
| 		}
 | |
| 		const id = this.seuraavaId++;
 | |
| 		this.muutokset.push({
 | |
| 			taulu,
 | |
| 			id,
 | |
| 			uusi: sisältö,
 | |
| 		});
 | |
| 		this.taulut.get(taulu).set(id, sisältö);
 | |
| 		return id;
 | |
| 	}
 | |
| 
 | |
| 	poista(taulu, id) {
 | |
| 		if (this.peruttu) {
 | |
| 			throw new Error(`yritys poistaa rivi perutussa transaktiossa`);
 | |
| 		}
 | |
| 		const vanha = this.hae(taulu, id);
 | |
| 		if (vanha === undefined) {
 | |
| 			throw new Error(`ei riviä ${id} taulussa ${taulu}`);
 | |
| 		}
 | |
| 		this.muutokset.push({
 | |
| 			taulu,
 | |
| 			id,
 | |
| 			vanha,
 | |
| 		});
 | |
| 		this.taulut.get(taulu).set(id, undefined);
 | |
| 	}
 | |
| 
 | |
| 	suodata(taulu, suodatin) {
 | |
| 		if (!this.taulut.has(taulu)) {
 | |
| 			throw new Error(`ei taulua ${taulu}`);
 | |
| 		}
 | |
| 		const suodatetut = [];
 | |
| 		for (const [id, sisältö] of this.taulut.get(taulu)) {
 | |
| 			// Jos sisältö on undefined, rivi on poistettu, eikä sitä tule ottaa
 | |
| 			// huomioon suodatettaessa
 | |
| 			if (sisältö !== undefined && suodatin(sisältö)) {
 | |
| 				suodatetut.push(id);
 | |
| 			}
 | |
| 		}
 | |
| 		for (const [id, sisältö] of this.tietokanta.taulut.get(taulu)) {
 | |
| 			// Älä huomio rivejä, jotka löytyvät transaktion tauluista. Ne on
 | |
| 			// joko käsitelty jo edellisessä silmukassa (jos ne on päivitetty)
 | |
| 			// tai niitä ei tulisi käsitellä ollenkaan (jos ne on poistettu).
 | |
| 			if (!this.taulut.get(taulu).has(id) && suodatin(sisältö)) {
 | |
| 				suodatetut.push(id);
 | |
| 			}
 | |
| 		}
 | |
| 		return suodatetut;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| class Tietokanta {
 | |
| 	seuraavaId = 0;
 | |
| 	taulut = new Map;
 | |
| 
 | |
| 	historia = [];
 | |
| 
 | |
| 	static serialisoidusta(serialisoitu) {
 | |
| 		const parsittu = JSON.parse(serialisoitu);
 | |
| 		const tietokanta = new this;
 | |
| 		tietokanta.seuraavaId = parsittu.seuraavaId;
 | |
| 		const muutokset = [];
 | |
| 		for (const taulu in parsittu.taulut) {
 | |
| 			for (let id in parsittu.taulut[taulu]) {
 | |
| 				id = Number.parseInt(id);
 | |
| 				const sisältö = parsittu.taulut[taulu][id];
 | |
| 				tietokanta.taulut.get(taulu).set(id, sisältö);
 | |
| 				muutokset.push({taulu, id, uusi: sisältö});
 | |
| 			}
 | |
| 		}
 | |
| 		return [tietokanta, muutokset];
 | |
| 	}
 | |
| 
 | |
| 	constructor() {
 | |
| 		for (let taulu in taulut) {
 | |
| 			this.taulut.set(taulu, new Map);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	transaktio(funktio) {
 | |
| 		const transaktio = new Transaktio(this);
 | |
| 		funktio(transaktio);
 | |
| 		return [this, this.suorita(transaktio)];
 | |
| 	}
 | |
| 
 | |
| 	suorita(transaktio) {
 | |
| 		if (transaktio.peruttu || transaktio.muutokset.length === 0) {
 | |
| 			return [];
 | |
| 		}
 | |
| 
 | |
| 		// Varmista, että invariantit ovat yhä totta
 | |
| 		for (const {taulu, id, vanha, uusi} of transaktio.muutokset) {
 | |
| 			if (uusi === undefined && taulu !== taulut.tunnit) {
 | |
| 				// Poistettu luokka, opettaja tai tila ei ole tunnin käytössä
 | |
| 				const roikkuvat = transaktio.suodata(taulut.tunnit, (tunti) => {
 | |
| 					if (taulu === taulut.luokat) {
 | |
| 						return tunti.luokat.includes(id);
 | |
| 					} else if (taulu === taulut.opettajat) {
 | |
| 						return tunti.opettajat.includes(id);
 | |
| 					} else if (taulu === taulut.tilat) {
 | |
| 						return tunti.tilat.includes(id);
 | |
| 					} else {
 | |
| 						throw new Error(`Ei-tunnettu taulu ${taulu}`);
 | |
| 					}
 | |
| 				});
 | |
| 				if (roikkuvat.length !== 0) {
 | |
| 					throw new Error(`Yritetty poistaa ${taulu}:${id}, joka on ${roikkuvat} käytössä`);
 | |
| 				}
 | |
| 			} else if (taulu === taulut.tunnit && uusi !== undefined) {
 | |
| 				// Uusi tunti käyttää vain olemassaolevia luokkia, opettajia ja
 | |
| 				// tiloja
 | |
| 				for (const luokka of uusi.luokat) {
 | |
| 					if (transaktio.hae(taulut.luokat, luokka) === undefined) {
 | |
| 						throw new Error(`Yritetty luoda tunti ${id} olemattomalla luokalla ${luokka}`);
 | |
| 					}
 | |
| 				}
 | |
| 				for (const opettaja of uusi.opettajat) {
 | |
| 					if (transaktio.hae(taulut.opettajat, opettaja) === undefined) {
 | |
| 						throw new Error(`Yritetty luoda tunti ${id} olemattomalla opettajalla ${opettaja}`);
 | |
| 					}
 | |
| 				}
 | |
| 				for (const tila of uusi.tilat) {
 | |
| 					if (transaktio.hae(taulut.tilat, tila) === undefined) {
 | |
| 						throw new Error(`Yritetty luoda tunti ${id} olemattomalla tilalla ${tila}`);
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Suorita muutokset
 | |
| 		for (const {taulu, id, uusi} of transaktio.muutokset) {
 | |
| 			if (uusi !== undefined) {
 | |
| 				this.taulut.get(taulu).set(id, uusi);
 | |
| 			} else {
 | |
| 				this.taulut.get(taulu).delete(id);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		this.historia.push({
 | |
| 			muutokset: transaktio.muutokset,
 | |
| 			idMuutos: transaktio.seuraavaId - this.seuraavaId,
 | |
| 		});
 | |
| 		this.seuraavaId = transaktio.seuraavaId;
 | |
| 		return transaktio.muutokset;
 | |
| 	}
 | |
| 
 | |
| 	kumoa() {
 | |
| 		if (this.historia.length === 0) {
 | |
| 			return [this, []];
 | |
| 		}
 | |
| 		const {muutokset, idMuutos} = this.historia.pop();
 | |
| 		this.seuraavaId -= idMuutos;
 | |
| 		const kumotut = [];
 | |
| 		for (const {taulu, id, vanha, uusi} of muutokset) {
 | |
| 			if (vanha !== undefined) {
 | |
| 				this.taulut.get(taulu).set(id, vanha);
 | |
| 			} else {
 | |
| 				this.taulut.get(taulu).delete(id);
 | |
| 			}
 | |
| 			kumotut.push({
 | |
| 				taulu,
 | |
| 				id,
 | |
| 				vanha: uusi,
 | |
| 				uusi: vanha,
 | |
| 			});
 | |
| 		}
 | |
| 		return [this, kumotut];
 | |
| 	}
 | |
| 
 | |
| 	hae(taulu, id) {
 | |
| 		if (!this.taulut.has(taulu)) {
 | |
| 			throw new Error(`ei taulua ${taulu}`);
 | |
| 		}
 | |
| 		return this.taulut.get(taulu).get(id);
 | |
| 	}
 | |
| 
 | |
| 	järjestyksessä(taulu, järjestys) {
 | |
| 		if (!this.taulut.has(taulu)) {
 | |
| 			throw new Error(`ei taulua ${taulu}`);
 | |
| 		}
 | |
| 		const taulukko = Array.from(this.taulut.get(taulu).entries());
 | |
| 		taulukko.sort(([xId, x], [yId, y]) => {
 | |
| 			const vertaus = järjestys(x, y);
 | |
| 			if (vertaus < 0 || vertaus > 0) {
 | |
| 				return vertaus;
 | |
| 			} else {
 | |
| 				return xId - yId;
 | |
| 			}
 | |
| 		});
 | |
| 		return taulukko.map(([id, _]) => id);
 | |
| 	}
 | |
| 
 | |
| 	suodata(taulu, suodatin) {
 | |
| 		if (!this.taulut.has(taulu)) {
 | |
| 			throw new Error(`ei taulua ${taulu}`);
 | |
| 		}
 | |
| 		const suodatetut = [];
 | |
| 		for (const [id, sisältö] of this.taulut.get(taulu)) {
 | |
| 			if (suodatin(sisältö)) {
 | |
| 				suodatetut.push(id);
 | |
| 			}
 | |
| 		}
 | |
| 		return suodatetut;
 | |
| 	}
 | |
| 
 | |
| 	serialisoi() {
 | |
| 		return JSON.stringify(this, (avain, arvo) => {
 | |
| 			if (avain === 'historia') {
 | |
| 				return undefined;
 | |
| 			}
 | |
| 			if (arvo instanceof Map) {
 | |
| 				return Object.fromEntries(arvo.entries());
 | |
| 			}
 | |
| 			return arvo;
 | |
| 		});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| function tallennaTietokanta(tietokanta) {
 | |
| 	window.localStorage.setItem('tietokanta', tietokanta.serialisoi());
 | |
| }
 | |
| 
 | |
| function lataaTietokanta() {
 | |
| 	const serialisoitu = window.localStorage.getItem('tietokanta');
 | |
| 	if (serialisoitu === null) {
 | |
| 		return;
 | |
| 	}
 | |
| 	let [tietokanta, muutokset] = Tietokanta.serialisoidusta(serialisoitu);
 | |
| 	_tietokanta = tietokanta;
 | |
| 	suorita([tietokanta, muutokset]);
 | |
| }
 |