Mikroandmebaasi ülesande soovitusi ja nõuandeid
Sisukord
Kuidas pihta hakata ja mis järjekorras programmeerida?
Põhimõte:
Hakka kirjutama valmis üksikuid juppe, alates lihtsamatest. Kui üks on valmis, testi ta läbi ja mine siis järgmise kallale. Testimiseks kirjuta trükkimisi vahele, hiljem kommenteeri mittevajalikud trükid välja või kustuta ära. Alles lõpuks pane kõik main-i sees kenasti kokku.
Väldi algul mistahes keerukusi ja lisavõimaluste tegemist! Ehita kõik algul valmis nii lihtsalt, kui saad, hiljem saad soovi korral koodi täiustada, kui aega jääb.
Soovitaksin teha tükid valmis näiteks sellises järjekorras:
- Funktsioon, mis loeb select lausest välja failinimed ja tingimused ja paneb need vastavatesse massiividesse.
- Funktsioon, mis saab ette failinime, seepeale loeb sisse vastava csv faili ja tagastab kahemõõtmelise stringimassiivi, mille sees selle csv faili väljad (stringid) ridade kaupa.
- Funktsioon, mis genereerib järjest kõik kombinatsioonid kõigi sisseloetud massiivide ridadest (täielik ristkorrutis).
- Funktsioon, mis võtab ette ühe taolise ristkorrutise rea (stringimassiivi) ja kontrollib, kas see rida vastab where tingimustele select lauses (kasutades muidugi varem-ehitatud massiivi, kus where tingimused sees).
- Pane tükid järjest kokku, kutsudes neid main funktsiooni sees järjest välja (kõigepealt select lause analüüs, siis tsükkel üle failinimede, kutsudes iga kord välja faili sisselugemise funktsiooni, siis ristkorrutise ehitamine, mis omakorda kutsub iga genereeritud rea jaoks välja where tingimuste kontrolli-funktsiooni.
Mis juhtub, kui sa kõiki asju ei oska või ei jõua teha? Kui mingi mõistlik hulk komponente on tehtud ja töötavad, saad oma ülesande siiski arvestatud, ainult et väiksema arvu punktidega (kui praktikumijuhendaja hindab, et tehtud on pool tööd, siis saadki 5 punkti). Kui praktikumijuhendajale tundub, et tehtud on ikka väga vähe (kolmandik või vähem) siis ta üldjuhul seda ülesannet ei arvesta.
Soovitusi funktsioonide kirjutamisel
Globaalsed muutujad
Päris mõistlik on kasutada mingit hulka globaalseid muutujaid ja massiive, näiteks:
- from: ühemõõtmeline massiiv tabelitest/failidest select lause from osast
- where: kahemõõtmeline massiiv tingimustest select lause where osast
- tables: kolmemõõtmeline massiiv sisseloetud tabelitest (iga tabel ise kahemõõtmeline massiiv)
- resultheader: ühemõõtmeline massiiv tulemuse päisereaga
- resultrow: ühemõõtmeline massiiv tulemuse ühe reaga
Funktsioonid võivad vajadusel vabalt otse nende globaalsete muutujatega manipuleerida.
Stringi lõhkumine eraldaja järgi
Sinu programm peab päris mitmes kohas lammutama eraldajaga stringi tükkideks:
- select lauses from (tabelite/failide) osa, eraldajaks komad: fail1, fail2, ...,failn
- select lauses where tingimused, eraldajaks and: tulp1=vaartus1 and .... and tulpm=vaartusm
- kettalt loetud csv faili rida, eraldajaks jällegi komad
- where lause iga üksik komponent tulp1=vaartus1, kus eraldajaks = ja komponente alati kaks.
Kuidas seda paremini teha?
Esiteks, iga sellise lammutamise tulemuseks tasub anda stringimassiivi, kus elementideks on järjest vajalikud stringid. Hea on, kui see stringimassiiv teha kohe õige pikkusega, siis saab mh lihtsamalt kontrollida, kui palju elemente temas on (massiiv.length annab massiivi pikkuse).
Teiseks, mõistlik on kirjutada funktsioon, mis võtab ette lammutatava stringijupi, eraldaja-stringi (koma või and vÕi =) ja annab tagasi stringimassiivi.
Stringi lammutamiseks on palju meetodeid, kuid kõige lihtsam on uurida näiteprogrammi splitproov.java ja kasutada seal olevaid ideid. NB! Selleks, et selle programmi lõike kasutada, on vaja, et sa temast aru saaksid. Vaata kindlasti ka massiivide kasutamise näiteprogrammi massiivproov.java
where lauseosa tükkideks lõhkumine
where lauseosa (tulp1=vaartus1 and .... and tulpm=vaartusm) lammutamise tulemuseks võiks olla kahemõõtmeline massiiv: m rida, kus igasühes kaks tulpa (tulba nimi ja vajalik väärtus). Selleks võid kõigepealt lammutada where lauseosa and-eraldaja järgi ühemõõtmeliseks massiiviks ja seejärel käia see massiiv tsükliga Üle, ja teha igast element-stringist omakorda kahe-elemendiline massiiv (eraldaja =). Nendest kahe-elemendilistest massiividest saadki kokku kahemõõtmelise massiivi kõigi vajalike stringidega:
tulp1 vaartus1 tulp2 vaartus2 ... tulpm vaartusm
NB! Pane tähele, et kui väärtus-stringil on apostroofid ümber (näiteks 'koer') siis ta tähistab konkreetset stringiväärtust, mis tulbas peab olema. Kui apostroofe pole, siis tähistab väärtus-string teise tulba sisu! Seega pead apostroofid (kui nad on) kindlasti alles jätma, sest selle kaudu saad aru, et kumba variandiga on tegemist.
Failide sisselugemine ja komade järgi splittimine
Sellest on veidi kirjas siinse jutu lõpuosas. Kindlasti tee funktsioon, mis saab ette failinime ja annab tagasi kahemõõtmelise massiivi temast leitud stringidest. Siis on lihtne seda funktsiooni tsüklis välja kutsuda, üle kõigi sisseloetavate failide.
Failide/tabelite ristkorrutis
Vaata ülesande spetsifikatsioonist uuesti üle, mida selle all mõeldakse. Arusaamine on siinkohal hädavajalik!
Kuidas seda aga kirjutada?
Esiteks, pane tähele, et ülesande spetsifikatsioon ei nõua tulemus-massiivi koostamist, vaid ainult tulemuse väljatrükki! See lihtsustab veidi asja. Mõttekas on koostada ristkorrutist nii, et:
- Kõigepealt teed ühemõõtmelise stringimassiivi (nt nimega resultheader) tulemuse päisega (st kõigi failide/tabelite päiseread järjest, iga originaal-päisevälja ette lisatud failinimi ja punkt).
- Siis hakkad järjest koostama tulemusridu, iga kord ehitades üheainsa tulemusrea ühte ühemõõtmelisse massiivi (nt resultrow), mis seega saab pidevalt üle kirjutatud.
- Hiljem lisad siiakohta kontrolli select lause where osaga: et kas rida välja trükkida või ei: kui vastab tingimustele, trükime välja, muidu mitte.
- Esialgu ära where-tingimusi kontrolli, vaid trüki debugimise jaoks iga genereeritud rida välja.
Nüüd ristkorrutise juurde. On arusaadav, et tuleb teha tsükleid tsüklite sees, kusjuures igale tabelile/failile vastab üks tsükkel üle kogu faili. Kui meil oleks näiteks kaks tabelit, oleks vaja teha umbes nii:
for(i=0;i<tables[0][i].length;i++) { for(j=0;j<tables[1][j].length;ij++) { .... liida read tables[0][i] ja tables[1][j] .... } }
Paraku ei saa me koodi aga kirjutada fikseeritud arvu tsükleid, sest sisseloetavate tabelite/failide arv sõltub kasutaja antud sisendist.
Mis siis teha? Üks võimalik meetod on järgmine. Olgu meil näiteks kolm massiivi, st tables.length==3. Teeme ühe integer-abimassiivi tmp, samuti pikkusega kolm, ja hakkame seal massiivis hoidma infot, et mitmendat rida massiivis i tuleks järgmisena kasutada. Kuna 0s rida on meil päiserida, ja kasutame alates reast 1 , siis tmp sisaldab algul ühtesid.
Teeme nüüd nii:
- Ehitame uue tulemusrea, kasutades igast tabelist/failist just seda rida, millele vastav tmp väli viitab (esialgu kõik ühed).
- Nüüd suurendame esimest tmp elementi (st tmp[0]=tmp[0]+1;) ja kui ta läheb liiga suureks (tmp[0]>tables[0].length) siis teeme ta uuesti 1-ks (tmp[0]=1) ja suurendame hoopis järgmist tmp elementi (tmp[1]=tmp[1]+1;). Kui see ka liiga suur, teeme temagi üheks, ja läheme järgmise juurde (tmp[2]). Kui kõige viimane tmp element ka liiga suureks läheb, siis on kõik kombinatsioonid tehtud ja meie lõpetame. Muidu aga läheme tsüklis jälle eelmise sammu juurde ehitame uue tulemuse, kasutades tmp faili sisu kui viitajat kasutatavatele ridadele).
Kood oleks umbes selline:
for(i=0;i<tmp.length;i++) tmp[i]=1; // algvaartused stopflag=false; for(;;) { // teeme uue rea for(i=0;i<tmp.length;i++) { ... kasuta rida tables[i][tmp[i]] ... } // muudame tmp sisu, et parast teha uus rida j=0; for(;;) { tmp[j]=tmp[j]+1; // votame jargmise rea if (tmp[j]<=tables[j].length) break; // koik ok, ei ole veel liiga suur vaartus // muidu aga paneme yheks ja votame jargmise j (jargmise tabeli/faili) tmp[j]=1; j++; // kontrollime, kas oli akki viimane tmp element if (j>=tmp.length) { stopflag=true; // peame valimisest tsyklist kah valjuma, st koik read tehtud! break; } } if (stopflag) break; }
where tingimuse kontroll
Iga uue rea tegemise järel on sul:
- muutumatu tulemus-päiserea massiiv resultheader kõigi tabel.tulbanimi väärtustega
- värskelt tehtud üks tulemusrida massiivis resultrow
Nüüd tuleb teha tsükkel üle kahemõõtmelise where-osa massiivi. Iga elemendi where[i] juures:
- leida, mitmes element resultheader massiivis on sama, kui where[i][0] (tulbanimi tulp=vaartus paaris)
- oletame, et selle elemendi number oli N. Siis kontrollime ühte kahest (olenevalt, kas where[i][1] esimene sümbol oli apostroof või ei):
- kas "'"+resultrow[N]+"'" väärtus on sama, kui where[i][1]? Kasuta string1.equals(string2)!
- leiame tulba M resultheader massiivis, mis sisaldab stringi where[i][1] ja kontrollime, kas resultrow[N].equals(resultrow[M])
Kah kasulikke soovitusi
Stringide võrdlemine
NB! Kui x ja y on stringid, siis võrdlemine
x==y
ei anna oodatud tulemust! See võrdlus ainult kontrollib, kas x ja y on võrdsed pointerid, st viitavad samale stringile mälus. Aga, kui teeme nii:
String x="ab"; String y="ab"; if (x==y) ...
siis x ja y on erinevad pointerid (kuigi stringide sisud on samad!) ja neid ei peeta võrdseks.
Selleks, et kontrollida, kas stringide sisud on samad, on vaja kasutada sellist spetsiaalset stringivõrdlust:
x.equals(y)
(selle funktsiooni ja muud head stringifunktsioonid leiab nii Ecki õpikust kui ametliku manuaali lingilt http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html)
Võrdlemaks, kas string x on suurem kui string y (vajalik sorteerimisel), kasuta järgmist funktsiooni:
x.compareTo(y)
Lisainfot (mitte väga olulist) leiad eelnevalt viidatud linkidest.
Mida compareTo välja annab? int-i. Mida see int näitab? Katseta ise: võrdle paari stringi (pane katse main funktsiooni) ja trüki tulemus-int välja.
Kuidas teha õige suurusega massiiv, kui näiteks failist loed?
Failist lugemise kohta vaata kindlasti ka näiteprogrammi ioproov.java
Probleem selles, et meil on vaja panna failist loetud stringid massiivi, aga me ei tea ette, kui suur on fail. Seega, kui suur massiiv tuleks teha?
Siin on, nagu alati mitu lahendust.
Hästi lihtne ja ebaefektiivne lahendus on kõigepealt lugeda fail sisse, aga mitte massiivi panna: selle asemel lugeda kokku ridade arv (ja tulpade arv ka).
Siis teeme õige suurusega massiivi ja loeme seejärel failist uuesti kõik read, seekord pannes nad massiivi sisse.
Elegantsem lahendus on vältida mitmekordset lugemist. Aga: kui sa endas väga kindel ei ole, soovitan algul realiseerida lihtsama lahenduse, mida äsja kirjeldasin.
Elegantsema lahenduse saab kirjutada näiteks nii:
Esiteks loeme sisse esimese rea ja vaatame, mitu tulpa seal on. Siis teame kohe massiivi teist (tulpade arvu) dimensiooni. Järgmistes ridades peab ju olema sama palju tulpasid.
Nüüd ei tea me ikkagi, mitu rida massiivis on.
Järgmisena teeme esialgse, ajutise massiivi näiteks suurusega 100 rida (ja õige arv tulpasid). Hakkame sinna lugema. Kui fail mahtus ära, teeme uue massiivi, seekord õige (lühema) pikkusega, ja kopeerime esialgse massiivi vajalikud read sinna massiivi. Vana massiiv visatakse mõne aja pärast prahikoristaja poolt automaatselt ära.
Kui fail ei mahtunud esialgsesse massiivi, teeme uue ajutise massiivi, seekord näiteks 2 korda suurema kui eelmine. Kopeerime vanast ajutisest kõik uude ajutisse ja jätkame failist uude massiivi lugemist. Võib juhtuda, et nüüd mahub ära. Siis lõpuks teeme õige pikkusega (lühema) massiivi.
Kui ikka ei mahtunud, kordame protseduuri: teeme jälle uue, viimasest masiivist jällegi kaks korda suurema ajutise masssiivi. Jne jne.
NB! Selliselt ajutisi massiive tehes pane tähele, et kuna tulpade arv on neis alati õige, ei ole tegelikult vaja kopeerida vanast massiivist uude üksikute stringide kaupa (st topelttsükliga). Kahemõõtmeline massiiv on ju massiiv mitmest ühemõõtmelisest! Kopeerimise käigus saame kopeerida korraga terve rea (käsitledes massiive kui ühemõõtmelisi) ja see on palju kiirem meetod. Siin on veel võimalik teha mitu täiendavat optimeeringut, aga liiale ei ole optimeerimis-pingutustega ka mõtet minna.