Kirjautuminen

Haku

Tehtävät

Koodit: Java: Yksinkertainen laskin

Kirjoittaja: fergusq

Kirjoitettu: 19.06.2013 – 21.07.2013

Tagit: teksti, koodi näytille, vinkki

Merkkijonojen parsiminen on hyvä taito osata, joten väsäsin tämän koodivinkin, joka parsii yksinkertaisia laskulausekkeita ja siinä samalla laskee niiden arvon.

Laskin koostuu lekseristä ja jäsentimestä eli parserista.

Lekseri yksinkertaisesti lukee merkkejä ja muodostaa niistä listan, jossa peräkkäiset numerot on liitetty yhteen ja muut merkit ovat omissa merkkijonoissaan. Näitä merkkijonoja kutsutaan englannin kielessä nimellä "token".

Tällainen toiminta riittää yksinkertaisen laskimen toteutukseen. Ohjelmointikielien parsimiseen tarkoitetut lekserit osaavat myös erotella kahden merkin operaattorit (kuten pienempi tai yhtäsuuri <=), sanat ja avainsanat.

Jäsennin (engl. parser) tutkii tokeneita ja jäsentelee ne syntaksin mukaan. Tämän laskimen parseri on nimeltään englanniksi "recursive descend parser". Se tarkoittaa, että parseri on järjestelty funktioihin, ja jokainen funktio jäsentelee yhden lausekkeen. Funktiot voivat kutsua toisiaan rekursiivisesti. Laskimessa rekursiota käytetään funktiossa "jäsennäJaLaskeNumeroTaiSulut", joka kutsuu funktiota "jäsennäJaLaskePlusMinus".

Esimerkiksi syntaksipuu lausekkelle 2*3+10/(1+2):

2 * 3   +  10    / (1 + 2)     Selitys                        Funktio laskimen koodissa
| | |   |   |    |  | | |      L = Luku                     / jäsennäJaLaskeNumeroTaiSulut
L | L   |   |    |  L | L      Y = Yhteen tai vähennyslasku / jäsennäJaLaskePlusMinus
\ | /   |   |    |  | | |      K = Kerto tai jakolasku      / jäsennäJaLaskeKertoJako
 \|/    |   |    |  K | K
  K     |   |    |   \|/
  |     |   |    |    Y
  |     |   |    |    |
  |     |   L    |    L
  |     |    \   |   /
  |     |     \  |  /
  |     |      \ | /
  |     |       \|/
  |     |        K
  |     |       /
  |     |      /
  |     |     /
   \    |    /
    \   |   /
     \  |  /
      \ | /
       \|/
        Y

Parseri kutsuu ensin funktiota Y. Funktio Y kutsuu funktiota K, lukee operaattorin ja kutsuu taas funktiota K. Funktio K toimii samalla tavalla, kutsuu funktiota L, lukee operaattorin ja kutsuu taas funktiota L. Rekursiivinen laskeutuva jäsennin on yleinen valinta, koska se on yksinkertainen ja sen toimintaa on helppo ymmärtää.

Laskimen parseri laskee laskuja yhtäaikaa parsittaessa. Tämä riittää laskimeen, mutta ohjelmointikielien parserit yleensä vain muuntavat tekstin johonkin toiseen muotoon, jota on helppo tulkata tai kääntää.

