PR2ET5: Qsort ja tükeldus

Praktikumi materjal

Praktikumi ülesanded

Selles praktikumis on kaks ülesannet.

  • Esimeses ülesandes õpid sorteerima arvumassiive ning struktuuridest koosnevaid massiive ja koodi tükeldama
  • Teises ülesandes õpid andmeid internetist alla laadima

Mõlemat ülesannet laiendab lisaülesanne

Ülesanne 1 [W05-1]: Quicksort

Selle ülesande eesmärk on harjutada qsort()  funktsiooni kasutamist ning koodi tükeldamist erinevate koodifailide vahel, sealjuures alustad oma esimese kergesti lisatava teegifaili loomist. Aluskoodis on koodi tükeldus failidesse sinu eest juba tehtud.

Lae alla aluskood: https://blue.pri.ee/ttu/files/iax0584/aluskoodid/5_1_qsort_task_basecode.zip

NB! Aluskood sisaldab nii baasülesannet kui ka lisaülesannet!

Nõuded
  • Realiseeri vajalikud funktsioonid, mis on ette antud sulle erinevates päisefailides prototüüpidena. Funktsiooni realisatsioon peab olema päisefailiga samanimelises .c koodifailis
  • Kutsu välja vajalikud funktsioonid andmete sorteerimiseks ja ekraanile kuvamiseks main funktsiooni switch lausest.
  • Kompileeri programm kokku mitmest koodifailist – vastavalt struktuurile mis oli antud aluskoodis. Kasuta kompileerimiseks käsurida (võid kasutada ka Makefile’i kui oskad).
Soovitusi lahendamiseks
  1. Tutvu arhiivis oleva koodi struktuuriga
  2. Realiseeri alustuseks qsort võrdlusfunktsioon täisarvude võrdlemiseks.
    Vihje: Testimaks ühe faili põhiselt (nt array_helpers.c) kood sai korralikult kirja, võid vajutada “compile” nuppu – selle põhjal tehakse objektfail ainult käesolevast failist, ignoreerides ülejäänud programmi. Sedasi saad kiirelt vigu lokaliseerida ühe faili sees.
  3. Lisa qsort väljakutse menüüsse, ehita kogu projekt ning proovi kas töötab
  4. Korda sama murdarvuliste massiivi sorteerimiseks (menüü valik 2). Arvesta, et pead leidma ka murdarvudest koosneva massiivi pikkuse ning lisama väljastamise väljakutse
  5. Nüüd liigu struktuuride juurde. Alusta struktuuri väljastamise funktsioonist. Kirjuta see valmis ning lisa väljakutse menüüsse, testi
  6. Struktuuride võrdlusfunktsioone kirjutades arvesta järgmist
    • Võrdlusfunktsiooni edastatakse void  tüüpi viidad on struktuurile endale (st struktuuri esimese baidi mäluaadress), mitte väljale, mille alusel sina soovid sorteerida
      TÜÜPVIGA: Teisendus peab toimuma struktuuri tüüpi viidaks, mitte int või float vms tüüpi viidaks
      Miks? sorteerimisel sorteeritakse terveid struktuure korraga. qsort ise ei tea mis välja alusel sorteerimist tuleks teha ega oska seda välja valida sinu eest. Samuti järjestamise vältel tuleb ümber paigutada kõigi struktuuri liikmete andmed, mitte ainult üks alamväli.
    • Ole ettevaatlik sulgude paigutusega – tehete järjekord on ülioluline! Esmalt tuleb võrdlusfunktsiooni parameeter teisendada sobilikuks struktuuri tüüpi viidaks. Alles seejärel võib struktuuri sees oleva välja poole pöörduda.
    • Mall teisendamiseks: ((struct MyStruct *)x)->member
Testimine
Ava mind väljundi nägemiseks

Ülesanne 2 [W05-2]: Väliste teekide kasutamine (Tallinna busside väljumised)

