Työaikaraportti
Nämä ovat "kädestä pitäen" -ohjeet komentorivikäyttöisen Työaikaraportti-ohjelman luomiseen Visual Studiolla.
Työaikaraportti lukee kaksi tekstitiedostoa ja tuottaa niistä projekteittain lasketun aikaraportin.
Katso ensin työn suunnitelma.
Tämän tutorialin ohjeet ovat sekä teksti- että videomuodossa.
Luentomoniste-linkeistä voit hypätä Ohjelmointi 1 -kurssin luentomonisteen eri osiin, joissa käsiteltävä asia on selitetty yksityiskohtaisemmin. Jos asia ei ole tuttu, klikkaa tällöin linkkiä ja perehdy! Linkki kannattaa avata selaimessa uuteen välilehteen.
Joka kappaleen lopusta löytyy plus-painikkeen alta vaiheen päätteeksi aikaansaatu koko projektin koodi .
Videossa kirjoitettava koodi löytyy aina videon yläpuolelta. Voit halutessasi kirjoittaa koodia videota katsellessasi (videot on tehty Visual Studiolla, mutta voit soveltaa niitä kun kirjoitat Riderilla tai VSCodella) ja tarvittaessa katsoa koodista mallia.
Jos haluat kokeilla ajaa koodia TIMissä, voit käydä painamassa Tallenna alla oleviin tiedostoihin (projektit.txt
ja ajat.txt
) ja halutessasi vaikkapa muokata tiedostoja.
Koodin saa TIMissä ajettua Aja-painikkeella, ComTestit testattua Test-painikkeella ja dokumentoinnin näkyviin Document
-linkistä.
Tavoite ja lopputulos
Ohjelma lukee kaksi tiedostoa. Ensimmäinen on projektit-tiedosto, joka on lista kaikista pääprojekteista ja niiden aliprojekteista ranskalaisilla viivoilla listattuna. Jokaisella pääprojektilla on vähintään yksi aliprojekti (mutta se voi olla tyhjäkin, eli pelkkä rivi -
). Aliprojekteilla ei voi olla aliprojekteja.
Tämän tutorialin ajettava koodi lukee tämän tiedoston. Voit halutessasi kokeilla muokata tiedostoa.
Toinen on ajat-tiedosto, joka sisältää tiedon projektin aloitus- ja lopetusajankohdasta sekä kestosta tolpilla eroteltuna. Sisältää myös valinnaisen kommentin.
Ohjelma tuottaa luetuista tiedoista raportin, jossa on eritelty eri projekteihin kulunut yhteisaika tunneissa aliprojektin tarkkuudella. Raportin tulostusjärjestyksen ja raporttiin tulevat projektit määrää projektitiedoston sisältö. Eli aikatiedostossa voi olla enemmän erilaisia projekteja ja eri järjestyksessä kuin raporttiin tulostetaan.
ohjelmointikurssi
demot 3,67
luennot 4,02
ht 3,33
------------ -------
yht 11,02
integrointikurssi
ex temporet 1,58
demot 2,00
luennot 3,22
------------ -------
yht 6,80
humanistikurssi
luennot 1,42
lukupiiri 1,00
------------ -------
yht 2,42
============ =======
kaikki yht 20,24
(Moi. Ylläoleva esimerkkitiedosto raportista on viite suunnitelmaan ja siksi ei muutu tämän dokumentin projekti- tai aikatiedostoja muuttamalla. Nähdäksesi tekemiesi muutosten vaikutukset avaa ajettava koodi kappaleen lopussa.)
Ohjelmalle voi antaa argumentteina projekti- ja aikatiedoston. Ilman argumentteja ohjelma käyttää käynnistyshakemiston kansiossa olevia oletusnimisiä tiedostoja (projektit.txt
, ajat.txt
). Tiedostojen puuttuessa tai niiden sisältämän datan ollessa väärässä muodossa ohjelma tulostaa virheilmoituksen (ja mahdollisuuksien mukaan myös käyttökelpoisesta datasta koostetun raportin).
Tässä tutorialissa tehtävällä ohjelmalla ei voi tuottaa datatiedostoja, vaan oletuksena on, että oikeanmuotoinen data on jo olemassa. Käyttöliittymäinen ohjelma, jolla voi aloittaa ja lopettaa ajanoton sekä tuottaa aikatiedostot kommentteineen, luodaan toisessa tutorialissa.
Tässä tutoriaalissa esitetty ohjelma, siellä olevat aliohjelmat sekä käytetyt algoritmit eivät suinkaan ole ainoita mahdollisuuksia toteutukseen. Päinvastoin, jokainen ohjelmoija todennäköisesti toteuttaisi asiat kussakin kohti hieman tai paljon eri tavalla, tuottaen silti saman sovitun lopputuloksen. Aivan kuten Jyväskylässäkään ei ole yhtä ainutta reittiä puistokadun yläpäästä Agoralle.
Ohjelmointi 2 -kurssilla:
- tiedostoja olisi useampia ja niissä viitattaisiin toisiinsa id-numeroiden (tunniste numeroiden) avulla
- tietoja ei käsiteltäisi niin paljoa merkkijonoina, vaan yhdestä rivistä muodostetaan olio (omien olioiden tekeminen ei kuulu Ohj1-kurssin osaamistavoitteisiin)
List
-luokka toteutettaisiin itse
Tietokantakurssilla tai Ohjelmointi 2 tietokantalisäosassa:
- tiedot relaatiotietokannassa
- laskut ja raportti tehtäisiin kannasta kyselyinä
Tiedostojen sisältö on kotoisin kurssin pitäjän 90-luvun työaikakirjanpito-ohjelmasta TimeFollow
. Esimerkkitehtävän motivaatio on se, että tarvittaessa pitää osata lukea "vanhojakin formaatteja".
Tämän ongelman ratkaisemiseksi tässä tutorialissa esitetyillä tavoilla, joudutaan käyttämään kaikkia kurssin oppimistavoitteissa asetettuja asioita, joten ongelma täytää hyvin kurssin esimerkin vaatimukset.
Jos projektitiedosto olisi esimerkiksi:
ohjelmointikurssi
-demot
integrointikurssi
-demot
olisi samasta ajat-tiedostosta syntyvä raportti seuraavan näköinen :
ohjelmointikurssi
demot 3,67
------------ -------
yht 3,67
integrointikurssi
demot 2,00
------------ -------
yht 2,00
============ =======
kaikki yht 5,67
1. Aloitus
1.1 Projektin luominen
Kun suunnitelma on laadittu, luodaan uusi projekti Visual Studiolla File/New/Project
. Asetetaan tämän komentorivikäyttöisen ohjelman tyypiksi C#/Jypeli/ConsoleMain
.
1.2 Argumentit komentoriviltä
Jotta ohjelmalle voi antaa argumentteja (tai komentoriviparametreja), lisätään pääohjelman parametreiksi string-taulukko
public static void Main(string[] args)
ruvetaan käyttämään powershelliä, eli kirjoita PowerShell ja käynnistä se
—Muistetaan, että harjoitustyön vaatimuksiin kuuluu dokumentointi.
Tuliko koodausvimman jälkeen hirmuinen määrä virheilmoituksia? Yksi virhe on helpompi metsästää ja korjata kuin monta. Kannattaa ajaa ja testata koodia "joka välissä", eli kirjoittaa vain vähän koodia kerrallaan, jolloin ajaessa ja testatessa ei tule niin paljon virheitä kerrallaan! Lisäksi on helpompi palata aikasempaan toimivaan versioon jos tarvitsee.
1.3 Argumenttien antaminen Visual Studiossa
Visual Studiossa ajettaessa (eli painettaessa Properties/Debug/Command line arguments
voidaan asettaa argumentit Visual Studiolla ohjelmaa kokeiltaessa.
Kirjoitetaan tänne suunnitelmasta tutut nimet projektit.txt
ja ajat.txt
välilyönnillä erotettuna.
Tämä tehdään vain, jotta ohjelmaa voidaan kokeilla Visual Studiolla, ettei vielä kehitysvaiheessa täytyisi vaihdella komentorivin ja Visual Studion välillä. Tämä ei vaikuta ohjelman toimivuuteen.
TIMissä malliohjelmia ajettaessa vastaavat komentorivin argumentit annetaan Args
-tekstin perään.
2. Tiedostosta lukeminen
2.1 Tiedoston lukeminen
Määritellään aluksi oletuspolku projektitiedostolle:
string projektipolku = "projektit.txt";
Nyt projektipolku
-merkkijonoon on tallennettu projektitiedoston tiedostopolku. Polku voi olla suhteellinen tai absoluuttinen.
Suhteellinen polku etsii siitä hakemistosta missä ohjelma käynnistetään jos ei tehdä muuta Visual Studio käynnistyy niin että käynnistys hakemiston on Exe tiedoston hakemisto. Projektin asetuksista tätä Voi tosin muuttaa
—Suhteellinen polku (kuten tässä) etsii annettua tiedostoa siitä hakemistosta lähtien, josta ohjelma käynnistetään (ei siis välttämättä sama kuin missä itse ohjelma on). Absoluuttinen polku olisi esimerkiksi "C:\MyTemp\projektit.txt"
, joka viittaisi C
-asemassa sijaitsevan MyTemp
-kansion tiedostoon.
Tiedostosta lukemisen mahdollistamiseksi täytyy koodin alkuun lisätä rivi
using System.IO;
Komennolla File.ReadAllLines
voi kaikki tiedoston rivit lukea merkkijonotaulukon alkioiksi.
string[] projektit = File.ReadAllLines(projektipolku);
Oikeasti pitää miettiä tarkkaan, että milloin on järkevä lukea koko tiedoston sisältö kerralla muistiin. Esimerkiksi erilaiset lokitiedosto voivat olla satoja gigoja kooltaan ja ne eivät edes mahtuisi käytettävän koneen muistiin.
Muilla kursseilla opetellaan lukemaan tiedostoa pienemmissä osissa. Se on kuitenkin hieman oikein tehtynä työläämpää, joten tämän kurssin puitteissa tehdään se oletus, että tiedosto ovat kohttuulisen pieniä. Pienen käsite tosin muuttuu ajan myötä. Nykyisiin gigojen keskusmuistiin mahtuu jo jonkin verran tietoa.
linkki Ohj2 monisteeseen tiedoston avaamiseen ja sulkemiseen
—2.2 Poikkeusten käsittely
Käsitellään tiedoston puuttumisesta mahdollisesti aiheutuva poikkeus Try-catch-rakenteella.
Catch
-lohkon sisälle kirjoitetaan tapahtuma, jonka haluamme toteutuvan tiedoston puuttumisesta johtuvan virheen (FileNotFoundException
) sattuessa. Tässä tapauksessa tulostetaan konsoliin selkokielinen virheilmoitus ja poikkeuksen sisältämä viesti (Message
):
catch (FileNotFoundException ex)
{
Console.WriteLine("Tiedostoa ei löytynyt\n" + ex.Message);
}
Tämän perään sitten vielä pikkututoriaali miten sama tehdään Xamarinilla (ja Mäcin näppäimillä).
Tähän viite myös monisteen tiedostonluku kappaleeseen
—Try
-lohkon sisällä voimme argumenttien määrän ollessa nollaa suurempi sijoittaa projektipoluksi ensimmäisen (eli C#-indeksoinnissa "nollannen") argumentin:
try
{
if (args.Length > 0) projektipolku = args[0];
string[] projektit = File.ReadAllLines(projektipolku);
}
(Viimeistään tässä kohti projektia lisätään käsiteltävät tiedostot polkujen osoittamiin kansioihin.)
Toteutetaan vastaava käsittely aikatiedostolle.
eli minne ja miten? Vaihtoehdoksi se bin hakemistoon tai sitten esim data-hakemistoon ja vastava polku projektin käynnistysshakemistoksi
—Visual Studiossa keskeytyskohta (breakpoint) pääohjelman viimeiseen aaltosulkuun.
-näppäimen painallus ajaa ohjelman Debug-tilassa, jolloin valitettavasti konsoli sulkeutuu välittömästi ohjelman suorituksen jälkeen. Tämä voidaan kiertää ajamalla ohjelma ilman Debug-tilaa painamalla + . Toinen vaihtoehto on laittaa3. Toteutuksen suunnittelu
Tässä luvussa mietitään miten ohjelma toteutetaan, eli millaisia algoritmeja siihen tarvitaan. Tämän vaiheen voi tehdä erinomaisesti kynällä ja paperilla pysyen kaukana tietokoneista. Algortimeja voisi kokeilla myös tekemällä tiedostojen riveistä paperilappuja ja siirtelemällä niitä pöydällä.
Ennen aloittamista pitäisi ilman mitään ajatustakaan ohjelmasta tai algoritmeista suorittaa itse vastaavat laskut usealle erilaiselle projekti- ja aikatiedostolle. Tällöin opitaan mitä asiassa pitää tehdä.
Vasta sitten kun ihan varmasti tiedetään mitä ollaan tekemässä, kannattaa ruveta harkitsemaan sen toteuttamista tietokoneella. Ohjelmoinnin vaikein osuus onkin siinä, että liian aikaisin riennetään kirjoittamaan koodia asiasta, jota ei täysin ymmärretä.
oikolue ja korjaa
—Miksi ohjelmoistoprojektit usein epäonistuvat? Siksi että kuvitellaan ohjelmoijien olevan rakennusmiehiä, joiden ainoa tehtävä on lyödä lauta siihen paikkaan, mihin arkkitehti on sen suunnitellut. Jos ohjelmoija ei ole täysin perillä ongelma-alueesta, jolle ohjelmaa ollaan tekemässä, joudutaan tilanteeseen, missä ohjelmoijalle pitää kirjoittaa erittäin tarkat ohjeet siitä, mitä pitää tehdä. Ja samalla vaivalla olisi kirjoitettu jo koko ohjelma. Eli on äärimmäisen tärkeää, että ohjelmaa kirjoittava tietää tarkkaan mitä on tekemässä ja tietää, mitä pitää tehdä silloin kun määrittelyssä ei ole jotakin sanottu. Aina ohjelmoijan ei ole tätä mahdollista tietää ja siksi ohjelmoija joutuu usein kommunikoimaan asiakkaan kanssa. Mitä pidempi ketju on asiakkaan ja ohjelmoijan välillä, sitä suurempi mahdollisuus on epäonnistumiseen tiedonkulun pätkiessä. Tätä ongelmaa yritetään parantaa ketterillä menetelmillä (Agile-menetelmät), joissa tärkein asia on ohjelmoijan ja asiakkaan välinen kommunikaatio.
Ohjelmointi 1 -kurssilla tätä ongelmaa ei pitäisi olla, koska ohjelmoija ja asiakas asuvat samassa henkilössä, joten ohjelmoijan pitäisi olla kohdealueensa asiantuntija samalla.
Oikolue tämän sisältö
—Näytti selkeältä. Joku , taisi puuttua ennen mitä sanaa
—3.1 Raporttiin vaadittavat tiedot
Seis. Ennen kuin rynnätätään koodaamaan, mietitään hetki mitä ollaan tekemässä. Mieti, mitä aliohjelmia tarvitaan, jotta saadaan raportti haluttuun muotoon (eli jokaisen aliprojektin kesto eriteltynä, jokaisen pääprojektin kesto eriteltynä, ja kokonaiskesto). Käytössämme on vain sekalainen taulukko pää- ja aliprojekteista sekä taulukko aikadatasta.
ohjelmointikurssi
demot 3,67
luennot 4,02
ht 3,33
------------ -------
yht 11,02
integrointikurssi
ex temporet 1,58
demot 2,00
luennot 3,22
------------ -------
yht 6,80
humanistikurssi
luennot 1,42
lukupiiri 1,00
------------ -------
yht 2,42
============ =======
kaikki yht 20,24
Tarvitsemme siis aliohjelmat ainakin seuraaviin tehtäviin:
- pääprojektit listaksi projektitaulukosta
- tiettyyn pääprojektiin kuuluvat aliprojektit listaksi projektitaulukosta
- aliprojektiin kuluneen ajan laskeminen aikadatataulukosta
- pääprojektiin kuluneen ajan laskeminen (aliprojekteihin kuluneesta ajasta)
- kaikkeen kuluneen ajan laskeminen (pääprojekteihin kuluneesta ajasta)
- raportin muotoilu, tulostus jne.
(Malliratkaisussa kolme viimeisintä tehtävää suoritetaan saman aliohjelman sisällä. Tämä ei ole ainoa ratkaisu tai välttämättä edes suositeltava.)
3.2 Algoritmi pääprojekteille
Käytössämme on siis taulukko, jonka alkiot ovat projektitiedoston rivit. Aliprojektirivit alkavat tiedostossa viivalla. Käykäämme siis läpi projektitaulukko, viivattomat alkiot talteen poimien.
ohjelmointikurssi
-demot
-luennot
-ht
integrointikurssi
-ex temporet
-demot
-luennot
humanistikurssi
-demot
-luennot
-lukupiiri
Näin saadaan siis tuotettua lista:
ohjelmointikurssi
integrointikurssi
humanistikurssi
3.3 Algortimi aliprojekteille
Koska eri pääprojekteilla voi olla samannimisiä aliprojekteja, on tässä vaiheessa turha etsiä koko projektitiedoston kaikkia aliprojekteja. Sen sijaan etsitään vain yhden pääprojektin aliprojektit kerrallaan.
Projektitiedostoa tuijottamalla huomataan, että tietyn pääprojektin aliprojektit ovat pääprojektia seuraavat viivalla alkavat rivit, ts. kaikki rivit kunnes rivit loppuvat tai vastaan tulee rivi, joka ei ala viivalla.
Käydään siis projektitaulukon alkioita läpi, kunnes löydämme haluamamme pääprojektin. Tämän jälkeen otetaan talteen kaikki seuraavat alkiot, kunnes vastaan tulee uusi pääprojekti, eli viivalla alkamaton alkio.
Näin saadaan esimerkiksi integrointikurssin
kohdalta lista:
-ex temporet
-demot
-luennot
3.4 Projekteihin käytetyn ajan selvittäminen
Aikadatataulukon alkiot ovat aikadatatiedoston rivit, jotka sisältävät tiedon pääprojektista, aliprojektista ja käytetystä ajasta (ym. laskun kannalta epäolennaista).
ohjelmointikurssi|luennot|20.7.2017 12:15:58|20.7.2017 13:47:02|01:31:04|luennoitsija tuli laatikosta
ohjelmointikurssi|demot|20.7.2017 13:47:39|20.7.2017 15:47:42|02:00:03|kolmostehtävä oli hurjan vaikee
integrointikurssi|luennot|21.7.2017 10:15:01|21.7.2017 11:49:04|01:34:03|
humanistikurssi|luennot|21.7.2017 12:15:49|21.7.2017 13:40:54|01:25:05|
integrointikurssi|ex temporet|22.7.2017 12:15:43|22.7.2017 13:50:46|01:35:03|runge-kutta-menetelmä on helppo
integrointikurssi|demot|22.7.2017 14:52:00|22.7.2017 16:52:03|02:00:03|altzheimer-messerschmidt on vaikea
humanistikurssi|lukupiiri|23.7.2017 8:53:02|23.7.2017 9:53:07|01:00:05|mietittiin elämää
integrointikurssi|luennot|23.7.2017 12:15:13|23.7.2017 13:54:15|01:39:02|mietittiin kuolemaa
ohjelmointikurssi|ht|23.7.2017 14:54:54|23.7.2017 18:14:57|03:20:03|aloitin harkkatyön
ohjelmointikurssi|luennot|24.7.2017 8:15:39|24.7.2017 10:45:43|02:30:04|foreach on kiva keksintö
ohjelmointikurssi|demot|24.7.2017 12:57:37|24.7.2017 14:37:41|01:40:04|sain guru-tehtävän tehtyä!
Em. aliohjelmilla saamme taulukon pääprojektin aliprojekteille. Tarkastellaan kerrallaan yhtä pääprojektin aliprojektia kokonaisuutena, jolloin voimme aikadatataulukosta hyödyntää vain ne rivit, joiden sekä pääprojekti että aliprojekti ovat tarkasteltavaa kokonaisuutta (esim. ohjelmointikurssi luennot
). Poimitaan tällaisesta rivistä kestoaika ja lisätään se aliprojektin kokonaiskestoon.
Kun kaikki rivit on käyty läpi kyseiselle aliprojektille, lisätään aliprojektin kokonaiskesto sen pääprojektin kokonaiskestoon. Tehdään tämä pääprojektin kaikille aliprojekteille ja lisätään pääprojektin kokonaiskesto kaikkien projektien kokonaiskestoon. Tehdään koko ruljanssi kaikille pääprojekteille.
Seuraava video on jokaisen pakko katsoa ennenkuin ryhtyy mihinkään. Ja muistettava tehdä vastaavaa jokaisen oman ongelmansa kanssa.
Jatkossa tämä suunniteltu algoritmi ohjaa vastaavaa koodin tekemistä ja vaikuttaa myös valittaviin tietorakenteisiin. Tämä algoritmi johtaa listojen käyttöön.
Joku toinen olisi voinut tehdä erilaisen algoritmin, missä olisi mennyt kerran ajat
-listan läpi ja lisännyt aina tuloksen vastaavaan kohtaan ja sitten lopuksi järjestänyt tuloksen haluttuun järjesteykseen. Tällöin luonnollisempi tietorakenne olisi ollut sanakirja (dictionary, hash map
tms).
Näistä asioista enemmän Algoritmit-kurssilla. Erilaisilla algoritmeilla voi olla erilainen suoritusaika. Nyt valittu algoritmi on tehtoton, mikäli projekteja ja niiden aliprojekteja on paljon, koska silloin ajat
-lista käydään läpi useita kertoja.
4. Ensimmäinen aliohjelma
4.1 Aliohjelman kutsuminen ja esittely
Luodaan tässä vaiheessa ensimmäinen aliohjelma Raportoi
, joka (aikanaan) palauttaa hienon raportin merkkijonona. Tässä vaiheessa meillä on projektit ja ajat luettu kahteen taulukkoon, joten viedään nämä parametreina aliohjelmalle. Vaihdetaan pääohjelman konsolitulostukseen aliohjelmakutsu:
Console.WriteLine(Raportoi(projektit, ajat));
tai jos tykkää apumuuttujista, niin
string raportti = Raportoi(projektit, ajat);
Console.WriteLine(raportti);
Nyt meidän pitäisi kirjoittaa aliohjelman esittelyrivi. Aliohjelmasta voidaan tehdä julkinen public
, sen ei tarvitse nähdä luokan attribuutteja, joten se voi olla staattinen static
, ja sen täytyy palauttaa merkkijono string
.
public static string Raportoi(string[] projektit, string[] ajat)
{
return ajat[0];
}
Aliohjelma on nyt tynkä, eli se palauttaa jotain enemmän tai vähemmän järkevää (tässä tapauksessa ajat-taulukon ensimmäisen alkion), mutta koodi kääntyy.
viite monisteeseen esittelyrivin kohdalta
—4.2 Testaaminen
Tällä kurssilla harjoitellaan TDD:tä (Test Driven Development), eli ennen koodin kirjoittamista pitäisi miettiä, miten koodia voi testata. Onneksi käytössämme on ComTest, jolla voimme suhteellisen vaivattomasti kirjoittaa tässä vaiheessa aliohjelmalle testit.
Tähän mennessä on jo jonkun verran kirjoitettu ohjelmaa ilman "testausta". Saisiko tämän pätkän jo ylemmäs, jos "TDD:tä" halutaan painottaa?
—ComTestiin tarvitsemme aliohjelmalle vietävät parametrit (tässä tapauksessa merkkijonotaulukko projekteista ja aikadatasta) ja malli, mihin verrata, menikö testi oikein. Kirjoitetaan tässä vaiheessa helposti käsin laskettavaa dataa, jotta voimme laskea ja kirjoittaa mallin raportin odotuarvosta.
/// <example>
/// <pre name="test">
/// string[] projektit = {"integrointikurssi", "-luennot", "-demot",
/// "humanistikurssi", "-lukupiiri", "-luennot"};
/// string[] ajat =
/// {"integrointikurssi|luennot|20.7.2017 10:15:01|20.7.2017 11:29:01|01:14:00|",
/// "humanistikurssi|luennot|21.7.2017 12:15:49|21.7.2017 13:45:49|02:30:00|pöö",
/// "integrointikurssi|demot|21.7.2017 14:15:54|21.7.2017 16:15:54|02:00:00|kommentti",
/// "integrointikurssi|luennot|22.7.2017 10:15:04|22.7.2017 11:31:04|01:16:00|"};
/// string odotusarvo = "integrointikurssi\n"+
/// " luennot 2,50\n"+
/// " demot 2,00\n"+
/// " ---------- -------\n"+
/// " yht 4,50\n"+
/// "\n" +
/// "humanistikurssi\n" +
/// " luennot 2,50\n"+
/// " ---------- -------\n"+
/// " yht 2,50\n"+
/// "\n" +
/// " ========== =======\n"+
/// " kaikki yht 7,00\n";
/// TyoaikaraporttiHarj.Raportoi(projektit, ajat) === odotusarvo;
/// </pre>
/// </example>
Huomoita ylläolevasta testistä:
integrointikurssi luennot
-rivi on kahdesti, joten ainakin yksi yhteenlasku tulee suoritettua (01:14:00 + 01:16:00 = 02:30:00 = 2,50 tuntia)- myös humanistikurssilla on luennot, joten se ei saa menna integrointikurssin luentojen kanssa sekaisin
humanistikurssi lukupiiri
-riviä ei ole, joten sitä ei saa tulostaa
Testin malliraporttia (odotusarvo
) kirjoittaessa täytyy olla rivinvaihtojen ja välilyöntien kanssa tarkkana. Desimaalipilkku aiheuttaa myös ongelmia, sillä joissain systeemeissä käytetään desimaalipistettä.
5. Pääprojektien etsiminen
5.1 Raportin vaatimukset
Raportoi
-aliohjelman tulee palauttaa raportti merkkijonona. Muuttuvan tekstin tallentamiseen kannattaa käyttää tässä tapauksessa StringBuilder
-tyyppiä ja muuntaa se merkkijonoksi vasta palautusvaiheessa.
StringBuilder raportti = new StringBuilder();
return raportti.ToString();
Itse raportin luomiseen tarvitsemme myös seuraavat muuttujat:
TimeSpan yhteiskesto = TimeSpan.Zero;
List<string> aliprojektit;
List<string> paaprojektit = HaePaaprojektit(projektit);
Kaikkiin projekteihin kulunut aika tallennetaan yhteiskesto
-muuttujaan, joka on TimeSpan
-tyyppiä (aikajakso).
5.2 Pääprojektin hakeminen
Kirjoitetaan esittelyrivi HaePaaprojektit
-aliohjelmalle ja tehdään siitä tynkä:
public static List<string> HaePaaprojektit(string[] projektitKaikki)
{
List<string> paaprojektit = new List<string>();
return paaprojektit;
}
Muistetaan myös dokumentointi ja testaaminen!
Seuraavaksi etsitään kaikki alkiot, jotka eivät ala viivalla (vertailuoperaattori !
), ja lisätään ne paaprojektit
-listaan foreach-silmukkaa käyttäen:
foreach (string p in projektitKaikki)
{
if (!p.StartsWith("-")) paaprojektit.Add(p);
}
Jos kaikki menisi niin kuin Strömsössä, kaikista testeistähän pitäisi tulla vihreää aliohjelman kirjoittamisen jälkeen, mutta Raportoi
-aliohjelman testi kummittelee yhä punaisena! Koska Raportoi
ei valmistu vielä hetkeen, voimme väliaikaisesti kirjoittaa "TODO:
" sen ComTestin eteen. Tämä vaimentaa testin ja lisää sen Task Listiin.
Palataan vielä Raportoi
-aliohjelmaan käymään läpi haetut pääprojektit esimerkiksi lisäämällä ne kokeilua varten palautettavaan merkkijonoon (raportti
) rivinvaihdolla erotettuna.
foreach (string paaprojekti in paaprojektit)
{
raportti.Append(paaprojekti + "\n");
}
HUOM! Videolla väärin. Aliohjelmalla ei ole muodostajaa, vaan aliohjelman esittelyrivi (otsikkorivi, eng. header).
—Pääprojektien etsimiseen voitaisiin käyttää myös C#:in valmista Array
-luokan taulukosta etsivää metodia Find
. Tosin tämä palauttaa taulukon ja jos funktion halutaan palauttavan lista, niin ottamalla käyttöö System.Linq;
, voidaan taulukko muuttaa listaksi.
6. Aliprojektien etsiminen
Muistellaan suunnittelemaamme toteutusta. Jokaiselle pääprojektille käydään erikseen läpi pääprojektin aliprojektit. Kirjoitetaan äsken lisäämämme foreach
-silmukan sisään rivi, jossa sijoitetaan aliprojektit
-listaan HaeAliprojektit
-aliohjelman paluuarvo.
foreach (string paaprojekti in paaprojektit)
{
raportti.Append(paaprojekti + "\n");
aliprojektit = HaeAliprojektit(projektit, paaprojekti);
}
Viedään HaeAliprojektit
-aliohjelmalle parametreina sekä tuttu sekalainen projektilista että pääprojekti jonka aliprojektit haluamme selvittää.
Luodaan luonnollisesti dokumentoitu aliohjelmatynkä HaeAliprojektit
testeineen:
/// <summary> Palauttaa pääprojektin aliprojektit merkkijonolistana ...
/// <example><pre name="test"> string[] proj = { "Ohj1", ...
public static List<string> HaeAliprojektit(string[] projektitKaikki, string paaprojekti)
{
List<string> aliprojektit = new List<string>();
return aliprojektit;
}
Luodaan uusi epätosi totuusarvo projektinKohdalla
, joka ilmaisee ollaanko parametrina annetun pääprojektin kohdalla taulukossa. Tehdään taas foreach
-silmukka, jonka sisällä oltaessa halutun pääprojektin kohdalla vaihdetaan totuusarvo todeksi.
bool projektinKohdalla = false;
foreach (string p in projektitKaikki)
{
if (p.Equals(paaprojekti)) projektinKohdalla = true;
}
Lisätään silmukan sisälle toinenkin ehtolause edellisen yläpuolelle. Totuusarvon projektinKohdalla
ollessa tosi, poistutaan silmukasta break
-lauseella mikäli rivi ei ala viivalla. Muussa tapauksessa lisätään rivi listaan.
if (projektinKohdalla)
{
if (!p.StartsWith("-")) break;
aliprojektit.Add(p.Substring(1));
}
if (p.Equals(paaprojekti)) projektinKohdalla = true;
Kirjoittamassamme projektitiedoston muodossa aliprojektirivit alkavat viivalla (esim. "-demot
"), joten lisätäksemme itse aliprojektin nimen, joudumme lisäämään alkion aloittaen toisesta merkistä.
Mitä jos viivan tilalla olisi tähti? Tai viiva ja välilyönti? Viivan käyttäminen aliprojektien erottamiseen ei sinänsä ole huono vaihtoehto, mutta jos joskus haluaisimme vaihtaa aliprojektien merkintätapaa, joutuisimme muuttelemaan koodia aika paljon. Hyvässä koodissa tämä erotinmerkki kannattaisi sijoittaa muuttujaan ja viedä se parametrina aliohjelmille.
Lisätään vielä Raportoi
-aliohjelman raportti
-muuttujaan haetut aliprojektit pääprojektien alle:
foreach (string paaprojekti in paaprojektit)
{
raportti.Append(paaprojekti + "\n");
aliprojektit = HaeAliprojektit(projektit, paaprojekti);
foreach (string aliprojekti in aliprojektit)
{
raportti.Append(" " + aliprojekti + "\n");
}
}
vaihtoehtoinen toteutus: etsi pääprojektin indeksi, etsi tästä indeksistä eteenpäin kaikki aliprojektit
—Aliprojektit voitaisiin etsiä myös seuraalla algortimilla:
1. etsi ensimmäisen pääprojektin paikka
2. tästä seuraavasta paikasta alkaen tee jokaiselle riville
3. - jos ei ala viivalla, lopeta
4. - muuten lisää aliprojketeihin
Ja sama C#:illa toteuttuna. Tässä on käytetty hyväksi valmista Array
-luokan taulukosta etsivää metodia Find
.
7. Kestoajan määrittäminen
7.1 Kestoaikojen lisääminen raporttiin
Luodaan sekä Raportoi
aliohjelman sisällä aliprojektille että pääprojektille TimeSpan
-muuttujat. Kummankin silmukan päätteeksi lisätään saatu kesto yhtä ylempään kestoon. TimeSpan
-olioita voi huoleti lisäillä toisiinsa tutuilla operaattoreilla.
foreach (string paaprojekti in paaprojektit)
{
TimeSpan paaprojektinKesto = TimeSpan.Zero;
raportti.Append(paaprojekti + "\n");
aliprojektit = HaeAliprojektit(projektit, paaprojekti);
foreach (string aliprojekti in aliprojektit)
{
TimeSpan aliprojektinKesto = TimeSpan.Zero
raportti.Append(" " + aliprojekti + " " + aliprojektinKesto + "\n");
paaprojektinKesto += aliprojektinKesto;
}
raportti.Append(" yht " + paaprojektinKesto + "\n");
yhteiskesto += paaprojektinKesto;
}
raportti.Append("yhteensä " + yhteiskesto);
Nyt raportti on muotoilua vaille halutun mallinen. Nykyinen koodi tietysti vain tulostaa pelkkiä nolla-aikoja 00:00:00
.
Koodissa raportti appendoiaan mallin mukaisesti kolmella välilyönnillä; videolla käytetään vielä tässä vaiheessa viivaa, mikä muistuttaa erehdyttävästi projektitiedoston muotoa.
—7.2 Aliohjelma aliprojektin kestolle
Muutetaan aliprojektinKeston
esittelyrivi aliohjelmakutsuksi:
TimeSpan aliprojektinKesto = LaskeAliprojektinKesto(ajat,
paaprojekti + "|" + aliprojekti + "|");
missä ensimmäinen parametri on aikadatataulukko ja toinen projektikokonaisuus. Projektikokonaisuudessa pääprojektin ja aliprojektin nimi on "liimattu yhteen" tolppa-erottimilla datan muotoisesti. Nyt esimerkiksi pääprojektin ollessa ohj1
ja aliprojektin demot
vietäisiin aliohjelmalle toisena parametrina ohj1|demot|
.
Luodaan uusi aliohjelmatynkä:
public static TimeSpan LaskeAliprojektinKesto(string[] ajat, string projektikokonaisuus)
{
TimeSpan aliprojektinKesto = TimeSpan.Zero;
return aliprojektinKesto;
}
Käydään for-silmukalla läpi aikadatataulukko ajat
. (Foreach toimii myös, mutta jos jostain syystä haluamme myöhemmin rivin indeksin selville, for-silmukalla se onnistuu näppärästi!)
for (int i = 0; i < ajat.Length; i++)
{
if (ajat[i].StartsWith(projektikokonaisuus))
{
string[] alkiot = ajat[i].Split('|');
TimeSpan kesto = TimeSpan.Parse(alkiot[4]);
aliprojektinKesto += kesto;
}
}
Alkion (eli alkuperäisen tiedoston rivin) alkaessa projektikokonaisuudella otetaan talteen neljännen tolpan jälkeinen osuus (eli kesto). String.Split
jakaa merkkijonon taulukkoon jakaen merkkijonon sille parametrina annetun merkin kohdilta "merkkien väliset" merkkijonot taulukon alkioiksi.
Koska aikadatarivit ovat tiedostossa muotoa
pääprojekti|aliprojekti|alkuaika|loppuaika|kesto|kommentti
otamme neljännessä indeksissä olevan alkion. Jos projektin nimi sisältää tolppia, neljännessä indeksissä on jotain muuta kuin kesto. Tästä syystä tässä työssä erottimeksi valittiin suht eksoottinen tolppa (eikä esimerkiksi pilkku).
8. Muotoilu
8.1 Muotoilun kokeilu
Tässä osiossa tehdään itselle pieni apuohjelma, jolla voidaan harjoitella, miten formaattia käytetään.
Kokeillaan ensin pienellä apuohjelmalla miltä tulostus näyttäisi ihan merkkijonoja liittämällä:
Tästä huomataan se vika, että eri levyiset projektien ajat tulostuvat rumasti väärään paikkaan.
Otetaan käyttöön String
-luokan muotoilufunktio Format
. Tällöin pitää olla käytössä System
-nimiavaruus.
Tässä on ongelman vielä se, että mikä on oikea leveys tuohon 10
kohdalle. Siihen pitäisi löytää pisimmän aliprojektin nimi ja sitähän emme tiedä vielä ohjelman kirjoitusvaiheessa.
Siksi edellistä esimerkkiä pitää vielä muuttaa niin, että leveys saadaan muuttujana. Koska muotoilun (String.Format
) ohjejono on tavallinen merkkijono, voimme muodostaa sen ohjelmallisesti. Muotoilu kannattaa muutenkin laittaa omaksi muuttujakseen, koska muuten sama sisältö toistetaan monta kertaa.
Nyt voimme muodostaa muotoilujonon leveyden avulla:
Sitten vielä viivan kikkailu oikean levyiseksi.
Muistathan, että jos koodissa haluat vaihtaa jonkun muuttujan tai aliohjelman nimeä, se kannattaa uudelleennimetä (eng. Rename,
+ + ), mikä muuttaa sen nimen joka kohtaan koodia kerralla kuitenkaan vaikuttamatta esimerkiksi samannimisiin paikallisiin muuttujiin toisissa aliohjelmissa.8.2 Testaus ja desimaalipilkku
Tällä hetkellä testit toimivat joillakin koneilla. Kulttuuriasetuksista riippuen ympäristö tuottaa desimaaliluvut joko pilkkua tai pistettä erottimena käyttäen. Mallitulosteessamme odotusarvo
on desimaalipilkku.
Jotta testit saataisiin toimimaan TIMissä (tai missä tahansa desimaalipistettä käyttävässä ympäristössä), muutetaan testin paluuarvosta väkisin pisteet pilkuiksi, kuten kirjoittamassamme odotusarvossa.
/// string odotusarvo = "integrointikurssi\n"+
/// " luennot 2,50\n"+
...
/// string paluuarvo = Tyoaikaraportti.Raportoi(projektit, ajat);
/// paluuarvo = paluuarvo.Replace('.', ',');
/// paluuarvo === odotusarvo;
8.3 Muotoilun soveltaminen
Raportin mallin mukaisesti raporttiin täytyy vielä muokata sisennykset, sievät jaottelut, ajan muoto ja katkoviivat.
ohjelmointikurssi
demot 3,67
luennot 4,02
ht 3,33
------------ -------
yht 11,02
integrointikurssi
ex temporet 1,58
demot 2,00
luennot 3,22
------------ -------
yht 6,80
humanistikurssi
luennot 1,42
lukupiiri 1,00
------------ -------
yht 2,42
============ =======
kaikki yht 20,24
Yllä opitun mukaisesti voimme nyt käyttää String.Format
-funktiota:
int leveys = PisinMerkkijononPituus(projektit, "-");
string muotoilu = " {0, " + -leveys + "} {1, 7:0.00}\n";
...
raportti.Append(String.Format(muotoilu, aliprojekti, aliprojektinKesto.TotalHours));
Luodaan yksinkertainen pisimmän merkkijonon pituuden palauttava aliohjelma, jolle viedään parametreina merkkijonotaulukko ja merkkijono, jolla rivin tulee alkaa, jotta merkkijono otetaan laskussa huomioon.
public static int PisinMerkkijononPituus(string[] jonot, string alkuehto)
{
int pisimmanPituus = 0;
foreach (string mj in jonot)
if (mj.StartsWith(alkuehto) && mj.Length > pisimmanPituus)
pisimmanPituus = mj.Length;
return pisimmanPituus;
}
Viivat saamme lisättyä seuraavanlaisesti:
string yksviiva = new String('-', leveys);
string tuplaviiva = new String('=', leveys);
...
raportti.Append(String.Format(muotoilu, yksviiva, "-------"));
...
raportti.Append(String.Format(muotoilu, tuplaviiva, "======="));
9. Virheiden käsittely
Aikadatatiedoston aikadata voi olla väärässä muodossa, mikä aiheuttaa poikkeuksia (engl. Exception). Virheitä käsiteltäessä on aina tehtävä päätös, kuinka niiden kanssa toimitaan. Tässä ohjelmassa on päätetty jättää kyseiset rivit suosiolla laskematta, mutta kuitenkin ilmoittaa niistä käyttäjälle.
9.1 Virhelista
Lisätään Raportoi
-aliohjelmaan virhelista ja viedään se parametrina LaskeAliprojektinKesto
-aliohjelmalle.
StringBuilder virhelista = new StringBuilder();
...
TimeSpan aliprojektinKesto = LaskeAliprojektinKesto(ajat, paaprojekti
+ "|" + aliprojekti + "|", virhelista);
Virhelista kannattaa lisätä palautettavaan raporttiin.
if (virhelista.Length > 0)
raportti.Append("\nRaporttiin ei laskettu virheellisiä rivejä:\n"
+ virhelista.ToString());
Muokataan tietysti vielä itse aliohjelmaan esittelyriville tämä uusi parametri. (
-näppäimellä pääsee kätevästi suoraan aliohjelmakutsusta määrittelyriville!) public static TimeSpan LaskeAliprojektinKesto(string[] ajat,
string projektikokonaisuus, StringBuilder virhelista)
9.2 Testien muokkaaminen
Lisätään LaskeAliprojektinKesto
-aliohjelman testeihin virhelistan käsittely. Virheiden loistaessa poissaolollaan tulisi palauttaa tyhjä merkkijono.
/// <example>
/// <pre name="test">
/// string[] ajat =
/// {"integrointikurssi|luennot|21.7.2017 10:15:01|21.7.2017 11:49:04|01:30:03|",
/// "humanistikurssi|luennot|21.7.2017 12:15:49|21.7.2017 13:40:54|01:25:05|",
/// "integrointikurssi|demot|21.7.2017 14:15:49|21.7.2017 14:40:54|00:25:05|",
/// "integrointikurssi|luennot|22.7.2017 10:15:01|22.7.2017 11:49:04|01:30:03|"};
/// string projektikokonaisuus = "integrointikurssi|luennot";
/// StringBuilder virhelista = new StringBuilder();
/// string tulos = Tyoaikaraportti.LaskeAliprojektinKesto(ajat,
/// projektikokonaisuus, virhelista).ToString();
/// tulos === "03:00:06";
/// virhelista.ToString() === "";
/// string[] ajat2 =
/// {"integrointikurssi|luennot|21.7.2017 10:15:01|21.7.2017 11:49:04|01:30:03|",
/// "humanistikurssi|luennot|21.7.2017 12:15:49|21.7.2017 13:40:54|01:25:05|",
/// "integrointikurssi|luennot21.7.2017 14:15:49|21.7.2017 14:40:54|00:25:05|",
/// "integrointikurssi|luennot|22.7.2017 10:15:01|22.7.2017 11:49:04|0130:03|"};
/// tulos = Tyoaikaraportti.LaskeAliprojektinKesto(ajat2,
/// projektikokonaisuus, virhelista).ToString();
/// tulos === "01:30:03";
/// string odotusarvo =
/// "Rivi 3: Väärä määrä tolppia: integrointikurssi|"+
/// "luennot21.7.2017 14:15:49|21.7.2017 14:40:54|00:25:05|\n"+
/// "Rivi 4: Kestoaika väärässä muodossa: integrointikurssi|"+
/// "luennot|22.7.2017 10:15:01|22.7.2017 11:49:04|0130:03|\n";
/// virhelista.ToString() === odotusarvo
/// </pre>
/// </example>
9.3 Aikatiedoston virherivien käsittely
Tolppia ollessa liian vähän LaskeAliprojektinKesto
-aliohjelman alkiot
-taulukon koko pienenee. Tätä voidaan tutkia helposti.
if (alkiot.Length < 6)
{
virhelista.Append("Rivi " + (i + 1) +
": Väärä määrä tolppia: " + ajat[i] + "\n");
continue;
}
Tolppia ollessa "liikaa", ei laskua kannata välttämättä vielä keskeyttää, sillä viimeisessä alkiossa eli kommenttikentässä saakin olla tolppia ilma että se häiritsee kestoajan tutkimista.
Jos ylimääräisiä tolppia taas on ennen viidettä alkiota eli kestoaikaa, yrittää koodi lukea jotain muuta kuin TimeSpan
-tyyppiä kestoksi, mistä sikiää System.FormatException
. Napataan tämä tutulla try-catch
-rakenteella.
try
{
TimeSpan kesto = TimeSpan.Parse(alkiot[4]);
aliprojektinKesto += kesto;
}
catch (System.FormatException)
{
virhelista.Append("Rivi " + (i + 1) +
": Kestoaika väärässä muodossa: " + ajat[i] + "\n");
}
Huonossa tilanteessa käyttäjä voi myös vahingossa muuttaa kestoa niin, että TimeSpan
-olio saa liian suuren arvon (esimerkiksi kaksoispisteen poistaminen voi aiheuttaa tämän). Voimme lisätä edellisen catchin
perään uuden:
catch (System.OverflowException)
{
virhelista.Append("Rivi " + (i + 1) +
": Kestoaika väärässä muodossa: " + ajat[i] + "\n");
}
9.4 Projektitiedoston virherivien käsittely
Voimme käsitellä vielä projektitiedoston mahdolliset sotkut. Huomataan, että nykyisessä koodissa jos projektitiedostossa on tyhjiä rivejä, rivi ei ala viivalla, joten loogisesti sen täytyy olla pääprojekti!
9.4.1 Pääprojektin rivin tarkastelu
Otetaan tämä huomioon pääprojektien etsinnässä lisäämällä continue
-rivi:
public static List<string> HaePaaprojektit(string[] projektitKaikki)
{
List<string> paaprojektit = new List<string>();
foreach (string p in projektitKaikki)
{
if (p.Equals("") || p.StartsWith("#")) continue;
if (!p.StartsWith("-")) paaprojektit.Add(p);
}
return paaprojektit;
}
Toi p.Equals("")
voisi kenties olla string.isNullOrWhitespace(p)
(miksi ihmeessä tuo on luokkametodi?). Väittäävät, että "" luo uuden merkkijonon, eli String.empty olisi parempi vaihtoehto sille. Toisaalta p.Length == 0 on vielä nopeampi, jos sen nopeampi tarvitsee olla – vrt. "premature optimization is the root of all evil".
Täällä Eliitti pui samaa asiaa: https://stackoverflow.com/questions/7872633/most-advisable-way-of-checking-empty-strings-in-c-sharp
—Yllä lisättiin loogisella operaattorilla "ehdollinen tai" (conditional if ||
) myös kommentointimahdollisuus projektitiedostoon kirjoittamalla rivin alkuun #
-merkki.
Edellä silmukan sisällä oleva if
-rakenne
if (p.Equals("") || p.StartsWith("#")) continue;
if (!p.StartsWith("-")) paaprojektit.Add(p);
voitaisiin toteuttaa monella muullakin eri tavalla. Esimerkiksi sisäkkäisenä ehtona ilman continue
-lausetta:
if ( !(p.Equals("") || p.StartsWith("#")) )
if (!p.StartsWith("-")) paaprojektit.Add(p);
tai ja-operaattoreilla käyttäen muunnoksessa de Morganin lakeja:
if ( !p.Equals("") && !p.StartsWith("#") )
if (!p.StartsWith("-")) paaprojektit.Add(p);
Yleensä sisäkkäiset ehdot voidaan korvata ja-lauseella (tai päinvastoin):
if ( (!p.Equals("") && !p.StartsWith("#") && !p.StartsWith("-"))
paaprojektit.Add(p);
Sama pelkästään sisäkkäisillä ehdoilla:
if (!p.Equals(""))
if (!p.StartsWith("#"))
if (!p.StartsWith("-")) paaprojektit.Add(p);
Samoin alkuperäinen versio voitaisiin esittää usealla lauseella:
if (p.Equals("")) continue;
if (p.StartsWith("#")) continue;
if (!p.StartsWith("-")) paaprojektit.Add(p);
tai kokonaan ilman ei-operaattoria (ensin väärät-taktiikka):
if (p.Equals("")) continue;
if (p.StartsWith("#")) continue;
if (p.StartsWith("-")) continue;
paaprojektit.Add(p);
tai kaikki ehdot yhdistettyinä tai-lauseella:
if (p.Equals("") || (p.StartsWith("#") || p.StartsWith("-")) continue;
paaprojektit.Add(p);
Mikä näistä on sitten paras? Tätä ei voida sanoa, sillä on useita "uskontokuntia", joista osa ei pidä goto
-lauseista, jonka sovellus continue
-lause on. Osa ei tykkää ehtojen pitkistä yhdistämisistä jne. Eli käytetty muoto on pitkälle makuasia ja testien tarkoitus onkin pitää huoli siitä, että saadaan sama tulos riippumatta siitä, missä muodossa ehdot esitetään.
Tähän liittyen ehkä "paras" ratkaisu olisikin tehdä koko ehtohässäkästä oma funktio OnPaaprojekti
(jota alla olevassa harjoituksessa on tarkoitus muutella) jota kutsuttaisiin:
if ( OnPaaprojekti(p) ) continue;
Huomattakoon että bool
-funktion tapauksessa koodi voisi olla myös:
return (!p.Equals("") && !p.StartsWith("#") && !p.StartsWith("-"));
tai
return (!(p.Equals("") || p.StartsWith("#") || p.StartsWith("-")));
Jos koodia haluttaisiin nopeuttaa niin monet StartsWith
-rivit hidastavat koodia ja voitaisiinkin kirjoittaa pidempi koodi, jossa ensin otetaan ensimmäinen kirjain (mikäli sellainen on) ja tarkasteltaiisiin sitä. Tosin jos alkuehto on useamman merkin yhdistelmä, esim kommenttin aloitukseksi sovittaisiin //
, niin yksittäisten merkkien tarkastelu johtaisi työläämpään muutokseen ehdon muuttuessa.
9.4.2 Aliprojektin rivin tarkastelu
Tehdään sama vielä aliprojekteille:
public static List<string> HaeAliprojektit(string[] projektitKaikki, string paaprojekti)
{
List<string> aliprojektit = new List<string>();
bool projektinKohdalla = false;
foreach (string p in projektitKaikki)
{
if (projektinKohdalla)
{
if (p.Equals("") || p.StartsWith("#")) continue;
if (!p.StartsWith("-")) break;
aliprojektit.Add(p.Substring(1));
}
if (p.Equals(paaprojekti)) projektinKohdalla = true;
}
return aliprojektit;
}
Kuten pääprojektien tapauksessa, ehtoja voitaisiin kirjoittaa eri tavoilla. Mikäli tehtävänä olisi etsiä kaikki aliprojektit, voitaisiin tehdä vastaava funktio kuin edellä, eli OnAliprojekti
. Nykyiseen tarkoitukseen se ei kuitenkaan sopisi, koska etsiminen pitää lopettaa jos vastaan tulee pääprojekti. Tai ehtoa pitäisi käyttää eri tavalla:
if ( OnPaaprojekti(p) ) break;
if ( OnAliprojekti(p) ) aliprojektit.Add(p.Substring(1));
jossa on jo useita ongelmia:
- jos aliprojektin alku merkittäisiin useammalla merkillä, niin tuo
Substring(1)
olisi väärin - koska kumpikin aliohjelma erikseen testaa tyhjän merkkijonon ja kommentin alun, tulee nämä testit tehtyä monta kertaa.
Mutta tarkastellaan teoreettiselta pohjalta silti funktioiden OnPaaprojekti
ja OnAliprojekti
eroja.
Kirjoitetaan aikaisempaa matkien uusi funktio:
Kun funktioita verrataan, huomataan, että ne eroavat toisistaan vaan yhden merkin, eli !
verran. Seuraava kysymys onkin, että voisiko tuon merkin viedä parametrina. Vastaus on että ei sellaisenaan, mutta käyttäen delegaatteja tämä onnistuisi.
Toisaalta ongelmaa voisi saman tien yleistää hieman ja miettiä että voitaisiinko ehto siitä, mikä on kiinostava rivi, viedä parametrina ja vastaus tähän on että toki voitasiin. Eli yleistetyn version kutsu voisi olla:
if ( OnTutkittava(p, p => p.Startswith("-") ) ... aliprojekti
if ( OnTutkittava(p, p => !p.Startswith("-") ) ... pääprojekti
Tässä p =>
tarkoittaa että uusi funktio jokaisella p:n arvolla, jota se tutkii, suorittaa bool-tyyppisen lauseen joka on tämän perässä ja käyttää sen totuusarvoa päättääkseen onko p
tutkittava vaiko ei.
C#:in systeemikirjastossa on valmis geneerinen delegaatti-tyyppi Predicate
joka sopii tähän tarkoitukseen meille kuin nenä päähän. Predikaatin on ideana olla funktio, joka palauttaa annettujen parametrien perusteella joko tosi tai epätosi. Siksi se soveltuu oikein hyvin käytettäväksi etsimistyylisissä toiminnoissa. Kun kirjoitamme Predicaten
perään vielä <string>
, saamme siitä toteutuksen, joka tarkoittaa funktiota, jolle viedään parametrina merkkijono ja se palauttaa siitä tosi tai epätosi.
Nyt muoto p => p.Startswith("-")
on Lambda-lauseke, joka on predikaation Predicate<string>
mukainen. Esitellään siis meidän OnkoTutkittava
-funktioon vielä toinen parametrin, joka on predikaattityyppiä.
Nyt voimme tehdä toteutuksen ja testit yleisemmälle versiolle:
Tuon iffittelyn saisi kai poistettua palauttamalla vain loogisen lauseen tuloksen, mutta onko se soveltumaton tapa kurssin opetettavaan asiaan verrattuna?
—Huomattakoon että lopun paluatus return ehto(s);
on siis sama kuin:
if ( ehto(s) ) return true;
return false;
mutta eihän kukaan itseään kunnioittava C
-ohjelmoija kirjoita ehtolausetta mikäli sen voi kirjoittaa helpommin :-) Mutta taas jonkin verran makuasia.
OnTutkittava
-funktiolla on se hyvä puoli, että se toimii oikein hyvin muutaman aikaisemman vaihtoehtoesimerkin predikaatti-funktiona.
Delegaatteja, lambda-lausekkeita ja listojen valmiita algoritmeja käyttäen olisi malliohjelmasta voitu jättää pois useita silmukoita sekä ehtolauseita. Malliohjelma on kuitenkin koetettu pitää rakenteiltaan yksinkertaisena, koska siinä on tarkoitus harjoitella Ohjelmointi 1 -kurssin perusasioita.
9.4.3 Julkaisukelpoinen koodi
Jee! Koodi on nyt julkaisukelpoisessa kunnossa! Voit halutessasi vielä leikkiä projekti- ja aikadatatiedostoilla (alussa).
10. TODO ja kehitysideoita
- tietojen lukeminen netistä (tehdään esimerkki pääohjelmasta joka lukee Git:stä tiedostot)
- toisenlainen toteutus (vaikka AJ:n versio)
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.