Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 293 additions & 0 deletions Solutions/EX10
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
EX10Stargate lahendusjuhend
===========================================================
(ehk kuidas ehitada planeete ja siis mõttetumad ära visata)
-----------------------------------------------------------
EX10Stargate'i lahendamisel tuleb kindlalt kasuks
PR10-e läbitegu, kuid siin las olla vaid EX10Stargate'i
ülesande juhend. Kui mõni samm tundub liiga lihtne, siis
võib selle julgelt ise ära teha. Siit aga leiab ühe suuremat
sorti rõõmusõnumi ehk evangeeliumi: päris kindlasti leiavad
juhendist kõik väga põhjaliku ülevaate lahendusest ja algajad palju
abi, sest küsija suu pihta ei lööda: kui paned Githubis
issue külge, täiendatakse loodetavasti juhendit vastavalt.
Hõissa ja laulupidu!

Mooduli ja pakkide ülesseadmine ehk korista enne segaduse teket
---------------------------------------------------------------
Selleks, et ülesannet alustada, klõpsame parema hiireklahviga
mõne mooduli peale, tekkinud valikmenüüs valime New->Module,
vaatame tekkinud aknas, et me looks Java mooduli, mis kasutab
mõnda juba paigaldatud Java JDK-d. Vajutame Next ja muudame
kausta nimeks EX10Stargate ja vajutame Finish. IntelliJ koostab
ise projekti uue mooduli ja paneb sinna allikkausta src meile
kasutamiseks. Milline rõõm!

Edasi tuleb luua pakk ee.ttu.iti0202.stargate klõpsates
src kausta peal parema hiireklahviga ja valides valikmenüüs
New->Package. Sisesta ee.ttu.iti0202 paki nimeks ja loo
samamoodi pakile kaks alampakki: planet ja space, seekord
ee.ttu.iti0202.stargate paki peal klõpsates. Siia lähevad
meie ülesande klassid. Nüüd on meil kõik olemas, et hakata
klasse lisama. Juhhei!

Klass Planet ehk teeme kodus ise oma planeedi, tuleb odavam
-----------------------------------------------------------
Klass Planet hoiab endas järgmist infot: planeedi nime (sõne),
elanikearvu (long ehk ülipikk täisarv), kas planeedil on
tähevärav (tõeväärtus), kas too tähevärav on kasutatav
või lihtsalt vanaraud (ehk kas DHD on olemas - veel üks
tõeväärtus) ja mis tiimid teda külastanud on (järjend sõnedest).
Tegu on ülestuunitud järjendiga: selle asemel, et hoida ühe
planeedi andmeid lihtsas listis, paneme nad meie loodud klassi,
millel on rohkem võimalusi, šarmi ja isikupära. On mugavam
käidelda andmeid, kui IntelliJ pakub muutujanimesid puuvilja-
vaagnal, kui et peab meeles hoidma, mis indeksiga on mingi
kirje järjendis. Kuis metsa hõikad, nõnna vastu kajab!

Selle jaoks paremklõpsame pakil planet ja valime New->Java class.
Nimeks paneme Planet ja juba saame Planet klassi sisse
kirjutada viis muutujat:
private String name;
private long inhabitants;
private boolean stargateAvailable;
private boolean dhdAvailable;
private List<String> teamsVisited;
Nüüd toimime järgnevalt: paremklõpsame koodiredaktori aknal
kuskil klassi sees ja valime Generate või olles kursoriga klassis
vajutame Alt+Insert. Uues menüüs valime Getter and Setter ja tekkinud
aknas valime kõik muutujad vajutades Shift ja nooleklahve.
Klikkame OK ja tekivad ülesandes vajaminevad getter ja setter
meetodid. Kuid see pole veel kõik! Samamoodi klõpsates ja
valides Generate ja Constructor, saame valida tekkinud aknas
kõik muutujad, vajutada OK ja äkitsi on meil konstruktor,
mis klassis samuti vajalik oli. Nüüd on vaja vaid kirjutada nendele funktsioonide
alla string, valida IntelliJ pakutud public toString()
funktsioon, panna funktsioon planeedi nime tagastama ja
klass on tehtud. Halleluuja! Kristus sündinud meil!