Tässä on koodi. "lueTokenit" on lekseri ja muut funktiot liittyvät parseriin.

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Laskin {

	public static void main(String[] args) {
		System.out.print("Syötä lasku: ");

		List<String> tokenit = new ArrayList<>();

		// Lue syöte lekseri-funktiolla
		try {
			lueTokenit(tokenit);
		} catch (IOException e) {
			e.printStackTrace();
		}

		// Kutsuu parseria, joka laskee laskun parsimisen lomassa
		System.out.println(jäsennäJaLaskePlusMinus(tokenit));
	}

	/**
	 * Lekserifunktio, erottelee numerot ja operaattorit toisistaan.
	 * @throws IOException
	 */
	public static void lueTokenit(List<String> tokenit) throws IOException {

		String luku = "";

		while (true) {
			// Lue seuraava merkki
			char seuraavaMerkki = (char) System.in.read();

			// Ohita välilyönnit
			while (seuraavaMerkki == ' ' | seuraavaMerkki == '\t')
				seuraavaMerkki = (char) System.in.read();

			// Jos seuraava merkki on rivinvaihto
			if (seuraavaMerkki == '\n') {

				// Jos muuttujassa "luku" on luku
				// lisää token-listaan
				if (luku.length() != 0) {
					tokenit.add(luku);
					luku = "";
				}

				// Käyttäjän syöttämä rivi on loppu, poistu loopista
				break;
			}

			// Jos merkki on numero, lisää se "luku" -muuttujaan
			if (Character.isDigit(seuraavaMerkki)) {
				luku += seuraavaMerkki;
			}

			// Muulloin merkki on operaattori
			else {

				// Jos muuttujassa "luku" on luku
				// lisää token-listaan
				if (luku.length() != 0) {
					tokenit.add(luku);
					luku = "";
				}

				// Lisää merkki token-listaan
				tokenit.add(""+seuraavaMerkki);

			}
		}
	}

	/**
	 * Lukee token-listasta seuraavan numeron
	 * Tulostaa virheen ja poistuu ohjelmasta jos
	 * seuraava tokeni ei ole numero
	 */
	public static int seuraavaNumero(List<String> tokenit) {
		String seuraava = tokenit.remove(0);

		try {
			return Integer.parseInt(seuraava);
		} catch (NumberFormatException ex) {
			// Tokeni ei ole numero!
			System.err.println(seuraava + " ei ole numero!");
			System.exit(0);
		}

		// Ohjelman ei koskaan pitäisi päästä tänne
		return 0;
	}

	/**
	 * Jos seuraava tokeni ei ole tietty, tulosta virheviesti ja poistu ohjelmasta
	 */
	public static void odota(List<String> tokenit, String odotettuToken) {
		String seuraava = tokenit.remove(0);

		if (!seuraava.equals(odotettuToken)) {
			System.err.println("odotettiin '" + seuraava + "':a");
			System.exit(0);
		}

	}

	/**
	 * Jäsennä ja laske yhteen- ja vähennyslaskut
	 */
	private static int jäsennäJaLaskePlusMinus(List<String> tokenit) {
		// laske edessä oleva numero tai kertolasku
		int numero = jäsennäJaLaskeKertoJako(tokenit);

		// Lue + ja - operaattorit
		while (tokenit.size() != 0 && Arrays.asList("+", "-").contains(tokenit.get(0))) {
			switch (tokenit.remove(0)) {
			case "+":
				numero += jäsennäJaLaskeKertoJako(tokenit);
				break;
			case "-":
				numero -= jäsennäJaLaskeKertoJako(tokenit);
				break;
			}
		}

		return numero;
	}

	/**
	 * Jäsennä ja laske kerto- ja jakolaskut
	 */
	private static int jäsennäJaLaskeKertoJako(List<String> tokenit) {
		// laske edessä oleva lauseke
		int numero = jäsennäJaLaskeNumeroTaiSulut(tokenit);

		// Lue * ja / operaattorit
		while (tokenit.size() != 0 && Arrays.asList("*", "/").contains(tokenit.get(0))) {
			switch (tokenit.remove(0)) {
			case "*":
				numero *= jäsennäJaLaskeNumeroTaiSulut(tokenit);
				break;
			case "/":
				numero /= jäsennäJaLaskeNumeroTaiSulut(tokenit);
				break;
			}
		}

		return numero;
	}

	/**
	 * Jäsennä ja laske sulkujen sisällä oleva lauseke
	 * tai numero
	 */
	private static int jäsennäJaLaskeNumeroTaiSulut(List<String> tokenit) {
		// Tarkista, että tokeneita on jäljellä
		if (tokenit.size() == 0) {
			System.err.println("teksti loppui kesken! odotettiin sulkua tai numeroa");
			System.exit(0);
		}
		// Lue sulkujen sisällä oleva lasku
		if (tokenit.get(0).equals("(")) {
			odota(tokenit, "(");
			int numero = jäsennäJaLaskePlusMinus(tokenit);
			odota(tokenit, ")");
			return numero;
		}
		// Muulloin lue numero
		return seuraavaNumero(tokenit);
	}

}

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta