Kursnotater for tema som er nye i denne laben:

Denne laben er litt annerledes enn tidligere laber siden noen av deloppgavene blir rettet manuelt. I tillegg er poengene fordelt slik at du får litt poeng for hver oppgave du gjennomfører. Det er maksimalt mulig å oppnå 25 poeng.

  • Oppvarmingsfeltet: 1 poeng for hver oppgave, til sammen 5 mulige poeng.
  • Hovedfeltet: 3 poeng for hver oppgave, til sammen 15 mulige poeng.
  • Ekstrafeltet: 1 poeng for hver oppgave, til sammen 5 mulige poeng.

I hovedfeltet i denne lab’en skal du generere dine egne QR-koder! Selv om stegene bygger på hverandre, kan de faktisk løses helt isolert fra hverandre, og de scores isolert fra hverandre på CodeGrade.

For oppgavene som blir rettet manuelt, vil du få poengscore og skriftlig tilbakemelding etter at vi er ferdige med å rette; men det er alltid mulig å oppsøke tilbakemelding fra gruppeleder underveis også.

  • Vi vil trekke poeng manuelt dersom vi oppdager at oppgaven åpenbart ikke er løst, eller man forsøker å trikse seg gjennom de automatiske testene.
  • Hver lab kan leveres så mange ganger du vil; det er siste innlevering som teller. Du kan umiddelbart se resultatene på de automatiske testene og hvor mange poeng du har fått etter hver innlevering.

Hver lab utgjør 2.5% av den endelige karakteren din i emnet. Du må få til sammen 100 poeng eller mer på labene for å kunne ta eksamen.

Det er veldig viktig at du siterer alle kilder og eventuelle samarbeid. Se mitt.uib for mer informasjon om vår policy på plagiat og samarbeid.

Det er lurt å levere inn mange ganger underveis. Du får umiddelbart en automatisk tilbakemelding på oppgavene, og du kan prøve igjen hvis noe ikke virker.

Innlevering av lab’er foregår via verktøyet Codegrade som du kommer til via mitt.uib.

  • Gå til «Oppgåver» i menyen til venstre
  • Klikk deg videre til CodeGrade ved å klikke på «Last lab i eit nytt vindauge» Klikk deg videre til CodeGrade
  • Last opp filene du har laget for lab’en og trykk på «Submit.»

Fremdeles problemer? Se

(merk at videoen er litt gammel, så det kan være noen små forskjeller i hvordan det ser ut nå.)

Ser du ikke knappen du skal trykke på i mitt.uib?

  • Dobbeltsjekk at du er innlogget på mitt.uib.
  • Prøv å laste siden i nettleseren på nytt.
  • Prøv en «force refresh» (F5 eller Ctrl+F5 eller Ctrl+Shift+F5 eller Ctrl+Shift+R eller Command+R eller Command+Shift+R avhengig av nettleser og operativsystem).
  • Prøv å tøm cache i nettleseren din (se refreshyourcache.com for hvordan du gjør dette) og prøv på nytt.
  • Prøv en annen nettleser.
Skotår

Tips: før du løyser denne oppgåva bør du lese kursnotata om moduler, særleg avsnittet om hovudfiler og modular.

I denne oppgåva skal du lage ein modul i fila leapyear.py. Den skal ha ein funksjon som fortel om eit år er eit skotår eller ikkje. Modulen du skriv skal både kunne køyrast som eit eige program der brukaren blir bede om å oppgi kva år hen vil vite meir om, i tillegg til at ein skal kunne importere funksjonen is_leap_year til bruk i andre modular.

Hugsar du oppgåva om skotår frå lab2? Her er ein repetisjon av reglane:

  • Vanlegvis er eit år som er deleleg med 4 eit skotår (til dømes 1996 var eit skotår);
  • bortsett frå år som også er delelege med 100 (til dømes 1900 er ikkje skotår);
  • men viss året som er delelege med 100 også er deleleg med 400, då er det eit skotår likevel (til dømes er 2000 eit skotår).

Del A

  • I fila leapyear.py, lag ein funksjon is_leap_year som tek inn eit årstal year og returnerar True viss året er eit skotår, og False elles. Ta utgangspunkt i følgjande kode du kan kopiere inn i fila di:
def is_leap_year(year):
    # TODO: skriv koden din for del A her
    # (har du allereie gjorde oppgåva i lab 2, kan du kopiere derfrå)
    ...

def main():
    # TODO: skriv koden din for del B her
    ...

if __name__ == "__main__":
    # Vi kallar main berre viss *denne* fila blir køyrd som hovudprogrammet
    main()

Du kan allereie no sjekke at det fungerer å bruke programmet ditt som ein modul. Last ned scriptet leapyear_test_A.py, legg det i same mappe som leapyear.py, og køyr det.

Del B

I denne delen av oppgåva skal vi gjere det mogleg å køyre leapyear.py som eit program som ein sluttbrukar kan nytte. Programmet skal:

  • be brukaren om å skrive inn eit årstal,
  • lese inn kva brukaren skriv (inkludert å konvertere til heiltal),
  • kalle på funksjonen frå del A for å avgjere om årstalet er eit skotår eller ikkje, og til slutt
  • skrive ut om året er eit skotår eller ikkje til skjermen. Sjå døme under for nøyaktig ordlyd.

Døme på interaksjonar med programmet:

Skriv inn eit årstal:
1996
1996 er eit skotår
Skriv inn eit årstal:
1800
1800 er ikkje eit skotår

Pass på at leapyear_test_A.py framleis fungerer!

Relaterte refleksjonsspørsmål vi forventar at du kan svare på under ein eventuell eksamen (diskuter gjerne med ein venn/ein gruppeleiar/på discord om du er usikker):

  • Kva er formålet med if __name__ == "__main__":?
  • Kva er forskjellen viss vi fjernar if __name__ == "__main__": og rett og slett berre kallar på main() uansett?
Hundefakta

I denne oppgaven skal vi bli kjent med modulen json fra standardbiblioteket og det eksterne biblioteket requests. Men aller først, gå til denne nettsiden:

Prøv å trykk refresh et par ganger, og observer at du får en ny streng hver gang. I denne oppgaven skal vi skrive et program slik at programmet ditt printer tre tilfeldige fakta om hunder hver gang du kjører programmet. Faktaene skal du laste ned på nytt fra adressen over hver gang du kjører programmet.

Del A: JSON

I filen dog_facts.py, skriv en funksjon get_dog_facts med en parameter json_string. Funksjonen vil få som argument en JSON-formatert streng på formatet du finner på nettsiden over og skal returnere en liste med de delene av strengen som inneholder selve faktaene. Du kan anta at det alltid er tre fakta (men kode som fungerer uansett antall fakta er selvfølgelig enda bedre enn kode som er hardkodet for akkurat tre).

Test deg selv:

def test_get_dog_facts()
    arg = '''\
{
  "data": [
    {
      "id": "574c4504-250f-4743-9ff2-124e5c61e79e",
      "type": "fact",
      "attributes": {
        "body": "Dogs live an average of 15 years."
      }
    },
    {
      "id": "cd131ea1-3327-4c2c-93e6-488582a3e2e7",
      "type": "fact",
      "attributes": {
        "body": "Dogs have a wet nose to help them absorb scents."
      }
    },
    {
      "id": "58d82e49-0b2f-4911-bdde-0d686e092b72",
      "type": "fact",
      "attributes": {
        "body": "Dogs can see in color, but not as vividly as humans."
      }
    }
  ]
}
'''
    expected = [
        'Dogs live an average of 15 years.',
        'Dogs have a wet nose to help them absorb scents.',
        'Dogs can see in color, but not as vividly as humans.',
    ]
    actual = get_dog_facts(arg)
    assert expected == actual

Legg merke til at JSON-formatet minner svært mye om Python sin syntaks for å opprette nye oppslagsverk og lister. Faktisk er de så tett beslektet at det i mange tilfeller er helt identisk. For å konvertere fra en JSON-streng til de tilsvarende objektene (oppslagsverk/lister) i Python, skal du bruke loads -funksjonen fra JSON-modulen. Se eksempel i notatene om json.

Etter at du har konvertert strengen til et oppslagsverk, gjenstår det å indeksere seg frem til riktig verdi og legge disse til i en liste du så returnerer.

Første hundefaktum finner du ved å indeksere oppslagsverket med

['data'][0]['attributes']['body']
Del B: requests

I samme fil, skriv en funksjon download_dog_facts uten parametre som laster tre fakta om hunder fra nettsiden oppgitt i begnnelsen av oppgaven og returnerer den json-formaterte strengen du får. Benytt den eksterne modulen requests for å laste ned nettsiden. Benytt utf-8 når du dekoder responsen til en streng.

Requests er en ekstern modul og må installeres før bruk. Men sannsynligvis har du den allerede installert, siden det er en av modulene som blir installert sammen med uib-inf100-graphics.

Siden denne funksjonen vil returnere en ny «tilfeldig» streng hver gang, har vi ikke tester for dette utover manuell testing. Du kan legge dette til nederst i filen.