Klass PlanetBuilder, sest allhanked on kasvava majanduse alus
--------------------------------------------------------------
Kui ütled PlanetBuilder, siis mõtled StringBuilder aga
planeetidega! Hetkekski ei tohiks olla kõhklust, mida PlanetBuilder
teeb -- ta ehitab planeete! Aga milleks meil siis Planet klassi
konstruktor? Teeb ta ju sama hästi planeedi valmis? Vastus on,
et kuigi IntelliJ ütleb mõnikord ette, mis parameetrid tuleb
klassis algväärtustada, siis ausalt öeldes on see mõnikord tüütu
ja kaka-pähh. Ja mida teha siis, kui tahad andmeid lisada omas
tempos, siis kui *sulle* sobib? Niisiis loome klassi, mis aitab
meil sutsu kergemalt hingata ja selgesti nimetatud funktsioonidega
lisada andmeid ja mis tagastab meile planeedi objekti siis, kui
*meie* seda soovime. Emantsipatsioon võidule!

Paremklõpsatus koodil (aga mitte näiteks String märksõnal, proovi parem
mõnd tühja rida selleks) ja valik Refactor->Replace Constructor with
Builder toob meie ette akna, mis täidab kõik me unistused.
Tekkinud aknas võtame veerus Setter Name kõikidelt muutujanimedelt
set eest ära ja teeme algustähe väiketäheks, sest ülesanne nõuab nii
ja vajutame Refactor. Tekib uus klass PlanetBuilder, mille objekti
saame luua, anda funktsioonidega sisse erinevaid väärtusi ja luua
Planet objekt funktsiooniga createPlanet(). Nõnnasi saame mängida
jumalat ja luua planeete oma elutoadiivanilt.

Klass Space ehk ülekosmoseline Rajaleidja sõnarohkete funktsioonidega
------------------------------------------------------------------------------
Kosmos on suur kant ja tal on palju planeete, algaja võibolla ei teagi, millisele minna.
Selleks on vaja neid sortida ja ülesande kirjelduses on kriteeriumid selgelt kirjas.
Järgnevalt on loetelu neist kõigist, võime nendest silmad üle libistada, sest võtame nad hiljem
niikuinii üksipulgi lahti.

Selleks, et orienteeruda suures komsoses on meil vaja 7
funktsiooni: üht, mis tagastab järjendi klassile teada-tuntud planeetidest,
teist, mis saab CSV (Comma separated values) vormis failst kätte plnaaetide andmed,
kolmandat, mis tagastab hulga planeetidest, kus pole elanikke, neljandat,mis annab hulga
planeete, mida saab külastada, aga pole veel külastatud, viiendat, mis tagastab keskmise
täheväravaga planeedi elanike arvu, kuuendat, mis tagastab tiimid, kes on külastanud vähe-
asustatuid planeete, ja seitsmendat, mis tagastab planeetide tüübi koos nende sagedustega.
Kuna tööd on parajalt, siis hakkame aga pihta. Tahad latva ronida, hakka tüvest peale!

List<Planet> getPlanets() ehk kui peaksid tahtma kõiki planeete uurida ja külastada
-----------------------------------------------------------------------------------

On ilmselge, et planeetide tagastamiseks järjendina võiks luua instantsimuutuja, mis
kätkeb endas planeetide järjendit. Niisiis loome ja algväärtustame selle tühjaks järjendiks,
et ei tekiks määramata väärtuse erindit:
private List<Planet> planets = new ArrayList<>();
Nüüd pole miskit, kui seada üles getter meetod nii, nagu seda teinud juba oleme. Kes tasa
sõuab, see kaugele jõuab.

List<Planet> csvDataToPlanets(String filePath) ehk teiste töö ärakasutamine autorile viitamata
----------------------------------------------------------------------------------------------
Selleks, et saaksime andmeid töödelda ja esitada, peame tundmatu autori CSV-formaadis andmed vormima ümber
Planet objektideks, esitama neid enda tehtu pähe, salvestama nad ja paluma jumalat, et nood andmed on
usaldusväärsed ja vigadeta. Niisiis peame tegema vähe failist lugemist ja planeediloomet. Lisaks hakkame
esimest korda kogu ülesande jooksul kasutama Stream API-t, niisiis en guarde, noor koodija!