Selles ülesande eesmärk on näidata, et programmid saavad suhelda ka internetiga. Selleks tuleb kõigepealt programm liidestada välise teegiga libcurl, mis on laialdaselt kasutusel võimaldades suhelda APIdega, laadida faile alla jne. Ülesande lahendamiseks kasutame Tallinna Transpordiameti poolt pakutavaid avaandmeid. Lisaks peate looma ja kompileerima projekti, mis koosneb mitmest koodi- ja päisefailist.

Selle lihtsustamiseks on tehtud esialgne koodijaotus ning internetist failide allalaadimiseks ja kettale salvestamiseks vajalik kood on juba loodud. Suurem osa sellest on libcurl teegi arendajate avalik näidiskood. Kood on pakitud täiendava turvakihiga, et vältida avalike andmete kuritarvitamist, piirates seejuures värskenduste allalaadimise sagedust. Ära eemalda leech-kaitsekoodi. Andmete liiga sage küsimine toob tõenäoliselt kaasa teenusest blokeerimise.

Kirjutatud kood ja tehtud lihtsustused on loodud Linuxi jaoks. Kui soovid seda koodi Windowsis käivitada, tuleb faili käsitlemise ja tuvastamise osad koodis ümber kirjutada.

Lae alla aluskood: https://blue.pri.ee/ttu/files/iax0584/aluskoodid/5_2_timetable_starter.zip

Nõuded:
  • Kasuta etteantud aluskoodi ja järgi faili struktuuri
  • Lae alla Raja peatuse andmed
  • Väljasta praegune kellaaeg (kasuta peatuse andmetes olevat kellaaega)
  • Väljasta busside väljumisajad bussipeatusest
    • Minimaalselt tuleb kuvata liininumber, väljumise aeg, sihtkoht (lõpp-peatus) ja aeg bussi saabumiseni (GPS-andmete põhjal)
    • Lisa hilinemise hoiatus, kui buss hilineb üle ühe minuti
    • Ära näita busse, mis on juba väljunud
    • Väljumisaeg peab olema formaadis h:mm:ss
    • Uuenda järelejäänud aega korra sekundis
  • Minimaalselt peab kood olema jaotatud kolmeks koodi- ja päisefailiks
    • file_helper.c  ja file_helper.h sisaldavad andmete allalaadimise ja ettevalmistamise koodi (antud algkoodis olemas, muuta ei ole vaja)
    • data_processor.c  ja data_processor.h sisaldavad andmete lugemise ja töötlemise koodi.
    • main.c  ja main.h sisaldavad programmi töövoo juhtimist ja üldisi makrosid.
    • Soovi korral võid faili lugemise ja andmete töötlemise eraldada täiendavatesse failidesse.
  • Kompileeri kõik koodifailid kokku kas käsurealt või kasutades Makefile’i
Soovituslik töövoog

Samm 0: Ettevalmistus

See on vajalik ainult siis, kui kasutad enda arvutit! Kooliarvutit kasutades pole ettevalmistus vajalik!

Installi  libcurl  teek. Debianil põhinevates süsteemides, sealhulgas Ubuntu ja Linux Mint, tuleb käivitada järgmine käsk:

Teiste distributsioonide puhul, mis ei kasuta apt-i, kasuta oma distributsioonile vastavat paketihaldurit.

See paigaldab vajaliku teegi, millega C-programm suhtleb failide internetist allalaadimiseks.

Samm 1: Leia Raja peatuse ID

Peatuse ID on täisarv, mis jääb tavaliselt vahemikku 100 kuni 10 000. Enamik Tallinna peatuste ID-d jäävad vahemikku 100 kuni 2000.

Tähelepanu: Mitmel peatusel on sama nimi – nt Keemia peatusel on kaks erinevat peatuse ID-d, millest üks on suunaga kesklinna ja teine on suunaga bussiparki. Samamoodi on Raja peatusel üks peatus IT-maja ees ja teine IT-maja kõrval. Mõlemal on erinev ID väärtus.

Eeldatud on, et leiad õige peatuse ID käsitsi. Programmi abil stopid väärtuse leidmine jääb ülesande skoobist välja.