if __name__ == '__main__':
    json_string = download_dog_facts()
    facts = get_dog_facts(json_string)
    print('\n'.join(facts))

Når du kjører programmet skal det skrives ut tre fakta om hunder. Nøyaktig hvilke fakta vil være forskjellig fra gang til gang. For eksempel:

Dogs live an average of 15 years.
Dogs have a wet nose to help them absorb scents.
Dogs can see in color, but not as vividly as humans.
Polynom

Forberedelser: Gjør tutorial for pyplot og les gjennom notatene om det eksterne biblioteket matplotlib

I denne oppgaven skal du lage en funksjon som bruker matplotlib til å visualisere en andregradsfunksjon. I filen function_visualiser.py lag en funksjon plot_polynomial som har parametre for koeffisientene a, b og c til en andregradsfunksjon, og en samling med tall xs som angir x-verdiene som skal plottes. Funksjonen skal plotte funksjonen for disse x-verdiene med matplotlib. Vi skal også sette navn på aksene og gi plottet en tittel. Aksenavnene skal være «x» og «f(x)», og tittelen skal være «f(x) = ax^2 + bx + c» hvor du setter inn riktige verdier for a, b og c (se også eksempel under).

Husk at en andregradsfunksjon er gitt ved formelen: $$f(x) = ax^2 + bx + c$$

For å teste funksjonen, legg til dette på slutten av filen (gitt at du har importert matplotlib ved import matplotlib.pyplot as plt):

import matplotlib.pyplot as plt

def plot_polynomial(a, b, c, xs):
    ...  # din kode her

if __name__ == "__main__":
    plot_polynomial(1, -5, 100, [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10])
    plt.show()  # ha gjerne plt.show() utenfor plot_polynomial

Du skal få et vindu som ser omtrent slik ut hvis du har gjort det riktig:

Illustrasjon av fullført oppgave

Siden det ser litt rart ut å ha «1x^2» i tittelen når a = 1, eller « + -3x» når b = -3, kan du lage en funksjon som setter riktig fortegn foran koeffisientene a, b og c i tittelen. Her er forslag til en måte å gjøre det på som blir litt finere, selv om heller ikke denne tar hensyn til alt:

def coeff_string(coefficient):
    if coefficient == 1:
        return " + "
    elif coefficient == 0:
        return None
    elif coefficient >= 0:
        return f" + {coefficient}"
    elif coefficient < 0:
        return f" - {abs(coefficient)}"

Du kan da bruke f-strengen

f'f(x) ={coeff_string(a)}x^2{coeff_string(b)}x{coeff_string(c)}'

som tittel på plottet.

Bonusoppgave (helt frivillig): lag en funksjon get_polynomial_string med en parameter coefficients som får gitt en liste med koeffisienter som argument. Funksjonen returnerer en pen strengrepresentasjon av funksjonsuttrykket.


def test_get_polynomial_string():
    assert '2x^2 + 4x + 5' == get_polynomial_string([2, 4, 5])
    assert '3x^3 + 2x^2 + 4x + 5' == get_polynomial_string([3, 2, 4, 5])
    assert '3x^3 - 2x^2 + 4x + 5' == get_polynomial_string([3, -2, 4, 5])
    assert '- 3x^3 + 2x^2 + 4x + 5' == get_polynomial_string([-3, 2, 4, 5])
    assert '3x^3 + 4x - 5' == get_polynomial_string([3, 0, 4, -5])
    assert '2x^2' == get_polynomial_string([2, 0, 0])
    assert '- 3x' == get_polynomial_string([0, -3, 0])
    assert '0' == get_polynomial_string([0, 0, 0])
    assert '5' == get_polynomial_string([5])
    assert '0' == get_polynomial_string([])

I funksjonen plot_function kan du gjøre følgende:

  1. Opprett en liste som inneholder \(f(x)\) for alle verdiene \(x\) i xs. Dette blir altså en liste over y-verdiene.
  2. Bruk plt.plot(xs, ys) for å plotte funksjonen hvor ys er y-verdiene du regnet ut i steg 1.
  3. Sett navn på aksene med plt.xlabel og plt.ylabel -funksjonene.
  4. Bruk plt.title og en f-streng for å sette tittel. På grunn av fortegnene kan det bli litt kluss når du får negative koeffisienter, så se gjerne på hintet over for å få det til.
Kolonnesum

