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. (Mac-käyttäjille on lisäksi Xamarin-ohje.)

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 .

Klikkaa minua ja lue myös tämä!

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.

projektit.txt

Toinen on ajat-tiedosto, joka sisältää tiedon projektin aloitus- ja lopetusajankohdasta sekä kestosta tolpilla eroteltuna. Sisältää myös valinnaisen kommentin.

ajat.txt

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.

Esim. suunnitelman tiedostoista koostettu raportti

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.

Valmis koodi

Valmiin ohjelman toiminta ja sen käyttämien tiedostojen esittely

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.

Miten sama tehtäisiin muilla kursseilla?

Taustaa tehtävän sisällöstä

Esimerkki raportista erilaisella projektit.txt-tiedostolla

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)
Viimeisellä rivillä esimerkki Tyoaikaraportti.exen kutsumisesta argumenteilla komentroriviltä (cmd) Windows 10 -ympäristössä
Viimeisellä rivillä esimerkki Tyoaikaraportti.exen kutsumisesta argumenteilla komentroriviltä (cmd) Windows 10 -ympäristössä

ruvetaan käyttämään powershelliä, eli kirjoita PowerShell ja käynnistä se

02 Aug 17 (edited 09 Aug 17)

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.

Koko koodi tässä vaiheessa

Projektin luominen Visual Studio 2017:llä

1.3 Argumenttien antaminen Visual Studiossa

Visual Studiossa ajettaessa (eli painettaessa F5) ohjelma ei automaattisesti saa mitään komentoriviargumentteja. Solution Explorerin kohdasta Properties/Debug/Command line arguments voidaan asettaa argumentit Visual Studiolla ohjelmaa kokeiltaessa.

Vastaava Xamarinissa

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

26 Jul 17

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);

Usein tietoa pitää lukea pienemmissä paloissa

tämän sisälle likki Ohj1 monisteeseen sopivaan kohtaan

11 Aug 17

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

25 Jul 17 (edited 25 Jul 17)

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

02 Aug 17 (edited 02 Aug 17)

Visual Studiossa F5-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 Ctrl+F5. Toinen vaihtoehto on laittaa keskeytyskohta (breakpoint) pääohjelman viimeiseen aaltosulkuun.

Koko koodi tässä vaiheessa

Argumentit Visual Studiolla ajettaessa, tiedostosta luku, poikkeusten käsittely

3. 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

02 Aug 17

Miksi ohjelmistoprojektit epäonnistuvat?

oikolue tämän sisältö!!!

02 Aug 17

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.

raportin muoto

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.

projektit

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).

ajat

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.

"Animaatio" toteutuksen suunnitelmasta

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

02 Aug 17

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?

09 Aug 17

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ä.

Koko koodi tässä vaiheessa

Ensimmäinen aliohjelma, ComTest

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");
        }

Koko koodi tässä vaiheessa

Pääprojektien hakeminen projektilistasta

HUOM! Videolla väärin. Aliohjelmalla ei ole muodostajaa, vaan aliohjelman esittelyrivi (otsikkorivi, eng. header).

28 Jul 17 (edited 28 Jul 17)

Pääprojektien etsiminen vaihtoehtoisella tavalla

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

02 Aug 17

Koko koodi tässä vaiheessa

Aliprojektien hakeminen projektilistasta

Aliprojektien etsiminen vaihtoehtoisella tavalla

7. Kestoajan määrittäminen

7.1 Kestoaikojen lisääminen raporttiin

Luodaan sekä Raportoialiohjelman 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.

31 Jul 17 (edited 31 Jul 17)

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).

Koko koodi tässä vaiheessa

Aliohjelma aliprojektin keston määrittämiseen

8. Muotoilu

8.1 Muotoilun kokeilu

Tässä osiossa tehdään itselle pieni apuohjelma, jolla voidaan harjoitella, miten formaattia käytetään.

Leikkiohjelmalla hyvän formaatin löytäminen

Muistathan, että jos koodissa haluat vaihtaa jonkun muuttujan tai aliohjelman nimeä, se kannattaa uudelleennimetä (eng. Rename, Ctrl+R+R), 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.

raportin muoto

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, "======="));

Koko koodi tässä vaiheessa

Raportin muotoilu lopulliseen muotoonsa

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. (F12-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

09 Aug 17

Yllä lisättiin loogisella operaattorilla "ehdollinen tai" (conditional if ||) myös kommentointimahdollisuus projektitiedostoon kirjoittamalla rivin alkuun #-merkki.

Edellä olevat ehtolauseet voisi kirjoittaa muillakin tavoilla

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;
    }

Erilaisia tapoja toteuttaa ehtolauseet

9.4.3 Julkaisukelpoinen koodi

Jee! Koodi on nyt julkaisukelpoisessa kunnossa! Voit halutessasi vielä leikkiä projekti- ja aikadatatiedostoilla (alussa).

# projektit2
ohjelmointikurssi
-demot
-luennot
-ht
integrointikurssi
-ex temporet
-demot
-luennot
humanistikurssi
-demot
-luennot
-lukupiiri

 

Valmis koodi

Virheellisten rivien käsittely ja testien muokkaaminen

10. TODO ja kehitysideoita

  • tietojen lukeminen netistä (tehdään esimerkki pääohjelmasta joka lukee svn: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.