Kõigepealt aga loeme failist kõik andmed ja paneme need muutujasse: selleks kasutame
Files.readAllLines(Path path) funktsiooni, mis mugavalt tagastab järjendi faili ridadest, millele path
parameeter viitab. Selleks aga, et parameeter filePath järsku Pathiks muutuks, kasutame järgmist koodirida,
mille leidsime Java dokumentatsioonist:
Path path = Paths.get(filePath);
Edasi valime ühe muutujanime, millesse salvestame kõik andmeread, näiteks lines. Nota bene! Faili näidisest järeldame, et esimene
rida sisaldab alati veeru päised, ehk siis metaandmeid, mis meile on silmaga kontrollimisel küll tulusad,
aga mida töödelda on asjatu. Niisiis jätame välja tolle esimese rea:
List<String> lines = Files.readAllLines(path).sublist(1, lines.size());
IntelliJ Java kompilaator aga pistab röökima selle peale, sest, nagu kõik head programmid, ta usaldab vähe kasutaja hooleks,
veel vähem usaldab kasutajat. Sellel real võib tekkida sisend-väljund erind (InputOutputError), mis võib
tulla sellest, et pole faili või faili ei saa avada (pole õigusi) või fail on kuskil mujal või universum
tahab sulle öelda, et sa võiksid lahendada teised meetodid kõigepealt ära ja minna selle konkreetse reaga
Ago juurde konsulli. Kuna aga Java tahab kindlalt koodiga edasi minna, olgu mis tahes, siis, olles kursoriga
tol real, vajuta Alt-Enter ja vali Surround with try-catch. IntelliJ genereerib koodi, mis Java kompilatori
maha rahustab, ja kõik on jälle korras. Kas pole mitte tore?

Nüüd on aeg kasutada Java vooge (Stream API-t). Teeme ridade järjendist voo, paneme voole vahemeetodi map(), kus
lisame hiljem andmed omatehtud andmestruktuuridesse ja kogume voo lõpus lisamisel tagastatu järjendisse, salvestades
kogutu intsantsimuutujasse, mille tegime eelmist funktsiooni tehes:
planets = lines.stream()
.map(
// Meetod, mis töötleb ühe rea andmeid, teeb neist PlanetBuilderiga ühe Planeti
)
.collect(Collectors.toList());
Meenutame, et .stream() tegi andmestruktuurist tema elementide voo, .map() töötleb igat voo elementi vastavalt
argumendiks antud funktsioonile ja .collect() kasutab argumendiks antud meetodit, mis kogub elemendid kokku uueks
andmestruktuuriks.

Edasi peame kirjutama igat rida töötleva koodi. Kasutame selleks lambdat ja PlanetBuilderit. Alustame lambdaga:

