Java fundamentals through coding exercises
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

23 KiB

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<Pvm>     paivaArvot = new ArrayList<Pvm>();
List<Integer> painoArvot = new ArrayList<Integer>();
// 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<Integer> jarjestaArvot(List<Pvm> paivaArvot, List<Integer> painoArvot) {
List<LocalDate>     paivaArvotValid = new ArrayList<LocalDate>();
List<LocalDate>     paivaErrors     = new ArrayList<LocalDate>();
Map<LocalDate, Integer> piirturiMap = new HashMap<LocalDate, Integer>();
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<LocalDate, Integer> piirturiTreeMap = new TreeMap<LocalDate, Integer>(piirturiMap);
// Java garbage collector
paivaArvotValid = null;
paivaErrors     = null;
return getValues(piirturiTreeMap);
}
// Get values from sorted input Map (sorted by date)
private static <Key, Value> List<Value> getValues(Map<Key, Value> inMap) {
List<Value> painoArvotSorted = new ArrayList<Value>();
for (Map.Entry<Key, Value> entry : inMap.entrySet()) {
painoArvotSorted.add(entry.getValue());
}
return painoArvotSorted;
}
private static void tulostaDiagrammi(List<Integer> syoteArvot) {
List<String> hashSymbols = new ArrayList<String>();
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<PainoMittaus> painoEntries = new ArrayList<PainoMittaus>();
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<String> 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;
}
}
}
*/