Vajaliku peatuse leidmiseks on kaks võimalust. Näidetes on toodud, kuidas leida Väike-Õismäe peatuse ID, milleks on 844.

  1. Kasuta peatuste andmeid avaandmete portaalist: https://andmed.eesti.ee/datasets/tallinna-uhistranspordi-peatused-ja-marsruudid
    Otselink faili, mis sisaldab nimesid ja ID-sid: https://transport.tallinn.ee/data/stops.xml
    Õige peatuse ID leidmiseks tuleks vaadata liininumbreid ja nende sõidusuundi.
  2. Mine transport.tallinn.ee lehele ja ava oma brauseris arendaja tööriistad (kiirklahv F12). Seejärel ava vahekaart Network.  Nüüd on sul kaks võimalust: kliki kaardil bussipeatusel ja vali väljumisaegade kuvamine; või vali sõiduplaanist buss ning klõpsa bussipeatuse nimele. Lõpuks otsi päringut nimega “siri-stop-departures” – see sisaldab parameetrit stopid=, kust leiad peatuse ID.

Samm 2: Andmefaili allalaadimine

Failide allalaadimise kood on juba valmis. Kasutame libcurl teegi dokumentatsioonist pärit näidiskoodi, millele on lisatud kaitsemehhanismid, et programm ei koormaks serverit üle, kui peaksid kogemata käivitama allalaadimisfunktsiooni liiga tihti.

Faili allalaadimiseks kutsu välja funktsioon DownloadFileToDisk() .  Funktsioon on kirjeldatud failis file_helper.h  ja implementeeritud failis file_helper.c . Kasutusjuhised leiad päisefailist. Esimese parameetrina tuleb sul koostada URL, mis sisaldab stopid  väärtust. URL peab olema defineeritud failis main.h .

Seejärel kompileeri programm koos kõigi kolme lähtefailiga ja käivita see. Kasuta lippu  -lcurl  et linkida libcurl teek (nt gcc -o transport_app file1.c file2.c file3.c -g -Wall -Wextra -Wconversion -lcurl ). Seejärel laetakse fail alla. Kontrolli käsitsi, et fail on kettale salvestatud (samas kataloogis, kus programm asub).

Samm 3: Andmete lugemine ja salvestamine

Failist saadud andmed tuleb lugeda struktuurimassiivi. Alusta sobiva struktuuri defineerimisest. Kõiki andmefailis olevaid välju ei ole vaja salvestada – osa neist on ülesande jaoks liiased.

Kirjuta struktuuride deklaratsioonid faili data_processor.h . Selles failis peavad olema ka kõikide failis data_processor.c realiseeritud funktsioonide prototüübid. Nende hulka kuulub ka andmefaili lugemise funktsioon. Vajadusel võid lisada abifunktsioone andmete eeltöötlemiseks.

Andmefaili lugemine on tavapärasest veidi keerulisem, sest fail sisaldab kahte päiserida, millele järgnevad sõidukite väljumised. Soovituslik on andmete lugemise ajal need ka ekraanile kuvada, et veenduda andmete lugemise korrektsuses.

Vihje 1: Faili kaks esimest rida sisaldavad päist, millega tuleb enne lugemistsükli algust tegeleda. Esimene rida sisaldab ajatemplit, mida saab kasutada leidmaks kui palju aega on väljumiseni jäänud. Kasuta seda arvutustes hetkeajana. See ei ole täiesti täpne, kuid piisavalt lähedane. Aja lugemiseks tühikutega eraldatud failist võid kasutada järgmist lugemislauset:

Kontrolli kindlasti tagastustatavat väärtust, et veenduda, kas andmed loeti korrektselt.

Vihje 2: Faili teine rida ei ole ülesande jaoks oluline (see kinnitab stopid väärtust). Võid selle vahele jätta, kasutades varasemalt õpitud võtteid, nt: fscanf(fp, "%*[^\n]\n");

Seejärel jätka lugemistsükliga, mis peaks olema sulle tuttav selle semestri varasematest praktikumiülesannetest. Soovitatav on mittevajalikud väljad lugemisel vahele jätta, kasutades formaati  %*s  (nagu näidatud currentTime lugemise näites).

Samm 4: Sõiduplaani väljastamine