line -> { // töötlev kood}

Seal sees peame rea jaotama sõnemassiiviks split() meetodiga, kasutame poolitajana koma, käivitame
uue PlanetBuilderi klassi, paneme nimeks nullinda elemendi sõnemassiivist (sest esimene
veerg hoiab endas planeedinime), elanike arvuks esimese, tähevärava olemasoluks teise, ja
DHD olemasoluks kolmanda. Neljanda väljaga, milleks on külastanud tiimide nimed, tuleb
rohkem tööd teha, seal peab vaatama, et ta oleks rohkem kui 2 tähemärki pikk (ehk siis
ei ole lihtsalt tühjad nurksulud), jaotama ta sõnemassiiviks split() funktsiooniga, kuid
seejuures ei tohi kaasa võtta esimest tähemärki ja viimast, sest nad on nurksulud ja
järjendi tähiseks, mitte ühegi planeedi nime osa. Viimaks, kui oleme lisanud kõik väljad,
loome planeedi ja tagastame selle lambdas. Seega oleks kogu töötlev kood selline:
line -> {
String[] lineData = line.split(",");
PlanetBuilder planetBuilder = new PlanetBuilder();
planetBuilder.name(lineData[0]);
planetBuilder.inhabitants(Long.parseLong(lineData[1]));
planetBuilder.stargateAvailable(Boolean.parseBoolean(lineData[2]));
planetBuilder.dhdAvailable(Boolean.parseBoolean(lineData[3]));

String teamsVisited = lineData[4];
if (teamsVisited.length() > 2) {
planetBuilder.teamsVisited(Arrays.asList(teamsVisited.substring(1, teamsVisited.length() - 1)
.split("; ")));
} else {
planetBuilder.teamsVisited(new ArrayList<>());
}

Planet planet = planetBuilder.createPlanet();
planets.add(planet);
return planet;
}

Viimaks tagastame csvDataToPlanets() funktsioonis planeedid, mida kogusime:

return planets;

getDeadPlanets(List<Planet> planets) ehk potentsiaalsed klassiekskursiooniideed
-------------------------------------------------------------------------------

Siin hakkame kasutama Stream API-d. Kõigepealt loome planeetidest voo:

planets.stream()

Filtrime välja need planeedid, kus pole elanikke, aga kuhu saab reisida:

planets.stream()
.filter(planet -> planet.getTeamsVisited().size() == 0
&& planet.isDhdAvailable() && planet.isStargateAvailable())

Ja kogume nad kokku hulgaks ja tagastame ta:

return planets.stream()
.filter(planet -> planet.getTeamsVisited().size() == 0
&& planet.isDhdAvailable() && planet.isStargateAvailable())
.collect(Collectors.toSet());


getAvgInhabitantsOnPlanetsWithStargate(List<Planet> planets) ehk statistiku maiuspala
-------------------------------------------------------------------------------------

Siin me peame samamoodi sõela peale saama planeedid, kuhu on võimalik reisida, ja võtma
nende elanike arvu ja tagastama kõigi planeetide keskmise elanike arvu. Võtmiseks sobib
mapToLong() funktsioon, millel on lambda-funktsioon sees, mis tagastab planeedi elanike
arvu, ja keskmise leidmiseks sobib .average() funktsioon. Nii tuleb funktsiooni sisuks:

return planets.stream().filter(planet -> planet.isStargateAvailable())
.mapToLong(planet -> planet.getInhabitants()).average();


getTeamsWhoHaveVisitedSmallNotDeadPlanets(List<Planet> planets) ehk keda tunnustada äärealase tegevuse eest
-----------------------------------------------------------------------------------------------------------

Siin alustame samuti sõelumisega: tahame sõela peale saada planeedid, mille elanike arv
on väiksem kui 2500, aga suurem kui 0. Peale seda peame tiimide järjendite voo muutma
tiimide vooks. Niisiis näeb meie koodirida välja nüüd selline:

planets.stream().filter(n -> n.getInhabitants() < inhabitantLimit && n.getInhabitants() > 0)
.flatMap(n -> n.getTeamsVisited().stream())

Siia lisandub see nõue, et me peame kordustest lahti saama, selleks sobib .distinct():

planets.stream().filter(n -> n.getInhabitants() < inhabitantLimit && n.getInhabitants() > 0)
.flatMap(n -> n.getTeamsVisited().stream())
.distinct()

Ülesandes on vaja too tiimide voog ära sortida, niisiis peame kasutama .sorted() funktsiooni.
Samas me ei saa puhtalt terve tiimi nime põhjal sortimist teha, peame kasutama numbrilist osa.
Selleks peame tegema võrdleja, ehk siis meetodi, mis tagastab positiivne täisarvu, kui
esimene objekt on teisest suurem, nulli, kui objektid on võrdsed, või negatiivse täisarvu,
kui teine on esimesest suurem. Lisandub kood:

.sorted((o1, o2) -> {
int first = Integer.parseInt(o1.substring(3, o1.length()));
int second = Integer.parseInt(o2.substring(3, o2.length()));
if (first > second) {
return 1;
} else if (first < second) {
return -1;
} else {
return 0;
}
})

Nüüd kogume tulemused kokku ja tagastame need:

return planets.stream()
...
.collect(Collectors.toList());


getCodeNameClassifierFrequency(List<Planet> planets) ehk vaatame planeetide seisu
---------------------------------------------------------------------------------

Siin peame planeetide voost sõeluma välja need, mis ei vasta ülesandes kirjeldatud mustrile.
Üks viis seda teha on poolitada planeedi nimi tolle mustri järgi ja vaadata, kas tekkinud
sõnemassiiv on võrdne tühja sõnemassiiviga. Viimaseks sobib staatiline meetod Arrays.equals(),
esimeseks on tarvis sutsu regulaaravaldisi tunda: kui meil on esimene täht P, millele
järgneb kaks numbrit või tähte, millele järgneb sidekriips ja numbririda, siis sel puhul
tuleb järgnev kood kasuks:

.filter(n -> Arrays.equals(
n.getName().split("P\\p{Alnum}\\p{Alnum}-\\p{Digit}*"), new String[]{}))

Sealt edasi peame ta koguma Mapiks, niisiis peame kasutama üht erilist kollektorit:
Collector.groupingBy(). Meie juhul anname talle kaks argumenti: esiteks funktsiooni,
mis tagastab võtme, millega ta Mapis toimtama hakkab, ja funktsiooni, mis täpsustab,
mida väärtusega teha. Esimeseks sobib lambda, mis tagastab planeedi identifikaatori,
teiseks sobib Collector.summingLong() funktsioon, mille sees on funktsioon, mis tagastab ühe.

.collect(Collectors.groupingBy(planet -> planet.getName().substring(0, 3),
Collectors.summingLong(e -> 1)));


Sellega on ülesanne lahendatud ja saab minna konsultatsiooni kaitsma!