From 668494892ec11ce8f9f8948a3db84288492c3312 Mon Sep 17 00:00:00 2001 From: Pekka Helenius Date: Wed, 18 Mar 2020 12:02:58 +0200 Subject: [PATCH] Add missing assingments; update README --- .../1_PainonVartijat.md | 732 ++++++++++++++++++ .../2_Yhteystiedot-ja-osoitekirja-1.md | 460 +++++++++++ .../3_Yhteystiedot-ja-osoitekirja-2.md | 255 ++++++ .../4_Yhteystiedot-ja-osoitekirja-3.md | 294 +++++++ 12_exception/1_KysyUudestaan.md | 55 ++ 12_exception/2_PoikkeuksenHeitto.md | 37 + 12_exception/3_VirheellinenSyote.md | 66 ++ 12_exception/4_Kello-ja-Poikkeukset.md | 150 ++++ 13_files/1_WordCount.md | 97 +++ README.md | 14 +- 10 files changed, 2159 insertions(+), 1 deletion(-) create mode 100644 11_object-oriented_programming/1_PainonVartijat.md create mode 100644 11_object-oriented_programming/2_Yhteystiedot-ja-osoitekirja-1.md create mode 100644 11_object-oriented_programming/3_Yhteystiedot-ja-osoitekirja-2.md create mode 100644 11_object-oriented_programming/4_Yhteystiedot-ja-osoitekirja-3.md create mode 100644 12_exception/1_KysyUudestaan.md create mode 100644 12_exception/2_PoikkeuksenHeitto.md create mode 100644 12_exception/3_VirheellinenSyote.md create mode 100644 12_exception/4_Kello-ja-Poikkeukset.md create mode 100644 13_files/1_WordCount.md diff --git a/11_object-oriented_programming/1_PainonVartijat.md b/11_object-oriented_programming/1_PainonVartijat.md new file mode 100644 index 0000000..b463091 --- /dev/null +++ b/11_object-oriented_programming/1_PainonVartijat.md @@ -0,0 +1,732 @@ +Tässä tehtävässä harjoitellaan olioiden luomista ja niiden metodien kutsumista käyttäen apuna alla olevaa valmista Auto-luokkaa. Tallenna tehtävänannon lopussa oleva luokka itsellesi tiedostoon Auto.java ja luo uusi luokka AutoOhjelma, jonka main-metodissa teet seuraavat toimenpiteet: + +1) Luo uusi "Tesla"-merkkinen auto ja laita viittaus tähän olioon talteen muuttujaan +2) Luo toinen "BMW"-merkkinen auto ja laita viittaus tähän olioon talteen eri muuttujaan +3) Kutsu Teslan aja-metodia arvolla 100. +4) Kutsu BMW:n aja-metodia arvolla 98. +5) Kutsu Teslan aja-metodia arvolla 23. +6) Tulosta Teslan merkkijonoesitys (toString) omalle rivilleen +7) Tulosta BMW:n merkkijonoesitys (toString) omalle rivilleen + +Huom! Koska kilometrit on yksityinen muuttuja, sen arvoa ei voida muuttaa luokan ulkopuolelta. Sinun on siis muutettava arvoa julkisen aja-metodin kautta. + + +# Toteuta painonhallintasovellus + +Tarkoituksena on, että sovellukseen voi noin päivittäin syöttää oman painonsa ja painot tallentuvat myös tiedostoon. Painokehityksestä saa niin sanotulla ASCII grafiikalla piirrettyä kuvaajia käyttäen # merkkiä piirtosymbolina. Kuvassa uusimmat mittaukset näkyvät alimpana. Päivämäärät merkitään muodossa, josta tässä esimerkki: 9.9.2019. Eli päivä.kuukausi.vuosi. + +Tarkoitus on palauttaa vain yksi luokka PainoSovellus, jonka sisässä ovat muut tarvittavat luokat: PainoMittaus, Pvm, Piirturi ja TiedostoTyokalut. Yhden Java-luokan sisään voi laittaa muita luokkia siten, että ennen PainoMittaus-luokan viimeistä sulkevaa aaltosuljetta }, lisätään muut luokat omista tiedostoistaan siten, että "public class" alkuinen muoto on muodettu muotoon "class". Kaikki import-määreet, mitä eri luokissa tarvitaan laitetaan PainoSovellus-ohjelman alkuun. + + +## PainoSovellus + +Sisältää ohjelman valikon tulostamisen silmukassa. Toteuta esimerkiksi while-silmukalla. + +## PainoMittaus + +Mallintaa yhtä painomittausta model-luokka, joka sisältää päivämäärän ja varsinaisen painon kiloina. Päivämäärä on luettavissa ja tuotettavissa Pvm-luokan avulla. Tee siis 1) yksityinen (private) attribuutti tietotyyppiä int, joka kertoo paljonko päivän paino oli 2) yksityinen (private) attribuutti tietotyyppiä Pvm (tarkoittaa paivamaaraa). Pvm-luokka toteutetaan seuraavassa kohdassa. + +## Pvm + +Pvm-luokka (model-luokka), jossa on attribuutit: 1) yksityinen (private) attribuutti tietotyyppiä int, jossa on kuukauden päivä 2) yksityinen (private) attribuutti tietotyyppiä int, jossa on kuukauden numero 3) yksityinen (private) attribuutti tietotyyppiä int, jossa on vuoden arvo. + +Parametrittömän konstruktorin kautta voidaan luoda päivämäärä, jonka arvoksi tulee automaattisesti tällähetkellä kuluva päivä. Eli parametritön konstruktori public Pvm() { } asettaa sisällään attribuuttien arvoiksi tämänhetkisen päivämäärän. + +Parametrillinen konstruktori pystyy asettamaan päivämäärän arvot sille tulleesta merkkijonosta, eli se pystyy käsittelemään tilanteen, jossa käyttäjä antaa merkkijonona päivämääräksi esimerkiksi 10.3.2020. Java-koodissa tämä käsitellään esimerkiksi seuraavasti: + +``` +public Pvm(String pvm) { + String rivi = pvm; + String[] pvmKirjaus = new String[3]; + pvmKirjaus = rivi.split("\\."); + this.pp = Integer.parseInt(pvmKirjaus[0]); + this.kk = Integer.parseInt(pvmKirjaus[1]); + this.vv = Integer.parseInt(pvmKirjaus[2]); +} +``` + +## Piirturi + +Luokka, joka kykenee skaalautuvasti tuottamaan näyttöä sopivasti käyttävän kuvaajan perustuen annettuun tietoon. + +## TiedostoTyokalut + +Luokka, joka kykenee kirjoittamaan tiedostoon talteen mitattuja painoja. + +Painot pitää tallentaa tiedostoon nimeltään painotcsv.txt + +**Toteuta PainoSovellus-luokkaan metodi:** + +`public void tulostaValikko()` + +Metodi tulostaa numeroilla valittavan valikon. + +**Toteuta Piirturi-luokkaan metodi:** + +`public void piirraNaytolle()` + +Metodi tulostaa sopivasti skaalatun kuvaajan painon kehityksestä. + +**Toteuta TiedostoTyokalut-luokkaan metodi:** + +`public PainoMittaus[] lue()` + +Metodi pystyy lukemaan tiedostosta painotietoja ohjelman ymmärtämään muotoon, niin että ne saadaan osaksi kuvaajaa. Toisin sanoen palautetaan siis taulukko PainoMittaus-olioita, joissa kuhunkin olioon on asetettu päivämäärä ja painolukema. + +Ohjelmiston tulisi toimia muun muassa kuten seuraavassa. + +``` +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): +1 +Anna paino (muodossa 9.9.2019,85): +1.9.2019,52 +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): +1 +Anna paino (muodossa 9.9.2019,85): +2.9.2019,48 +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): +1 +Anna paino (muodossa 9.9.2019,85): +3.9.2019,49 +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): 1 +Anna paino (muodossa 9.9.2019,85): +4.9.2019,55 +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): +1 +Anna paino (muodossa 9.9.2019,85): +5.9.2019,48 +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): +1 +Anna paino (muodossa 9.9.2019,85): +6.9.2019,53 +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): +2 +############ +######## +######### +############### +######## +############# +Valikko +0) Lopeta +1) Lisää painokirjaus menneelle päivälle +2) Tulosta painokuvaaja +3) Lisää painokirjaus tälle päivälle +Anna valintasi (0, 1, 2 tai 3): +0 +``` + +**PainoSovellus.java** + +``` +import java.io.*; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +public class PainoSovellus { + + public static String tiedostopolku = System.getProperty("user.dir"); + public static String tiedosto = "painotcsv.txt"; + + private static int painoMin = 40; + private static int painoMax = 300; + + private static String piirturiSymbol = "#"; + + // NOTE: Private preferred, as this is being read just after the application entry point + private static TiedostoTyokalut tiedostoTyokalu = new TiedostoTyokalut(tiedostopolku, tiedosto); + + public static void main(String[] args) { + while (true) { + tulostaValikko(); + } + } + + // Method prints a numerical menu with various options. + public static void tulostaValikko() { + + String kayttajaArvo; + Scanner syote = new Scanner(System.in); + + System.out.printf( + "Valikko\n" + + "0) Lopeta\n" + + "1) Lisää painokirjaus menneelle päivälle\n" + + "2) Tulosta painokuvaaja\n" + + "3) Lisää painokirjaus tälle päivälle\n" + + "Anna valintasi (0, 1, 2 tai 3):\n" + ); + try { + int valinta = syote.nextInt(); + + switch (valinta) { + + case 0: + System.exit(0); + + case 1: + System.out.println("Anna paino (muodossa 9.9.2019,85):"); + kayttajaArvo = syote.next(); + + tiedostoTyokalu.kirjoita(kayttajaArvo); + + break; + case 2: + + //Piirturi piirturi = new Piirturi(); + Piirturi.piirraNaytolle(); + + break; + case 3: + System.out.println("Anna paino (muodossa 85):"); + + // Since this variable can also use syntax [date,weight] in case 1, we don't use nextInt() method. + // Input check is done by TiedostoTyokalut.inputValidator() method. + kayttajaArvo = syote.next(); + tiedostoTyokalu.kirjoita(kayttajaArvo); + + break; + ////////// + default: + System.err.println("Antamasi valinta ei ole kelvollinen"); + } + + } catch (Exception e) { + System.err.printf("Luettu syöte ei ole kelvollinen. Syy:\n%s\n", e); + // Do not exit the application here + } + } + + static class PainoMittaus { + + private Pvm pvm; + private int paino; + + private Object[] data = new Object[2]; + + public Pvm getPvm() { + return this.pvm; + } + + public int getPaino() { + return this.paino; + } + + public Object[] getObject() { + this.data[0] = this.pvm; + this.data[1] = this.paino; + return this.data; + } + + public PainoMittaus(Pvm pvm, int paino) { + if (paino >= PainoSovellus.painoMin && paino <= PainoSovellus.painoMax) { + this.paino = paino; + this.pvm = pvm; + } + } + } + + static class Pvm { + + // NOTE: These variables are not used in critical date checks. + + // WARNING: + // Doing manual check for these is error-prone as we need to validate ranges of vuosi, then kuukausi + // and then paiva + // Therefore, the manual check methods were replaced with built-in Java date classes. + + // Old Pvm class code with manual checks is as commented-out attachment in this document + + private int vuosi; + private int kuukausi; + private int paiva; + + private LocalDate hetki; + private DateTimeFormatter paivaMuoto = DateTimeFormatter.ofPattern("d.M.yyyy"); + + public void setVuosi(int vuosi) { + this.vuosi = vuosi; + } + + public void setKuukausi(int kuukausi) { + this.kuukausi = kuukausi; + } + + public void setPaiva(int paiva) { + this.paiva = paiva; + } + + public int getVuosi() { + return this.vuosi; + } + + public int getKuukausi() { + return this.kuukausi; + } + + public int getPaiva() { + return this.paiva; + } + + public void setHetki(LocalDate hetki) { + this.hetki = hetki; + } + + public void setHetki(String hetki) { + this.hetki = paivaTarkistaja(hetki); + } + + public LocalDate getHetki() { + return this.hetki; + } + + // Parameterless constructor automatically gets the current day + public Pvm() { + this.hetki = LocalDate.now(); + } + + // Read day from user input string + // Validate the format/syntax + public Pvm(String paivamaara) { + this.hetki = paivaTarkistaja(paivamaara); + } + + private LocalDate paivaTarkistaja(String paivamaara) { + LocalDate hetkiTestaaja = LocalDate.parse(paivamaara, paivaMuoto); + LocalDate nykyHetki = LocalDate.now(); + + // Can't add values for future days + if (!hetkiTestaaja.isAfter(nykyHetki)) { + return hetkiTestaaja; + } + + // TODO add proper error message, avoid unclear NullPointerException + return null; + } + + @Override + public String toString() { + return this.hetki.format(paivaMuoto); + } + + } + + static class Piirturi { + + public Piirturi() {} + + public static void piirraNaytolle() { + + PainoMittaus[] painot = PainoSovellus.tiedostoTyokalu.lue(); + + List paivaArvot = new ArrayList(); + List painoArvot = new ArrayList(); + + // Extract and map gathered painoMittaus objects + for (int i = 0; i < painot.length; i++) { + + // These variables have been validated by TiedostoTyokalut.inputValidator + // Mixed data types are not recommended in general in Java. + Pvm paiva = (Pvm) painot[i].getObject()[0]; + int paino = (int) painot[i].getObject()[1]; + + paivaArvot.add(paiva); + painoArvot.add(paino); + + } + tulostaDiagrammi(jarjestaArvot(paivaArvot, painoArvot)); + } + + // Sort mapped painoMittaus object [key,value] pairs by keys (dates) + // Outputs values (weights) in sorted order + private static List jarjestaArvot(List paivaArvot, List painoArvot) { + + List paivaArvotValid = new ArrayList(); + List paivaErrors = new ArrayList(); + + Map piirturiMap = new HashMap(); + + for (int i = 0; i < paivaArvot.size(); i++) { + + LocalDate syoteHetki = paivaArvot.get(i).getHetki(); + + // This is not needed for Map data type as it automatically sorts out + // duplicate keys. This condition was added to be more informative + // for the end user about errors in value parsing operations + if (paivaArvotValid.contains(syoteHetki)) { + if (!paivaErrors.contains(syoteHetki)) { + paivaErrors.add(syoteHetki); + System.err.printf("Varoitus: päivälle %s on määritelty useampi painoarvo.\n" + + "Hyväksytään ensimmäisenä luettu arvo.\n", syoteHetki); + } + } + paivaArvotValid.add(syoteHetki); + } + + for (int i = 0; i < paivaArvot.size(); i++) { + piirturiMap.put(paivaArvot.get(i).getHetki(), painoArvot.get(i)); + } + + // Use TreeMap to sort key, value pairs by keys (dates in our case) + Map piirturiTreeMap = new TreeMap(piirturiMap); + + // Java garbage collector + paivaArvotValid = null; + paivaErrors = null; + + return getValues(piirturiTreeMap); + + } + + // Get values from sorted input Map (sorted by date) + private static List getValues(Map inMap) { + + List painoArvotSorted = new ArrayList(); + + for (Map.Entry entry : inMap.entrySet()) { + painoArvotSorted.add(entry.getValue()); + } + + return painoArvotSorted; + } + + private static void tulostaDiagrammi(List syoteArvot) { + + List hashSymbols = new ArrayList(); + + for (int i = 0; i < syoteArvot.size(); i++) { + + for (int a = 0; a < (syoteArvot.get(i) - PainoSovellus.painoMin); a++) { + hashSymbols.add(PainoSovellus.piirturiSymbol); + } + System.out.println(String.join("", hashSymbols)); + hashSymbols.clear(); + } + + } + } + + static class TiedostoTyokalut { + + private String tiedostoPolku, tiedosto; + private File tiedostoKohde; + + public TiedostoTyokalut(String tiedostopolku, String tiedosto) { + this.tiedostoPolku = tiedostopolku; + this.tiedosto = tiedosto; + } + + // Write a line to the file + public void kirjoita(String newLine) throws Exception { + + try { + // TODO Add charset encoding check (UTF-8) + this.tiedostoKohde = new File(this.tiedostoPolku, this.tiedosto); + FileWriter kirjoitin = new FileWriter(this.tiedostoKohde, true); + + // TODO Add proper error handling + Object[] painoDataParsed = inputValidator(newLine, false).getObject(); + // Alternatively: + // PainoMittaus painoDataParsed = inputValidator(newLine, false); + // painoDataParsed.getPvm() + "," + painoDataParsed.getPaino(); + + if (!painoDataParsed.toString().isEmpty()) { + kirjoitin.append(painoDataParsed[0] + "," + painoDataParsed[1]); + kirjoitin.append(System.getProperty("line.separator")); + kirjoitin.close(); + } + + } catch (IOException o) { + System.err.printf("Ei voitu luoda tai avata tiedostoa: %s (polku: %s)", this.tiedosto, this.tiedostoPolku); + } + } + + // Read lines from the file + public PainoMittaus[] lue() { + + this.tiedostoKohde = new File(this.tiedostoPolku, this.tiedosto); + + try { + FileReader lukija = new FileReader(tiedostoKohde); + BufferedReader lukijaVirta = new BufferedReader(lukija); + String lukijaRivi; + + int entryCount = 0; + List painoEntries = new ArrayList(); + + try { + + lukijaRivi = lukijaVirta.readLine(); + + while (lukijaRivi != null) { + + // TODO Add proper error handling + PainoMittaus painoData = inputValidator(lukijaRivi, true); + + // Skip invalid entries + if (painoData == null) { + System.err.println("Varoitus: luettu arvo oli tyhjä"); + continue; + } else { + painoEntries.add(painoData); + entryCount++; + + //System.out.println((String.valueOf(painoData.getPaino()))); + + } + // Move to the next line + lukijaRivi = lukijaVirta.readLine(); + } + + // Add object painoTauluOlio which contains PainoMittaus objects + PainoMittaus[] painoTauluOlio = new PainoMittaus[entryCount]; + + // Add PainoMittaus objects + for (int i = 0; i < painoEntries.size(); i++) { + painoTauluOlio[i] = painoEntries.get(i); + } + +/* + for (int s = 0; s < painoTauluOlio.length; s++) { + System.out.println(painoTauluOlio[s]); + } +*/ + + return painoTauluOlio; + + } catch (IOException e) { + System.err.printf("Ei voitu lukea tiedostoa: %s\n", this.tiedostoKohde); + } + + } catch (FileNotFoundException e) { + System.err.printf("Ei voitu avata tiedostoa: %s\n", this.tiedostoKohde); + } + + // TODO add proper error message, avoid unclear NullPointerException + return null; + + } + + private PainoMittaus inputValidator(String paivaPaino, boolean onLukuOperaatio) { + + List inData = Arrays.asList(paivaPaino.split(",")); + Pvm nykyHetki = new Pvm(); + + if (inData.size() == 2) { + + Pvm tamaHetki = new Pvm(inData.get(0)); + + // Only past days accepted in write operations when [date,weight] input used + if (!onLukuOperaatio && nykyHetki.equals(tamaHetki)) { + + // TODO add proper error message, avoid unclear NullPointerException + return null; + } + + return new PainoMittaus(tamaHetki, Integer.parseInt(inData.get(1))); + + // Date from current date + } else if (inData.size() == 1) { + return new PainoMittaus(nykyHetki, Integer.parseInt(paivaPaino)); + } + + // TODO add proper error message, avoid unclear NullPointerException + return null; + + } + + } + +} + +//////////////////////////////////////////////////////////// + +// OLD Pvm class +// Has manual check methods for input date validity (format and ranges of year, month and day) + +/* +class Pvm { + + // NOTE: Using DateTimeFormatter without additional getter/setter filler code instead + // is highly recommended! This code contains a lot of overhead due to task requirements + // to have these attributes individually defined in this class. + private int paiva; + private int kuukausi; + private int vuosi; + + private String hetki; + private boolean isPast; + private boolean isCurrentDate; + + private String formattedDate(int paiva, int kuukausi, int vuosi) { + + return paiva + "." + kuukausi + "." + vuosi; + + } + + private boolean inValueChecker(int input, int min, int max) { + try { + if (input < min || input > max) { + //System.out.printf("min: %s, max: %s, input: %s", min,max,input); + System.err.println("Annettu päivämäärä ei ole hyväksytyllä arvovälillä"); + return false; + // TODO what to do next? Ask date again or abort execution? + } else { + return true; + } + } catch (InputMismatchException e) { + System.err.println("Annettu päivämäärä ei ole kelvollisessa muodossa"); + } + return false; + } + + public boolean setVuosi(int vuosi) { + if (this.inValueChecker(vuosi, PainoSovellus.minVuosi, LocalDate.now().getYear())) { + this.vuosi = vuosi; + return true; + } + return false; + } + + public boolean setKuukausi(int kuukausi) { + + int maxMonth; + if (this.getVuosi() < LocalDate.now().getYear()) { + this.isPast = true; + maxMonth = 12; + } else { + maxMonth = LocalDate.now().getMonthValue(); + this.isPast = false; + } + + if (this.inValueChecker(kuukausi, PainoSovellus.minKuukausi, maxMonth)) { + this.kuukausi = kuukausi; + return true; + } + return false; + } + + public boolean setPaiva(int paiva) { + + int maxDay; + //if (this.getKuukausi() < LocalDate.now().getMonthValue()) { + if(this.isPast) { + switch(this.getKuukausi()) { + case 2: + maxDay = 29; + break; + case 4: + case 6: + case 9: + case 11: + maxDay = 30; + break; + default: + maxDay = 31; + } + } else { + // Exclude the current day. Should be yesterday, at maximum + maxDay = LocalDate.now().getDayOfMonth() - 1; + } + + if (this.inValueChecker(paiva, PainoSovellus.minPaiva, maxDay)) { + this.paiva = paiva; + return true; + } + return false; + } + + public int getVuosi() { + return this.vuosi; + } + + public int getKuukausi() { + return this.kuukausi; + } + + public int getPaiva() { + return this.paiva; + } + + // Empty constructor + public Pvm() {} + + // Date from LocalDate variable + public Pvm(LocalDate date) { + this.vuosi = date.getYear(); + this.kuukausi = date.getMonthValue(); + this.paiva = date.getDayOfMonth(); + + this.hetki = this.formattedDate(this.paiva, this.kuukausi, this.vuosi); + } + + // Date from manual input. Has additional checks + public Pvm(int paiva, int kuukausi, int vuosi) { + + boolean isValidDate = false; + + if (this.setVuosi(vuosi)) { + if (this.setKuukausi(kuukausi)) { + if (this.setPaiva(paiva)) { + isValidDate = true; + } + } + + if (isValidDate) { + paiva = this.paiva; + kuukausi = this.kuukausi; + vuosi = this.vuosi; + + this.hetki = this.formattedDate(paiva, kuukausi, vuosi); + } + } + } + + + public String toString() { + if (!this.hetki.isEmpty()) { + return this.hetki; + } else { + return null; + } + } + +} +*/ +``` diff --git a/11_object-oriented_programming/2_Yhteystiedot-ja-osoitekirja-1.md b/11_object-oriented_programming/2_Yhteystiedot-ja-osoitekirja-1.md new file mode 100644 index 0000000..41a9fcf --- /dev/null +++ b/11_object-oriented_programming/2_Yhteystiedot-ja-osoitekirja-1.md @@ -0,0 +1,460 @@ +# Yhteystiedot ja osoitekirja 1/3 + +Tämä harjoitus pohjautuu seuraavaan kahteen luokkaan sekä erikseen toteutettavaan `AddressBookApp`-luokkaan. + +**Contact-luokka:** + +``` +// tiedosto Contact.java + +public class Contact { + + private String name; + private String email; + private String phone; + + public Contact(String name, String email, String phone) { + this.name = name; + this.email = email; + this.phone = phone; + } + + public String getName() { + return this.name; + } + + public String toString() { + // Haluttu muoto: "Maija Meikäläinen (email: foo@bar.fi, phone: 5555)" + return this.name + " (email: " + this.email + ", phone: " + this.phone + ")"; + } +} +``` + +**AddressBook-luokka:** + +``` +// tiedosto AddressBook.java + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class AddressBook { + + private List contacts; + + public AddressBook() { + this.contacts = new ArrayList<>(); + } + + public boolean add(Contact newContact) { + this.contacts.add(newContact); + return true; + } + + public Contact search(String keyword) { + for (Contact current : this.contacts) { + String name = current.getName(); + if (name != null && name.toLowerCase().contains(keyword.toLowerCase())) { + return current; // palautetaan löytynyt arvo heti + } + } + return null; // palautetaan null, jos ei löytynyt + } + + @Override + public String toString() { + // TODO: toteuta tämä metodi. Metodin tulee muodostaa merkkijono, + // joka sisältää kaikki yhteystiedot omilla riveillään. + return "TODO"; + } +} +``` + +## Ohjelman komentorivikäyttöliittymä + +Tehtävänä on tällä kertaa rakentaa uusi ohjelmaluokka `AddressBookApp`, joka mahdollistaa yhteystietojen käsittelyn `AddressBook` ja `Contact`-olioiden kanssa seuraavilla komennoilla: + +``` +This is an address book application. Available commands: + list + help + add , , + search + exit +``` + +Ohjelma tulee jakaa osiin siten, että vain `AddressBookApp`-luokka pyytää käyttäjältä syötteitä ja tulostaa tekstiä. `Contact` ja `AddressBook` ovat vain datan varastointia ja käsittelyä varten. + +## Syötteiden pyytäminen + +Tämän sovelluksen käyttöliittymässä syötettä pyydettäessä ruudulle tulostetaan `>` -merkki syötteen odottamisen merkiksi: + +``` +System.out.print("> "); +``` + +Koska `>`-merkki tulostettiin `print` eikä `println`-metodilla, syöttää käyttäjä komentonsa samalle riville. Käyttäjän komento saattaa koostua yhdestä tai useasta osasta. Yhden sanan komentoja ovat esimerkiksi: + +``` +> list +``` + +ja + +``` +> exit +``` + +Moniosaisia komentoja ovat puolestaan esimerkiksi: + +``` +> add Maija Meikäläinen, maija@example.com, +35850555556 +``` + +``` +> search Matti +``` + +## Syötteiden lukeminen Scannerilla + +Syötteet kannattaa tässä sovelluksessa lukea kahdessa osassa. Ensin luetaan syötteen ensimmäinen sana ja sen jälkeen koko loppurivi: + +``` +boolean running = true; +while (running) { + System.out.print("> "); + String command = input.next(); + String theRest = input.nextLine().trim(); + + // ... toimintalogiikka +} +``` + +Rivin ensimmäinen sana on aina komento (`command`), jonka perusteella valitaan mikä operaatio osoitekirjalle suoritetaan. Mahdollisesti annettua loppuriviä käytetään `add`- ja `search`-komentojen tapauksessa uuden yhteystiedon tietoina tai hakusanana. + +Jos käyttäjän syöte on `add Maija Meikäläinen, maija@example.com, +35850555556`, tulee muuttujien arvoiksi seuraavat: + +| command | theRest | +|---------|----------------------------------------------------| +| add | Maija Meikäläinen, maija@example.com, +35850555556 | + +`theRest`-muuttuja siis sisältää rivin koko loppuosan, joka `add`-komennon tapauksessa kannattaa pilkkoa String-luokan `split`-metodilla pilkkujen kohdalta: + +``` +String[] parts = theRest.split(","); + +String name = parts[0].trim(); +String email = parts[1].trim(); +String phone = parts[2].trim(); +``` + +## Ohjelman logiikan haarauttaminen + +Syötteen lukemisen jälkeen voidaan koodi haarauttaa joko Javan perusteista tutulla `if-else if-else`-ketjulla tai `switch case`-rakenteella: + +``` +switch (command) { +case "help": + // tulosta ohje + break; +case "list": + // tulosta osoitekirjan koko sisältö + break; +case "add": + // käytä annettua nimeä, emailia ja puhelinnumeroa luodaksesi uuden yhteystiedon + break; +case "search": + // etsi yhteystietoa ja tulosta se + break; +case "exit": + // poistu ohjelmasta + break; +default: + // tunnistamaton komento: + System.out.println("Unknown command"); + break; +} +``` + +`Switch-case` -rakenne ei ole tässä välttämätön, mutta se sopii tilanteeseen hyvin ja sen opettelu voi olla hyödyksi sinulle myös myöhemmin. Koska ohjelman halutaan toimivan toistaiseksi ja kysyvän käyttäjän syötettä aina uudelleen, on edellä esitetyt syötteen kyselyt ja ehtorakenteet toteutettava jonkin toistorakenteen sisään. + +## Käyttöliittymän ominaisuudet + +Sinun ei tarvitse toteuttaa kaikkia komentoja ohjelman ensimmäiseen versioon, vaan **toteuta ominaisuudet yksittäin sitä mukaan kun tarvitset niitä.** + +### ADD + +add-komennon tulee lisätä yhdelle riville lisätyt yhteystiedot uutena oliona `AddressBook`-osoitekirjaan: + +``` +> add Maija Meikäläinen, maija@example.com, +35850555556 +Added Maija Meikäläinen (email: maija@example.com, phone: +35850555556) +``` + +### SEARCH + +Jos annettu hakusana löytyy jostain osoitelistan yhteystiedosta, ohjelman tulee tulostaa kyseinen yhteystieto. Jos yhteystietoa ei löydy, tulostetaan siitä kertova viesti: + +``` +> search Maija +Maija Meikäläinen (email: maija@example.com, phone: +35850555556) + +> search Ville +Ville does not match any contact. +``` + +### LIST + +Komennon `list` jälkeen ohjelman tulee tulostaa kukin yhteystieto omalla rivillään: + +``` +> list +Maija Meikäläinen (email: maija@example.com, phone: +35850555556) +Matti Meikäläinen (email: matti@example.com, phone: +35850555555) +``` + +### HELP + +Käyttäjän syötettyä komennon `help` ohjelman tulee tulostaa sama ohjeteksti kuin ohjelman käynnistyksessä: + +``` +This is an address book application. Available commands: + list + help + add , , + search + exit +``` + +### EXIT + +Käyttäjän kirjoittaessa syötteeksi "exit" ohjelman tulee tulostaa teksti "Bye!" ja lopettaa suorituksensa: + +``` +> exit +Bye! +``` + +### Esimerkki + +``` +This is an address book application. Available commands: + list + help + add , , + search + exit + +> add Maija Meikäläinen, maija@example.com, +35850555556 +Added Maija Meikäläinen (email: maija@example.com, phone: +35850555556) + +> add Matti Meikäläinen, matti@example.com, +35850555555 +Added Matti Meikäläinen (email: matti@example.com, phone: +35850555555) + +> search Matti +Matti Meikäläinen (email: matti@example.com, phone: +35850555555) + +> search Maija +Maija Meikäläinen (email: maija@example.com, phone: +35850555556) + +> list +Maija Meikäläinen (email: maija@example.com, phone: +35850555556) +Matti Meikäläinen (email: matti@example.com, phone: +35850555555) + +> search Ville +Ville does not match any contact. + +> exit +Bye! +``` + +**AddressBookApp.java + +``` +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class AddressBookApp { + + private static String welcomeText = "This is an address book application. Available commands:\n"; + private static String commandMenu = " list\n" + + " help\n" + + " add , , \n" + + " search \n" + + " exit\n"; + + // Keep here instead of translating to a local variable + private static String inputCLIPrefix = "> "; + + // Keep added addressBook values during application runtime + private static AddressBook addressBook = new AddressBook(); + + public static void main(String[] args) { + + System.out.print(welcomeText + commandMenu); + + while (true) { + runApp(); + } + } + + private static void runApp() { + + Scanner inputPrompt = new Scanner(System.in); + Object[] getInput = parseInput(inputCLIPrefix, inputPrompt); + + String inputCmd = String.valueOf(getInput[0]); + + // TODO Avoid risk of ClassCastException + List inputArgs = (ArrayList)getInput[1]; + int argsCount = 3; + + switch(inputCmd) { + + case "list": + System.out.println(addressBook.toString()); + break; + + case "help": + System.out.print(welcomeText + commandMenu); + break; + + case "add": + + // TODO Should this be checked in AddressBook.add method instead? + if (inputArgs.size() != argsCount) { + System.err.printf("Invalid count of input arguments (expected %d).\n", argsCount); + break; + } + + String name = String.valueOf(inputArgs.get(0)); + String email = String.valueOf(inputArgs.get(1)); + String phone = String.valueOf(inputArgs.get(2)); + + Contact contact = new Contact(name, email, phone); + + // NOTE: Else condition not needed as 3 arguments is always passed + // to this object method call + if(addressBook.add(contact)) { + System.out.printf("Added %s\n", contact.toString()); + } + + break; + + case "search": + + String searchTerm = String.valueOf(inputArgs.get(0)); + Contact match = addressBook.search(searchTerm); + + if (match == null) { + System.out.printf("%s does not match any contact.\n", searchTerm); + } else { + System.out.println(match.toString()); + } + + break; + + case "exit": + System.out.print("Bye!\n"); + System.exit(0); + + default: + System.err.println("Command not found."); + //break; + } + } + + private static Object[] parseInput(String prefix, Scanner inputRaw) { + + System.out.print(prefix); + String inputR = inputRaw.nextLine(); + + String command = inputR.split(" ")[0]; + + String[] theRest = inputR.split(","); + theRest[0] = theRest[0].replaceAll("^\\s*" + command + "\\s*",""); + List arguments = new ArrayList(); + for (int i = 0; i < theRest.length; i++) { + arguments.add(theRest[i].trim()); + } + + Object[] parsedOutput = new Object[2]; + parsedOutput[0] = command; + parsedOutput[1] = arguments; + + return parsedOutput; + } + +} +``` + +**AddressBook.java** + +``` +import java.util.ArrayList; +import java.util.List; + +public class AddressBook { + + private List contacts; + + public AddressBook() { + this.contacts = new ArrayList<>(); + } + + public boolean add(Contact newContact) { + this.contacts.add(newContact); + return true; + } + + public Contact search(String keyword) { + for (Contact current : this.contacts) { + String name = current.getName(); + if (name != null && name.toLowerCase().contains(keyword.toLowerCase())) { + return current; // palautetaan löytynyt arvo heti + } + } + return null; // palautetaan null, jos ei löytynyt + } + + @Override + public String toString() { + String returnString = ""; + for (Contact contact : this.contacts) { + returnString += contact + "\n"; + } + return returnString; + } +} +``` + +**Contact.java** + +``` +public class Contact { + + private String name; + private String email; + private String phone; + + // NOTE: This does not validate or check input String values + // (Is name actually a name, email a valid email, and phone number an actual phone number?) + public Contact(String name, String email, String phone) { + this.name = name; + this.email = email; + this.phone = phone; + } + + public String getName() { + return this.name; + } + + public String toString() { + // Haluttu muoto: "Maija Meikäläinen (email: foo@bar.fi, phone: 5555)" + return this.name + " (email: " + this.email + ", phone: " + this.phone + ")"; + } +} +``` diff --git a/11_object-oriented_programming/3_Yhteystiedot-ja-osoitekirja-2.md b/11_object-oriented_programming/3_Yhteystiedot-ja-osoitekirja-2.md new file mode 100644 index 0000000..2b6e788 --- /dev/null +++ b/11_object-oriented_programming/3_Yhteystiedot-ja-osoitekirja-2.md @@ -0,0 +1,255 @@ +# Yhteystiedot ja osoitekirja 2/3 - Olioiden vertaileminen + +Kuten merkkijonoja käsiteltäessä olemme huomanneet, merkkijonojen vertailu `==`-vertailuoperaattorilla vertailee olioviittauksia eikä merkkijonojen sisältöä. Samalla tavoin käy vertailtaessa Contact-olioita. Merkkijonoille on toteutettu oma `equals`-metodinsa, jonka avulla pystytään vertailemaan kahden eri merkkijonon sisältöä. Tässä tehtävässä sinun tulee toteuttaa vastaavasti `Contact`-luokkaan `equals`-metodi, joka vertailee, ovatko kaksi `Contact`-oliota sisällöiltään samanlaiset. + +Lue sivu [Olioiden samankaltaisuus](https://ohjelmointi-19.mooc.fi/osa-6/3-olioiden-samankaltaisuus) saadaksesi lisätietoa olioiden vertailusta ja `equals`-metodin toteuttamisesta. + +## ADD-KOMENTO + +Jatkokehitä myös `AddressBook`-osoitekirjan ominaisuutta yhteystietojen lisäämiseksi siten, että osoitekirjaan ei lisätä kahta sisällöiltään täysin samanlaista yhteystietoa: + +``` +> add Maija Meikäläinen, maija@example.com, +35850555556 +Added Maija Meikäläinen (email: maija@example.com, phone: +35850555556) + +> add Maija Meikäläinen, maija@example.com, +35850555556 +That contact already exists. +``` + +Jos käyttäjän syöttämä yhteystieto löytyy jo valmiiksi osoitekirjasta, sitä ei pidä lisätä, vaan ohjelman tulee tulostaa teksti `That contact already exists.` + +## ESIMERKKI + +``` +This is an address book application. Available commands: +list +help +add , , +search +exit + +> add Maija Meikäläinen, maija@example.com, +35850555556 +Added Maija Meikäläinen (email: maija@example.com, phone: +35850555556) + +> add Matti Meikäläinen, matti@example.com, +35850555555 +Added Matti Meikäläinen (email: matti@example.com, phone: +35850555555) + +> add Maija Meikäläinen, maija@example.com, +35850555556 +That contact already exists. +... +``` + +**AddressBookApp.java** + +``` +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class AddressBookApp { + + private static String welcomeText = "This is an address book application. Available commands:\n"; + private static String commandMenu = " list\n" + + " help\n" + + " add , , \n" + + " search \n" + + " exit\n"; + + // Keep here instead of translating to a local variable + private static String inputCLIPrefix = "> "; + + // Keep added addressBook values during application runtime + private static AddressBook addressBook = new AddressBook(); + + public static void main(String[] args) { + + System.out.print(welcomeText + commandMenu); + + while (true) { + runApp(); + } + } + + private static void runApp() { + + Scanner inputPrompt = new Scanner(System.in); + Object[] getInput = parseInput(inputCLIPrefix, inputPrompt); + + String inputCmd = String.valueOf(getInput[0]); + + // TODO Avoid risk of ClassCastException + List inputArgs = (ArrayList)getInput[1]; + int argsCount = 3; + + switch(inputCmd) { + case "list": + System.out.println(addressBook.toString()); + break; + case "help": + System.out.print(welcomeText + commandMenu); + break; + case "add": + + // TODO Should this be checked in AddressBook.add method instead? + if (inputArgs.size() != argsCount) { + System.err.printf("Invalid count of input arguments (expected %d).\n", argsCount); + break; + } + + String name = String.valueOf(inputArgs.get(0)); + String email = String.valueOf(inputArgs.get(1)); + String phone = String.valueOf(inputArgs.get(2)); + + Contact contact = new Contact(name, email, phone); + + // NOTE: Else condition not needed as 3 arguments is always passed to this object method call + if(addressBook.add(contact)) { + System.out.printf("Added %s\n", contact.toString()); + } + + break; + case "search": + + String searchTerm = String.valueOf(inputArgs.get(0)); + Contact match = addressBook.search(searchTerm); + + if (match == null) { + System.out.printf("%s does not match any contact.\n", searchTerm); + } else { + System.out.println(match.toString()); + } + + break; + case "exit": + System.out.print("Bye!\n"); + System.exit(0); + default: + System.err.println("Command not found."); + //break; + } + } + + private static Object[] parseInput(String prefix, Scanner inputRaw) { + + System.out.print(prefix); + String inputR = inputRaw.nextLine(); + + String command = inputR.split(" ")[0]; + + String[] theRest = inputR.split(","); + theRest[0] = theRest[0].replaceAll("^\\s*" + command + "\\s*",""); + List arguments = new ArrayList(); + for (int i = 0; i < theRest.length; i++) { + arguments.add(theRest[i].trim()); + } + + Object[] parsedOutput = new Object[2]; + parsedOutput[0] = command; + parsedOutput[1] = arguments; + + return parsedOutput; + } + +} +``` + +**AddressBook.java** + +``` +import java.util.ArrayList; +import java.util.List; + +public class AddressBook { + + private List contacts; + + public AddressBook() { + this.contacts = new ArrayList<>(); + } + + public boolean add(Contact newContact) { + + // Create a temporary ArrayList, if we have contacts already + // Get list of all contacts, put them into the new ArrayList contactsTemp + // Loop through all contacts in contactsTemp + // Check the current contact in contactsTemp against newContact + if (this.contacts.size() > 0) { + List contactsTemp = new ArrayList<>(this.contacts); + + for (Contact tempContact : contactsTemp) { + if (tempContact.equals(newContact)) { + System.out.println("That contact already exists."); + return false; + } + } + + } + + this.contacts.add(newContact); + return true; + } + + public Contact search(String keyword) { + for (Contact current : this.contacts) { + String name = current.getName(); + if (name != null && name.toLowerCase().contains(keyword.toLowerCase())) { + return current; // palautetaan löytynyt arvo heti + } + } + return null; // palautetaan null, jos ei löytynyt + } + + @Override + public String toString() { + String returnString = ""; + for (Contact contact : this.contacts) { + returnString += contact + "\n"; + } + return returnString; + } +} +``` + +**Contact.java** + +``` +public class Contact { + + private String name; + private String email; + private String phone; + + // NOTE: This does not validate or check input String values + // (Is name actually a name, email a valid email, and phone number an actual phone number?) + public Contact(String name, String email, String phone) { + this.name = name; + this.email = email; + this.phone = phone; + } + + public String getName() { + return this.name; + } + + @Override + public boolean equals(Object contact) { + + if (this == contact) { return true; } + if (!(this instanceof Contact)) { return false; } + + Contact contactT = (Contact) contact; + + if (this.name.equals(contactT.name) && this.email.equals(contactT.email) && this.phone.equals(contactT.phone)) { + return true; + } + + return false; + + } + + public String toString() { + // Haluttu muoto: "Maija Meikäläinen (email: foo@bar.fi, phone: 5555)" + return this.name + " (email: " + this.email + ", phone: " + this.phone + ")"; + } +} +``` diff --git a/11_object-oriented_programming/4_Yhteystiedot-ja-osoitekirja-3.md b/11_object-oriented_programming/4_Yhteystiedot-ja-osoitekirja-3.md new file mode 100644 index 0000000..90ee934 --- /dev/null +++ b/11_object-oriented_programming/4_Yhteystiedot-ja-osoitekirja-3.md @@ -0,0 +1,294 @@ +# Yhteystiedot ja osoitekirja 3/3 - Yhteystietojen järjestäminen aakkosjärjestykseen + +Tässä tehtävässä sinun tehtäväsi on järjestää yhteystiedot nimien mukaiseen aakkosjärjestykseen. Tämä tehtävä on ohjelmointi 1 -kurssin näkökulmasta edistynyttä lisäsisältöä, minkä vuoksi se on julkaistu bonustehtävänä. + +## Taustatietoa + +Jos yritämme järjestää oman Contact-luokan oliot Collections.sort-metodin avulla, saamme seuraavan virheilmoituksen: + +``` +"The method sort(List) in the type Collections is not applicable for the arguments (List)" +``` + +Tämä johtuu siitä, että luokkamme ei ole yhteensopiva `Comparable`-tyypin kanssa. `Collections.sort`-metodi ei siis pysty vertailemaan olioitamme keskenään ilman, että määrittelemme sille lisäksi käytettävän vertailuoperaation. + +Tutustu Javan `Comparator.comparing`-metodiin, jonka avulla voit määritellä vertailijan kutsumaan mitä tahansa oman luokkasi metodia olioiden järjestämiseksi: [https://www.baeldung.com/java-8-comparator-comparing](https://www.baeldung.com/java-8-comparator-comparing). Tällä kurssilla sinun kannattaa lukea artikkelista kohta [3.1. Key Selector Variant](https://www.baeldung.com/java-8-comparator-comparing#1-key-selector-variant) ja sitä aikaisemmat kappaleet, mutta ei välttämättä tätä kappaletta pidemmälle. + +Kuten artikkelissa kerrotaan, `Comparator.comparing`-metodille voidaan antaa metodiviittaus mihin tahansa metodiin. `Collections.sort` käyttää tällöin vertailussa juuri haluamasi metodin palauttamia arvoja, joita vertaillaan keskenään. Voisimme siis esimerkiksi järjestää merkkijonot pituusjärjestykseen vertailemalla merkkijonojen pituuksia, jotka selviävät `length()`-metodin avulla: `Comparator.comparing(String::length)`. + +`Collections.sort` käyttää tässä esimerkissä järjestelemiseen `Comparator`-oliota, joka vertaa merkkijonojen pituuksia toisiinsa: + +``` +List nesteet = Arrays.asList("maito", "Vesi", "ketsuppi", "bensa", "Limu"); +// tehdään järjestely merkkijonojen length-metodin mukaan: +Collections.sort(nesteet, Comparator.comparing(String::length)); + +System.out.println(nesteet); // lyhimmästä merkkijonosta pisimpään: [Limu, Vesi, bensa, maito, ketsuppi] +``` + +## Tehtävä + +Jatkokehitä nyt edellisissä tehtävissä käyttämiäsi kolmea osoitekirja- ja yhteystietoluokkaa siten, että osoitekirjan sisältöä listattaessa "list"-komennolla yhteystiedot esitetään aakkosjärjestyksessä nimen mukaan. + +Sinun kannattaa hyödyntää edellä käsiteltyä `Collections.sort`-metodia sekä `Comparator.comparing`-metodia siten, että annat comparing-metodille metodiviittauksen `Contact`-luokkasi `getName`-metodiin. + +## ESIMERKKI + +``` +This is an address book application. Available commands: +list +help +add , , +search +exit + +> add Maija Meikäläinen, maija@example.com, +35850555556 +Added Maija Meikäläinen (email: maija@example.com, phone: +35850555556) + +> add Matti Meikäläinen, matti@example.com, +35850555555 +Added Matti Meikäläinen (email: matti@example.com, phone: +35850555555) + +> add Benjamin Meikäläinen, benjamin@example.com, +35850555557 +Added Benjamin Meikäläinen (email: benjamin@example.com, phone: +35850555557) + +> add Abraham Meikäläinen, abraham@example.com, +35850555558 +Added Abraham Meikäläinen (email: abraham@example.com, phone: +35850555558) + +> add Wolverine Meikäläinen, wolverine@example.com, +35850555559 +Added Wolverine Meikäläinen (email: wolverine@example.com, phone: +35850555559) + +> list +Abraham Meikäläinen (email: abraham@example.com, phone: +35850555558) +Benjamin Meikäläinen (email: benjamin@example.com, phone: +35850555557) +Maija Meikäläinen (email: maija@example.com, phone: +35850555556) +Matti Meikäläinen (email: matti@example.com, phone: +35850555555) +Wolverine Meikäläinen (email: wolverine@example.com, phone: +35850555559) + +> exit +Bye! +``` + +**AddressBookApp.java** + +``` +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class AddressBookApp { + + private static String welcomeText = "This is an address book application. Available commands:\n"; + private static String commandMenu = " list\n" + + " help\n" + + " add , , \n" + + " search \n" + + " exit\n"; + + // Keep here instead of translating to a local variable + private static String inputCLIPrefix = "> "; + + // Keep added addressBook values during application runtime + private static AddressBook addressBook = new AddressBook(); + + public static void main(String[] args) { + + System.out.print(welcomeText + commandMenu); + + while (true) { + runApp(); + } + } + + private static void runApp() { + + Scanner inputPrompt = new Scanner(System.in); + Object[] getInput = parseInput(inputCLIPrefix, inputPrompt); + + String inputCmd = String.valueOf(getInput[0]); + + // TODO Avoid risk of ClassCastException + List inputArgs = (ArrayList)getInput[1]; + int argsCount = 3; + + switch(inputCmd) { + case "list": + System.out.println(addressBook.toString()); + break; + case "help": + System.out.print(welcomeText + commandMenu); + break; + case "add": + + // TODO Should this be checked in AddressBook.add method instead? + if (inputArgs.size() != argsCount) { + System.err.printf("Invalid count of input arguments (expected %d).\n", argsCount); + break; + } + + String name = String.valueOf(inputArgs.get(0)); + String email = String.valueOf(inputArgs.get(1)); + String phone = String.valueOf(inputArgs.get(2)); + + Contact contact = new Contact(name, email, phone); + + // NOTE: Else condition not needed as 3 arguments is always passed to this object method call + if(addressBook.add(contact)) { + System.out.printf("Added %s\n", contact.toString()); + } + + break; + case "search": + + String searchTerm = String.valueOf(inputArgs.get(0)); + Contact match = addressBook.search(searchTerm); + + if (match == null) { + System.out.printf("%s does not match any contact.\n", searchTerm); + } else { + System.out.println(match.toString()); + } + + break; + case "exit": + System.out.print("Bye!\n"); + System.exit(0); + default: + System.err.println("Command not found."); + //break; + } + } + + private static Object[] parseInput(String prefix, Scanner inputRaw) { + + System.out.print(prefix); + String inputR = inputRaw.nextLine(); + + String command = inputR.split(" ")[0]; + + String[] theRest = inputR.split(","); + theRest[0] = theRest[0].replaceAll("^\\s*" + command + "\\s*",""); + List arguments = new ArrayList(); + for (int i = 0; i < theRest.length; i++) { + arguments.add(theRest[i].trim()); + } + + Object[] parsedOutput = new Object[2]; + parsedOutput[0] = command; + parsedOutput[1] = arguments; + + return parsedOutput; + } + +} +``` + +**AddressBook.java** + +``` +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class AddressBook { + + private List contacts; + + public AddressBook() { + this.contacts = new ArrayList<>(); + } + + public boolean add(Contact newContact) { + + // Create a temporary ArrayList, if we have contacts already + // Get list of all contacts, put them into the new ArrayList contactsTemp + // Loop through all contacts in contactsTemp + // Check the current contact in contactsTemp against newContact + if (this.contacts.size() > 0) { + List contactsTemp = new ArrayList<>(this.contacts); + + for (Contact tempContact : contactsTemp) { + if (tempContact.equals(newContact)) { + System.out.println("That contact already exists."); + return false; + } + } + + } + + this.contacts.add(newContact); + return true; + } + + public Contact search(String keyword) { + for (Contact current : this.contacts) { + String name = current.getName(); + if (name != null && name.toLowerCase().contains(keyword.toLowerCase())) { + return current; // palautetaan löytynyt arvo heti + } + } + return null; // palautetaan null, jos ei löytynyt + } + + @Override + public String toString() { + String returnString = ""; + + // Sort by contact names. Define new Comparator object + Collections.sort(this.contacts, new Comparator() { + public int compare(Contact contact1, Contact contact2) { + return contact1.getName().compareTo(contact2.getName()); + } + }); + + for (Contact contact : this.contacts) { + returnString += contact + "\n"; + } + return returnString; + } +} +``` + +**Contact.java** + +``` +public class Contact { + + private String name; + private String email; + private String phone; + + // NOTE: This does not validate or check input String values + // (Is name actually a name, email a valid email, and phone number an actual phone number?) + public Contact(String name, String email, String phone) { + this.name = name; + this.email = email; + this.phone = phone; + } + + public String getName() { + return this.name; + } + + @Override + public boolean equals(Object contact) { + + if (this == contact) { return true; } + if (!(this instanceof Contact)) { return false; } + + Contact contactT = (Contact) contact; + + if (this.name.equals(contactT.name) && this.email.equals(contactT.email) && this.phone.equals(contactT.phone)) { + return true; + } + + return false; + + } + + public String toString() { + // Haluttu muoto: "Maija Meikäläinen (email: foo@bar.fi, phone: 5555)" + return this.name + " (email: " + this.email + ", phone: " + this.phone + ")"; + } +} +``` diff --git a/12_exception/1_KysyUudestaan.md b/12_exception/1_KysyUudestaan.md new file mode 100644 index 0000000..83f0abc --- /dev/null +++ b/12_exception/1_KysyUudestaan.md @@ -0,0 +1,55 @@ +# Kysy Uudestaan + +Kirjoita luokka `KysyUudestaan` ja lisää siihen `main`-metodi. + +Main-metodissa sinun tulee kysyä käyttäjältä kokonaislukutyyppistä syötettä. Jos käyttäjä antaa syötteen, joka ei ole kelvollinen kokonaisluku, ohjelmasi tulee kysyä syötettä uudelleen esimerkkisuorituksen mukaisesti. + +Kun käyttäjä syöttää kelvollisen kokonaisluvun, ohjelmasi tulee tulostaa annettu luku esimerkkisuorituksen mukaisesti ja sen suorituksen tulee päättyä. + +Totetutustavastasi riippuen sinun tulee varautua tehtävässä joko InputMismatchException tai NumberFormatException -tyyppisiin poikkeuksiin. + +**Huom:** ratkaisutavastasi riippuen saatat tässä tehtävässä törmätä ikuiseen silmukkaan, jossa ohjelmasi ei kysy virheellistä arvoa uudelleen. Tämä johtuu siitä, että virheellinen syöte jää System.in-syötepuskuriin ja seuraavalla kerralla ohjelmasi yrittää lukea samaa virheellistä arvoa uudelleen. Stack Overflow -palvelussa on tarkempi keskustelu tästä aiheesta. + +``` +Syötä kokonaisluku: 1_000 +Virheellinen luku! + +Syötä kokonaisluku: ? +Virheellinen luku! + +Syötä kokonaisluku: + +Virheellinen luku! + +Syötä kokonaisluku: a b c +Virheellinen luku! + +Syötä kokonaisluku: -8 +Syötit luvun -8. +``` + +**KysyUudestaan.java** + +``` +import java.util.InputMismatchException; +import java.util.Scanner; + +public class KysyUudestaan { + public static void main(String[] args) { + + while (true) { + System.out.print("Syötä kokonaisluku: "); + + Scanner syote = new Scanner(System.in); + + try { + int luku = syote.nextInt(); + System.out.printf("Syötit luvun %d.", luku); + System.exit(0); + + } catch (InputMismatchException e) { + System.out.println("Virheellinen luku!"); + } + } + } +} +``` diff --git a/12_exception/2_PoikkeuksenHeitto.md b/12_exception/2_PoikkeuksenHeitto.md new file mode 100644 index 0000000..bd8df24 --- /dev/null +++ b/12_exception/2_PoikkeuksenHeitto.md @@ -0,0 +1,37 @@ +# Poikkeuksen heittäminen + +Kirjoita ohjelma `ArvonTarkastus`, joka kysyy käyttäjältä yhden luvun. Ohjelmasi tulee luvun kysymisen jälkeen tarkastaa, että luku on vähintään 0 ja korkeintaan 23. + +Mikäli luku on sallittu, tulee ohjelmasi tulostaa teksti "Luku X on sallittu." ja ohjelman suorituksen pitää päättyä. Mikäli luku ei ole sallittu, tulee ohjelmasi heittää Javan valmis [IllegalArgumentException-poikkeus](https://docs.oracle.com/javase/9/docs/api/java/lang/IllegalArgumentException.html), minkä jälkeen ohjelmasi "kaatuu". Voit antaa poikkeukselle konstruktoriparametrina minkä tahansa virheilmoituksen tai jättää merkkijonon antamatta. + +``` +Syötä luku väliltä 0-23: -1 +Exception in thread "main" java.lang.IllegalArgumentException +``` + +**ArvonTarkastus.java** + +``` +import java.util.Scanner; + +public class ArvonTarkastus { + + public static void main(String[] args) throws IllegalArgumentException { + + Scanner syote = new Scanner(System.in); + int min=0, max=23; + + System.out.printf("Syötä luku väliltä %d-%d: ", min, max); + int luku = syote.nextInt(); + + if (luku >= min && luku <= max) { + System.out.printf("Luku %d on sallittu.", luku); + System.exit(0); + } else { + throw new IllegalArgumentException(); + } + + } + +} +``` diff --git a/12_exception/3_VirheellinenSyote.md b/12_exception/3_VirheellinenSyote.md new file mode 100644 index 0000000..914e824 --- /dev/null +++ b/12_exception/3_VirheellinenSyote.md @@ -0,0 +1,66 @@ +# Virheellisen syötteen lukeminen + +Kirjoita luokka `Summaaja` ja siihen `main`-metodi, jossa lasket käyttäjän syöttämien kokonaislukujen summan. Lukujen pyytäminen lopetetaan, kun käyttäjä syöttää luvun 0. Lopuksi ohjelmasi tulee tulostaa annettujen lukujen summa. + +Tässä tehtävässä ohjelmasi täytyy käsitellä virheelliset syötteet kaatumatta. Jos käyttäjä syöttää arvon, joka ei ole kelvollinen kokonaisluku, tulee ohjelman tulostaa teksti "Virheellinen syöte!" ja jatkaa lukujen kysymistä. + +**Huom:** ratkaisutavastasi riippuen saatat tässä tehtävässä törmätä ikuiseen silmukkaan, jossa ohjelmasi ei kysy virheellistä arvoa uudelleen. Tämä johtuu siitä, että virheellinen syöte jää System.in-syötepuskuriin ja seuraavalla kerralla ohjelmasi yrittää lukea samaa virheellistä arvoa uudelleen. [Stack Overflow -palvelussa on tarkempi keskustelu tästä aiheesta](https://stackoverflow.com/a/3572233). + +``` +Syötä seuraava luku (0 lopettaa): 5 +Syötä seuraava luku (0 lopettaa): 4 +Syötä seuraava luku (0 lopettaa): kolme +Virheellinen syöte! + +Syötä seuraava luku (0 lopettaa): 3 +Syötä seuraava luku (0 lopettaa): a b c d +Virheellinen syöte! + +Syötä seuraava luku (0 lopettaa): 1 +Syötä seuraava luku (0 lopettaa): 0 + +Lukujen summa on 13. +``` + +**Summaaja.java** + +``` +import java.util.InputMismatchException; +import java.util.Scanner; +import java.util.ArrayList; + +public class Summaaja { + + public static void main(String[] args) { + + int i, sum = 0; + ArrayList lukulista = new ArrayList(); + + while (true) { + System.out.print("Syötä seuraava luku (0 lopettaa): "); + + Scanner syote = new Scanner(System.in); + + try { + int luku = syote.nextInt(); + + if (luku == 0) { + break; + } else { + lukulista.add(luku); + } + + } catch (InputMismatchException e) { + System.out.println("Virheellinen syöte!"); + } + } + + for (i = 0; i < lukulista.size(); i++) { + sum += lukulista.get(i); + } + + System.out.printf("Lukujen summa on %d.\n", sum); + } + +} +``` diff --git a/12_exception/4_Kello-ja-Poikkeukset.md b/12_exception/4_Kello-ja-Poikkeukset.md new file mode 100644 index 0000000..84b8e5e --- /dev/null +++ b/12_exception/4_Kello-ja-Poikkeukset.md @@ -0,0 +1,150 @@ +# Kello ja poikkeukset + +## 1. Kello-luokka + +Kirjoita luokka `Kello`. Kello-luokassa on oltava seuraavat ominaisuudet: + +- yksityiset oliomuuttujat `tunnit` (int) ja `minuutit` (int) + +- konstruktori `Kello(int tunnit, int minuutit)`, joka asettaa annetut arvot oliomuuttujiin + +- getterit ja setterit molemmille oliomuuttujille + +- `public void lisaaMinuutit(int mins)` -metodi, joka kasvattaa kellonaikaa annettujen minuuttien verran. Jos minuutit ovat lisäyksen jälkeen 60 tai yli, kasvatetaan myös tunteja siten, että minuuttiosuus pysyy aina välillä 0 ≤ minuutit ≤ 59. Jos vastaavasti tunnit ovat lisäyksen jälkeen 24 tai enemmän, pyörähtää kello ympäri ja tunnit alkavat jälleen nollasta. + +- `toString`-metodi, joka palauttaa kellonajan merkkijonoesityksen muodossa "22:52". Jos kellonajan minuutit ovat alle 10, tulee minuuttien edessä olla etunolla, esim. "0:06". + +## 2. Parametriarvojen tarkastaminen ja poikkeusten heittäminen + +Toteuta `Kello`-luokkaan tarkastukset, joissa tarkastetaan, että luokalle annetut tunnit ovat väliltä `0` ≤ tunnit ≤ `23` ja minuutit väliltä `0` ≤ minuutit ≤ `59`. Mikäli kellolle annetaan aika sallitun välin ulkopuolelta, tulee sen heittää `IllegalArgumentException`-poikkeus. Joudut huomioimaan oikeellisen ajan tarkastamisen sekä konstruktorissa että set-metodeissa. + +Tarkasta myös `lisaaMinuutit`-metodissa, että kelloon ei yritetä lisätä negatiivista aikaa. Mikäli lisättävät minuutit on negatiivinen luku, heitä `IllegalArgumentException`. + +`IllegalArgumentException`-luokan konstruktorille tulee antaa parametrina virheilmoitus. Tämän tehtävän kannalta ei ole merkitystä, minkä virheviestin annat poikkeuksille, esim: + +``` +throw new IllegalArgumentException("Virheellinen kellonaika"); +``` + +Tässä tehtävässä sinun ei tarvitse toteuttaa tekstikäyttöliittymää, mutta sinun kannattaa testata luokkasi erillisellä main-metodilla. + +## ESIMERKKI + +``` +=== Suoritettava testikoodi === +public class KelloTest { + + public static void main(String[] args) { + + Kello kello = new Kello(22, 12); + System.out.println(kello); + + kello.lisaaMinuutit(40); + System.out.println(kello); + + kello.lisaaMinuutit(29); + System.out.println(kello); + + kello.lisaaMinuutit(45); + System.out.println(kello); + } +} + +=== Ohjelman tulosteet === +22:12 +22:52 +23:21 +0:06 +``` + +**Kello.java** + +``` +public class Kello { + + private int tunnit, minuutit; + + private static int hour_min = 0; + private static int hour_max = 23; + + private static int mins_min = 0; + private static int mins_max = 59; + + public void setTunnit(int tunnit) { + this.tarkistaKello(tunnit,hour_min,hour_max); + this.tunnit = tunnit; + } + + public void setMinuutit(int minuutit) { + this.tarkistaKello(minuutit, mins_min, mins_max); + this.minuutit = minuutit; + } + + public int getTunnit(){ + return this.tunnit; + } + + public int getMinuutit() { + return this.minuutit; + } + + public void lisaaMinuutit(int mins) throws IllegalArgumentException { + + // kasvattaa kellonaikaa annettujen minuuttien verran + // jos minuutit yli 60, kasvattaa tunteja siten, että minuutit pysyvät välillä 0-59 + // + // jos tunnit ovat lisäyksen jälkeen 24 tai enemmän, "pyörähtää kello ympäri" ja + // tunnit alkavat jälleen nollasta. + + if (mins < 0) { + throw new IllegalArgumentException("Virheellinen minuuttiarvo"); + } + + this.minuutit += mins; + + if (this.minuutit > 60) { + + // kokonaiset tunnit kaikista minuuteista + int hours = (int)((double)this.minuutit/60.0); + this.tunnit += hours; + + // (minuutit/60 - kokonaiset tunnit) * 60 = jäljelle jäävät minuutit + this.minuutit = (int)((((double)this.minuutit/60.0) - hours) * 60); + + } + + if (this.tunnit >= 24) { + this.tunnit = 0; + } + + } + + public Kello(int tunnit, int minuutit) { + // konstruktori, joka asettaa annetut arvot (tunnit, minuutit) oliomuuttujiin 'tunnit' ja 'minuutit' + + this.tarkistaKello(minuutit, mins_min, mins_max); + this.tarkistaKello(tunnit,hour_min,hour_max); + + this.setTunnit(tunnit); + this.setMinuutit(minuutit); + } + + private void tarkistaKello(int tyyppi, int min, int max) { + if (!(tyyppi >= min && tyyppi <= max)) { + throw new IllegalArgumentException("Virheellinen kellonaika"); + } + } + + @Override + public String toString() { + // palauttaa kellonajan merkkijonoesityksen muodossa "22:52" + // jos minuutit alle 10, tulee minuuttien edessä olla etunolla, esim. "0:06". + + if (this.minuutit < 10) { + return this.tunnit + ":" + "0" + this.minuutit; + } else { + return this.tunnit + ":" + this.minuutit; + } + } +} +``` diff --git a/13_files/1_WordCount.md b/13_files/1_WordCount.md new file mode 100644 index 0000000..d5bab59 --- /dev/null +++ b/13_files/1_WordCount.md @@ -0,0 +1,97 @@ +# WordCount + +Kirjoita ohjelma `WordCount`, joka kysyy käyttäjältä tiedoston nimeä ja tulostaa kyseisessä tiedostossa olevien rivien, sanojen ja merkkien määrän. + +Luettavan tiedoston on oltava Java-projektin juuressa. Esimerkiksi suorituksesta: + +``` +Anna tiedoston nimi: loremipsum.txt + +Tiedostossa on: +2 riviä +8 sanaa +55 merkkiä +``` + +Esimerkissä on käytetty tiedostoa `loremipsum.txt`: + +``` +Lorem ipsum dolor sit amet, +consectetur adipiscing elit. +``` + +Riveksi lasketaan myös tyhjät rivit ja merkeiksi myös välilyönnit. Sanojen laskemiseksi voit käyttää String-luokan `split`-metodia, jolla pilkot kunkin rivin välilyöntien kohdalta. Huomaa kuitenkin, että tyhjällä rivillä ei saa laskea yhtään sanaa, vaikka `split`-metodi palauttaakin yhden pituisen taulukon. + +Voit hyödyntää omaa ohjelmaa testatessasi myös [tekstitiedosto.txt](https://gist.githubusercontent.com/swd1tn002/92d5a1082c978e2d0e90a082be1fca0b/raw/c5199dda06f2e34590301b1b711d4b157cc02c76/tekstitiedosto.txt)-tiedostoa. + +``` +Anna tiedoston nimi: loremipsum.txt + +Tiedostossa on: +2 riviä +8 sanaa +55 merkkiä +``` + +**WordCount.java** + +``` +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.BufferedReader; +import java.util.Arrays; +import java.util.Scanner; + +public class WordCount { + + public static void main(String[] args) { + + Scanner syote = new Scanner(System.in); + System.out.print("Anna tiedoston nimi: "); + String tiedostoNimi = syote.next(); + + try { + File tiedosto = new File(System.getProperty("user.dir"), tiedostoNimi); + + int i, rivit = 0, sanat = 0, merkit = 0; + FileReader lukija = new FileReader(tiedosto); + BufferedReader riviVirta = new BufferedReader(lukija); + String nykyRivi = riviVirta.readLine(); + + while (nykyRivi != null) { + + // Laske rivit + rivit++; + + if (!nykyRivi.isEmpty()) { + + // Laske sanat + i = 0; + while (i < Arrays.asList(nykyRivi.split(" ")).size()) { + sanat++; + i++; + } + + // Laske merkit + i = 0; + while (i < Arrays.asList(nykyRivi.split("")).size()) { + merkit++; + i++; + } + } + // Siirry seuraavaan riviin + nykyRivi = riviVirta.readLine(); + } + lukija.close(); + + System.out.printf("Tiedostossa on:\n%d riviä\n%d sanaa\n%d merkkiä\n", rivit, sanat, merkit); + + } catch (IOException e) { + System.err.printf("Tiedostoa %s ei löydy tai sen lukemisessa kävi virhe", tiedostoNimi); + } + + } + +} +``` diff --git a/README.md b/README.md index 02ec788..24315be 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Contents of this repository are based on [Java programming course material](http **NOTE**: Assignments and their descriptions are in Finnish for meanwhile. I have a plan to translate them to English. -**NOTE**: Code indentation in markdown (`.md`) files is incorrectly rendered in Gitea. Click `Raw` to see the correct indentation. +**NOTE**: Code indentation in markdown (`.md`) files is incorrectly rendered in Gitea. Click `Raw` to see the correct indentation of the provided Java code. ## Contents @@ -49,3 +49,15 @@ Contents of this repository are based on [Java programming course material](http - [Classes and objects](src/branch/master/10_classes-and-objects) - Description: Custom classes and objects in Java + +- [Object-oriented programming](src/branch/master/11_object-oriented_programming) + + - Description: Multi-class Java programs + +- [Exception](src/branch/master/12_exception) + + - Description: Exception handling in Java + +- [Files](src/branch/master/13_files) + + - Description: File reading in Java