Alustuseks väljasta sõiduplaan ühe korra. Peale kuvamist peaks programm tavapäraselt sulguma. Loo funktsioon, millele antakse parameetritena praegune aeg ja struktuur, mis sisaldab väljuvate sõidukite sõiduplaani andmeid. See peaks olema midagi sellist

Kuva kõik liininumbrid, sihtkohad, eeldatavad väljumisajad ja ajad eeldatava väljumiseni. Veendu, et väljastus oleks korrektselt vormistatud ja loetav.

Vihje 1: Failis on kaks erinevat aega – ExpectedTimeInSeconds ja ScheduleTimeInSeconds. Kasuta väljumisaja ja väljumiseni jäänud aja kuvamiseks sõiduki GPS-positsiooni põhjal antud hinnangulist aega.

Vihje 2: Kuna aega on vaja kuvatada tundides, minutites ja sekundites, kuid see on antud sekundites alates südaööst, võid oma koodi lihtsustamiseks kasutada järgmist abifunktsiooni. Võid seda kasutada ka andmete eeltöötlemisel faili lugemise ajal (kuid veendu, et jätad aja sekundite kujul alles).

Selle kasutamiseks on vaja lisada järgnev koodilõik päisefaili

Tähelepanu: UTF-8 sihtkohanimed

Ilmselt märkad, et mõned sihtkohanimed sisaldavad märke, mida ASCII-tabelis ei ole – nt Väike-Õismäe, Mustamäe, Männiku. Kui soovid vormindada väljundit vormingumäärajatega nagu  %20s , siis arvesta, et number 20 tähistab baitide arvu, mitte kuvatavate tähemärkide arvu. Seetõttu ei pruugi väljund joonduda korrektselt.

NB! Selle probleemi lahendamine ei kuulu selle ülesande skoobi hulka!

Aga kui oled huvitatud...

C23 sisaldab uusi võimalusi UTF-8 vormingus teksti töötamiseks. Ka vanemad C-keele versioonid toetavad UTF-8 kodeeringut, kuid nende tugi on mõnevõrra kohmakam. Tavaliselt käsitletakse seda mitmebaidiste märkide (multibyte characters) abil. See on korrektne ja robustne lahendus ning tõenäoliselt esimene vastus, mille keelemudelid lahendust küsides pakuvad. Kuid meie eesmärgi jaoks oleks see tarbetult keeruline.

