Tämä sitten kun silmukat käsitelty: Onko vaikeuksia silmukoissa ja taulukoissa?
- lue luentomonisteesta Silmukat-luku
- aloita ensimmäistä Taunoista ja tee ne Taunolla ja sitten silmukalla. Tarvittaessa katso avuksi demonpalautusvideoita Tauno-tehtävän käsittelystä.
- sitten siirry seuraaviin Taunoihin ja jatka kunnes kaikki käyty läpi!
- tee kaikki taulukkotehtävät demojen harjoittelusivuilta
- katso eri taulukkotehtävien vastauksia demojen mallivastauksista
- katso luentovideoista taulukoista ja silmukoista
- katso luento 11 esimerkkejä ja animaatioita
- lue lisämateriaalista taulukoista ja silmukoista
Demojen harjoittelu v/2024/syksy
Tässä dokumentissa voit harjoitella demoja vastaavia tehtäviä. Näistä ei saa oikeasti demopisteitä, toki osaan tehtävistä tulee "leikisti" pisteitä.
Tehtävien perässä on tehtävään malliratkaisu, jonka voit avata sitten kun tuntuu siltä, että tarvitset apua, tai olet ratkaissut tehtävän.
Demo 3
3.4 Funktioita
Tämä on malli demo 3 tehtävään Funktioita. Tässä mallissa pitää tehdä funktio NelionAla
.
Tehtävän aluksi kannattaa katsoa alta Näytä koko koodi
. Ja sitten välillä pistää se pois, koska aliohjelmaa tehdessä ei saa enää ajatella, että mistä sitä kutsuttiin.
Jos olet ihan ulkona, katso ensin yllä oleva video.
Aloita miettimällä alla olevaan funktioon ISOLLA kirjoitettu sana kerrallaan, mitä pitää tulla siihen kohti ja kirjoita vastaava sana siihen. Jos et keksi, avaa vastaava vinkki. Katso vinkki sitten joka tapauksessa sanan täydentämisen jälkeen ja mieti oliko päättelysi oikein.
Kun valmis, muista Aja
ja Test
sekä katso myös Document
ajatuksen kanssa.
Alla harjoittelutehtävän tekemistä ohjaava video kokonaisena. Mutta voi olla järkevää katsoa se alempaa pala kerrallaan, kun on ensin miettinyt itse mitä kunkin sanan kohdalle tulee ja sitten avaa vastaavan vinkin, lukee siinä olevan tekstin ja tarvittaessa katsoo vielä siinä olevan videon palan. Vinkit kannattaa sulkea lukemisen jälkeen niin sivun pituus ei kasva liikaa.
Aloitus: Tehtävän voi tehdä suoraan TIMIssä, mutta videossa se on tehty käyttäen Visual Studiota.
Nyt siirry tehtävään ja rupea sana kerrallaan miettimään mitä kirjoitetaan ja sitten katso vinkit.
- NÄKYVYYS: Mieti onko funktio käyttökelpoinen muidenkin mahdollisesti käytettäväksi? Jos on, niin
public
. Esim jos funktiota aiotaan testata, pitää sen ollapublic
.
- STATICvaiEI: voiko funktion tehtävän suorittaa pelkästään sillä datalla, joka tuodaan parametreissa. Tässä voi kannattaa avata
Näytä koko koodi
ja katsoa miten funktiota kutsutaan ja miettiä riittäisikö itselle tuo kutsussa oleva parametri asian laskemiseksi. Jos riittää, niinstatic
.
- PALUUARVONTYYPPI: Katso
Näytä koko koodi
. Minkä tyyppiselle muuttujalla funktion tulos sijoitetaan. Jos ei sijoiteta mihinkään,void
, muuten sama tyyppi kuin on sillä muuttujalla johon tulos sijoitetaan. Olisiko se tällä kertaaala
-muuttujan tyyppi?
- FUNKTIONNIMI: Avaa taas (jollei jo ole auki)
Näytä koko koodi
. Katso minkä nimistä funktiota pääohjelmassa kutsutaan. Tälle pitää antaa sama nimi.
- PARAMETRINTYYPPI: Katso minkä tyyppinen arvo on funktion kutsussa (eli nyt pääohjelmassa) suluissa funktion nimen perässä. Tähän sama tyyppi. Nyt kutsussa on muuttuja
sivunpituus
. Minkä tyyppiseksi se on esitelty?
- NIMI: parametrin nimeksi saa laittaa mitä tahansa. Kunhan lopussa koodissa käyttää samaa nimeä. Nimen kannattaa kuvata ihmisille tuttua asiaa. Esimerkkisi ympyrän tapauksessa hyviä nimiä olisivat
sade
tair
. Jossakin toisessa yhteydessä nimet ovat toki eri. Jos funktiolle on jo kirjoitettu dokumenttikommentit (kuten tässä esimerkissä, muista katsoa se koko koodi) ja niissä on kuvattu parametrin nimi, pitää toki käyttää sitä nimeä. Usein ensin kirjoitetaan funktion esittelyrivi ja siitä pyydetään dokumentin runko, eli silloin muuttujan nimen voi valita vapaasti. - Tässä tapauksessa dokumentaatiokommenteissa on käytetty nimeä
s
, joten sitä on käytettävä parametrin nimenä. Nimen ei tarvitse olla sama kuin kutsussa, mutta se saa olla. - Muista että muuttajien nimet alkavat pienellä kirjaimella!
- Nimellä ei ole mitään tekemistä kutsussa olevan nimen kanssa, sillä aliohjelma (nyt funktio) muodostaa oman kokonaisuutensa ja se ei tiedä mitään pääohjelmasta. Aliohjelma saa tietyn määrän parametreja kutsuvalta aliohjelmalta (pääohjelmakin on vain aliohjelma) ja ottaa ne vastaan omiin parametreihinsa samassa järjestyksessä. Vertaa jos esimerkiksi työntäisit kaverille lappuja oven alta.
- LASKUTOIMITUS: Oikeastaan tähän pitää kirjoittaa lauseke, joka tuottaa halutun tuloksen. Joku voi haluta laskea ensin tuloksen apumuuttujaan ja palauttaa tässä sitten apumuuttujan arvon. Yksi muuttuja on sinällään hyvä lauseke. Joku toinen voi haluta kirjoittaa tuloksen laskevan lausekkeen suoraan tähän. Lauseke riippuu tietysti valitusta parametrin nimestä sekä itse ongelmasta.
Videossa sanon vahingossa mock kun piti sanoa tynkä.
public static double NelionAla(double s)
{
return s * s;
}
tai
public static double NelionAla(double s)
{
double ala = s * s;
return ala;
}
tai
public static double NelionAla(double s)
{
double ala:
ala = s * s;
return ala;
}
Huomaa että tämä sama malli pätee kaikkiin aliohjelmiin joita kirjoitetaan. Voit aina aloittaa kopioimalla itsellesi tämän tehtävän lähtöpohjan (pääset siihen Alusta
-linkillä) itsellesi.
Demo 4
4.2 Merkkijonofunktio
Esimerkki, jossa on samoja elementtejä kuin Tehtävässä 2. Tämä tehtävä on "vaikeampi" kuin itse demotehtävä.
Huom. Tehtävää tehdessä ei pidä miettiä sitä, mitä järkeä funktiossa on, tehtävä on laadittu vaan sellaiseksi, että funktiossa tulee käytettävksi eri tyyppisiä parametreja.
Tehtävänä on tehdä ohjelma, joka kysyy käyttäjältä merkkijonon ja sitten sen mukaan onko jono pidempi kuin 4 vai ei, tulostetaan Aika pitkä
tai lyhyt
.
Pohjassa on pääohjelma valmiina ja toteutettavaksi jää vain funktio OnkoPitka
.
Pääohjelmassa olevat \"
ovat sitä varten, koska jos halutaan tulostaa lainausmerkit, niin nehän katkaisisivat merkkijono. Joten siihen, mihin halutaan tulostuvan lainausmerkki, pitää laittaa lainausmerkin eteen takakeno \
. Tällöin lainausmerkki tulkitaan tavalliseksi merkiksi, ei merkkijonon sulkevaksi lainausmerkiksi.
- Katso
Näytä koko koodi
- Tutki miten aliohjelmaa/funktiota kutsutaan pääohjelmasta, entä testeistä. Jos aliohjelman palauttama tulos sijoitetaan johonkin, on kyseessä funktio (eli ei
void
). - Montako parametria on kutsuissa? Vastaus 3
- Ensimmäinen tavoite on kirjoittaa funktion esittelyrivi ja tynkätoteutus.
koska funktiota voisi käyttää muutkin, tehdään siitä julkinen. Esim testejä ei voitaisi ajaa jos funktio ei olisi julkinen, koska itse testit menevät eri tiedostoon ComTest-ajon tuottamassa toteutuksessa (ks VisualStudiossa
Test
-loppuisia projekteja ja niissäTest.cs
loppuisia tiedostoja.koska pelkissä parametreissä on riittävästi tietoa tehtävän suorittamiseen, tehdään funktiosta staattinen, eli alkuun:
public static
pääohjelmassa funktion palauttama arvo on sijoitettu muuttujalle
tulos
, joka näyttää olevastring
-tyyppinen.siis funktion paluuarvonkin pitää olla
string
, eli kirjoitetaanpublic static string
paluuarvon kirjoittamisen jälkeen kirjoitetaan funktion nimi. Funktion nimi on yleensä kiinnitetty, kun mietitään miten funktiota kutsutaan. Niin nytkin. Pääohjelmasta ja testeistä selviää että nimi on
OnkoPitka
. Kirjoitetaan se ja saman tien pakolliset sulut ja ei missään nimessä puolipistettä funktion esittelyriville. Eli toistaiseksi meillä on:public static string OnkoPitka() { }
nyt pitäisi saada kaarisulkuihin oikea määrä oikeantyyppisiä ja -nimisiä parametreja
pääohjelmassa ja jokaisessa testissä näyttää kutsuissa olevan 3 parametria, joten kaarisulkuihin pitää saada vastaava määrä.
kutsun ensimmäinen parametri näyttää olevan merkkijono ja dokumenteissa sen nimenä on
jono
, eli lisätään se ensin:public static string OnkoPitka(string jono) { }
kutsuissa toisena parametrina näyttää olevan aina jokin kokonaislukuarvo. Siis toisen paranetrin tyyppi on
int
. Nimi selviääkin dokumentaatiosta, joten listään 2. parametri:public static string OnkoPitka(string jono, int rajapituus) { }
kutsuissa on myös kolmas parametri joka on esim pääohjelmassa olevassa kutsussa:
"Aika pitkä"
. Kaksinkertaiset lainausmerkit? Eli tämähän on "merkkijonovakio" ja silloin parametrin pitää olla sellainen, johon tämä voidaan sijoittaa, elistring
. Taas muuttujan nimi selviää dokumentaatiokommenteista (koska ne olivat nyt valmiina, jos eivät olisi olleet, niin nimi keksittäisiin itse ja kerrottaisiin se dokumentaatiokommenteissa). Kutsuissa oli kolme parametria ja meillä on nyt esittelyrivillä kolme parametria, joten on esittelyrivi valmis:public static string OnkoPitka(string jono, int rajapituus, string pitkaJono) { }
seuraava vaihe on saada funktiosta tynkä, eli sellainen, mikä on mahdollisimman lyhyt syntaktisesti oikein oleva toteutus, joka ei kuitenkaan missään nimessä saa mennä testeistä läpi. Yksi hyvä tynkä olisi palauttaa tyhjä merkkijono, eli:
return "";
kuitenkin tehtävän määrityksen mukaan joudumme useassa tapauksessa (eli jonon ollessa lyhyt) palauttamaan vakiojonon
"lyhyt"
, joten voimme laittaa tämän suoraan jo tynkään. Eli silloin koko toteutus olisi toistaiseksi:public static string OnkoPitka(string jono, int rajapituus, string pitkaJono) { return "lyhyt"; }
- Oikeasti seuraava vaihe olisi kirjoittaa funktion dokumentaatiokommentit ja sitten riittävän kattavat testit, mutta koska meillä ne on jo tässä esimerkissä valmiina, siirrymme seuraavaan vaiheeseen.
- Ja se seuraava vaihe on ajaa testit ja nähdä punaista. Koko ohjelman on luonnollisesti oltava syntaksiltaan oikein, jotta testit voidaan kääntää ja ajaa. Ja sitä se onkin koska esittelyrivi on oikein ja toteutuksena tynkä.
- Eli aja testit ja näe punaista niistä!
Sitten seuraa ohjelmoinnin vaikein vaihe: Pitää kyetä sanomaan omalla äidinkielellä yksikäsitteisesti se mitä ollaan tekemässä. Tämä usein unohtuu ja jos sitä ei osaa sanoa omalla kielellä, ei sitä pysty sanomaan vieraallakaan kielellä.
Eli nyt tuo olisi jotakin tyyliin:
- Jos jonon pituus on suurempi kuin rajapituus niin palauta pitkaJono.
- muuten palauta teksti "lyhyt".
Nyt kun tiedetään mitä ollaan tekemässä, voidaan aloittaa toteutuksen tekemistä "kääntämällä" algortimia pala kerrallaan kohdekielelle, eli tällä kurssilla C#-kielelle.
Ensin pitäisi saada selville jonon pituus. Etsitään luentomonisteesta merkkijonojen kohdalta tai Microsoftin merkkijonodokumentaatiosta vastausta kysymykseen. MS:n dokumentaatiosta tätä voi olla haastavaa löytää, mutta Googlen hakusanalla msdn string length voi löytyä oikea kohta. Luentomonisteesta vastaus löytyy helpommin :-)
eli selviää että merkkijonolla on ominaisuus (ei funktio/metodi)
Length
joka antaa jonon pituuden kokonaislukuna. Sijoitetaan pituus talteen kokonaislukutyyppiseen apumuuttujaan. Lisätään siis oman funktiomme ensimmäiseksi riviksi:int jononPituus = jono.Length;
Loppuun ei tule sulkuja, koska kyseessä ei ole metodikutsu.
Huomaa että pääohjelmassa esiintynyttä vakiota 4 ei missään tapauksessa saa esiintyä funktiossa! Testeissä se voi toki olla tarvittaessa.
Nyt kun jonon pituus on tiedossa, voidaankin verrata sitä rajapituuteen:
if ( jononPituus > rajapituus )
ja jos ehto toteutui, piti tehtävän määrityksen mukaan palauttaa kolmantena parametrina tuotu merkkijono, eli koko seuraava rivi olisi silloin:
if ( jononPituus > rajapituus ) return pitkaJono;
jotkut tykkäävät kirjoittaa toteutuksen eri riville muodossa:
if ( jononPituus > rajapituus ) return pitkaJono;
tässä on se riski, että jos halutaan useampia lauseita ehdon ollessa voimassa ja lauseita lisätään luullen sisennysten vaikuttavan suoritukseen. Sen takia monet pitävät varmempana käyttää aina lohkosulkuja jos suoritettavaa lausetta ei kirjoiteta samalle riville ehdon kanssa:
if ( jononPituus > rajapituus ) { return pitkaJono; }
huomaa että on myös muita vertailuoperaattoreita ja tilanteen mukaan niistä pitää valita oikea. Ensin mietitään huolella tarvitaanko erisuuruutta jonkin suuntaan, yhtäsuuruutta samalla vai pelkästään. Tässä tapauksessahan vaadittiin nimenomaan puhdasta suuremmuutta.
- Jos ehto ei toteudu, eli jonon pituus onkin rajan kanssa yhtäsuuri tai pienempi, niin silloin suoritusta jatketaan seuraavalta riviltä ja tynkätoteutuksemme ansiosta siinä tapauksessa palautetaan teksti "lyhyt" kuten pitikin. Eli mitään lisäehtoa tai
else
-osaa ei tässä tarvita. - vaikuttaisikin että olemme valmiita ja voimme ajaa testit ja meidän pitäisi nyt nähdä vihreää.
Koska oikeasti aliohjelma/funktio tehdään tarpeeseen ja meillä se tarve oli tuolla pääohjelmassa, niin ehkä pääohjelmakin on syytä ajaa. TIMissä ajamista varten
Aja
-painikkeen yläpuolelle kirjoitetaan pääohjelman tarvitsema syöte. Kirjoitetaan vaikka ensin siihenkissa
ja ajetaan ohjelma. Pitäisi tulostua haluttu viesti.
kokeillaan seuraavaksi kirjoittaa:
koi
ja ajetaan taas. Nyt pitäisi tulostua
lyhyt
.Taidamme olla valmiita?
Ei, katsotaan vielä huolella
Document
-linkistä minkälainen dokumentaatio syntyy kommenttiemme ansiosta (jotka nyt oli valmiina, mutta usein pitää kirjoittaa itse). Klikkaa dokumentaatiossa sanaaclass PidempiSana
ja katso miten itse funktion kuvaus näkyy. Sitten rullaa alaspäin ja katso miten parametrit näkyvät ja vielä alempana miten testit näkyvät "esimerkkeinä".
public static string OnkoPitka(string jono, int rajapituus, string pitkaJono)
{
int jononPituus = jono.Length;
if ( jononPituus > rajapituus ) return pitkaJono;
return "lyhyt";
}
Apumuuttuja pois:
public static string OnkoPitka(string jono, int rajapituus, string pitkaJono)
{
if ( jono.Length > rajapituus ) return pitkaJono;
return "lyhyt";
}
tai "funktionaalinen" ratkaisu (ei tarvitse osata):
public static string OnkoPitka(string jono, int rajapituus, string pitkaJono)
{
return jono.Length > rajapituus ? pitkaJono: "lyhyt";
}
Demo 5
Aloita aina tehtävä avaamalla Näytä koko koodi
ja sitten tutustu huolella pääohjelmaan.
5.1 StringBuilder
Kirjoita ruutupaperille merkkijonoja ja sitten lisää niiden keskelle joku teksti. Eli harjoittele ensin ongelman kanssa.
Mieti ensin useilla eri jonoilla mitä pitäisi tehdä. Eli piirrä jonoja ja niiden päälle indeksit:
Esim kana
ja pitäisi lisätä mala
:
0123 01234567
kana lisäys tulee paikan 2 eteen, eli kamalana
Esim kissa
ja lisätään va
:
01234 0123456
kissa lisäys taas paikan 2 eteen, eli kivassa
Mieti mitä tarkoittaa puoliväli? Jos on parillinen määrä, kuten abba
-sanassa, niin lisäyksen paikka on jonon pituuden puolikas, eli 4/2 = 2
. Jos pituus on pariton, niin joudutaan valitsemana jompikumpi paikka, sillä paikkaa 2.5
ei ole. Valitaan alempi, eli katkaistaan jakolaskun tulos. Mieti toimiiko valittu idea myös vaikka 8 ja 9 mittaisille jonoille?
Klikkaa
Näytä koko koodi
.Kirjoita aikaisempien oppien perusteella aliohjelman esittelyrivi ja tee tynkä. Lue tyypit huolella.
- toki alkuun
public static
- tulosta ei sijoiteta pääohjelmassa mihinkään =>
void
- aliohjelman nimi on
LisaaKeskelle
- kutsussa kaksi parametria
- tyyppi on
StringBuilder
ja dokumentaatiossa sille on annettu nimijono
- tyyppi on
- on pääohjelmassa
"kivi"
eli tuon voi sijoittaa merkkijonolle, joten tyyppi onstring
ja dokumentaatiossa nimenä onlisateksti
.
- on pääohjelmassa
=>
public static void LisaaKeskelle(StringBuilder jono, string lisateksti) { }
- toki alkuun
Etsi muuttuvan merkkijonon avustuksista millä metodilla voi lisätä muuttuvan merkkijonon keskelle merkkijonon:
Mieti, miten saat selville
jono
-muuttujan puolivälin indeksin. Tallenna tämä puolivälin sijainti uuteen muuttujaanint puolivali
.jono.Insert(puolivali, lisateksti);
public static void LisaaKeskelle(StringBuilder jono, string lisateksti)
{
int puolivali = jono.Length/2;
jono.Insert(puolivali, lisateksti);
}
Testien tekeminen
void
-aliohjelmille on hieman työlästä.
Tässä esimerkissä jokaista testiä varten tarvittiin 3 riviä. Tätä voi helpottaa tekemällä niin, että aliohjelma palauttaa lopuksi viitteen muuttamaansajono
-olioon. Tällöin aliohjelmasta ei tule puhdas funktio, koska sillä on sivuvaikutus; se muuttaa itsejono
-oliota. Tällöin sanotaan, että funktio on sivuvaikutuksellinen.Yleensä aliohjelman (
void
) muuttaminen sivuvaikutukselliseksi funktioksi ei riko jo tehtyä koodia, koska funktion palauttamaa arvoa ei ole pakko C#-kielessä ottaa kiinni.Muutetaan
LisaaKeskelle
sivuvaikutukselliseksi funktioksi ja tehdään testit ketjuttamallaLisaaKeskelle
- jaToString
-aliohjelmien kutsut:/// <pre name="test"> /// LisaaKeskelle(new StringBuilder("Abba"), "c").ToString() === "Abcba"; /// LisaaKeskelle(new StringBuilder(""), "a").ToString() === "a"; /// LisaaKeskelle(new StringBuilder("123"), "4").ToString() === "1423"; /// </pre> /// </example> public static StringBuilder LisaaKeskelle(StringBuilder jono, string lisateksti) { int puolivali = jono.Length/2; jono.Insert(puolivali, lisateksti); return jono; }
5.2 Taulukot ilman silmukkaa
- Sinun pitää laskea taulukossa niiden lukujen määrä, jotka halutaan laskea.
- Mieti ensin algoritmi miten itse tekisit:
- sinulle annetaan ruutupaperilla rivi lukuja (=taulukko)
- alusta lkm-laskuri nollaksi
- käy taulukon jokaisessa alkiossa
- jos alkio on tutkittava luku, niin
- lisää lkm laskuria yhdellä
- jos alkio on tutkittava luku, niin
- kun kaikki kuvut käyty läpi, tulos on laskurissa lkm
Siten sama ohjelmaksi. Avaa ensin koko ohjelman näkyviin jotta näet kutsun muodon.
Kirjoita funktion esittelyrivi
luonnollisesti
public static
aliohjelman tulos on sijoitettu pääohjelmanssa
maara
-muuttujaan joka on tyypiltäänint
, eli funktion tyypiksiint
.pääohjelmassa on kutsussa kaksi parametria
ensimmäinen on
luvut
joko on näköjään tyypiltäänint[]
eli ensimmäisen parametrin tyyppi onint[]
ja dokumentissa sen nimi onluvut
toinen kutsuparametri on
6
eli se voidaan sijoittaaint
-tyyppiselle muuttujalle ja parametrin nimeksi on annnettu dokumentissaluku
näin ollen esittelyrivi on:
public static int Montako(int[] luvut, int luku)
Tee tynkä ja kokeile että toimii ja antaa testeistä punaista
koska funktio palauttaa kokonaisluvun, niin tynkätoteus voisi olla:
public static int Montako(int[] luvut, int luku) { return 0; }
Lisää apumuuttuja lukumäärän laskemiseksi ja alusta se nollaksi
lkm = 0;
Tee tässä tehtävässä "tyhmästi", eli tutki jokaisen alkion kohdalta erikseen onko se haluttu, esim indeksin 1 kohdalla olevasta kirjoita:
if ( luvut[1] == luku ) lkm++;
public static int Montako(int[] luvut, int luku)
{
int lkm = 0;
if ( luvut[0] == luku ) lkm++;
if ( luvut[1] == luku ) lkm++;
if ( luvut[2] == luku ) lkm++;
return lkm;
}
5.3 Lukujen tulostaminen
Tulostuksen on oltava täsmälleen alla olevan kaltainen:
Luku Toiseen
0 0
1 1
2 4
3 9
4 16
5 25
12345678
eli luku
-muuttujalle varataan tilaa 2 paikkaa ja sitten kolme välilyöntiä ja toisen potenssin kohdalla varataan tilaa 3 paikkaa. Alin rivi on vain luettelo sarakkeista, mihin mikäkin pitää tulostaa, sitä ei tulosteta oikeasti.
Kopioi tehtävn pohja Visual Studioon uudeksi projektiksi:
- Näytä koko koodi
- paina Copy-linkkiä (pohja menee leikepöydälle)
- luo uusi
ConsoleMain
-projekti nimelleTulostaPotensseja
- Avaa syntynyt
TulostaPotensseja.cs
- valitse kaikki
Ctrl-A
- liitä pohja maalatun tilalle
Ctrl-V
Sitten tehdään tynkä:
kirjoita aliohjelman esittelyrivi
tee tynkä:
public static void TulostaToiseen(int alku, int loppu) { }
kokeile että kääntyy ja tulostaa sen mitä pääohjelmassa on tulostettu
Fiksu vielä tässä vaiheessa dokumentoi aliohjelman eli laittaa edellisellä rivillä
///
ja sitten täydentää syntyneen tekstin. Tällaista tulostavaa aliohjelmaa ei ole helppoa testata, joten tällä kertaa jätämme testit kirjoittamatta.
Aloita miettimällä miten tulostaisit luvut ilman mitään silmukoita tai muita kotkotuksia:
Console.WriteLine(" 0 0"); Console.WriteLine(" 1 1"); Console.WriteLine(" 2 4"); Console.WriteLine(" 3 9"); Console.WriteLine(" 4 16"); Console.WriteLine(" 5 25");
Tämä ei tietenkään kelpaa vastaukseksi, sillä tässä rivien määrä olisi aina sama, mutta tästä on hyvä lähteä liikkeelle. Kokeile!
Vaihdetaan seuraavaksi kiinteiden vakionumeroiden paikalle muuttujia:
int tulo; int luku = 0; tulo = luku * luku; Console.WriteLine("{luku} {tulo}"); Console.WriteLine("{luku} {tulo}"); Console.WriteLine("{luku} {tulo}");
// jne
Kokeile!
Tämä ei tietenkään toimi, koska joka kerta käytetään samoja muuttujien arvoja. Siksi seuraavaa lausetta varten muutetaan muuttujien arvoja niin, että seuraavakin lause toimii (alla olevia kommentteja ei tarvitse kirjoittaa, niillä vaan näytetään samoja asioita joita näkisi debgerilla)
int tulo; int luku = 0; tulo = luku * luku; // 0*0 = 0 Console.WriteLine("{luku} {tulo}"); // tuostuu 0 0 luku++; // luku muuttuu 1:ksi tulo = luku * luku; // 1*1 = 1 Console.WriteLine("{luku} {tulo}"); // tulostuu 1 1 luku++; // luku muuttuu 2:ksi tulo = luku * luku; // 2*2 = 4 Console.WriteLine("{luku} {tulo}"); // tulostuu 2 4 luku++; // luku muuttuu 3:ksi
// jne
Tuloksen muotoilu ei mene aivan kuten tehtävässä haluttiin!
Lue luentomonisteesta lukujen muotoilusta. Tulosta luku ja sen toinen potenssi oikein muotoiltuna
Console.WriteLine($"{luku,2} {tulo,3}");
Vaihda em. rivi kaikkien tulostusrivien kohdalle ja kokeile näyttääkö nyt hyvälle.
Nyt meillä toistuu monta kertaa identtinen kolmen lauseen lohko:
tulo = luku * luku; Console.WriteLine($"{luku,2} {tulo,3}"); luku++;
Tehdään tästä oikea lohko lisäämällä lohkosulut (eli aaltosulut):
{ tulo = luku * luku; Console.WriteLine($"{luku,2} {tulo,3}"); luku++; }
Tehdään tästä
while
-silmukka joka jatkaa niin kauan kuin luku on pienempi tai yhtäsuuri kuinloppu
. Älä vaan laita puolipistettäwhile
-rivin loppuun, sillä se tekisi ikuisen silmukan jonka suoritettava lause on se, mitä mahtuu)
ja;
väliin.while (luku <= loppu) { tulo = luku * luku; Console.WriteLine($"{luku,2} {tulo,3}"); luku++; }
Poista ylimääräiset samanlaiset lohkot.
Kokeile toimiiko oikein.
Laita Visual Studiossa keskeytyskohta riville
int tulo;
Aja debuggerilla keskeytyskohtaan
Askella silmukkaa niin, että aina ennen seuraava askelta yrität miettiä mitä tulee mihinkin muuttujaan ja mitä tulee tapahtumaan kun rivi suoritetaan. Kun joka kerta "arvaat" oikein, voit lopettaa askeltamisen.
Tee em. debuggausharjoitus JOKAISELLE muullekin tämän harjoitussivun tehtävistä.
Tässä pärjättäisiin ilmankin apumuuttujaa tulo, sillä lasku voitaisiin laskea tulostuslauseessa suoraan:
Console.WriteLine($"{luku,2} {luku*luku,3}");
tai:
Console.WriteLine("{0,2} {1,3}", luku, luku*luku);
Muotoilun vaatimuksen kolmesta välilyönnistä lukujen välillä voi hoitaa myös jommalla kummalla seuraavista:
Console.WriteLine($"{luku,2}{' ',3}{luku*luku,3}"); Console.WriteLine("{0,2}{1,3}{2,3}", luku, " ", luku*luku);
Kokeile myös mitä tapahtuu jos
WriteLine
sijaan kirjoitatWrite
.
public static void TulostaToiseen(int alku, int loppu)
{
int luku = alku;
while ( luku <= loppu )
{
Console.WriteLine($"{luku,2} {luku*luku,3}");
luku++;
}
}
C#-kielen for
-silmukka on (melkein) vain erilainen tapa sanoa while
-silmukka. Siinä while
-silmukan kolme osaa:
- alustuslause (nyt se oli
int alku = 0;
) - ehto (nyt se oli
luku <= loppu
) - kasvatuslause (nyt se oli
luku++
)
on kirjoitettu yhteen paikkaan, jotta silmukan toimintaa on helpompi kontrolloida.
Aloita while
-silmukkavastauksesta. Silmukan muuttaminen for
-silmukaksi:
Kirjoita muuttujan alustuksen eteen
for
välilyönti ja kaarisulku:for (int luku = alku; while ( luku <= loppu ) { Console.WriteLine($"{luku,2} {luku*luku,3}"); luku++; }
Pyyhi
while (
ja sen edellä oleva rivinvaihto pois niin, että ehto nousee samalla rivillefor
kanssa:for (int luku = alku; luku <= loppu ) { Console.WriteLine($"{luku,2} {luku*luku,3}"); luku++; }
Siirrä kasvatuslause
luku++
for:in ehdon perään puolipisteellä erotettuna:for (int luku = alku; luku <= loppu; luku++) { Console.WriteLine($"{luku,2} {luku*luku,3}"); }
Koska suoritettavana on nyt vain yksi lause, voisi silmukan kirjottaa myös ilman lohkosulkuja:
for (int luku = alku; luku <= loppu; luku++) Console.WriteLine($"{luku,2} {luku*luku,3}");
Lopullinen vastaus
for
-silmukkana:public static void TulostaToiseen(int alku, int loppu) { for (int luku = alku; luku <= loppu; luku++) { Console.WriteLine("{0,2}{1,3}{2,3}", luku, " ", luku*luku); } }
do-while
-silmukan ero on, että silmukka tehdään aina yhden kerran.while
jafor
-silmukoissa voi käydä niin, ettei silmukkaa tehdä yhtään kertaa.Aloitetaan taas
while
-vastauksesta.Jos
while
-silmukka on:int luku = alku; while ( luku <= loppu ) { ... jotakin ... luku++; }
niin leikkaa pois
while
ja ehto ja korvaa nedo
-sanalla:int luku = alku; do { ... jotakin ... luku++; }
liimaa leikkaamasi
while
ja ehto silmukan loppusulun perään välilyönnillä erotettuna ja lisää sinne vielä puolipiste. Huomaa ettädo-while
rakenteen pitää päättyä puolipisteeseen.while
-silmukka ei ole vielä valmis silläwhile
-rivillä, joten silloin puolipistettä ei saa laittaa.int luku = alku; do { ... jotakin ... luku++; } while ( luku <= loppu );
Lopullinen vastaus:
public static void TulostaToiseen(int alku, int loppu) { int luku = alku; do { // Console.WriteLine($"{luku,2} {luku*luku,3}"); // Console.WriteLine($"{luku,2}{' ',3}{luku*luku,3}"); // Console.WriteLine("{0,2} {1,3}", luku, luku*luku); Console.WriteLine("{0,2}{1,3}{2,3}", luku, " ", luku*luku); luku++; } while ( luku <= loppu ); }
5.4 Taulukot silmukalla
kirjoita funktion esittelyrivi
ota mallivastaus 3:n alkion tehtävästä:
int lkm = 0; if ( luvut[0] == luku ) lkm++; if ( luvut[1] == luku ) lkm++; if ( luvut[2] == luku ) lkm++; return lkm;
muuta vakioindeksien tilalle muuttuja
int lkm = 0; int i = 0; if ( luvut[i] == luku ) lkm++; if ( luvut[i] == luku ) lkm++; if ( luvut[i] == luku ) lkm++; return lkm;
Nyt vain 1. rivi toimii, eli indeksiä on kasvatettava lauseiden välillä:
int lkm = 0; int i = 0; if ( luvut[i] == luku ) lkm++; i++; if ( luvut[i] == luku ) lkm++; i++; if ( luvut[i] == luku ) lkm++; i++; return lkm;
Nyt löytyy nätti lohko jossa kaikki lauseet ovat samanlaisia:
if ( luvut[i] == luku ) lkm++; i++;
Sulje samanlainen lohko while-silmukaksi:
int lkm = 0; int i = 0; while ( i < luvut.Length ) { if ( luvut[i] == luku ) lkm++; i++; }
public static int Montako(int[] luvut, int luku)
{
int lkm = 0;
int i = 0;
while ( i<luvut.Length )
{
if ( luvut[i] == luku ) lkm++;
i++;
}
return lkm;
}
Kun olet saanut vastauksen valmiiksi, debuggaa sitä Visual Studiolla.
public static int Montako(int[] luvut, int luku)
{
int lkm = 0;
for (int i=0; i<luvut.Length; i++)
{
if ( luvut[i] == luku ) lkm++;
}
return lkm;
}
Kun olet saanut vastauksen valmiiksi, debuggaa sitä Visual Studiolla.
Demo 6
6 Tauno 2. Matriisin indeksit
Tarkoituksena on laskea tietyn ruudun naapureiden lukumäärä, ks alempana oleva kuva.
Tutustu ensin huolella demo 5:n sopulipeliin ja mieti miten laskit siinä naapureiden lukumäärän kullekin ruudulle. Naapureiden lukumäärän laskemiseksihan riittää laskea yhteen kussakin naapuriruudussa olevat luvut. Tällöin ei tarvita siihen mitään ehtolauseita.
Demo 5 sopulipelistä poiketen tässä naapureiksi lasketaan vaan "pääilmansuunnissa" olevat naapurit, eli suoraan yläpuolella, sivulla tai alapuolella olevat naapurit (max 4 kappaletta jos olla ruudukon laidalla). Alla olevassa mallikuvassa ruudun [1,2] naapurit ovat vihreällä.
Aluksi lasketaan naapurit vain ruudulle [1,2]
:
- Ruudun
[1,2]
pääilmansuuntanaapureiden "osoitteet" ovat:- yläpuolella:
[0,2]
- vasemmalla:
[1,1]
- oikealla
[1,3]
- alapuolella
[2,2]
- yläpuolella:
n += sukupolvi[0,2];
n += sukupolvi[1,1];
n += sukupolvi[1,3];
n += sukupolvi[2,2];
Seuraavaksi muutetaan niin, että lasketaan naapurit yleiselle "sisäpisteelle" [iy,ix]
. Edellinen tilanne saadaan siis tämän erikoistapauksena iy=1, ix=2
. Reunapisteet ovat vielä tässä vaiheessa hankalampia, koska niillä on eri määrä naapuriruutuja.
Ruudun [ix,iy]
pääilmansuuntanaapureiden “osoitteet” ovat:
- yläpuolella:
[iy-1,ix]
- vasemmalla:
[iy,ix-1]
- oikealla
[iy,ix+1]
- alapuolella
[iy+1,ix]
Kannattaa kirjoittaa niin, että jokaisessa kohdassa on laskutoimistus, niin kaikista riveistä tulee "samanlaisia":
n += sukupolvi[iy-1,ix+0];
n += sukupolvi[iy+0,ix-1];
n += sukupolvi[iy+0,ix+1];
n += sukupolvi[iy+1,ix+0];
Nyt lähdetään yleistämään tilannetta niin, että olisi jatkossa mahdollisimman helppo tehdä silmukka naapureiden laskemiseksi.
- Tavoitellaan tilannetta, jossa itse laskeminen on aina samanlaista, vain
y
jax
muuttuvat. Tässä ei pääse samanlaiseen säännönmukaisuuteen kun varsinaisessa Tauno 2-tehtävässä.
y = -1; x = 0;
n += sukupolvi[iy+y,ix+x];
y = 0; x = -1;
n += sukupolvi[iy+y,ix+x];
y = 0; x = 1;
n += sukupolvi[iy+y,ix+x];
y = 1; x = 0;
n += sukupolvi[iy+y,ix+x];
Tästä pyritään tekemään silmukka niin, että laitetaan naapuriruutujen erokoordinaatit aputaulukkoon, ja käydään sieltä hakemassa äsken vakioina olleet arvot.
Varsinainen Tauno 2-tehtävä on tarkoitus ratkaista sisäkkäisillä silmukoilla, mutta koska tässä naapureita on vähän, tehdään taulukko, jolla voidaan selvittää naapureiden indeksit. Toki tämä tapa toimisi "hätätilassa" myös oikeassa Tauno 2-tehtävässä, mutta taulukosta tulisi aika iso. Tosin tässä esitettävän tavan etuna on, että naapurina oleminen voidaan määritellä paljon monipuolisemmin. Voidaan esimerkiksi sanoa että 2 pykälää idässä oleva on myös naapuri (miten?).
Edellä oli jo jonkinlainen toistuva säännönmukaisuus, mutta koodissa
y = -1; x = 0; n += sukupolvi[iy+y,ix+x]; y = 0; x = -1; ...
y:n ja x:n arvot vaihtelevat "epämääräisesti". Yritetään korjata tämä puute taulukolla, josta löytyvät nuo vastaavat y:n ja x:n arvot.
Tehdään taulukko, jossa on eri naapureiden koordinaatien suhteellinen ero tutkittavaan pisteeseen. Huomaa että matematiikasta poiketen pidetään y ensin.
int[,] erot= {// 0 1 indeksit {-1, 0}, // 0 yläpuolisen naapurin indeksien ero { 0,-1}, // 1 vasemman naapurin indeksien ero { 0, 1}, // 2 oikean naapurin indeksien ero { 1, 0} // 3 alapuolisen naapurin indeksien ero };
nyt ensimmäinen
y = -1;
korvataan sillä, mistä sama-1
löytyy taulukosta,
vastaavastix = 0;
korvataan sillä, mistä0
löytyy taulukostakirjoitetaan
erot
-taulukon avulla aikaisempi summa:y = erot[0,0]; x = erot[0,1]; // vrt y = -1; x = 0; n += sukupolvi[iy+y,ix+x]; y = erot[1,0]; x = erot[1,1]; n += sukupolvi[iy+y,ix+x]; y = erot[2,0]; x = erot[2,1]; n += sukupolvi[iy+y,ix+x]; y = erot[3,0]; x = erot[3,1]; n += sukupolvi[iy+y,ix+x];
eli tästä löytyy tarvittava silmukka, kun otetaan kullakin kierroksella
erot
-taulukosta vastaavay
jax
ero:y = erot[i,0]; x = erot[i,1]; n += sukupolvi[iy+y,ix+x];
int[,] erot= {{-1,0}, {0,-1}, {0,1}, {1,0}};
for (int i=0; i<erot.GetLength(0); i++)
{
y = erot[i,0]; x = erot[i,1];
n += sukupolvi[iy+y,ix+x];
}
Korjataan koodia vielä niin, että voidaan laskea minkä tahansa ruudun naapurit. Jopa sellaisten, jotka ovat reilusti alueen ulkopuolella.
Aloitetaan muuttamalla edellistä laskua siten, että lasketaan sukupolvitaulukossa tarvittavat indeksit etukäteen tyyliin
y = iy + erot[i,0];
x = ix + ???;
jolloin summaa voidaan kirjoittaa:
n += sukupolvi[y,x];
kunhan ensin ollaan varmoja, että y
ja x
ovat laillisia, eli esimerkin sukupolvitaulukossa välillä 0-3
. Tietysti yleisessä taulukossa yläraja voi olla muukin.
int[,] erot= {{-1,0}, {0,-1}, {0,1}, {1,0}};
int maxy = sukupolvi.GetLength(0);
int maxx = sukupolvi.GetLength(1);
for (int i=0; i<erot.GetLength(0); i++)
{
y = iy + erot[i,0]; x = ix + erot[i,1];
if ( y < 0 ) continue;
if ( y >= maxy ) continue;
if ( x < 0 ) continue;
if ( x >= maxx ) continue;
n += sukupolvi[y,x];
}
6.2 Ehto ja etumerkin vaihtaminen
/// <summary>
/// Vaihdetaan parillisen luvun etumerkki
/// </summary>
/// <param name="luku">muutettava luku</param>
/// <returns>parilliset luvut etumerkki vaihtuneena, parittomat sellaisenaan</returns>
/// <example>
/// <pre name="test">
/// VaihdaParillisenEtumerkki(-8) === 8;
/// VaihdaParillisenEtumerkki(0) === 0;
/// VaihdaParillisenEtumerkki(8) === -8;
/// VaihdaParillisenEtumerkki(1) === 1;
/// VaihdaParillisenEtumerkki(-1) === -1;
/// </pre>
/// </example>
public static int VaihdaParillisenEtumerkki(int luku)
{
return 0;
}
Luvun parillisuutta voidaan testata kokeilmalla jakaa se kakkosella ja jos jakojäännös on nolla, on luku parillinen:
luku % 2 == 0
luvun etumerkin vaihtamiseen riittää ihan vastaluku:
luku = -luku;
public static int VaihdaParillisenEtumerkki(int luku)
{
if ( luku % 2 == 0 ) return -luku;
return luku;
}
public static int VaihdaParillisenEtumerkki(int luku)
{
if ( luku % 2 == 0 ) return -luku;
return luku;
}
/// <summary>
/// Lisätään lukuun 10 ja palautetaan tulos sellaisenaan jos se on parillinen,
/// muuten tuloksen vastaluku.
/// </summary>
/// <param name="???">?????</param>
/// <returns>luku+10 jos parillinen, muuten -(luku+10)</returns>
/// <example>
/// <pre name="test">
/// VaihdaParittomanSummanEtumerkki(-8) === 2;
/// VaihdaParittomanSummanEtumerkki(-10) === 0;
/// VaihdaParittomanSummanEtumerkki(-1) === -9;
/// VaihdaParittomanSummanEtumerkki(-12) === -2;
/// VaihdaParittomanSummanEtumerkki(-11) === 1;
/// VaihdaParittomanSummanEtumerkki(3) === -13;
/// </pre>
/// </example>
public static int VaihdaParittomanSummanEtumerkki(int luku)
{
return 0;
}
Kun luku on summattu, pitäisi oikeastaan tehdä edellisestä funktiosta sellainen, joka vaihtaa tuloksen parittoman tapauksessa. Mutta koska edellinen funkiot kääntää aina parillisen arvon, niin
int a = 2;
VaihdaParillisenEtumerkki(a) === -2
-VaihdaParillisenEtumerkki(a) === 2
int b = 3;
VaihdaParillisenEtumerkki(a) === 3
-VaihdaParillisenEtumerkki(b) === -3
eli voidaan käyttää edellisen funktion palauttama tulos ja palauttaa sen vastaluku kaikissa tapauksissa. Eli yhtään uutta ehtolausetta ei tarvitse kirjoittaa.
Kopioi edellisen tehtävän mallivastaus tähän etupuolelle ja sitten
public static int VaihdaParittomanSummanEtumerkki(int luku)
{
return -VaihdaParillisenEtumerkki(luku+10);
}
6.3 Etsiminen
Tämän tehtävän ymmärtäminen, kuten kaikkien muidenkin, vaatii sen, että käsin lasketaan eri luvuille jakojäännöksiä ja kirjoitetaan niitä lukujen viereen paperille ja sitten itse etsitään haluttua tulosta. Kun tätä on tehnyt riittävän usein, voi ymmärtää ongelman ja sitten pystyä miettimään sitä ohjelmaksi.
Tämän tehtävän vaikeusaste on sama kuin demo 6:n tehtävä 3:ssa vaadittava EtsiLahin
. Molemmissa mitataan "paremmuutta" jollain tavalla ja etsitään sitä alkiota, jolla on mittarin mukana paras tulos.
/// <summary>
/// Etsitään luku, jolla on pienin jakojäännös kun se jaetaan jakajalla.
/// </summary>
/// <param name="luvut">taulukko josta etsitään</param>
/// <param name="jakaja">jakaja jolla jaetaan, ei saa olla 0</param>
/// <returns>luku jolla pienin jakojäännös</returns>
/// <example>
/// <pre name="test">
/// int[] luvut = {2344, 1023, 97, 2343, 1236};
/// EtsiLukuJollaPieninJakojaannos(luvut, 7) === 1023; // jj: 6 1 5 6
/// EtsiLukuJollaPieninJakojaannos(luvut, 6) === 1236; // jj: 4 3 1 3
/// EtsiLukuJollaPieninJakojaannos(luvut, 4) === 2344; // jj: 0 3 1 3
/// int[] luvut2 = {-5, -9, -11};
/// EtsiLukuJollaPieninJakojaannos(luvut2, 10) === -9; // jj: -5 -9 -1
/// EtsiLukuJollaPieninJakojaannos(luvut2, 4) === -11; // jj: -1 -1 -3
/// EtsiLukuJollaPieninJakojaannos(luvut2, 2) === -5; // jj: -1 -1 -1
/// </pre>
/// </example>
public static int EtsiLukuJollaPieninJakojaannos(int[] luvut, int jakaja)
{
return 0;
}
Yleensä kaikissa etsimisongelmissa on tarkoitus löytää joku "voittaja". Joskus etsitään pienintä, suurinta tai ehkä itse lukua. Usein etsimisehtona on jokin muu kuin luvun suora arvo. Ja tällöin "hyvyyttä" mittataan jollakin mittarilla, joka antaa jonkin selkeän tuloksen. Esim mittari voisi olla "kuinka kaukana" tms. Nyt mittarina on se, mikä on luvun jakojäännös ja voittaja on se, jolla on pienin jakojäännös. Tällöin voittamiseen liittyy voittajan tulos. Eli normaalin voittajan ylläpitämisen lisäksi, täytyy pitää yllä voittajan tulosta. Itse etsiminen tehdään normaalilla silmukalla
voittajan seuraamiseen tarvitavat muuttujat alustetaan niin, että kuka tahansa voittaa ne, esim:
int voittaja = int.MinValue; int voittajanTulos = int.MaxValue; // tämän voittaa kuka vaan
itse silmukka tehdään kuten aina etsimistehtävissä. Kun tutkittava luku (tai muu alkio) on saatu, muodostetaan sille "tulos", jota verrataan voittajan tulokseen:
int tulos = luku % jakaja; // % antaa jakojäännöksen if ( tulos < voittajanTulos )
jos voitetaan, eli tässä tehtävässä pienempi on parempi. vaihdetaan voittaja ja sen tulos:
voittaja = luku; voittajanTulos = tulos;
lopulta kun kaikki ovat saaneet yrittää, löytyy noista muuttujista voittaja ja voittotulos. Niitä käsitellään lopuksi tarvittavalla tavalla, eli tässä funktiossa palautetaan voittaja.
public static int EtsiLukuJollaPieninJakojaannos(int[] luvut, int jakaja)
{
int voittaja = int.MinValue;
int voittajanTulos = int.MaxValue; // tämän voittaa kuka vaan
for (int i=0; i<luvut.Length; i++)
{
int luku = luvut[i];
int tulos = luku % jakaja;
// Console.WriteLine($"{luku} {tulos}"); // kokeilua varten
if ( tulos < voittajanTulos )
{
voittaja = luku;
voittajanTulos = tulos;
}
}
return voittaja;
}
...
foreach (int luku in luvut)
{
int tulos = luku % jakaja;
if ( tulos < voittajanTulos )
{
voittaja = luku;
voittajanTulos = tulos;
}
}
...
Mitä asioita mallivastauksessa on käsittelemättä, testaamatta ja dokumentoimatta?
Ei ole erikseen tarkastettu että onko taulukko tyhjä. Tästä tosin selvitään sillä, että funktion nykyinen toteutus palauttaa
int.MinValue
jos silmukkaa ei tehdä yhtään kertaa, mutta tämä olisi silloin syytä erikseen dokumentoida ja testata.ei ole tarkistettu että jaetaanko nollalla. On vaikea sanoa mikä olisi oikea vastaus tässä tapauksessa. Jos dokumentoidaan että tällöinkin palautetaan
int.MinValue
, niin sekin olisi syytä testata.kumpikin tapaus voidaan laittaa heittämään poikkeus (nollalla jako heittääkin se jo nyt) mutta tämäkin olisi silloin syytä kommentoida ja testata.
jos pidättäydytään nykyisessä koodissa, niin tehtäisiin muutokset:
/// <summary> /// Etsitään luku, jolla on pienin jakojäännös kun se jaetaan jakajalla. /// Tyhjän taulukon tapauksessa palautetaan int.MinValue. /// Jos jakaja on nolla heitetään poikkeus DivideByZeroException. /// </summary>
ja testeihin lisää:
/// EtsiLukuJollaPieninJakojaannos(new int[0], 2) === int.MinValue; /// EtsiLukuJollaPieninJakojaannos(luvut2, 0) === -5; #THROWS DivideByZeroException
Tuo
#THROWS
-lause kertoo että funktion kuuluisi tässä tapauksessa heittää perässä mainittu poikkeus. Jos se EI heitä, niin testi ei mene läpi. Paluuarvolla ei ole tässä testissä merkitystä, koska poikkeuksen tapauksessa palautukseen asti ei koskaan päästä. Lue halutessasi tästä lisää.jos halutaan palauttaa 0:lla jaon tapauksessa sama kuin tyhjällä taulukolla, niin muutetaan sitten kommentit tämän mukaiseksi ja testissä
/// EtsiLukuJollaPieninJakojaannos(luvut2, 0) === int.MinValue;
ja sitten funktion alkuun:
if ( jakaja == 0 ) return int.MinValue;
vielä yksi vaihtoehto olisi tuoda funktiolle parametrina arvo, joka palautetaan virhetilanteissa, eli lisättäisiin parametrilistaan:
public static int EtsiLukuJollaPieninJakojaannos(int[] luvut, int jakaja, int virhe=int.MinValue)
testeihin lisää:
/// EtsiLukuJollaPieninJakojaannos(new int[0], 2, -99) === -99; /// EtsiLukuJollaPieninJakojaannos(luvut2, 0, -99) === -99;
ja toteutuksen alkuun:
if ( jakaja == 0 || luvut.Length == 0 ) return virhe;
Demo 7
7.1 Vektoreita taulukossa
Tässä esimerkissä on tehty Jypelin Vector
luokkaa vastaava minimaalinen versio, jossa ainoa mikä toimii on ToString()
.
HUOM! Vaikka tässä harjoituksessa tulostetaankin pisteitä, ei vastaavassa demotehtävässä ole missään tapauksessa tarkoitus tulostaa, vaan luoda palloja, joiden keskipiste on paikassa p
(tai pisteet[i]
jos ei käytetä apumuuttujaa).
Tässä Vector
-luokassa on metodi ToString
joka muuttaa lukuparin merkkijonoksi ja siten se voidaan tulostaa sen avulla. Eli voitaisiin kirjoittaa myös
Console.WriteLine(p.ToString());
mutta jos käytetään oliota sellaisessa paikassa jossa tarvitaan merkkijonoa, kutsutaan ToString
automaattisesti.
/// <summary>
/// Tulostetaan taulukossa olevat pisteet kukin omalle rivilleen
/// </summary>
/// <param name="pisteet">Tulostettavat pisteet</param>
public static void TulostaPisteet(Vector[] pisteet)
{
Vector p = pisteet[0]; Console.WriteLine(p);
}
Koska p
on jo kerran esitelty, ei sen esittelyä enää saa kopioida muille riveille. Tai kukin rivi pitäisi tehdä omaksi lohkokseen tyyliin:
{ Vector p = pisteet[0]; Console.WriteLine(p); }
Tehdään kuitenkin neljän pisteen tulostus aluksi ilman lohkoja:
public static void TulostaPisteet(Vector[] pisteet)
{
Vector p = pisteet[0]; Console.WriteLine(p);
p = pisteet[1]; Console.WriteLine(p);
p = pisteet[2]; Console.WriteLine(p);
p = pisteet[3]; Console.WriteLine(p);
}
Muutetaan juosevat numerot indekseiksi ja sitten silmukaksi:
/// <summary>
/// Tulostetaan taulukossa olevat pisteet kukin omalle rivilleen
/// </summary>
/// <param name="pisteet">Tulostettavat pisteet</param>
public static void TulostaPisteet(Vector[] pisteet)
{
for (int i = 0; i < pisteet.Length; i++)
{
Vector p = pisteet[i];
Console.WriteLine(p);
}
}
public static void TulostaPisteet(Vector[] pisteet)
{
foreach (Vector p in pisteet)
Console.WriteLine(p);
}
7.2 Arvo lukuja taulukkoon
public static double[] ArvoLukuja(int n, double x1, double x2)
{
double[] t = new double[n];
return t;
}
public static double[] ArvoLukuja(int n, double x1, double x2)
{
double[] t = new double[n];
for (int i = 0; i < t.Length; i++)
{
double x = RandomGen.NextDouble(x1, x2);
t[i] = x;
}
return t;
}
7.3 Etsi vektoritaulukosta
/// <summary>
/// Etsitään taulukosta ensimmäisen pisteen paikka, joka on etsittävästä korkeintaan etaisyyden paassa.
/// </summary>
/// <param name="pisteet">taulokko josta etsitään</param>
/// <param name="etsittava">piste jonka lähellä olevaa etsitään</param>
/// <param name="etaisyys">etäisyys jonka alle oleva piste hyväksytään</param>
/// <returns>lähellä oleva pisteen indkesi jos löytyy, muuten -1</returns>
/// <example>
/// <pre name="test">
/// Vector[] pisteet = { new Vector(3, 1), new Vector(9, 2), new Vector(2, 9), new Vector(12, 5) };
/// EtsiPisteenPaikka(pisteet, new Vector(3,1), 0.1) === 0;
/// EtsiPisteenPaikka(pisteet, new Vector(8,3), 2.0) === 1;
/// EtsiPisteenPaikka(pisteet, new Vector(3,8), 2.0) === 2;
/// EtsiPisteenPaikka(pisteet, new Vector(11,5), 1.5) === 3;
/// EtsiPisteenPaikka(new Vector[0], new Vector(8,3), 2.0) === -1;
/// </pre>
/// </example>
public static int EtsiPisteenPaikka(Vector[] pisteet, Vector etsittava, double etaisyys)
{
return -1;
}
public static int EtsiPisteenPaikka(Vector[] pisteet, Vector etsittava, double etaisyys)
{
for (int i = 0; i < pisteet.Length; i++)
{
Vector p = pisteet[i];
if (Vector.Distance(p, etsittava) <= etaisyys) return i;
}
return -1;
}
7.5 Kirjainten tutkiminen
Alla oleva tehtävä on oikestaan sama kuin aikaisempi Taulukot silmukalla-tehtävä. Kannattaa ensin katsoa sen vinkit ja yrittää tätä ilman vinkkejä.
/// <summary>
/// Lasketaan jonossa olevien isojen kirjainten lukumäärä
/// </summary>
/// <param name="jono">tutkittava merkkijono</param>
/// <returns>isojen kirjainten lukumäärä</returns>
/// <example>
/// <pre name="test">
/// LaskeIsot("Kissa") === 1;
/// LaskeIsot("kissa") === 0;
/// LaskeIsot("kiva Kissa Istuu Puussa") === 3;
/// LaskeIsot("iso LopussA") === 2;
/// </pre>
/// </example>
public static int LaskeIsot(string jono)
{
return 0;
}
Char
-luokan dokumentaatiosta löytyy metodi IsUpper
, jolla voidaan tarkistaa onko kyseessä iso kirjain. Käydään läpi kaikki jonon merkit ja katsotaan jokaisen kohdalla onko kyseessä iso kirjain. Jos on, listään laskuria.
public static int LaskeIsot(string jono)
{
int lkm = 0;
for (int i=0; i<jono.Length; i++)
{
char merkki = jono[i];
if ( Char.IsUpper(merkki) ) lkm++;
}
return lkm;
}
7.6 Taulukon vastinalkioiden summataulukko
public static double[] Summa(double[] a, double[] b)
{
return new double[0];
}
Vastaavasti kuin vaikkapa alkioden summan laskemisesa, tarvitaan tulosta varten apumuuttuja, tarvitaan tässäkin vastaava.
Koska tulos pitää saada uuteen taulukkoon, pitää aluksi luoda tulostaulukko. Ensin sille pitää saada sopiva koko. Eli selvitetään kumpiko taulukoista a
vai b
on lyhyempi. Tämä voitaisiin tehdä normaailla if
-lauseella, mutta tähän on olemassa valmiskin funktio:
int n = Math.Min(a.Length, b.Length);
Kun tulostaulukon koko on saatu selville, luodaan itse summaa varten tarvittava tulostaulukko:
double[] s = new double[n];
Sitten onkin jäljellä enää normaali kaikkien taulukon alkioiden läpikäynti. Jos toinen taulukko on pidempi, niin sen loput alkiot jätetään käsittelemättä.
for (int i = 0; i < n; i++)
{
// Mitä tehdään alkiolle
}
public static double[] Summa(double[] a, double[] b)
{
int n = Math.Min(a.Length, b.Length);
double[] s = new double[n];
for (int i = 0; i < n; i++)
s[i] = a[i] + b[i];
return s;
}
7.6 Matriisin palauttamien
public static double[,] Kerro(double[,] a, double kerroin)
{
return new double[0, 0];
}
Koska tuloksena pitää palauttaa uusi matriisi, pitää se ensin luoda:
double[,] m = new double[riveja, sarakkeita];
Tätä varten tarvitaan tietysti ensin normaalisti selvittää tarvittavat rivi- ja sarakemäärät:
int riveja = a.GetLength(0);
int sarakkeita = a.GetLength(1);
Seuraavaksi käydään normaalilla tavalla läpi kaikki alkiot:
for (int iy = 0; iy < riveja; iy++)
for (int ix = 0; ix < sarakkeita; ix++)
{
// Mitä tehdään alkioille???
}
public static double[,] Kerro(double[,] a, double kerroin)
{
int riveja = a.GetLength(0);
int sarakkeita = a.GetLength(1);
double[,] m = new double[riveja, sarakkeita];
for (int iy = 0; iy < riveja; iy++)
for (int ix = 0; ix < sarakkeita; ix++)
m[iy, ix] = kerroin * a[iy, ix];
return m;
}
Demo 8
8.1 Jaollisuus
Pelissä Saalista Silakka on pisteiden suhteen sääntö, että pelin voi voittaa vain jos:
- Pisteiden lukumäärä on 3,6,9,.. eli jaollinen 3:lla
- Pisteitä on enemmän kuin 0.
- Pisteitä ei ole 50, 100, 150,... eli lukumäärä ei ole jaollinen 50:llä
Vaikka funktio ei olekkaan maailman yleiskäyttöisin, voidaan se silti julistaa public
-näkyvyysmääreellä. Staattinen se on ilman muuta, koska laskemiseen ei muuta tarvita kuin pistemäärä. Jo nimi VoikoVoittaa
viittaa sihen, että tulos pitäisi olla totuusarvoinen. Parametrina on pistemäärä. Kommentteihin kerrotaan mitä funktio tekee. Sitten testeihin määritelmän mukaisia testejä niin, että vähintään kerran testataan kutakin tapausta. Tässä vaiheessa ei vielä toteutuksesta kannata murehtia. Pistemärän pitäisi olla kokonaisluku (int
) mikä riittääkin tälle kurssille, mutta jos ollaan tarkkoja, niin kannattaa estää negatiiviset luvut laittamalla tyypiksi uint
(unsigned int, eli etumerkitön kokonaisluku).
/// <summary>
/// Tutkitaan voiko pelissä voittaa.
/// Voiton ehtona on että pistemäärä on kolmella jaollinen, mutta
/// ei 50 jaollinen. Myöskään 0 ei kelpaa.
/// </summary>
/// <param name="pisteet">pelissä saadut pisteet</param>
/// <returns>true jos pelissä voi voittaa</returns>
/// <example>
/// <pre name="test">
/// VoikoVoittaa(0) === false;
/// VoikoVoittaa(1) === false;
/// VoikoVoittaa(3) === true;
/// VoikoVoittaa(5) === false;
/// VoikoVoittaa(12) === true;
/// VoikoVoittaa(50) === false;
/// VoikoVoittaa(100) === false;
/// VoikoVoittaa(300) === false;
/// </pre>
/// </example>
public static bool VoikoVoittaa(uint pisteet)
{
return false;
}
Jos kokonaisluku jaetaan kolmella, se voidaan esittä kokonaisosana ja jakojäännöksena. Esimeriksi
\[ 11/3 = 3 + 2/3 \]
Useissa kielissä jakolasku (/
) antaa tuon kokonaisosan ja modulo (%
) jakojäännöksen. (Prosenttimerkki lienee valittu koska se muistuttaa jakomerkkiä kun unohdetaan pylpyrät?). Luku on jaollinen kolmella jos sen jakojäännös kolmella jaettuna on nolla. Eli jos lauseke
luku % 3 == 0
on tosi. Vastaavasti tietysti 50:llä jaollisuus. Tehtävässä varma "häviö" tulee, jos pistemäärä on jaollinen 50:llä, eli se kannattaa testata ensin ja palauttaa siinä tapauksessa että voiton mahdollisuus on false
. Toisaalta pitäisi myös katsoa tuo että onko pistemäärä eri kuin 0. Mutta koska 0 on jaollinen 50:llä (jakojäännös on nolla), tulee se oikeastaan testattua jo tuon 50:n yhteydessä. Tämän jälkeen varma tapaus voittomahdollisuuteen on kolmella jaollisuus, eli kokeillaan se ja palautetaan siinä tapauksessa true
. Jos tämä ei onnistu, palautetaan false
.
public static bool VoikoVoittaa(int pisteet)
{
if ( pisteet % 50 == 0 ) return false;
if ( pisteet % 3 == 0 ) return true;
return false;
}
Koska tarkoituksena on palauttaa tosi tai epätosi, voidaan muotoilla totuusarvoinen lauseke, jonka arvo palautetaan sellaisenaan:
public static bool VoikoVoittaa(uint pisteet)
{
return ( pisteet % 50 != 0 && pisteet % 3 == 0 );
}
8.2 Luvun sanallinen vastike
Pelissä Nappaa naakka on keräyttyjen naakkojen lukumäärän mukaan seuraavat arvostelut:
- 0-3: umpisurkeaa
- 4: aloittelija
- 5: kohtuullinen nappaaja
- 6-7: mestari naakan nappaaja
- 8: naakan nappaajien kuningas
- 9-: huijari, näin montaa naakkaa ei ole pelissä
Muuten kuten aina, paluuarvo on tyyppiä string
:
/// <summary>
/// Palautetaan sanallinen arvostelu pelaajn suoriutumisesta
/// </summary>
/// <param name="pisteet">pelaajan pisteet</param>
/// <returns>arovstelu pelaajan suoriutumisesta</returns>
/// <example>
/// <pre name="test">
/// NaakkaArvostelu(0) === "umpisurkeaa"
/// NaakkaArvostelu(3) === "umpisurkeaa"
/// NaakkaArvostelu(4) === "aloittelija"
/// NaakkaArvostelu(5) === "kohtuullinen nappaaja"
/// NaakkaArvostelu(6) === "mestari naakan nappaaja"
/// NaakkaArvostelu(7) === "mestari naakan nappaaja"
/// NaakkaArvostelu(8) === "naakan nappaajien kuningas"
/// NaakkaArvostelu(9) === "huijari, näin montaa naakkaa ei ole pelissä"
/// NaakkaArvostelu(200) === "huijari, näin montaa naakkaa ei ole pelissä"
/// </pre>
/// </example>
public static string NaakkaArvostelu(uint pisteet)
{
return "umpisurkeaa";
}
public static string NaakkaArvostelu(uint pisteet)
{
if ( pisteet < 4 ) return "umpisurkeaa";
if ( pisteet == 4 ) return "aloittelija";
if ( pisteet == 5 ) return "kohtuullinen nappaaja";
if ( 6 <= pisteet && pisteet <= 7 ) return "mestari naakan nappaaja";
if ( pisteet == 8 ) return "naakan nappaajien kuningas";
return "huijari, näin montaa naakkaa ei ole pelissä";
}
public static string NaakkaArvostelu(uint pisteet)
{
switch (pisteet)
{
case 0:
case 1:
case 2:
case 3: return "umpisurkeaa";
case 4: return "aloittelija";
case 5: return "kohtuullinen nappaaja";
case 6:
case 7: return "mestari naakan nappaaja";
case 8: return "naakan nappaajien kuningas";
}
return "huijari, näin montaa naakkaa ei ole pelissä";
}
public static string NaakkaArvostelu(uint pisteet)
{
string[] arvio = {"aloittelija", "kohtuullinen nappaaja",
"mestari naakan nappaaja", "mestari naakan nappaaja",
"naakan nappaajien kuningas"};
if ( pisteet < 4) return "umpisurkeaa";
if ( pisteet > 8 ) return "huijari, näin montaa naakkaa ei ole pelissä";
return arvio[pisteet-4];
}
Demo 9
9.1 Montako samojen joukkoa
Kirjoitetaan kommentit ja normaali tynkä opituilla tavoilla. Testeihin kannattaa laittaa tyhjä joukko, sellainen missä kaikki luvut ovat samoja ja ainakin yksi yhden alkion joukko. Kannattaa myös testata tilanteet, joissa alkaa ja loppuu yhden alkion joukolla.
/// <summary>
/// Lasketaan joukossa olevien samoista luvuista muodostuvien
/// osajoukkojen määrä. Yhden luvun joukkoja ei lasketa.
/// </summary>
/// <param name="luvut">tutkittavien lukujen joukko</param>
/// <returns>osajoukkojen lukumäärä</returns>
/// <example>
/// <pre name="test">
/// int[] luvut = {3,3, 4,4,4 ,1, 2,2, 9,9, 1, 9,9};
/// SamojenJoukkojenMaara(luvut) === 5;
/// SamojenJoukkojenMaara(new int[]{1}) === 0;
/// SamojenJoukkojenMaara(new int[]{1,1}) === 1;
/// SamojenJoukkojenMaara(new int[]{1,2}) === 0;
/// SamojenJoukkojenMaara(new int[]{1,1,1,9}) === 1;
/// SamojenJoukkojenMaara(new int[]{1,1,9,9}) === 2;
/// SamojenJoukkojenMaara(new int[]{1,9,9,9}) === 1;
/// SamojenJoukkojenMaara(new int[]{}) === 0;
/// </pre>
/// </example>
public static int SamojenJoukkojenMaara(int[]luvut)
{
return 0;
}
Tarvitaan yksi laskuri, jolla lasketaan löytyneiden osajoukkojen määrä, olkoon se vaikka lkm
. Lähtötilanteessa ei ole yhtään löytynyttä osajoukkoa. Toinen laskuri tarvitaan siihen, millä lasketaan peräkkäisten samojen alkioiden määrää. Jos joukossa on yksikin alkio, on peräkkäisten määrä lähtötilanteessa 1
. Eli tehdään apumuuttujat:
int lkm = 0;
int samoja = 1;
Periaattessa jatkossa verrataan aina lukuja
luvut[0] == luvut[1]
luvut[1] == luvut[2]
luvut[2] == luvut[3]
...
Eli tyyliin:
luvut[i-1] == luvut[i]
Kun itse silmukkaa aloitetaan paikasta 1, voidaan verrata nykyistä alkiota edelliseen koska edellinen on olemassa kierroksella i=1
. Silmukassa katsotaan onko nykyinen alkio sama kuin edellinen ja jos on, niin samojen määrä kasvaa:
for (int i=1; i<luvut.Length; i++)
{
if ( luvut[i-1] == luvut[i] ) samoja++;
Vastaavasti jos peräkkäiset eivät ola samoja, alkaa laskeminen alusta. Mutta sitä ennen kannattaa katso löytyikö tätä ennen samoja peräkkäisiä jolloin lukumäärä kasvaa;
else
{
if ( samoja > 1 ) lkm++;
samoja = 1;
}
Periaattessa homma olisi valmis ja silmukan jälkene lukumäärä olisi selvillä ja se voitaisiin palauttaa:
return lkm;
Paitsi jos kävisi niin "huonosti", että samat luvut ovat joukon lopussa, niin viimeisellä (tai ainoalla) joukolla ei käytäisi lainkaan else
-osassa ja lukumäärä jäisi kasvattamatta. Siksi silmukan jälkeen on vielä lisättävä tarkistus siitä, että onko menossa keskeneräinen samojen joukko:
if ( samoja > 1 ) lkm++;
public static int SamojenJoukkojenMaara(int[]luvut)
{
int lkm = 0;
int samoja = 1;
for (int i=1; i<luvut.Length; i++)
{
if ( luvut[i-1] == luvut[i] ) samoja++;
else
{
if ( samoja > 1 ) lkm++;
samoja = 1;
}
}
if ( samoja > 1 ) lkm++;
return lkm;
}
Tässä tehtävässä onnistuttiin aika hyvin aloittamaan niin, että indeksi alkoi 1:stä ja siten oli mahdollista katsoa yhtä alkiota taaksepäin. Joskus ongelmaa voidaan myös lähestyä niin, että pidetään kirjaa edellisestä arvosta ja silloin taulukosta riittää katsoa vain nykyistä arvoa ja voidaan aloittaa 0:sta. Ensimmäistä kierrosta varten tosin pitää edellinen alustaa niin, ettei se ole sama muiden lukujen kanssa. Valinta tosin syö yhden mahdollisen luvun pois joukosta:
public static int SamojenJoukkojenMaara(int[]luvut)
{
int lkm = 0;
int samoja = 0;
int edellinen = int.MinValue;
for (int i=0; i<luvut.Length; i++)
{
if ( edellinen == luvut[i] ) samoja++;
else
{
if ( samoja > 1 ) lkm++;
samoja = 1;
edellinen = luvut[i];
}
}
if ( samoja > 1 ) lkm++;
return lkm;
}
Kun päästiin aloittamaan nollasta, käydään itse asiassa kaikki luvut läpi ja koska indeksi sinällään ei tarvita mihinkään, voidaan käyttää myös foreach
-silmukkaa:
public static int SamojenJoukkojenMaara(int[]luvut)
{
int lkm = 0;
int samoja = 0;
int edellinen = int.MinValue;
foreach (int luku in luvut)
{
if ( edellinen == luku ) samoja++;
else
{
if ( samoja > 1 ) lkm++;
samoja = 1;
edellinen = luku;
}
}
if ( samoja > 1 ) lkm++;
return lkm;
}
Toisaalta tehtävää voidaan ajatella niin, että silloin kun samojen määrä on kasvamassa, niin ensimmäisellä kasvatuksellahan alkaa uusi joukko. Jos lukumäärää muutetaan tällöin, niin ei tarvitse silmukan jälkeen huolehtia asioista. Koska vain silloin kun samoja
kasvaa, on mahdollista että on uuden joukon alku. Tämä ratkaisu voisi olla myös jonkin indeksiin perustuvan mallinkin yhteydessä, eli tämä ei sinällään liitty foreach
-silmukkaan.
public static int SamojenJoukkojenMaara(int[]luvut)
{
int lkm = 0;
int samoja = 0;
int edellinen = int.MinValue;
foreach (int luku in luvut)
{
if ( edellinen == luku )
{
if ( samoja == 1 ) lkm++;
samoja++;
}
else
{
samoja = 1;
edellinen = luku;
}
}
return lkm;
}
Jos samoja++
pistettäisiin ennen if-lausetta, mitä muuta pitäisi muuttaa? Nörtti voisi jopa jättää erillisen kasvatusrivin pois ja kirjoittaa:
if ( samoja++ == 1 ) lkm++;
Miksi tämäkin toimisi? Tai
if ( ++samoja == 2 ) lkm++;
9.2 Vektorit ja viivat
Piirrä seuraavan kuvan mukaiset portaat viivoilla. Eli tee aliohjelma PiirraViiva
niin, että se aloittaa viivan parametrina viedystä pisteestä ja piirtää se parametrina vietyyn suuntaan. Lopuksi palautetaan piirretyn viivan loppupiste. Ideana on että, kun viivan piirtävä aliohjelma aina palauttaa tiedon siitä mihin lopetti, niin seuraava viiva voidaan suoraan aloittaa tästä.
Pääohjelman kutsusta nähdään että aliohjelman (sanotaan sitä aliohjelmaksi vaikka se palauttaakin arvon, koska sillä on merkittävä sivuvaikutus: se lisää peliin viivan) pitää palauttaa Vector
-tyyppinen arvo. Muuten parametrit, niiden tyypit ja nimet kuten ennenkin. Tyngäksi kelpaa aluksi ihan pisteen palauttaminen.
public static Vector PiirraViiva(PhysicsGame peli, Vector piste, Vector suunta)
{
return piste;
}
Tämä koodi kääntyy nyt, mutta ei luonnollisesti piirrä mitään.
Aloitetaan sillä, että yritetään laskea tarvittavalla viivalle tiedot niin, että se voidaan piirtää suorakaiteena. Jos suunta on x-akselin suunta (vektori 1,0), niin pitäisi saada
w = VIIVANPITUUS
h = VIIVANLEVEYS
ja vastaavasti jos viiva pitää piirtää y-akselin suuntaan (vektori 0,1), niin vastaavasti
w = VIIVANLEVEYS
h = VIIVANPITUUS
Tämä voidaan toki tehdä if-lauseilla, mutta koska toinen koordinaatti suunnasta on 0 ja toinen yksi, niin
w = suunta.X * VIIVANPITUUS
h = suunta.Y * VIIVANPITUUS
tuottaisi jompaan kumpaan VIIVANPITUUS
arvon ja toiseen 0. Se johon tulee 0, pitäisi saada arvoon VIIVALEVEYS
. Toki tämä voitaisiin tehdä if-lauseella tyyliin:
if ( w == 0 ) w = VIIVANLEVEYS;
if ( h == 0 ) h = VIIVANLEVEYS;
Toisaalta näkyvältä if-lauseelta ja useammalta riviltä säästyään jos otetaan suurempi luivuista suunta.X * VIIVANPITUUS
ja VIIVANLEVEYS
, eli:
double w = Math.Max(suunta.X * VIIVANPITUUS, VIIVANLEVEYS);
double h = Math.Max(suunta.Y * VIIVANPITUUS, VIIVANLEVEYS);
Nyt meillä on tiedossa tarvittavan viivaa esittävän suorakaiteen leveys ja korkeus. Luodaan suorakaide normaalisti ja lisätään se peliin:
GameObject viiva = new GameObject(w, h);
peli.Add(viiva);
Näillä lisäyksillä ohjelma voidaan ajaa, mutta se piirtää ristin, koska kaikkien viivojen keskipiste on samassa kohdassa origossa.
Edellä on käyetty GameObject
koska uusimman Jypelin fysiikkamoottorissa toisiinsa osuvia fysiikkaolioita siirretään hieman.
Seuraavaksi pitää miettiä miten viivoja siirretään niin, että viiva alkaa alkupisteestä (ensimmäisen viivan tapauksessa punaisesta pisteestä, eli origosta).
Jos viivalle ei tehdä mitään, sen keskipiste on origossa. Jos tehdään (kokeile):
viiva.Position = piste;
sen keskipiste paikassa piste. Ajatellaanpa pystysuoraa viivaa. Jotta sen päätypiste olisi paikassa piste, pitäisi viivan keskipiste siirtää puolet viivan pituudesta ylöspäin. Eli tehdään tätä varten siirtovektori
Vector siirto = new Vector(0, h);
ja siirretään viivan keskipiste tämän avulla:
viiva.Position = piste + 0.5 * siirto;
Toisaalta jos kyseessä on vaakaviiva, pitäisi keskipistettä siirtää oikealle, eli siirtovektorin pitäisi silloin olla:
Vector siirto = new Vector(w, 0);
Tämä voitaisiin taas tehdä ehtolauseella, mutta voimme käyttää hyväksi sitä, että suunnan toinen komponentti on aina 1 ja toinen 0. Eli:
Vector siirto = new Vector(suunta.X * w, suunta.Y * h);
ja itse asiassa tämän jälkeen tuleva rivi:
viiva.Position = piste + 0.5 * siirto;
kelpaakin nyt kummassakin tapauksessa, koska vektorin kertominen luvulla kertoo kunkin komponentin kertoimella. Eli tästä tulee nyt lisättäväksi vektoriksi joko
(0, 0.5*h) jos suunta on y-akselin suuntaan
tai
(0.5*w, 0) jos suunta on x-akselin suuntaan
eli juuri mitä tarvitaan. Vielä pitää saada palautettua lisätyn viivan päätepisteen koordinaatti ja se on helposti laskettavissa siirron avulla:
return piste + siirto;
public static Vector PiirraViiva(PhysicsGame peli, Vector piste, Vector suunta)
{
double w = Math.Max(suunta.X * VIIVANPITUUS, VIIVANLEVEYS);
double h = Math.Max(suunta.Y * VIIVANPITUUS, VIIVANLEVEYS);
GameObject viiva = new GameObject(w, h);
Vector siirto = new Vector(suunta.X * w, suunta.Y * h);
viiva.Position = piste + 0.5 * siirto;
peli.Add(viiva);
return piste + siirto;
}
Mitä pitäisi vielä korjata jotta pääohjelmaa voitaisiin jatkaa riveillä:
piste = PiirraViiva(this, piste, -pysty);
piste = PiirraViiva(this, piste, -pysty);
piste = PiirraViiva(this, piste, -vaaka);
Pitäisi estää se, ettei w
ja h
pääse negatiivisiksi. Tämä voitaisiin toki tehdä if-lauseella muuttujien arvojen laskemisen jälkeen:
double w = suunta.X * VIIVANPITUUS
double h = suunta.Y * VIIVANPITUUS
if ( w < 0 ) w = -w;
if ( h < 0 ) h = -h;
if ( w == 0 ) w = VIIVANLEVEYS;
if ( h == 0 ) h = VIIVANLEVEYS;
Toisaalta käyttämällä itseisarvoa (muistele vanha demotehtävä) voidaan etumerkki pitää aina positiivisena:
double w = Math.Abs(suunta.X * VIIVANPITUUS)
double h = Math.Abs(suunta.Y * VIIVANPITUUS)
if ( w == 0 ) w = VIIVANLEVEYS;
if ( h == 0 ) h = VIIVANLEVEYS;
Kuten edellä 0-tapauksessa if-lauseista päästään eroon maksimia käyttäen:
double w = Math.Max(Math.Abs(suunta.X * VIIVANPITUUS), VIIVANLEVEYS);
double h = Math.Max(Math.Abs(suunta.Y * VIIVANPITUUS), VIIVANLEVEYS);
Itseisarvo voitaisiin ottaa myös ennenkin kertolaskua, koska VIIVANPITUUS
oletetaan aina positiiviseksi:
double w = Math.Max(Math.Abs(suunta.X) * VIIVANPITUUS, VIIVANLEVEYS);
double h = Math.Max(Math.Abs(suunta.Y) * VIIVANPITUUS, VIIVANLEVEYS);
9.3 Portaat ylös ja alas
Muutetaan seuraavaksi pääohjelmaa niin, että sillä voidaan piirtää mielivaltainen määrä portaita.
Ylöspäin menevä portaiden osa saadaan piirrettyä kun entinen pääohjelman koodi siirretään aliohjelmaan ja tehdään paria
p = PiirraViiva(this, p, pysty);
p = PiirraViiva(this, p, vaaka);
tarvittava määrä. Alaspäin menevä osa on hieman kimurantimpi, koska jos ei haluta aloittaa ylimääräisellä tasanteella, niin yksi ylös, yksi alas pitäisi näyttää seuraavalta:
__
| |
Eli ensimmäinen alaspäin menevä porras olisi pelkkä pystyosa.
p = PiirraViiva(peli, p, -pysty);
Seuraavat muodostuvat sitten parista:
p = PiirraViiva(peli, p, vaaka);
p = PiirraViiva(peli, p, -pysty);
Nyt siis ensimmäinen alaspäin menevä pitää käsitellä erikoistapauksena, ja se johtaa myös siihen, että nolla alas pitää käsitellä erikositapauksena. Toki silmukkaan voitaisiin laittaa ehto, että jos ollaan ensimmäisessä, niin vaakaosa jätetään piirtämättä. Mutta tämä rasittaisi jokaista muuta silmukan kierrosta turhalla ehdolla.
Koska ensimmäinen alas menevä porras on jo piirretty, aloitetaan silmukka yhtä pidemmältä.
public static Vector PiirraPortaat(PhysicsGame peli, Vector piste, int ylos, int alas)
{
Vector vaaka = new Vector(1, 0);
Vector pysty = new Vector(0, 1);
Vector p = piste;
for (int i=0; i<ylos; i++)
{
p = PiirraViiva(peli, p, pysty);
p = PiirraViiva(peli, p, vaaka);
}
if ( alas <= 0 ) return p;
p = PiirraViiva(peli, p, -pysty);
for (int i=1; i<alas; i++)
{
p = PiirraViiva(peli, p, vaaka);
p = PiirraViiva(peli, p, -pysty);
}
return p;
}
Silmukat voisivat olla myös muodossa:
for (int i=1; i<=ylos; i++)
...
for (int i=2; i<=alas; i++)
jos ajatellaan että i numeroi monettako porrasta piirretään "kansankielellä", eli ekan portaan numero on 1.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.