I filen sum_of_column.py, skriv en funsjon sum_of_column(path, col) som returnerer summen av verdiene fra den oppgitte kolonnen i csv-filen med filsti path. Ikke inkluderer verdier i summen som ikke er flyttall; hvis det ikke finnes noen tallverdier i oppgitt kolonne, skal funksjonen returnere 0. Kolonne 0 er den første kolonnen fra venstre i csv-filen. Du kan anta at csv-filen benytter komma (,) som skilletegn og dobbel rett apostrof (") som anførselstegn/grupperingssymbol («quotechar»).

Eksempelkjøringer med foo.csv, Statistikk_Tilsyn_ar.csv og airport-codes.csv

def test_sum_of_column():
    print('Tester sum_of_column... ', end='')
    assert(42.0 == sum_of_column('foo.csv', 0))
    assert(95.0 == sum_of_column('foo.csv', 1))
    assert(0.0 == sum_of_column('foo.csv', 2))
    assert(76363.0 == sum_of_column('Statistikk_Tilsyn_ar.csv', 1))
    assert(46007.0 == sum_of_column('Statistikk_Tilsyn_ar.csv', 2))
    assert(5024518.0 == sum_of_column('airport-codes.csv', 3))
    print('OK')

if __name__ == '__main__':
    test_sum_of_column()
Studentregister

Du har et register med studentdata i en JSON-fil. I filen student_registry.py skal du lage en funksjon convert_students_to_csv som tar inn en sti til en JSON-fil med studentdata og en sti til en CSV-fil, og som så skriver dataen til CSV-filen med CSV-format. Bruk semikolon som skillesymbol i CSV-filen du oppretter.

Eksempler på studentdata i en JSON-fil:

{
  "students": [
    {
      "id": 1,
      "name": "Alice",
      "area": "Biology",
      "year": "1"
    },
    {
      "id": 2,
      "name": "Bob",
      "area": "Chemistry",
      "year": "2"
    },
    {
      "id": 3,
      "name": "Charlie",
      "area": "Physics",
      "year": "4"
    }
  ]
}

JSON-filen representerer et oppslagsverk med én nøkkel, «students», der tilhørende verdi er en liste med oppslagsverk. Hvert oppslagsverk i denne listen representerer en student og har nøklene «id», «name», «area» og «year».

Når du har konvertert dataen fra students.json til en csv-filen, skal innholdet den se slik ut:

id;name;area;year
1;Alice;Biology;1
2;Bob;Chemistry;2
3;Charlie;Physics;4

Du kan laste ned eksemplene students.json og students2.json, programmet ditt skal fungere for begge disse samt andre JSON-filer som følger samme struktur.

  • Lag en funksjon convert_students_to_csv som tar inn en filsti til en JSON-fil med studentdata og en filsti til en CSV-fil.

  • Les innholdet fra json-filen til en streng og konverter strengen til et oppslagsverk data med loads -funksjonen fra den innbygde json modulen.

  • Legg merke til at for hver student skal du til syvende og sist opprette én linje i den endelige strengen som skal bli innholdet i csv-filen. I tillegg kommer den øverste linjen i CSV-filen med overskriftene.

  • Gå gjennom data['students'] med en for-løkke. I hver iterasjon av løkken vil iteranden referere til et nytt oppslagsverk som representerer en student.

For å teste funksjonen din, legg til den følgende koden på slutten av filen din og sjekk at csv-filen blir skrevet til disk slik som vist over.

if __name__ == "__main__":
    convert_students_to_csv("students.json", "students.csv")
    convert_students_to_csv("students2.json", "students2.csv")
Introduksjon

I denne oppgaven skal vi lære hvordan vi genererer en QR-kode! Når vi er helt ferdige skal programmet vårt fungere som følger:

  • Input: en nettadresse, for eksempel strengen 'https://inf100.ii.uib.no'
  • Resultat: en QR-kode vises på skjermen. Om vi scanner QR-koden med mobilen kommer vi til nettadressen oppgitt som input.
Illustrasjon av ferdig program

Før du vi begynner, anbefaler vi sterkt at du ser igjennom denne videon som forklarer hvordan QR-koder fungerer (det er greit om du ikke klarer å følge med på de aller mest tekniske detaljene om Reed-Solomon -enkoding):


Prosessen med å lage en QR-kode går igjennom følgende steg:

Oversikt over steg

Selv om prosessen begynner med å konvertere en streng til en liste med bits (1’ere og 0’ere), vil oppgavene i denne lab’en være organisert «baklengs», slik at vi begynner med å skrive funksjonen for å tegne QR-koden. Grunnen til dette er rett og slett at du får prøve at koden din fungerer etter hvert steg ved å scanne QR-kodene du produserer!


Kom i gang

Last ned disse filene til arbeidsmappen din:

qr_dummies.pyInneholder placeholder -varianter for funksjonene vi skal skrive, samt noen ekstra funksjoner som gir oss dummy-data for å hjelpe oss å sjekke underveis at vi er på riktig vei. Når vi er helt ferdige trenger vi ikke denne filen lengre.
qrv2_layout.jsonInformasjon om layouten for en QR-kode i versjon 2. Denne informasjonen benytter vi flere steder. Hvis vi er flinke til å skrive koden vår basert på verdiene vi finner her, kan vi senere bytte ut denne filen med f. eks. qrv3_layout.json og umiddelbart være i stand til å generere QR-koder i versjon 3.

Opprett også en fil qr0_main.py og lim inn følgende kode:

import json
from pathlib import Path
from qr1_draw import display

# from qr3_masking import get_refined_matrix
from qr_dummies import get_refined_matrix

# from qr4_zigzag import bit_list_to_raw_matrix
from qr_dummies import bit_list_to_raw_matrix

# from qr5_bit_list import string_to_bit_list
from qr_dummies import string_to_bit_list

def get_qr_matrix(content_string):
    qr_layout = json.loads(Path('qrv2_layout.json').read_text(encoding='utf-8'))
    bit_list, err_corr = string_to_bit_list(content_string, qr_layout)
    raw_matrix = bit_list_to_raw_matrix(bit_list, qr_layout)
    matrix = get_refined_matrix(raw_matrix, err_corr, qr_layout)
    return matrix

if __name__ == '__main__':
    url = 'https://inf100.ii.uib.no'
    qr_matrix = get_qr_matrix(url)
    display(qr_matrix)

Æsj, programmet krasjer allerede. Kan det være fordi vi ikke er ferdige med qr1_draw.py enda? Gjennomfør neste steg og finn ut!

1: Tegning

I denne oppgaven skal vi skrive et program som tegner en QR-kode til skjermen, gitt at vi får en matrise (2D-liste) med hvilke felter som skal være svarte og hvilke som skal være hvite i et rutenett. I denne deloppgaven skal vi opprette filen qr1_draw.py.


draw_qr

I filen qr1_draw.py skriven funksjon draw_qr med fem parametre:

  • canvas: et lerret QR-koden skal tegnes på;
  • x_left og y_top: koordinatene til hjørnet øverst til venstre;
  • size: bredden og høyden til QR-koden som skal tegnes (tegningen skal være like høy som den er bred); og
  • qr: en kvadratisk 2D-dimensjonell liste som inneholder 1’ere og 0’ere. En 1’er betyr at ruten skal være svart, mens en 0’er betyr at ruten skal være hvit.

For å teste programmet ditt kan du legge til disse linjene nederst i filen qr1_draw.py. Når du kjører programmet, skal bildet under vises.

def display(matrix):
    from uib_inf100_graphics.simple import canvas, display as dsp
    canvas.create_rectangle(0, 0, 400, 400, fill='white', outline='')
    draw_qr(canvas, 25, 25, 350, matrix)
    dsp(canvas)

if __name__ == '__main__':
    sample_grid = [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1],
        [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0],
        [0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0],
        [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0],
        [0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1],
        [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0],
        [0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0],
        [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0],
        [0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    ]
    
    display(sample_grid)
Kjøring av programmet over

Ta utgangspunkt i kode du har skrevet tidligere for Rutenett (lab4) eller Snake (lab5) og gjør noen små tilpasninger slik at funksjonen din for å tegne et rutenett passer med de nye parametrene.

  • Husk: både bredde og høyde er lik size.

  • Husk: høyresidens x-koordinat er lik venstresidens x-koordinat pluss bredde. Tilsvarende er bunnen lik topp pluss høyde.

Sjekk deg selv

Gå tilbake til qr0_main.py fra introduksjonssteget og kjør den nå. Om du har gjort alt riktig, skal det vises en QR-kode. Scan QR-koden med mobilen og se hvor du kommer… forhåpentligvis fant du en spennende nettside. Dessverre er det ikke riktig nettside, siden vi ikke ender opp på hovedsiden til https://inf100.ii.uib.no/ slik vi forventer ut i fra å lese koden i qr0_main.py. Det viser seg nemlig at get_refined_matrix-funksjonen som ligger i qr_dummies.py ikke bryr seg om argumene den får, men alltid gir samme matrise tilbake uansett. Dette skal vi rette opp i løpet av de neste to stegene i oppgaven.


Guide til QR (c) Torstein Strømme og Aleksandr Popov 2024
2: Faste mønstre og meta-felter

Det finnes mange ulike typer QR-koder. Den mest fremtredende forskjellen er kanskje dimensjonen til QR-koden, som avgjør hvor mange piksler det er i den. Jo større QR-kode, jo mer informasjon (f. eks. lengre nettadresser) kan den inneholde. I denne oppgaven skal vi for enkelhets skyld begrense oss til å kun jobbe med versjon 2 (QRv2), noe som innebærer at rutenettet vi skal lage er en matrise (2D-liste) med størrelse 25x25.

Matrisen for en QRv2 -kode er delt inn i følgende hovedkomponenter:

  • En fast komponent som er lik for alle QRv2 -koder (markert i blått/lyseblått under). Disse feltene hjelper en QR-scanner å finne i hvilken del av bildet QR-koden befinner seg og hvordan den er vinklet og orientert. For oss som ikke er i businessen av å scanne QR-koder men istedet bare produsere dem, er ikke dette noe vi graver oss videre ned i utover at vi selvfølgelig må følge reglene og alltid lage koder som følger dette faste mønsteret.
  • En datakomponent som inneholder selve data’en som er lagret (i svart/hvitt under). Dette datafeltet inneholder også underkomponenter for feilretting (error correction), men det kommer vi tilbake til senere.
  • En komponent for metadata som sier noe om hvordan datafeltet er «maskert» og hvilket feilrettingsnivå koden benytter seg av i datafeltet (i rødt/rosa under).
De ulike hoveddelene av en QR-matrise

I filen qrv2_layout.json (som du allerede har lastet ned til arbeidsmappen din, se introduksjon) finner du en datastruktur som forteller oss hvilke posisjoner i matrisen som hører til den faste komponenten og hvilke verdier de skal ha, samt hvilke posisjoner i matrisen som hører til metadata og hvilke verdier disse posisjonene kan ha ved ulike konfigurasjoner. Ta en kikk på filen den og prøv å forstå hvilken informasjon den inneholder.

En QRv2 -kode kan variere på følgende måter:

  • Selve dataen som er lagret i QR-koden kan selvfølgelig variere. Ofte er dette en nettadresse, men det kan i prinsippet være hvilken som helst data. I denne oppgaven begrenser vi oss til å kun jobbe med strenger som data. Nettadresser er en form for streng, så vi vil kunne bruke programmet vårt til å lage QR-koder som fører oss til en nettside.
  • Alle QR-koder har en viss kapasitet til å rette opp feil i avlesningen på egen hånd; men hvor stor andel av QR-koden som er feilaktig tolket før lesingen feiler kan variere. Du kan velge mellom fire nivåer av feilretting: L, M, Q og H. Disse ulike nivåene gjør at du kan hente ut korrekt informasjon selv om respektivt 7%, 15%, 25% eller 30% av matrisen blir feiltolket av programmet som leser bildet fra kameraet.
  • Alle QR-koder har gått igjennom en prosess som heter «maskering», slik at bildet får en jevn fordeling av svarte og hvite piksler uavhengig av hvilken data bildet representerer. Prosessen innebærer å «flippe» enkelte ruter i matrisen etter et bestemt mønster. Det finnes 8 ulike mønstre man kan velge mellom, og ideelt sett velger vi mønsteret som gir den jevnest mulige fordeling av hvite og svarte ruter.

Når vi oppretter matrisen for QR-koden, gjøres det i fire hovedsteg:

RåversjonMaskeringMetadataFaste komponenter
Råversjon
Maskert
Med metadata
Med faste komponenter

Vi skal nå fokusere på de to siste stegene i denne helheten, nemlig fra vi har en ferdig maskert matrise til vi får en komplett QR-kode. Vi begynner med det siste steget først: å lime inn de faste komponentene. Deretter lager vi en funksjon for å lime inn feltene for metadata.

Begge oppgavene på denne siden skal skrives i filen qr2_matrix_completion.py.


set_fixed_fields

Opprett funksjonen set_fixed_fields som har to parametere

  • matrix en kvadratisk matrise (2D-liste), og
  • qr_layout et oppslagsverk på samme format du ser i qrv2_layout.json.

Funksjonen skal (destruktivt) endre 2D-listen matrix slik at de faste feltene får riktig verdi i henhold til qr_layout.

def test_set_fixed_fields():
    print('Testing set_fixed_fields...', end='')
    matrix = [
        [1, 0, 1, 0, 1],
        [0, 1, 0, 1, 0],
        [1, 0, 1, 0, 1],
        [0, 1, 0, 1, 0],
        [1, 0, 1, 0, 1],
    ]
    sample_layout = {
        'about': 'A fake and incomplete QR layout for testing only',
        'side_length': 5,
        'fixed_positions': {
            'zeros': [
                [3, 2], [3, 3], [3, 4], [4, 2], [4, 3], [4, 4]
            ],
            'ones': [
                [0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]
            ]
        }
        # skipping keys 'byte_capacity', 'meta_positions' and 'meta_patterns'
        # since they are irrelevant here 
    }
    set_fixed_fields(matrix, sample_layout)
    assert matrix == [
        [1, 1, 1, 0, 1],
        [1, 1, 1, 1, 0],
        [1, 0, 1, 0, 1],
        [0, 1, 0, 0, 0],
        [1, 0, 0, 0, 0],
    ]

    print(' OK')

Benytt en løkke over punktene i qr_layout['fixed_positions']['zeros']. Iteranden i en slik løkke blir [rad, kolonne] for et sted i matrisen som skal settes til 0. Tilsvarende for «ones».


set_meta_fields

Metadata-feltet varierer basert på to parametre:

  • Feilrettingsnivå (L, M, Q eller H), og
  • Maskenummer (angitt ved et tall mellom 0 og 7)

Opprett funksjonen set_meta_fields som har fire parametere:

  • matrix en kvadratisk matrise (2D-liste),
  • err_corr en streng på ett tegn som indikerer feilrettingsnivå (enten L, M, Q eller H),
  • mask_no et tall 0-7 som angir hvilken maske som er benyttet, og
  • qr_layout et oppslagsverk på samme format du ser i qrv2_layout.json.

Husk, vi skal ikke gjøre noe med feilrettingskoder eller maskering akkurat nå; vi kan anta at dette allerede er tatt hånd om. Vi skal bare gjøre informasjon om valgene som er gjort tilgjengelig i QR-kodens metafelter.

Funksjonen skal (destruktivt) endre 2D-listen matrix slik at metafeltene får riktig verdi i henhold til qr_layout. Merk at for å vite hvilket mønster som skal brukes, må du slå opp i «meta_patterns» i qr_layout i henhold til parametrene err_corr og mask_no; og for å vite posisjonene hvor dette mønsteret skal plasseres, må du slå opp i «meta_positions». Studer også testen for et forenklet eksempel.

def test_set_meta_fields():
    print('Testing set_meta_fields...', end='')
    # For easier visualization the test uses a matrix of strings rather
    # than 0's and 1's, but ultimately 1's and 0's should also work
    matrix = [
        ['-', '|', '-', '|', '-'],
        ['|', '-', '|', '-', '|'],
        ['-', '|', '-', '|', '-'],
        ['|', '-', '|', '-', '|'],
        ['-', '|', '-', '|', '-'],
    ]
    sample_layout = {
        'about': 'A fake and incomplete QR layout for testing only',
        'side_length': 5,
        # skipping key 'fixed_positions' since it is irrelevant here 
        'meta_positions': {
            'first': [
                [0, 0], [0, 1], [0, 2]
            ],
            'second': [
                [0, 4], [4, 4], [3, 1]
            ]
        },
        'meta_patterns': {
            'L': [
                ['A', 'B', 'C'], # mask_no = 0
                ['a', 'b', 'c']  # mask_no = 1
            ],
            'Q': [
                ['Q', 'R', 'S'], # mask_no = 0
                ['q', 'r', 's']  # mask_no = 1
            ],
        }
    }
    err_corr = 'L'
    mask_no = 0
    set_meta_fields(matrix, err_corr, mask_no, sample_layout)
    assert matrix == [
        ['A', 'B', 'C', '|', 'A'],
        ['|', '-', '|', '-', '|'],
        ['-', '|', '-', '|', '-'],
        ['|', 'C', '|', '-', '|'],
        ['-', '|', '-', '|', 'B'],
    ]

    err_corr = 'Q'
    mask_no = 1
    set_meta_fields(matrix, err_corr, mask_no, sample_layout)
    assert matrix == [
        ['q', 'r', 's', '|', 'q'],
        ['|', '-', '|', '-', '|'],
        ['-', '|', '-', '|', '-'],
        ['|', 's', '|', '-', '|'],
        ['-', '|', '-', '|', 'r'],
    ]

    print(' OK')

Begynn med å finne hvilket bit-mønster du skal bruke (qr_layout['meta_patterns'][err_corr][mask_no]) og hvor det skal plasseres (qr_layout['meta_positions']['first'/'second']). Legg merke til at disse listene er like lange, og derfor har de samme indeksene. Bruk en løkke over indeksene (for ... in range(len(...))). For hver indeks i, hent ut [rad, kolonne] fra plassering-listene og hent ut riktig verdi fra bit-mønster-listen; sett så riktig verdi i matrisen på disse posisjonene.


Sjekk deg selv

Legg til denne snutten nederst i qr2_matrix_completion.py. Om du har gjort alt rett skal du kunne besøke en nettside med en hyggelig melding når du scanner QR-koden som vises.

if __name__ == '__main__':
    from qr_dummies import sample_masked_matrix
    from qr1_draw import display
    from pathlib import Path
    import json

    # Retrieve sample of premasked matrix without meta/fixed fields
    # and getting the matching config for it.
    matrix, error_correction_level, mask_no = sample_masked_matrix()
    qr_layout = json.loads(Path('qrv2_layout.json').read_text(encoding='utf-8'))

    # Try the functions we have created here
    set_meta_fields(matrix, error_correction_level, mask_no, qr_layout)
    set_fixed_fields(matrix, qr_layout)

    # Draw the picture
    display(matrix)

Guide til QR (c) Torstein Strømme og Aleksandr Popov 2024
3: Maskering

I dette steget skal vi maskere en matrise. Hensikten med å maskere matrisen er å få en jevn fordeling av svarte og hvite ruter i QR-koden, slik at den blir lettere å lese for en QR-scanner. Dette innebærer å «flippe» noen av verdiene i en matrise i henhold til et bestemt mønster. QR -standarden spesifiserer 8 ulike masker man kan velge mellom. Gitt et radnummer row og et kolonnenummer col så skal en posisjon i matrisen flippes dersom en gitt betingelse er oppfylt:

Maske nrBetingelse for flippingIllustrasjon
0(row + col) % 2 == 0
Maske 0
1row % 2 == 0
Maske 1
2col % 3 == 0
Maske 2
3(row + col) % 3 == 0
Maske 3
4(row//2 + col//3) % 2 == 0
Maske 4
5(row*col) % 2 + (row*col) % 3 == 0
Maske 5
6((row*col) % 2 + (row*col) % 3) % 2 == 0
Maske 6
7((row+col) % 2 + (row*col) % 3) % 2 == 0
Maske 7

I illustrasjonene er de svarte rutene posisjoner hvor betingelsen er oppfylt, og rutene skal flippes.

Funksjonen beskrevet på denne siden skal skrives i filen qr3_masking.py.


should_flip

En funksjon should_flip som skal avgjøre hvorvidt en posisjon skal flippes eller ikke. La funksjonen ha parametre:

  • row hvilken rad posisjonen er i,
  • col hvilken kolonne posisjonen er i, og
  • mask_no hvilken maske som skal benyttes.

La funksjonen returnere True dersom verdien i cellen skal flippes i henhold til gitt maske, og False hvis ikke. Denne funksjonen kan du benytte som en hjelpefunksjon for get_masked_matrix.


get_masked_matrix

En ikke-destruktiv funksjon get_masked_matrix med parametre:

  • matrix en matrise (2D-liste) hvor alle verdiene er 0 eller 1, og
  • mask_no et nummer som indikerer hvilken maske som skal benyttes.

Funksjonen skal returnere en ny matrise (2D-liste) med samme dimensjoner som matrix. Verdiene skal også være de samme som i matrix, bortsett fra at alle 1’ere er omgjort til 0’ere og alle 0’ere er omgjort til 1’ere i de posisjonene hvor verdien skal flippes i henhold til maskenummeret.

Legg merke til: det er oppgitt at funksjonen skal være «ikke-destruktiv». Det betyr at funksjonen ikke skal mutere matrix.

def test_get_masked_matrix():
    print('Testing get_masked_matrix...', end='')
    blank_5x5_matrix = [[0] * 5 for _ in range(5)]

    actual = get_masked_matrix(blank_5x5_matrix, 0)
    expected = [
        [1, 0, 1, 0, 1],
        [0, 1, 0, 1, 0],
        [1, 0, 1, 0, 1],
        [0, 1, 0, 1, 0],
        [1, 0, 1, 0, 1],
    ]
    assert expected == actual
    assert blank_5x5_matrix == [[0] * 5 for _ in range(5)]

    actual = get_masked_matrix(expected, 1)
    expected = [
        [0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0],
    ]
    assert expected == actual
    print(' OK')

score_matrix

En ikke-destruktiv funksjon score_matrix med én parameter:

  • matrix, en matrise (2D-liste) med 1’ere og 0’ere.

Funksjonen skal returnere absolutt-forskjellen på antall enere og nullere i matrisen.

PS: Dette er en forenkling; hvis du er interessert i å skrive en ordentlig score-funksjon, kan du lese deg opp hos Thonky sin QR-tutorial. Men: benytt forenklingen vår når du leverer på CodeGrade, siden testene våre forventer det slik.

def test_score_matrix():
    arg = [
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 0]
    ]
    assert 5 == score_matrix(arg)

    arg = [
        [1, 1, 0],
        [1, 1, 0],
        [0, 0, 1]
    ]
    assert 1 == score_matrix(arg)

get_refined_matrix

En ikke-destruktiv funksjon get_refined_matrix med tre parametere:

  • raw_matrix, en matrise (2D-liste) med 1’ere og 0’ere som representerer en «rå» matrise med umaskert data;
  • error_correction_level, enten L, M, Q eller H som indikerer hvilket nivå av feilretting som ligger i den rå matrisen;
  • qr_layout et oppslagsverk på formatet gitt i qrv2_layout.json som angir detaljer om hvor de faste kompontentene og meta-komponentene i matrisen befinner seg og hvordan de skal fylles ut.

Funksjonen skal returnere en komplett ferdig matrise som er både maskert og fylt med riktige verdier i de faste komponentene og i meta-komponentene. Importer og benytt funksjoner fra tidligere steg som nødvendig. Funksjonen skal velge det maskenummeret som gir lavest score (og hvis det er to masker som gir samme score, skal den med lavest maskenummer blant dem velges). De faste komponentene og meta-feltene skal legges til matrisen før den scores.

I denne funksjonen skal du «binde sammen» alt du har gjort så langt.

Merk: Dersom du på forhånd vet hvilket maskenummer du skal bruke, kan du fullføre oppgaven ved å bruke (1) get_masked_matrix, (2) set_meta_positions og (3) set_fixed_positions. Tenk på disse tre stegene som å «bygge matrisen».

Det jobben get_refined_matrix skal gjøre, er å prøve ut alle muligheter for maskenummer (bruk en for-løkke). For hvert maskenummer, bygg matrisen og se hvordan den scorer (etter at matrisen er ferdig bygget). Ta vare på matrisen med best score etter hver iterasjon av løkken. Til slutt returnerer du den beste matrisen du fant etter at løkken er ferdig.

Sjekk deg selv

Gå tilbake til qr0_main.py fra introduksjonssteget og endre importene slik at get_refined_matrix importeres fra qr3_masking og ikke fra qr_dummies. Kjør programmet og scan QR-koden som vises. Om du har gjort alt riktig, skal du kunne lese en hyggelig melding.


Guide til QR (c) Torstein Strømme og Aleksandr Popov 2024
4: Sikksakk

Det neste steget er å skape en «råmatrise» fra en streng. Denne prosessen vil foregå i to hovedsteg:

  • først konverteres strengen til en (1-dimensjonell) liste med «bits» (1’ere og 0’ere), og
  • så plasseres den 1-dimensjonelle listen av bits som en slange gjennom en matrise.
Illustrasjon av steg for å lage rå-matrise

Vi skal fokusere på det siste først.

Meme

Når vi skal legge listen med bits inn i slangen, følges det et spesielt mønster som slanger seg gjennom matrisen. Vi begynner nederst til høyre og legger bitene som en slange som går opp og ned gjennom matrisen mot venstre. Det som gjør det litt komplisert er at man går «to og to» kolonner om gangen i et sikksakk -mønster. Se illustrasjonen under.

Sikksakk-mønster i QR-kode

Noen steder (i nærheten av de faste komponentene) ser det ut som mønster av og til avviker fra det vanlige; men det som er kult, er at vi faktisk kan følge det vanlige mønsteret hele tiden, bare at vi hopper over de posisjonene som tilhører faste komponententer og meta-komponentene.


get_next_pos

I filen qr4_zigzag.py skriv en funksjon get_next_pos med parametre:

  • row og column, heltall som beskriver en posisjon i en matrise (2D-liste), og
  • size et heltall som beskriver sidelengden i en kvadratisk matrise.

Funksjonen skal returnere en tuple (next_row, next_column) i henhold til slange-sikksakk-mønsteret illustrert over. Du skal ikke ta hensyn til at noen deler av matrisen er forbudt, men la mønsteret fylle hele matrisen.

Du kan anta at size én mer enn delelig på fire, altså at size % 4 er 1 (dette er sant for alle QR-størrelser).

  • Hvis man er i en partallskolonne, flytter man seg ett steg til venstre. Unntaket er hvis man er i kolonne 0, da flytter man seg et steg opp i stedet.

  • Hvis man er i en oddetallskolonne som er én mer enn delelig med fire, er man på vei nedover. Man flytter seg da én ned og én til høyre. Unntaket er hvis man allerede er nederst, da flytter man seg et steg til venstre.

  • Ellers er man i en oddetallskolonne som er tre større enn delelig med fire. Da er man på vei oppover. Man flytter seg da én opp og én til høyre. Unntaket er hvis man allerede er øverst, da flytter man til venstre.

def test_get_next_pos_basic():
    print('Testing get_next_pos (basic)...', end='')
    size = 25
    # De første flyttene
    assert (24, 23) == get_next_pos(24, 24, size)
    assert (23, 24) == get_next_pos(24, 23, size)
    assert (23, 23) == get_next_pos(23, 24, size)
    ...
    # Når vi har sikksakket helt til topps
    assert (0, 23) == get_next_pos(0, 24, size)
    assert (0, 22) == get_next_pos(0, 23, size)
    assert (0, 21) == get_next_pos(0, 22, size)
    assert (1, 22) == get_next_pos(0, 21, size)
    assert (1, 21) == get_next_pos(1, 22, size)
    assert (2, 22) == get_next_pos(1, 21, size)
    ...
    # Når vi har ned helt til bunns igjen
    assert (24, 20) == get_next_pos(24, 21, size)
    assert (24, 19) == get_next_pos(24, 20, size)
    ...
    # Siste kolonne
    assert (24, 0) == get_next_pos(24, 1, size)
    assert (23, 0) == get_next_pos(24, 0, size)
    assert (22, 0) == get_next_pos(23, 0, size)
    print(' OK')
def test_get_next_pos_5x5():
    print('Testing get_next_pos (5x5)...', end='')

    size = 5
    expected = [
        [24, 11, 10, 9, 8],
        [23, 13, 12, 7, 6],
        [22, 15, 14, 5, 4],
        [21, 17, 16, 3, 2],
        [20, 19, 18, 1, 0]
    ]
    actual = [[-1] * size for _ in range(size)]
    row, col = 4, 4
    for i in range(size * size):
        actual[row][col] = i
        row, col = get_next_pos(row, col, size)

    assert expected == actual
    print(' OK')
def test_get_next_pos_9x9():
    print('Testing get_next_pos (9x9)...', end='')
    size = 9
    expected = [
        [80, 55, 54, 53, 52, 19, 18, 17, 16],
        [79, 57, 56, 51, 50, 21, 20, 15, 14],
        [78, 59, 58, 49, 48, 23, 22, 13, 12],
        [77, 61, 60, 47, 46, 25, 24, 11, 10],
        [76, 63, 62, 45, 44, 27, 26, 9, 8],
        [75, 65, 64, 43, 42, 29, 28, 7, 6],
        [74, 67, 66, 41, 40, 31, 30, 5, 4],
        [73, 69, 68, 39, 38, 33, 32, 3, 2],
        [72, 71, 70, 37, 36, 35, 34, 1, 0]
    ]
    
    actual = [[-1] * size for _ in range(size)]
    row, col = 8, 8
    for i in range(size * size):
        actual[row][col] = i
        row, col = get_next_pos(row, col, size)

    assert expected == actual
    print(' OK')

bit_list_to_raw_matrix

I filen qr4_zigzag.py skriv en funksjon bit_list_to_raw_matrix med parametre:

  • bit_list en liste med bits (1’ere og 0’ere) som skal limes inn i en matrise
  • qr_layout et oppslagsverk med formatet du finner i qrv2_layout.json.

Funksjonen skal opprette en kvadratisk matrise som i utgangspunktet består av kun 0’ere (størrelsen på matrisen som opprettes er angitt i qr_layout). Du skal deretter legge inn elementene fra bit_list: begynn i hjørnet nederst til høyre og benytt get_next_pos -funksjonen for å finne neste posisjon. Hopp over alle posisjoner som er tilhører en fast komponent eller en meta-komponent (informasjon om hvilke posisjoner dette er finnes i qr_layout).

  • Hvis det ikke er plass til alle elementene i bit_list, bør programmet krasje.
  • Hvis det er plass til overs etter at bitene fra bit_list er plassert, bare la det være 0 i de siste posisjonene.
def test_bit_list_to_raw_matrix():
    print('Testing bit_list_to_raw_matrix...', end='')
    # To make the test easier to read, bit_list contain distinct elements here
    # (in actual applications, bit_list would only have 0's and 1's)
    arg_bit_list = list(range(1, 72))
    arg_qr_layout = {
        'about': 'A fake and incomplete QR layout for testing only',
        'side_length': 9,
        'fixed_positions': {
            'ones': [
                [1, 3], [1, 4], 
            ],
            'zeros': [
                [2, 3], [2, 4],
            ]
        },
        'meta_positions': {
            'first': [
                [5, 2], [5, 3]
            ],
            'second': [
                [6, 2], [6, 3]
            ]
        }
        # key 'meta_patterns' skipped, since it is irrelevant for this task
    }

    expected = [
        [ 0, 50, 49, 48, 47, 20, 19, 18, 17],
        [ 0, 52, 51,  0,  0, 22, 21, 16, 15],
        [71, 54, 53,  0,  0, 24, 23, 14, 13],
        [70, 56, 55, 46, 45, 26, 25, 12, 11],
        [69, 58, 57, 44, 43, 28, 27, 10,  9],
        [68, 59,  0,  0, 42, 30, 29,  8,  7],
        [67, 60,  0,  0, 41, 32, 31,  6,  5],
        [66, 62, 61, 40, 39, 34, 33,  4,  3],
        [65, 64, 63, 38, 37, 36, 35,  2,  1]
    ]
    actual = bit_list_to_raw_matrix(arg_bit_list, arg_qr_layout)
    assert expected == actual
    print(' OK')

Det kan være lurt å samle alle de ulovlige posisjonene i én datastruktur (aller mest effektivt med tanke på kjøretid er det å bruke en mengde av tupler (rad, kolonne) til dette, men en vanlig liste med [rad, kolonne] -elementer fungerer også helt fint med så små matriser vi jobber med nå).


Sjekk deg selv

Gå tilbake til qr0_main.py fra introduksjonssteget og endre importene slik at bit_list_to_raw_matrix importeres fra qr4_zigzag og ikke fra qr_dummies. Kjør programmet og scan QR-koden som vises. Om du har gjort alt riktig, skal du kunne lese en hyggelig melding.


Guide til QR (c) Torstein Strømme og Aleksandr Popov 2024
5: Feilretting

I denne siste delen skal vi se nærmere på hvordan vi skal representere en streng som en liste av bits (1’ere og 0’ere). Du husker kanskje fra kursnotatene om unicode at en streng kan representeres som en sekvens av enere og nullere. Det skal vi ser nærmere på her.

Listen med bits i en QR-kode er delt inn i seks deler:

De ulike delen av bit-listen
  • Modus er fire bits som forteller hvordan resten av bit-listen skal tolkes. For våre formål skal denne verdien alltid være 0100, som betyr at data’en er lagret som «binær» data; de fleste QR-scannere tolker dette i praksis som at dataen representerer en streng i latin-1 (eller ASCII) enkoding (andre mulige verdier som vi altså ikke skal bruke er: 0001 numerisk modus, 0010 alfanumerisk modus, 1000 japansk kanji, og 0111 ECI-modus).
  • Lengde er et tall (i totallsystemet) som indikerer hvor mange bytes dataen består av. Åtte bits utgjør én byte, så vi kan gange tallet med 8 for å finne ut for mange bits som befinner seg i data-seksjonen.
  • Data er selve dataen. For våre formål et dette en ASCII-enkodet streng.
  • Terminator er fire nuller som følger etter dataen (disse brukes egentlig ikke til noen ting i binærmodus, men må være til stede for å fylle opp plassen).
  • Padding er noen faste mønstre som gjentar seg. Hensikten er å bruke opp resten av plassen før feilrettingen begynner. Hvor mye padding det er kommer an på hvor mye data det er og hvor mye plass feilrettingen tar (paddingen kan også være ingenting).
  • Feilretting er spesielle bits som gjør det mulig å gjenopprette den originale dataen selv om noen bits av en eller annen grunn har blitt flippet. Denne delen regnes ut på bakgrunn av de foregående delene av bit-listen.

Funksjonene på denne siden skal opprettes i filen qr5_bit_list.py.


string_to_data

Opprett en funksjon string_to_data med én parameter:

  • content_string en streng du kan anta består av ASCII -symboler.

Funksjonen skal returnere en liste av bits som representerer strengen i ASCII-enkoding.

def test_string_to_data():
    print('Testing string_to_data...', end='')
    assert [0,1,0,0,0,0,0,1] == string_to_data('A')
    assert [0,1,0,0,0,0,0,1,0,1,0,0,0,0,1,1] == string_to_data('AC')
    foo = [0,1,1,0,0,1,1,0,0,1,1,0,1,1,1,1,0,1,1,0,1,1,1,1]
    assert foo == string_to_data('foo')
    print(' OK')

Denne koden konverterer ett symbol til sin ASCII-enkoding:

c = 'A'
bit_list = [int(x) for x in f'{ord(c):08b}'] 

Hva skjer her da? ord-funksjonen gir oss ordinalen (tallverdien) for symbolet. b-en inne i krøllparantesene betyr at tallet skal skrives i totall-systemet i strengen som opprettes. 08 i krøllparantesene betyr at tallet skal skrives med minst åtte siffer, og bruk ledende 0’er om nødvendig. F-strengen evaluerer altså til strengen ‘01000001’. For-løkken går gjennom alle symbolene x i strengen og int konverterer hvert symbol til et heltall. Det blir da 1 eller 0 alt etter som. Verdiene legges til i en liste ved bruk av listebygger-notasjon.

For å løse oppgaven, bruk en løkke og gjenta operasjonen over for hvert symbol c i strengen. La bit-listen du finner utvide en resultat-liste du har opprettet før løkken begynner.

# la 'result' være er en tom liste
# for hvert symbol i content_string:
#    finn bit-listen for symbolet
#    utvid (extend) 'result' med bit-listen
# returner resultatet

get_core_bit_list

Opprett en funksjon get_core_bit_list med én parameter:

  • content_string en streng du kan anta består av ASCII -symboler.

Funksjonen skal returnere de fire første delene av en bit-liste: modus, lengde, data og terminator for den gitte strengen

def test_get_core_bit_list():
    print('Testing get_core_bit_list...', end='')
    arg = 'hei'
    expected_head = [0, 1, 0, 0]
    expected_len = [0, 0, 0, 0, 0, 0, 1, 1] # 3 in binary
    expected_data = [int(x) for x in '011010000110010101101001']
    expected_term = [0, 0, 0, 0]
    expected = expected_head + expected_len + expected_data + expected_term

    actual = get_core_bit_list(arg)
    assert expected == actual
    print(' OK')
my_len = 42
len_bit_list = [int(x) for x in f'{my_len:08b}'] 

pad_bit_list

Opprett en funksjon pad_bit_list med to parameter:

  • core_bit_list en liste med bits
  • pad_to_bytes et tall som sier hvor mange bytes den ferdig paddeded bit-listen skal inneholde

Funksjonen skal destruktivt legge til padding-mønstre på slutten av core_bit_list slik at den til sammen blir pad_to_bytes lang. Husk at én byte er åtte bit, så lengden på den endelige listen skal være 8 * pad_to_bytes.

Paddingen som legges til skal veksle mellom [1, 1, 1, 0, 1, 1, 0, 0] og [0, 0, 0, 1, 0, 0, 0, 1] annenhver gang helt til listen er tilstrekkelig lang.

def test_pad_bit_list():
    print('Testing pad_bit_list...', end='')
    PAD1 = (1, 1, 1, 0, 1, 1, 0, 0)
    PAD2 = (0, 0, 0, 1, 0, 0, 0, 1)
    
    arg = [1, 1, 1, 1, 1, 1, 1, 1]
    expected = arg + list(PAD1) + list(PAD2) + list(PAD1)
    pad_bit_list(arg, 4)
    assert expected == arg

    arg = [1, 1, 1, 1, 1, 1, 1, 1]
    expected = arg + list(PAD1) + list(PAD2) + list(PAD1) + list(PAD2)
    pad_bit_list(arg, 5)
    assert expected == arg

    arg = [1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1]
    expected = arg + list(PAD1) + list(PAD2) + list(PAD1) + list(PAD2)
    pad_bit_list(arg, 6)
    assert expected == arg
    print(' OK')

string_to_bit_list

Opprett en funksjon string_to_bit_list med to parametere:

  • content_string, en streng du kan anta består av ASCII -symboler, og
  • qr_layout, et oppslagsverk på formatet du finner i qrv2_layout.json.

Funksjonen skal returnere to ting i en tuple:

  • en komplett bit-liste, inkludert modus, lengde, data, terminator, padding, og feilretting, og
  • en streng på ett tegn: ‘L’, ‘M’, ‘Q’ eller ‘H’ som indikerer hvilket feilrettingsnivå som brukes i bit-listen.

Før vi begynner:

En QRv2-kode har 25x25 piksler, og representerer dermed 625 bit med informasjon. Mye av dette brukes imidlertid til faste komponenter og meta-komponenter, så i praksis er det bare 352 bits (44 bytes) vi har til rådighet (nøkkelen ‘byte_capacity’ i qr_layout forteller oss dette). Denne plassen skal fordeles mellom feltene for modus, lengde, data, terminator, padding og feilrettingskode.

En QR-kode kan ha fire ulike nivåer av feilretting: L, M, Q eller H som gir bedre og bedre nivåer av feilretting. Disse nivåene krever henholdsvis 80, 128, 176 og 224 bits (eller 10, 16, 22 og 28 bytes om du vil). Jo bedre feilretting vi ønsker oss, jo mer plass krever altså feilrettings-komponenten. Konsekvensen av dette er at vi bare kan bruke et høyt nivå av feilretting dersom dataen vi lagrer er tilstrekkelig liten.

Når vi velger nivå av feilretting, gjør vi som følger:

  • Vi finner først bit-listen for modus, lengde, data og terminator og ser hvor stor plass dette tar.
  • Vi velger deretter nivå av feilretting L, M, Q eller H så høyt som mulig, men slik at det likevel er plass til begge deler (hvis det ikke er plass til laveste feilrettingsnivå, krasj programmet og gi en hensiktsmessig feilmelding).
  • Hvis vi ser at det vil bli plass til overs, legger vi til padding før vi regner ut selve feilrettingsmønsteret.

Husk å holde tungen rett i munnen med tanke på bytes og bits. 1 byte = 8 bits.

Forresten, en funksjon for å regne ut selve feilrettingsmønsteret kan du laste ned her: qr6_error_correction.py (du må også installere den eksterne pakken reedsolo. Prøv å skrive python3 -m pip install reedsolo i terminalen eller se notatene om eksterne pakker for eksempler på hvordan man installerer eksterne pakker).

Vil du lære å regne ut feilrettingsmønsteret selv? Sjekk ut INF243.


Ferdigstilling

Gå tilbake til qr0_main.py fra introduksjonssteget og endre importene slik at string_to_bit_list importeres fra qr5_bit_list og ikke fra qr_dummies. Kjør programmet og scan QR-koden som vises. Om du har gjort alt riktig, programmet nå fungerer. Yay!


Guide til QR (c) Torstein Strømme og Aleksandr Popov 2024
Tilfeldig bursdag

På forhånd bør du lese om:

Det er også nyttig å søke opp eksempelkode for å se hvordan disse bibliotekene brukes.

I denne oppgaven skal du lage en funksjon som finner en tilfeldig bursdag i et år. I filen random_birthday.py, lag en funksjon random_birthday som tar inn et årstall year og returnerer en tilfeldig dato i det året som en streng. Du kan bruke datetime og random for å lage en dato og utføre addisjon slik at du får en tilfeldig dato i det gitte året. For at testene skal fungere, må du bruke random slik at du genererer en dag i et år basert på dager fra 1.januar. For eksempel vil datoen bli 30.januar hvis du legger til 29 dager, eller 1. januar hvis du legger til 0 dager. Du må ta hensyn til skuddår.

Vi ønsker at datoen skal returneres som en string på formatet dd.mm.yyyy, og til dette kan vi bruke strftime fra datetime. Denne funksjonen gjør et datetime-objekt om til en string, og argumentet spesifiserer formatet. %d gir dag, %m gir måned og %Y gir år.

Siden vi skal vite hvor mange dager det er i året, må vi ta hensyn til skuddår. Hvis du har gjort oppvarmingsoppgaven Skuddår-modul, kan du importere funksjonen is_leap_year fra der. Hvis ikke, se hintet under.

def is_leap_year(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

I funksjonen random_birthday kan du gjøre følgende:

  1. Bruk datetime til å lage et datetime-objekt som representerer 1. januar i year.
  2. Bruk timedelta og random til å legge til et tilfeldig antall dager til dette objektet.
  3. Bruk strftime for å returnere datoen som en string.

For å teste funksjonen, legg til dette på slutten av filen (gitt at du har importert datetime og random) og sjekk at du får noen tilfeldige datoer i 2022, 2023 og 1912:

if __name__ == "__main__":
    print(random_birthday(2022))
    print(random_birthday(2023))
    print(random_birthday(1912))
Fredag 13.

I filen friday_13th.py skriv en funksjon first_friday_13th_after(date) som har som parameter et datetime -objekt. Funksjonenen returnerer et nytt datetime-objekt, som befinner seg på første fredag den 13. etter den gitte datoen. Dersom input-datoen selv er på en fredag den 13., er det neste datoen som skal returneres.

Denne oppgaven skal løses ved hjelp av datetime -modulen fra Python sitt standardbibliotek.

Test koden din:

def test_first_friday_13th_after():
    print('Tester first_friday_13th_after... ', end='')
    # Test 1
    result = first_friday_13th_after(datetime(2022, 10, 24))
    assert (2023, 1, 13) == (result.year, result.month, result.day)
    # Test 2
    result = first_friday_13th_after(datetime(2023, 1, 13))
    assert (2023, 10, 13) == (result.year, result.month, result.day)
    # Test 3
    result = first_friday_13th_after(datetime(1950, 1, 1))
    assert (1950, 1, 13) == (result.year, result.month, result.day)
    print('OK')
  • Se eksempel på bruk av datetime på den offisielle dokumentasjonen for datetime.

  • Kan være lurt å ha en hjelpefunksjon som sjekker om et gitt tidspunkt er på en fredag den trettende.

  • Forsøk å øke datoen med én dag helt til du treffer en dag som er på en fredag den trettende.

Kombinere CSV-filer

I denne oppgaven skal vi bruke CSV- biblioteket til å håndtere CSV-filer. Derfor bør du lese om dette i kursnotatene om csv

I denne oppgaven skal vi kombinere flere CSV-filer som ligger i en mappe til én stor CSV-fil. Hver av CSV-filene har det samme formatet, som også er det formatet den kombinerte CSV-filen skal ha:

uibid,karakter,kommentar
abc101,A,"Veldig bra, fra start til slutt"
abc102,B,"Denne kandidaten kan sin INF100, men bommer litt i oppgave 2 og 3"
abc103,C,"Denne kandidaten valgte å kun svare på partallsoppgavene"

I csv_combiner.py skriv en funksjon combine_csv_in_dir(dirpath, result_path) som har to parametre:

  • dirpath er en sti til en mappe som inneholder en rekke CSV-filer som skal kombineres. Det er kun filene som slutter på .csv i mappen som skal inkluderes, andre filtyper skal vi overse.
  • result_path er en sti til en CSV-fil som skal opprettes, som inneholder de kombinerte dataene fra alle CSV-filene.

For å teste funksjonen kan du laste ned samples.zip og pakke ut innholdet i samme mappe hvor du også finner csv_combiner.py. Innholdet skal ligge i en mappe som heter samples. Du kan så teste funksjonen din med denne koden:

print("Tester combine_csv_in_dir... ", end="")
# Mappen samples må ligge i samme mappe som denne filen
dirpath = os.path.join(os.path.dirname(__file__), "samples")
combine_csv_in_dir(dirpath, "combined_table.csv")
with open("combined_table.csv", "rt", encoding='utf-8') as f:
   content = f.read()
   assert("""\
uibid,karakter,kommentar
abc104,C,hei
abc105,D,"med komma, her er jeg"
abc106,E,tittit
abc101,A,Her er min kommentar
abc102,B,"Jeg er glad, men her er komma"
abc103,C,Katching
""" == content or """\
uibid,karakter,kommentar
abc101,A,Her er min kommentar
abc102,B,"Jeg er glad, men her er komma"
abc103,C,Katching
abc104,C,hei
abc105,D,"med komma, her er jeg"
abc106,E,tittit
""" == content)
print("OK")

Merk: csv-biblioteket sin standard oppførsel når du lagrer 2D-lister som CSV er at det kun benyttes hermetegn dersom det er nødvendig. Det er nødvendig med hermetegn dersom en celle inneholder skilletegn (komma), linjeskift eller hermetegn. Dersom cellen ikke inneholder noen av de tre tegnene, vil det ikke inkluderes hermetegn i filen. Denne oppførselen kan endres ved å kalle på write_csv_file -funksjonen i kursnotatene med argumentene quoting=csv.QUOTE_NONNUMERIC eller quoting=csv.QUOTE_ALL (se også offisiell dokumentasjon).

Dette betyr at selv om det kanskje er hermetegn i input-filen, vil disse ikke nødvendigvis bli med i resultat-filen. Assert-setningene over viser resultatet slik det blir med standard-innstillingene til csv-biblioteket.

  • CSV-filene i dette eksempelet er best lest med csv-biblioteket. Det blir fort komplisert å tolke dem selv, siden det kan være komma-tegn i kommentar-feltet.

  • Les om os -modulen, og legg spesielt merke til os.walk -funksjonen.

  • Bruk gjerne en hjelpefunksjon merge_table_into(master_table, new_table) som tar som input en 2D-liste master_table som skal muteres, og en 2D-liste new_table som inneholder det nye innholdet som skal legges til. For hver rad i new_table (bortsett fra første rad), kopier raden inn i master_table.

  • Opprett først en 2D-liste for resultat-tabellen vår, som initielt inneholder én rad (overskriftene). På slutten skal vi konvertere denne listen til CSV.

  • Bruk os.walk eller os.listdir for å gå igjennom alle filene i mappen gitt ved dirpath (os.walk vil også gå inn i undermapper, og du trenger en nøstet løkke inne i os.walk for å gå gjennom listen med filer; ellers fungerer de nokså likt). For hver fil som ender på .csv (bruk f. eks. .endswith -metoden på strenger), åpne filen og les innholdet.

  • Husk å bruke os.path.join -funksjonen for å omgjøre filnavn til filstier.

  • For hver .csv -fil du finner, omgjør den til en 2D-liste, og legg til radene i resultat-tabellen (bruk hjelpefunksjonen beskrevet over).

Kvittering

I filen receipt.py skal du bruke modulen decimal. til å skrive et program som produserer en kvittering fra en butikk.

Koden din vil sannsynligvis ikke fungere hvis du ikke bruker decimal-modulen for å lagre tall, siden avrundingsfeil er veldig viktig å unngå, samtidig som vi skal bruke desimal-tall.

Ta utgangspunt i koden nedenfor:

def receipt_content(prices_filename, cash_register_filename):
    """Construct contents of a receipt of the cash register events,
    given the store prices."""

    # din kode her


def receipt(prices_filename, cash_register_filename):
    """Construct a receipt of the cash register events,
    given the store prices."""

    # get receipt content from receipt_content()
    purchases_returns, total, vat, payment, change = receipt_content(
        prices_filename, cash_register_filename
    )

    # the formatted lines of the receipt
    receipt_lines = [f"{'Nr.':>4}  {'Item':18}  {'Price':>10}"]

    for nr, item, price in purchases_returns:
        receipt_lines.append(f"{nr:4d}  {item:18}  {price:10.2f}")

    receipt_lines.append(f"Total: {total}")
    receipt_lines.append(f"Of which VAT: {vat:.2f}")
    receipt_lines.append(f"Payment: {payment}")
    receipt_lines.append(f"Change {change}")

    # add some dividers
    max_len = max(len(line) for line in receipt_lines)
    divider = "-" * max_len
    receipt_lines.insert(1, divider)
    receipt_lines.insert(-4, divider)
    receipt_lines.insert(-2, divider)

    return "\n".join(receipt_lines)

Du skal skrive koden til funksjonen receipt_content() som beregner inneholdet i kvitteringen utifra en fil med butikkens priser og en fil med hendelsene ved kassen. Argumentet prices_filename er en streng med navnet til filen som inneholder prisene. Argumentet cash_register_filename er en streng med navnet til filen som inneholder hendelsene ved kassen.

Funksjonen receipt_content() skal returnere en tupel som inneholder følgende (i denne rekkefølgen):

  1. En liste med tupler som inneholder (i følgende rekkefølge) antall, produkt og total pris, først for alle ting som har blitt kjøpt, i alfabetisk orden, og så for alle ting som har blitt returnert, i alfabetisk orden (se eksempelkvittering nedenfor). For de returnerte produktene blir det negative tall.
  2. Den totale prisen.
  3. Hvor mye av den totale prisen som er mva.
  4. Hvor mye som har blitt betalt.
  5. Hvor mye som blir betalt tilbake (her blir det ikke-positive tall).

Filen med butikkens priser er formattert slik som eksempelfilen prices.txt (som du kan bruke til å teste programmet ditt). Først står produkt, så semikolon (;), og så prisen. Dette er egentlig en CSV-fil som bruker semikolon som skilletegn.

Filen med hendelsene ved kassen er formattert slik som eksempelfilen cash_register.txt (som du kan bruke til å teste programmet ditt). Først står hva som skjer (kjøp, retur eller betaling), så semikolon (;), og så produkten/verdien. Dette er også egentlig en CSV-fil som bruker semikolon som skilletegn.

Funksjonen receipt() skal du ikke endre. Den bruker receipt_content() til å beregne inneholdet i kvitteringen og så produserer receipt() en pen kvittering utifra det innholdet. Du skal bare skrive kode til receipt_content().

Om din kode til receipt_content() er riktig så skal denne testen passere:

print("Tester receipt... ", end="")
expected_value = """\
 Nr.  Item                     Price
------------------------------------
   2  apple                    10.00
   1  chips                    24.30
   1  dish soap                26.20
   1  frozen pizza             54.40
   1  peanuts                  18.50
   1  toilet paper             34.00
   3  tomato                   30.00
  -1  pocket book            -149.00
  -1  toothpaste              -13.70
------------------------------------
Total: 34.70
Of which VAT: 6.94
------------------------------------
Payment: 100.00
Change -65.30"""
assert(expected_value == receipt("prices.txt", "cash_register.txt"))
print("OK")
  • Du må bruke biblioteket decimal for at beregningene skal bli riktige (prøv hvordan kvitteringen blir om du bruker float).

  • Decimal aksepterer både float og string for å lage decimals. Prøv hvordan kvitteringen blir om du gir decimals en float. Hvorfor skjer dette?

  • Det er mulig at betaling skjer flere ganger underveis ved kassen.

  • Du kan anta at alle betalinger ved kassen har positiv verdi.

  • Du kan anta at det ikke er noen feil i filene som inneholder prisene og hendelsene ved kassen.

  • Det er mulig at den totale prisen blir negativt (om kunden for eksempel bare returnerer ting).

  • Gitt en pris inklusive mva så multipliserer du prisen med 0.2 for å finne ut hvor mye av prisen som er mva.

  • Det kan være lurt å bruke dictionaries til prislisten og det som blitt kjøpt og det som blitt returnert.

  • Du må ikke skrive all kode innen funksjonen receipt_content(). Du kan dele opp koden i flere funksjoner på en måte du synes føles naturlig.

Værmelding

I filen forecast.py skriv en funksjon weather_in_bergen_next_hour() uten parametre som returnerer en streng som sammenfatter været i Bergen den neste timen. Funksjonen skal hente vær-data fra meterologisk instutt, og bruke informasjon fra feltet «symbol_code» om den nærmeste timen.

Eksempelkjøring: print("Været i Bergen neste time:", weather_in_bergen_next_hour())

Været i Bergen neste time: cloudy

For å løse denne oppgaven, skal vi

  1. Finne ut hvilken url vi kan bruke for å få informasjon om været fra meterologisk institutt
  2. Bruke requests -pakken for å laste ned informasjonen fra meterlogisk institutt inn i python-programmet
  3. Bruke json -pakken for å konvertere den nedlastede informasjonen til et oppslagsverk, og til slutt
  4. Slå opp på riktig sted i oppslagsverket og returnere denne informasjonen.

Steg A: nettadresse fra meterologisk institutt

Meterologisk instutt tilbyr massevis av vær-informasjon gratis (under lisensen CC-BY 4.0) fra sine nettsider. Slå opp på nettsiden https://api.met.no/weatherapi/locationforecast/2.0/ og finn OPENAPI UI fannen (Øverst i siden ved siden av DOCUMENTATION). Les om GET /compact under data.

  • Du vil under «parameters» se to felter lat og lon hvor du kan fylle ut henholdsvis breddegrad og lengdegrad for hvilken posisjon du ønsker.

For å finne breddegrad og lengdegrad for et gitt sted, kan du på til https://maps.google.com og trykke på kartet. Da vil koordinatene til det gitte stedet vises.

Klikke på kartet på Google Maps

  • Når du har fylt ut breddegrad og lengdegrad, trykk på «Try it out!»
  • Kopier med deg internettadressen under «Request URL». Dette er adressen til siden vi skal laste ned i programmet vårt (prøv den direkte i nettleseren også og se hva du får).
  • Undersøk innholdet under «Response Body». Dette er den strengen vi får når vi laster ned innholdet fra den overnevnte nettadressen. Hvor ligger informasjonen vi er ute etter? Husk, vi ser etter verdier knyttet til nøkkelen «symbol_code».

Steg B: bruk requests for å laste ned informasjon

Bruk nettadressen vi fant i forrige steg som url. Merk at vilkårene til Meterologisk institutt sier at vi må identifisere hvem vi er i User-Agent -feltet i «header». Det er tilstrekkelig å bruke "inf100.ii.uib.no <ditt uib-brukernavn>" som verdi for dette feltet.

Steg C: bruk JSON for å konvertere til oppslagsverk

Du kan bruke json.loads() for å konvertere en streng til et oppslagsverk. Her er et eksempel på hvordan du kan gjøre det:

import json

s = '{"a": 1, "b": 2}'
d = json.loads(s)
print(d["a"]) # 1

Her blir d et oppslagsverk med nøklene “a” og “b” og verdiene 1 og 2.

Steg D: returner verdien knyttet til nøkkelen «symbol code» i oppslagsverket

Det gjelder å holde tungen rett i munnen når du skal slå opp i oppslagsverket. En strategi kan være å begynne med å returnere d["properties"] og så spesifisere videre ved å returnere d["properties"]["timeseries"] og så videre, helt til riktig verdi blir returnert.

Det er ikke nødvendig å finne nøyaktig riktig time for å få oppgaven godkjent, det holder å returnere den første timen du får informasjon om (selv om den var i fortiden)

Steg E (frivillig): finn nøyaktig riktig time å rapportere for

For å rapportere for nøyaktig riktig time, må du matche den timen du får fra meterologisk instiutt med tiden akkurat nå. Les deg opp på datetime -modulen og se om du kan bruke informasjonen fra meterologisk institutt for å finne den nærmeste timen å rapportere fra.

Steg F (frivillig): mer omfattende værmelding

Kan du finne mer informasjon å rapportere? Nedbørsmengder? Vind? Sannsynlighet for nedbør (nå må du over på GET /complete hos meterologisk institutt)?