On olemas ka lihtsam / häkk-lahendus ilma mitmebaidiste märkide keerukuseta. Me teame, et andmetes ei esine kolme ega nelja-baidiseid märke. Vaadates UTF-8 standardit näeme, et 2-baidise tähemärgi esimese baidi kolm esimest bitti on fikseeritud (vt 2-baidise märgi esimene bait: https://en.wikipedia.org/wiki/UTF-8#Description ). Kasutades bitimaske saame kontrollida kolme esimese biti sisu.  Järgnevat koodilõiku saab professionaalsemalt kirjutada kasutades viitasid tähemärgile ja 16-süsteemis kodeeritud konstanti, kuid jätan järgneva lahenduse lihtsamini (otsemini) loetavaks.

Nüüd võime printf("%20s", ...)  asemel kasutada printf("%*s", 20 + wideChars, ...) . See arvestab lisabaitidega, mida on vaja märkide korrektseks kuvamiseks.

Samm 5: Sõiduplaani uuendamine

Sõiduplaani tuleb värskendada ühe korra sekundis. Viivituse tekitamiseks kasuta funktsiooni sleep() teegist unistd.h .

Teiseks tuleb enne iga uut väljatrükki ekraan puhastada. Selleks kasutatakse ANSI escape jada (escape sequence). See on eelistatum lahendus võrreldes system()  funktsiooni väljakutsetega, mida internetist sageli soovitatakse. Jada sisaldab käske ekraani puhastamiseks ( [2J ) ja kursori liigutamiseks vasakusse ülanurka ( [1;1H ).

Vihje: Allalaetud failist saadud hetkeaeg on antud sekundites alates keskööst. Kui suurendad seda muutujat iga tsükli läbimisega ühe võrra, saad selle anda funktsioonile PrintTimeTable() kaasa. Sel juhul piisab järelejäänud aja kuvamiseks sellest, kui lahutad eeldatavast väljumisajast praeguse aja.

NB! Tegemist on lõpmatu tsükliga, millel puudub lõputingimus! Korrektne programmi lõpetamine ei kuulu selle ülesande skoopi. Programmi peatamiseks kasuta Ctrl + C.

Aga kui oled huvitatud...

Selle käsitlemiseks on kaks (kolm) levinud viisi

  • Kasuta lõimesid (thread), mis kuulab klaviatuurisisendeid ja võimaldab sellel lõimel seada muutuja, mis tagab tsüklist väljumise. See on mõnevõrra keeruline lahendus.
  • Kasuta katkestussignaalide käsitlejaid (signal handlers, intterupt handler). Selleks on vaja luua globaalne volatile tüüpi muutuja, mida juhib sinu loodud signaalikäitleja (funktsioon). Muutuja kontrollimisel tsükli vältel saad tsükli katkestada. Signaalikäitleja tuleb rakenduses registreerida. Tegu on lihtsaima võimaliku loogikaga. Programmi sulgemine toimuks endiselt klahvikombinatsiooniga ctrl + c, kuid väljumine oleks korrektne. See on soovitatav viis, kui tahad proovida.
  • Viimaks on võimalik ka terminali töörežiimi ümber seadistada interaktiivseks kasutuseks, kuid see on märksa keerulisem ja valesti käsitlemisel veelgi veaohtlikum.
Interaktiivsed programmid kasutavad sageli teeke, mis toetuvad nendele funktsioonidele, näiteks ncurses.
Testimine

Testimiseks kasuta esmalt Raja peatust. Seda on ka lihtne kontrollida, sest saad aknast välja vaadata ja veenduda, kas buss saabus siis, kui sinu programm seda väidab.

Piirjuhtude testimine on siin keeruline, sest kasutad reaalaja liiklusandmeid ning kõiki olukordi ei pruugi alati ette tulla.

Kasuta populaarsemat bussipeatust, et testida erijuhte. Näiteks kasutasin peatust Taksopark (ID 1135). Seal on võimalik näha

  1. Hilinemisi
  2. Olukordi, kus järelejäänud aeg on ainult sekundites
  3. Väljunud bussi eemaldamist nimekirjast
  4. Sihtkohti, mis sisaldavad mitte-ASCII märke – nt Männiku, Tallinn-Väike

Pane tähele, et kõike ei ole testitud

  1. Peatused, kus on ka trammi- või trolliliinid (trollid on saadaval loomulikult alles 2027 versioonis)
  2. Kuidas käsitletakse järelejäänud aega busside puhul, mis saabuvad pärast südaööd, kui praegune aeg on veel enne südaööd.
  3. Kui ühtegi bussi ei ole tulemas
MS Windows

Antud ülesannet on võimalik lahendada ka Windowsil, kuid selleks tuleb teha mõned täiendavad liigutused. Sammud on testitud ja töötasid kevadel 2022 kasutades chocolatey kaudu installeeritud 64 bitist MinGW-d (see oli osa lihtsast tarkvara paigaldamise juhendist Windowsil kasutades käsurida). Väljavõte eelmise aasta vestlusest, kus sammud kirja said (inglise keeles)

1. Install libcurl on Windows. Uses chocolatey package manager

choco install curl

2. Make the library (dll file) available to the compiled program. Copy libcurl-x64.dll from libcurl installation directory to your program directory (assumes 64-bit compiler).

3. You need to download CA certificate so cURL can verify the certificate on the opendata website https://curl.se/ca/cacert.pem.

4. You need to specify the certificate authority file location to the code. Add this to the setup of the download in the file_helper.c, replacing the path\\to\\file with the path to the downloaded .pem file.

curl_easy_setopt(curl_handle, CURLOPT_CAINFO, "path\\to\\file");

5. You need to rewrite the check if a file is already downloaded because that uses POSIX library available on UNIX systems – unistd.h. You should rewrite it with windows solution of that. Haven’t tested, but you’ll likely find the function BOOL FileExists(LPCTSTR szPath) in the library io.h. You may need to add additional Windows-specific libraries.

6. You can only download as CSV with the code provided. To download space delimited, you need to rewrite the ReplaceChars function. Easy option would be to open up a second file pointer for writing to a different file and remove the fseek, Simplest would be to do getchar -> putchar with a check in between for commas to be replaced with spaces.

7. You need to add -I and -L flags to your compiler command line arguments. Mine were:

-I C:\ProgramData\chocolatey\lib\curl\tools\curl-7.81.0-win64-mingw\include\ -L C:\ProgramData\chocolatey\lib\curl\tools\curl-7.81.0-win64-mingw\lib\

Lisaülesanne 1 [W05-3]: Mitme struktuuri liikme põhjal sorteerimine

Lisaülesande lahendamiseks peab olema realiseeritud menüü valik 5 jaoks vajaminev funktsionaalsus. Selleks pead:

  • Realiseerima qsort võrdlusfunktsioonid struktuurimassiivi sorteerimiseks perenime järgi. Kui perenimed on samad, tuleb teha täiendav otsus sorteerimiseks eesnime alusel.
  • Kutsuma välja qsort funktsiooni struktuurimassiivi sorteerimiseks vastavalt menüü valikule ning seejärel massiivi väljatrüki funktsiooni
  • Kutsuma välja qsort funktsiooni struktuurimassiivi sorteerimiseks vastavalt menüü valikule ning seejärel massiivi väljatrüki funktsiooni
Testimine

Järgnev väljund sisaldab vaid lisaülesande tulemust

Lisaülesanne 2 [W05-4]: Sõidukite asukohad

Lisaülesande lahendamiseks pead alla laadima ka teise andmefaili, mis sisaldab kõigi sõidukite GPS asukohakordinaate.

  • Lae GPS asukohti alla perioodiliselt. Aadressi leiad samuti arendaja tööriistu kasutades või avaandmete portaalist.
    Soovitav andmete uuendamisintervall on 5 .. 10 sekundit
  • Leia ICT hoone asukohakordinaadid
  • Arvuta kõigi sõidukite kaugus ICT hoonest
  • Järjesta sõidukid kauguse järgi kasvavas järjekorras
  • Uuenda väljundit perioodiliselt.
  • Piira väljudnite arvu nii, et nii baas- kui lisaülesande väljundid mahuksid mõistlikult ekraanile

NB! Uuendamise intervalli miinimumaeg on 15 sekundit, mis on antud aluskoodiga kaasa. Muuda see lühemaks, et võimaldada asukohti tihedamalt alla laadida.

Testimine

Märka, et videos uuendatakse asukohakordinaate ning kaugust hoonest ligikaudu iga 5 sekundi tagant. Kõigi sõidukite asukohad ei muutu – näiteks osad võivad parasjagu paikneda lõppjaamas, oodates oma väljumisaega.

Vihjed
  • Kauguse leidmiseks kahe punkti vahel maakeral on mitmeid valemeid. Haversine on üks võimalikest – see pole küll kõige täpsem, kuid on võrdlemisi lihtne ja arvutuslikult kiire.
  • Soovitav on tehta loendur, mis “tiksub” nagu kell, et käivitada perioodiliselt andmefaili allalaadimine ja töötlemine
  • Kui soovid kauakestvat ja täpsemat programmi, saad uuendamist rakendada ka väljuvatele sõidukitele

Pärast seda tundi peaksid

  • Oskama tükeldada oma programmi mitme koodifaili vahel mitmest koodifailist koosnevat programmi kompileerida
  • Oskama kasutada qsort funktsiooni
  • Mõistma qsort funktsiooni võrdlusfunktsiooni nõudeid ning suutma kirjutada võrdlusfunktsiooni lihtsatele massiividele ning ka struktuurimassiividele
  • Teadma viitade eksisteerimisest funktsioonidele ning oskama funktsiooni edastada viita teisele funktsioonile
  • Teadma üldistatult samme, mis on vajalikud kolmandate osapoolte teekide kasutamiseks ning oskama neid oma programmi kompileerida

Täiendav materjal