Eksamen

24. november 2025
4 timer
Lukket digital eksamen (med safe exam browser).
Tillatte hjelpemiddel: bøker fra litteraturlisten og opp til 6 tosidige A4 ark med egne notat.
 

Oppgavetekster, løsningsforslag og sensorveiledning finner du på denne siden.

  1. Automatisk rettet
  2. Forklaring
  3. Kodeskriving

Fordi eksamen var en lukket digital eksamen uten tilgang til å kjøre koden eller bruke internett, bes sensor ikke gi poengtrekk for forhold som enkelt ville blitt oppdaget og raskt rettet ved kjøring av koden. Dette inkluderer blant annet:

  • manglende import-setninger,
  • Feilstavede kodeord eller syntaks inspirert av andre programmeringsspråk (for eksempel hvis kandidaten skriver fun i stedet for def ved funksjonsdefinisjoner),
  • feil navn på funksjoner ved funksjonskall til egne funksjoner, innbygde funksjoner eller til funksjoner fra importerte moduler (såfremt det fremgår av funksjonsnavnet hva kandidaten egentlig mener – for eksempel hvis studenten har definert en funksjon som heter rise_up men kaller en funksjon som heter stand_up blir det ikke trekk for dette),
  • feil navn på variabler (f. eks. kalle den samme variabelen både total og sum i ulike deler av koden),
  • enkle syntaks-feil (f. eks. manglende kolon etter if-setninger),
  • og så videre.

Logiske feil skal det likevel bli trukket for; selv om man kunne ha oppdaget at noe var feil ved kjøring av koden. Dette inkluderer blant annet:

  • presedensfeil,

  • forveksling av indekser og elementer,

  • off-by-one -feil,

  • setninger plassert med feil innrykk,

  • feil grunnet for tidlig return (f. eks. return i løkke),

  • feil i algoritmer,

  • og så videre.


1 Automatisk rettet

Du kan prøve deg på oppgavene og sjekke deg selv ved å åpne lukene under.

x = 2
y = {
    x: 42,
    'y': x,
    1: 3
}
y[42] = 'x'
z = [-1, x, y, 'z']

Anta at kodesnutten over har blitt kjørt, og at en av setningene under er den neste setningen som utføres. Hva skrives ut? Hvis programmet krasjer, skriv kun Error.

(Husk at apostrofer og hermetegn som omgir strenger i kildekoden ikke er blir inkludert i utskriften.)

print(x)
print('x')
print(y[1])
print(y[2])
print(y['x'])
print(z[0])
print(z[1])
print(z[x]['y'])
print(y[z[x][x]])

Klikk på de grå feltene for å se svaret.

x = 2
y = {
    x: 42,
    'y': x,
    1: 3
}
y[42] = 'x'
z = [-1, x, y, 'z']

expressions = '''\
print(x)
print('x')
print(y[1])
print(y[2])
print(y['x'])
print(z[0])
print(z[1])
print(z[x]['y'])
print(y[z[x][x]])
'''

for expression in expressions.splitlines():
    print(f'{expression:24}-->   ', end='')
    try:
        eval(expression)
    except Exception:
        print('Error')
        

For å se fasit, klikk «kjør».

x = 3
y = 2 * x
x = 4
print(x + y)

Hva skriver dette programmet ut? (hvis programmet krasjer, skriv kun Error)

For å se fasit, klikk «kjør».

def foo(x):
    x = x + x
    return x
    
def bar(x):
    x = foo(x)
    return x + foo(x)

Anta at funksjonane ovanfor er definerte, og at neste linje med kode er ei av setningane under. Kva skriv programmet då ut? (dersom programmet krasjar, skriv berre Error)

print(foo(3))
print(bar(3))
print(foo(foo('foo')))

Klikk på dei grå felta for å sjå svaret.

def foo(x):
    x = x + x
    return x
    
def bar(x):
    x = foo(x)
    return x + foo(x)

print('print(foo(3))          -->', end=' ')
print(foo(3))

print('print(bar(3))          -->', end=' ')
print(bar(3))

print("print(foo(foo('foo'))) -->", end=' ')
print(foo(foo('foo')))

For å sjå fasit, klikk «kjør».

line = 'Per;Ida;Kari;Are;Christine;Hans;Une'
p = line.split(';')
r = []
for x in p:
    r.append(x[0])
s = ''.join(r)
print(s)

Kva skriv dette programmet ut? (Dersom programmet krasjar, skriv berre Error)

For å sjå fasit, klikk «kjør».

def foo(x, y):
    if x > y:
        x -= 10
    if y < 10:
        if y < 5:
            return 0
        if x > 5:
            y = 1000
        elif y > 2:
            x += y
        x += 1
    return x + y

Anta at funksjonen ovanfor har blitt definert, og at neste linje med kode er ei av setningane under. Kva skriv programmet då ut? (dersom programmet krasjar, skriv berre Error)

print(foo(2, 1))
print(foo(1, 6))
print(foo(11, 6))
print(foo(20, 6))
print(foo(20, 30))

Klikk på dei grå felta for å sjå svaret.

def foo(x, y):
    if x > y:
        x -= 10
    if y < 10:
        if y < 5:
            return 0
        if x > 5:
            y = 1000
        elif y > 2:
            x += y
        x += 1
    return x + y


print('print(foo(2, 1))    -->', end=' ')
print(foo(2, 1))

print('print(foo(1, 6))    -->', end=' ')
print(foo(1, 6))

print('print(foo(11, 6))   -->', end=' ')
print(foo(11, 6))

print('print(foo(20, 6))   -->', end=' ')
print(foo(20, 6))

print('print(foo(20, 30))  -->', end=' ')
print(foo(20, 30))

For å sjå fasit, klikk «kjør».

a or b in c and d

Kva for eitt av uttrykka under er nøyaktig det same som uttrykket over?

((a or b) in c) and d
(a or (b in c)) and d
(a or b) in (c and d)
a or ((b in c) and d)
a or (b in (c and d))

Klikk på det grå feltet hjå den du trur er riktig for å sjekke svaret ditt.


Oppgave 1(g) ba kandidatene om å skrive en funksjon. Noen tester var oppgitt, og man kunne teste/kjøre koden sin underveis på eksamen i denne oppgaven. Kandidatene fikk vite umiddelbart om koden man skrev passerte de oppgitte testene. De fikk ikke ikke vite resultatet på de hemmelige testene (som også måtte passeres for å få full score på oppgaven).

Skriv ein funksjon find_shortest med ein parameter li. Anta at li er ei liste med strengar. La funksjonen returnere den kortaste strengen i lista, eller None dersom lista er tom. Dersom det er fleire kortaste strengar, skal du returnere den av dei som kjem først i lista.

I denne oppgåva kan du prøve koden du skriv mot nokre testar undervegs i eksamen. For å få full utteljing på oppgåva må alle testane (inkludert hemmelege testar) passere. Testane blir køyrde når du trykkjer på knappen «test kode» under kodefeltet.

Dersom du ikkje passerer alle testane, kan likevel sensor gi delvis utteljing etter ei manuell vurdering.

Test case # Input Forventet output
1 find_shortest([‘x’, ‘yy’, ‘zzz’]) x
2 find_shortest([‘xxx’, ‘yy’, ‘zzz’, ‘æææ’]) yy
3 find_shortest([]) None
4 find_shortest([‘ccc’, ‘bb’, ‘aa’]) bb
5 find_shortest([‘abcdefg’]) abcdefg
6 find_shortest([‘foo’, ‘’])
# Skriv din kode her:
def find_shortest(li):
    ...

I tillegg til de synlige testene, måtte koden også passere de hemmelige testene for at den skulle gi full uttelling. De hemmelige testene var:

Test case # Input Forventet output
7 find_shortest([‘aa’]) aa
8 find_shortest([‘abc’, ‘aa’]) aa
9 find_shortest([‘hundemat’, ‘kattemat’, ‘grisemat’]) hundemat
10 find_shortest([‘det’, ‘korteste’, ‘ordet’, ’er’, ‘ikke’, ‘alltid’, ‘først’]) er

Oppgaven kunne løses på ulike måter. Noen eksempler

def find_shortest(li):
    if len(li) == 0:
        return None
    best = li[0]
    for s in li:
        if len(s) < len(best):
            best = s
    return best
def find_shortest(li):
    best = None
    for s in li:
        if best is None or len(s) < len(best):
            best = s
    return best

Denne virker også, men lov meg at du aldri 😜 skriver kode som dette:

def find_shortest(li): return (sorted(li,key=len)or[None])[0]

Sensor benytter skjønn, og vurderer hvordan besvarelsen stiller seg relativt til besvarelser beskrevet som følger:

  • 8 poeng: koden har alle nødvendige elementer, men liten bug eller skrivefeil hindrer koden i å svare rett.
  • 6 poeng: koden fungerer i grove trekk, men håndterer ikke edge-caser som for eksempel den tomme listen eller når svaret er først eller sist i listen.

Hvis koden ikke er i en av kategoriene over, kan den rettes oppover fra 0 poeng ved tilstedeværelsen av elementer:

  • 1 poeng: koden inneholder en løkke med riktig antall iterasjoner.
  • 2 poeng: koden håndterer relevante hjørnetilfeller, som for eksempel den tomme listen.
  • 2 poeng: fornuftig bokføring (variabel som holder på beste løsning) og oppdatering av denne i løkken

Det er ikke mulig å få mer enn 8 poeng hvis de automatiske testene ikke passerer.

2 Forklaring
2(a)  Snu ulikheten i en bitteliten språkmodell (12 poeng)

Bakgrunn

(ikkje eigentleg viktig for denne oppgåva)

I førelesing 18. oktober dette semesteret, skreiv vi ein bitteliten språkmodell som ein kunne trene opp til å finne på nye ord som liknar på ulike språk, avhengig av kva treningsdata vi brukte.

Språkmodellen vår bestod av to algoritmar:

  • Ein treningsalgoritme som las treningsdata og tel kor mange gongar ein bokstav opptrer i ulike kontekstar. Dette blir lagra som vekter i ei json-fil.
  • Ein inferens-algoritme som finn på nye ord basert på ein kombinasjon av vektene i json-fila og tilfeldighet.

I vedlagte kode (pdf, eller sjå under) ser du ein versjon av inferens-algoritmen, som er litt forbetra i forhold til den vi såg i førelesinga. Då vi i førelesinga berre såg på den relevante delen av konteksten som eitt enkelt symbol, tek denne varianten av koden også omsyn til større lengder på den relevante konteksten; først prøver den ein relevant kontekst på storleik 3, og deretter forsøker den gradvis kortare og kortare relevante kontekstar heilt til den finn ein kontekst som er kort nok til at det finst noko å velje mellom i vektene.

PS: hugs at ein god utviklar har evna til å oversjå informasjon som ikkje er relevant.

import json
import random
from pathlib import Path


def main():
    weights = json.loads(Path('weights.json').read_text(encoding='utf-8'))

    res = ''
    for _ in range(12):
        res += get_token(res, weights)
    print(res)
    

def get_token(all_context, weights):
    context_length = 3
    while context_length > 0:
        relevant_context = all_context[-context_length:]
        if relevant_context in weights:
            vector = weights[relevant_context]
            if len(vector) > 2:
                break
        context_length -= 1

    letters = list(vector.keys())
    rel_weights = list(vector.values())

    random_letter = random.choices(letters, weights=rel_weights, k=1)[0]
    return random_letter


if __name__ == '__main__':
    main()

Oppgåvetekst

Betinginga i while-løkka er context_length > 0. Kva ville skjedd om vi snudde ulikskapen slik at det vart context_length < 0 i staden? Forklar så detaljert som mogleg kva som ville skjedd. Kva slags feil ville oppstått? Kva klasse av feil (syntaks, køyringstid, logisk) høyrer feilen til? Kan du seie noko om kva som skil denne typen feil frå andre typer feil?

Maksimalt 600 ord.

Hva som skjer

  • Dersom vi snur ulikheten, vil betingelsen i while-løkken ikke være oppfylt. Det innebærer at løkkekroppen aldri utføres, og neste setning blir i stedet letters = list(vector.keys()).
  • Denne setningen utføres ved at vi først evaluerer uttrykket på høyre side, list(vector.keys())
  • Dette uttrykket evalueres ved at Python først ser på del-uttrykket som er inne i parantesene, vector.keys().
  • Nå leter Python etter en variabel som heter vector. Fordi løkkekroppen i while-løkken aldri ble utført, vil det imidlertid ikke eksistere en slik variabel. Programmet krasjer med en kjøretidsfeil fordi navnet vector ikke ble funnet (også kjent som NameError*).

*Akkurat her er feiltypen faktisk UnboundLocalError, men dette er en undertype av NameError.

Om kjøretidsfeil

I Python er NameError en klassisk kjøretidsfeil. Kjøretidsfeil er den typen feil som blir oppdaget underveis i kjøring av et Python-program. Det er også kjent som at programmet krasjer.

  • Til forskjell fra syntaks-feil blir ikke kjøretidsfeil oppdaget før programmet starter, men gjør seg først til kjenne etter at Python allerede har begynt å jobbe seg gjennom koden.
  • Til forskjell fra logiske feil fører kjøretidsfeil til at programmet stopper opp og gjør til kjenne for brukeren at noe har gått galt.

Forklaring av hva som skjer (6 poeng)

  • 2 poeng: identifiserer at løkkekroppen ikke utføres.
  • 3 poeng: identifiserer at vector ikke blir definert, og at programmet krasjer på linjen som henviser til denne variabelen.
  • 1 poeng: programmet krasjer med NameError (eller UnboundLocalError).

Feilklasser (3 poeng)

  • 1 poeng: programmet krasjer med en kjøretidsfeil.
  • 2 poeng: god drøfting av feiltypen.

Helhetsvurdering (3 poeng)

Poengene i denne kategorien deles ut helhetlig, ikke punkt for punkt. Sensor kan bruke dennne kategorien for å trekke opp eller ned kandidater som demonstrerer større eller mindre forståelse enn hva rubrikken forøvrig tilsier. Sensor ser etter:

  • Det er lett å forstå hva kandidaten mener.
  • God/relevant bruk av faguuttrykk der det er formålstjenelig.
  • Begir seg ikke ut på forklaringer av kode som ikke er relevant.
  • Demonstrerer generelt god forståelse.

3 Kodeskriving
3(a)  Lister i minnet (3 poeng)
Illustrasjon av variabler og minnets tilstand slik det skal gjenskapes

Skriv ein kodesnutt slik at minnet får den tilstanden som er vist ovanfor.

Klikk på «se steg» -knappen for å verifisere at denne koden gir riktig bilde av minnet.

a = [["hello", "world"], ["hello", "world"]]
b = a
  • 1 poeng hvis a og b begge er lik [["hello", "world"], ["hello", "world"]]
  • 1 poeng hvis a[0] og a[1] ikke er aliaser, a[0] er alias med b[0] og a[1] er alias med b[1]
  • 1 poeng hvis a og b er aliaser`

Sensor har anledning til å gjøre en skjønnsmessig justering i tilfeller der hen mener disse testene ikke gir et representativt bilde av hva studenten demonstrerer.

Sensor skal ikke kreve syntaktisk korrekthet – men hvis besvarelsen er syntaktisk korrekt eller er rettet opp til å bli det kan poengsummen som beskrevet over regnes ut med følgende kode:

points = sum([
    a == b == [["hello", "world"], ["hello", "world"]],
    a[0] is not a[1] and a[0] is b[0] and a[1] is b[1],
    a is b,
])
# PS: i kontekst av matematisk aritmetikk regnes True som 1 og False som 0.
3(b)  Lister som peker på hverandre (3 poeng)
Illustrasjon av variabler og minnets tilstand slik det skal gjenskapes

Skriv ein kodesnutt slik at minnet får den tilstanden som er vist ovanfor. Hint: Du må mutere minst ei av listene etter at ho blir oppretta første gong.

Et knippe løsninger som alle er riktige. Klikk på «se steg» -knappen for å verifisere at koden gir riktig bilde av minnet.

a = ['hello']
b = ['world', a]
a.append(b)
a = ['hello']
b = ['world', a]
a += [b]
a = ['hello']
b = ['world']
a.extend([b])
b.extend([a])
a = ['hello', 'anything']
b = ['world', a]
a[1] = b
  • 3 poeng gis for en korrekt løsning. Da evaluerer dette uttrykket til True:
(
  a[0] == 'hello' and b[0] == 'world' and 
  a[1] is b and b[1] is a and
  len(a) == len(b) == 2
)

Det skal ikke gis trekk dersom .append er navngitt feil eller dersom kandidaten har blandet .append med .extend eller .add eller andre tilsynelatende muterende metoder.

  • 2 poeng gis for løsninger som har riktig idé, men hvor muterende operasjoner ikke er gyldige, for eksempel
a = ['hello']
b = ['world', a]
a[1] = b
  • 2 poeng gis for løsninger som har riktig idé, men hvor det benyttes en ikke-destruktiv operasjon for å endre på listen i stedet for en muterende opersajon, for eksempel
a = ['hello']
b = ['world', a]
a = a + [b]
  • 1 poeng gis for løsninger hvor det er mer tydelig at man lager en ny liste i stedet for å mutere, for eksempel
a = ['hello']
b = ['world', a]
a = ['hello', b]
  • 1 poeng gis for en løsning som krasjer fordi variabler ikke er definert enda på tidspunktet de referes til, for eksempel:
a = ['hello', b]
b = ['world', a]
  • 1 poeng gis for løsninger som mangler én av pekerne til den andre listen, for eksempel
a = ['hello']
b = ['world', a]

Løsninger som er lengre unna en riktig løsning enn eksemplene over gir 0 poeng..

3(c)  Iskremmaskinens logg (10 poeng)

Bakgrunn

På iskremfabrikken er det en iskremmaskin. Maskinen har mulighet for å angi hvilken smak iskremen som blir produsert skal ha. Maskinen skriver ned alt som skjer i en logg-fil.

  • Logg-filen er en JSON-fil, og objektet filen beskriver er et oppslagsverk med en nøkkel “log” som viser til en liste med hendelser.
  • En hendelse er representert som et oppslagsverk, og har alltid nøklene “timestamp” og “event_type”.
  • Dersom “event_type” er strengen “set_flavour”, finnes det også en nøkkel “value” i samme hendelse hvor den tilhørende verdien er en streng som beskriver smaken maskinen fra nå av er innstilt på.

Maskinen vil aldri lage en iskrem uten at det er valgt en smak først. Du kan anta at tidspunktene for hendelsene i loggen alltid er i stigende rekkefølge. Se et eksempel på en slik logg-fil machine_log.json i vedlegget til oppgaven (pdf eller åpne luken under).

{
  "log": [
    {
      "timestamp": "2024-11-25T08:00:00",
      "event_type": "machine_started"
    },
    {
      "timestamp": "2024-11-25T08:00:01",
      "event_type": "set_flavour",
      "value": "vanilje"
    },
    { "timestamp": "2024-11-25T08:00:02",
      "event_type": "ice_cream_created"
    },
    {
      "timestamp": "2024-11-25T08:00:03",
      "event_type": "ice_cream_created"
    },
    {
      "timestamp": "2024-11-25T09:00:04",
      "event_type": "set_flavour",
      "value": "sjokolade"
    },
    {
      "timestamp": "2024-11-25T09:00:05",
      "event_type": "ice_cream_created"
    },
    {
      "timestamp": "2024-11-25T10:00:06",
      "event_type": "set_flavour",
      "value": "vanilje"
    },
    {
      "timestamp": "2024-11-25T10:00:07",
      "event_type": "ice_cream_created"
    }
  ]
}

Oppgavetekst

Skriv en funksjon production_summary med en parameter path. Anta at path er et filnavn som viser til en logg-fil fra iskremmaskinen.

La funksjonen returnere et oppslagsverk der de ulike smakene som finnes i loggen er nøkler, mens tilhørende verdier er antallet iskrem av den gitte typen som ble produsert. Det er kun hendelser av typen “ice_cream_created” som skal bli talt opp for hver av de ulike smakene.

For eksempel, hvis path viser til eksempelfilen skal funksjonen returnere et oppslagsverk

{
    "vanilje": 3,
    "sjokolade": 1
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from pathlib import Path 
import json

def production_summary(path):
    # Read the JSON file
    raw = Path(path).read_text(encoding='utf-8')
    data = json.loads(raw)

    # Create the summary
    summary = {}
    flavour = None
    for entry in data['log']:
        if entry['event_type'] == 'set_flavour':
            flavour = entry['value']
            if flavour not in summary:
                summary[flavour] = 0
        elif entry['event_type'] == 'ice_cream_created':
            summary[flavour] += 1
    return summary

Sensorveiledningen tar utgangspunkt i løsningsforslaget. Dersom kandidaten sin løsning benytter seg av andre datastrukturer eller algoritmer, gjør sensor en skjønnsmessing vurdering av hvor egnet fremgangsmåten er, og i hvor stor grad løsningen ivaretar korresponderende hensyn.

Linjenummerne under henviser til de delene av løsningsforslaget som er relevant for det gitte kulepunktet. Hvert av punktene under gir 1 poeng hver:

  • Leser inn JSON-fil på fornuftig måte (linje 6–7).
  • Det opprettes en eller annen egnet form for telleverk (typisk et tomt oppslagsverk) på et egnet sted (typisk før løkken) (linje 10).
  • Det opprettes en form for bokføring (en variabel) for å huske hvilken smak som til en hver tid er gjeldende (linje 11, men det holder egentlig med kun linje 14).
  • En løkke som går igjennom elementene i listen tilhørende "log" -nøkkelen i JSON-filen (line 12).
  • Kandidaten viser forståelse for den nøstede strukturen av dataen, altså at oppslag/indekseringer gir mening i forhold til JSON-filens struktur (linjer 12, 13, 14 og 17).
  • Den gjeldende smak oppdateres i løkken under riktige betingelser (linje 13–14).
  • Telleverket oppdateres dynamisk med nye smaker basert på informasjonen i loggen (linje 15–16).
  • Telleverket økes under betingelsen av at man ser en “ice_cream_created” (linje 17–18).

Til slutt gjøres en helhetsvurdering verdt 2 poeng. Sensor vurderer her helheten av besvarelsen og om delene fungerer i sammenheng. Sensor skal ikke trekke for syntaksfeil dersom intensjonen er tydelig og algoritmen er presist beskrevet.

3(d)  Varetelling på iskremfabrikken (14 poeng)

Bakgrunn

Vareteljing ved iskremfabrikken finn som vanleg stad 24. november, så akkurat denne dagen skjer det ingenting anna ved fabrikken. Og no som vi endeleg er ferdige med å telje, har tida kome for å ta ein liten fot i bakken for å sjå om tala stemmer. Vi skal samstemme informasjon frå fleire kjelder:

  • machine_log.json er ei fil som blir produsert av iskremmaskina. I denne fila finn vi informasjon om alle hendingar som skjer i iskremmaskina mellom vareteljinga i fjor og i år. Loggfila inkluderer ei hending for kvar gong ein boks med iskrem kjem ut på enden av samlebandet. Denne fila inneheld altså informasjon om kva som kjem inn på iskrem-lageret.
  • packing_orders.json er ei fil som inneheld oversikt over alle pakkeordrar. Det er éi pakkeordre for kvar lastebil som forlèt lageret, og kvar pakkeordre inneheld informasjon om kor mange boksar iskrem av kvar type som blir med i lastebilen.
  • inventory_2024.csv og inventory_2025.csv er semikolon-separerte CSV-filer som inneheld talet på iskremboksar av kvar type for vareteljingane i høvesvis i fjor og i år.

Du kan sjå eksempel på korleis filene ser ut i vedlegg (pdf eller sjå lukane under). Gjer ditt beste for å forstå strukturen i korleis filene er bygde opp.

{
  "log": [
    {
      "timestamp": "2024-11-25T08:00:00",
      "event_type": "machine_started"
    },
    {
      "timestamp": "2024-11-25T08:00:01",
      "event_type": "set_flavour",
      "value": "vanilje"
    },
    { "timestamp": "2024-11-25T08:00:02",
      "event_type": "ice_cream_created"
    },
    {
      "timestamp": "2024-11-25T08:00:03",
      "event_type": "ice_cream_created"
    },
    {
      "timestamp": "2024-11-25T09:00:04",
      "event_type": "set_flavour",
      "value": "sjokolade"
    },
    {
      "timestamp": "2024-11-25T09:00:05",
      "event_type": "ice_cream_created"
    },
    {
      "timestamp": "2024-11-25T10:00:06",
      "event_type": "set_flavour",
      "value": "vanilje"
    },
    {
      "timestamp": "2024-11-25T10:00:07",
      "event_type": "ice_cream_created"
    }
  ]
}
{
  "orders": [
    {
      "date": "2024-12-01",
      "recepient": {
        "name": "Bellevuebakken",
        "street": "Nystuveien 11",
        "zip": 5019,
        "town": "Bergen"
      },
      "items": [
        { "flavour": "vanilje", "quantity": 8 },
        { "flavour": "sjokolade", "quantity": 10 },
        { "flavour": "pistasj", "quantity": 10 }
      ]
    },
    {
      "date": "2025-01-14",
      "recepient": {
        "name": "Colonialen",
        "street": "Strandgaten 18",
        "zip": 5013,
        "town": "Bergen"
      },
      "items": [
        { "flavour": "sjokolade", "quantity": 1 },
        { "flavour": "vanilje", "quantity": 1 }
      ]
    }
  ]
}
name;count
vanilje;10
sjokolade;100
pistasj;20
name;count
vanilje;1
sjokolade;90
pistasj;5


Oppgåvetekst

Skriv eit program som reknar ut (og skriv ut) kor mange iskremer av kvar type som på mystisk vis har forsvunne i løpet av året.

For eksempel, dersom filene er som i det viste dømet, skal programmet skrive ut

Forsvunne iskrem
vanilje: 3
pistasj: 5

Forklaring på kvifor det manglar tre vaniljeis: I fjor hadde vi 10. I løpet av året vart det produsert 3, så til saman 13. Det vart sendt ut 8+1=9 is til kundar. Då er det forventa at det skal vere 13-9=4 vaniljeis på lageret, men vi fann berre 1 i årets vareteljing. Så då må 3 ha forsvunne.

Du kan bruke ein antatt fungerande versjon av funksjonen du skreiv i førre oppgåve fritt utan å skrive ho på nytt her.

I dette løsningsforslaget benytter vi oss av oppslagsverk hvor nøkler er iskremsmaker og tilhørende verdier er antall, for eksempel

{
    "vanilje": 3,
    "sjokolade": 4
}

Vi bruker dette formatet for å representere alle de ulike kildene for input: både varetellinger, produksjon og solgte iskrem representeres slik. Så første steg er i bunn og grunn å konvertere alle inputfiler til dette formatet.

Etter at det er gjort, ønsker vi å regne ut forventet antall iskrem av hver type. Til slutt sammenligner vi dette med siste varetelling, og skriver ut dersom tallene ikke stemmer overens.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from task3c import production_summary
from pathlib import Path
from csv import DictReader
from io import StringIO
import json


MACHINE_LOG_PATH = 'machine_log.json'
PACKING_ORDERS_PATH = 'packing_orders.json'
PREVIOUS_INVENTORY_PATH = 'inventory_24.csv'
CURRENT_INVENTORY_PATH = 'inventory_25.csv'


def main():
    expected_inventory = get_expected_inventory()
    actual_inventory = read_inventory(CURRENT_INVENTORY_PATH)

    print('Forsvunnede iskrem')
    for flavour in expected_inventory:
        expected_count = expected_inventory[flavour]
        actual_count = actual_inventory.get(flavour, 0)
        difference = expected_count - actual_count
        if difference != 0:
            print(f'{flavour}: {difference}')


def get_expected_inventory():
    inventory_last_year = read_inventory(PREVIOUS_INVENTORY_PATH)
    items_produced = production_summary(MACHINE_LOG_PATH)
    items_shipped = shipment_summary(PACKING_ORDERS_PATH)

    expected_inventory = inventory_last_year.copy()
    for flavour in items_produced:
        if flavour not in expected_inventory:
            expected_inventory[flavour] = 0
        expected_inventory[flavour] += items_produced[flavour]

    for flavour in items_shipped:
        expected_inventory[flavour] -= items_shipped[flavour]
    
    return expected_inventory


def shipment_summary(path):
    raw = Path(path).read_text(encoding='utf-8')
    data = json.loads(raw)

    summary = {}
    for order in data['orders']:
        for item in order['items']:
            flavour = item['flavour']
            count = item['quantity']
            if flavour not in summary:
                summary[flavour] = 0
            summary[flavour] += count

    return summary


def read_inventory(path):
    raw = Path(path).read_text(encoding='utf-8')
    reader = DictReader(StringIO(raw), delimiter=';')
    data = list(reader)

    counts = {}
    for row in data:
        flavour = row['name']
        count = int(row['count'])
        counts[flavour] = count
    return counts


if __name__ == '__main__':
    main()
from task3c import production_summary
from pathlib import Path
from csv import DictReader
from io import StringIO
import json


def main():
    old_inv = read_inventory('inventory_24.csv')
    produced = production_summary('machine_log.json') 
    shipped = shipment_summary('packing_orders.json')
    cur_inv = read_inventory('inventory_25.csv')

    diffs = minus(minus(plus(old_inv, produced), shipped), cur_inv)

    print('Forsvunnede iskrem')
    for flavour, missing in diffs.items():
        if missing:
            print(f'{flavour}: {missing}')


def plus(a, b):
    result = a.copy()
    for key, value in b.items():
        if key not in result:
            result[key] = 0
        result[key] += value  # minus-metoden er lik, men med minus her
    return result


def minus(a, b):
    # Alternativ måte å skrive plus/minus -funksjonene på som one-liner
    return { k: a.get(k, 0) - b.get(k, 0) for k in a.keys() | b.keys() }


def shipment_summary(path):
    data = json.loads(Path(path).read_text(encoding='utf-8'))
    summary = {}
    for order in data['orders']:
        for item in order['items']:
            flavour = item['flavour']
            count = item['quantity']
            if flavour not in summary:
                summary[flavour] = 0
            summary[flavour] += count
    return summary


def read_inventory(path):
    raw = Path(path).read_text(encoding='utf-8')
    data = list(DictReader(StringIO(raw), delimiter=';'))
    return { row['name']: int(row['count']) for row in data }


if __name__ == '__main__':
    main()

Sensorveiledningen tar utgangspunkt i løsningsforslaget, og linjenummerne i parantes viser til hvilke linjer i løsningsforslaget som ivaretar et gitt hensyn fra sensorveiledningen.

Dersom kandidaten benytter en annen fremgangsmåte og andre datastrukturer, må sensor gjøre en skjønnsmessig vurderig av i hvor stor grad fremgangsmåten er hensiktsmessig, og hvordan de ulike elementene korresponderer med sensorveiledningen

Finne antall av hver type fra vareopptellinger (2 poeng)

  • (1 poeng) Leser CSV-filer på fornuftig vis (linje 61–63). CSV-filen kan også leses som en 2D-liste, og det gir også full uttelling om man håndterer det manuelt med .split-metoden.
  • (1 poeng) God strategi for å hente ut hvor mange som finnes av en gitt smak (linje 65–69). I eksempelløsningen konstruerer vi et oppslagsverk, men det gir også full uttelling å lagre dette på en annen måte og heller gjøre «arbeidet» ved hjelp av løkker som søker gjennom dataen på leting etter smaker når de faktisk trengs senere.

Finne antall av hver type fra produksjonslogg (1 poeng)

  • Viser forståelse for argumenter og returtype fra production_summary (linje 29, 33, 36).

Finne antall av hver type fra pakke-ordrer (5 poeng)

  • (1 poeng) Viser forståelse for JSON-strukturen (riktige oppslag) (linje 49–52).
  • (1 poeng) En løkke over ordrer (linje 49).
  • (2 poeng) En nøstet løkke over bokstyper i hver ordre (linje 50).
  • (1 poeng) Hensiktsmessig bokføring/bruk av tellevariabler (linje 48, 55). Løkkene og bokføringen kan gjøres i forbindelse med at man oppretter et oppslagsverk like etter at dataen leses (slik som i eksempelløsningen), men det gir også full uttelling å gjøre dette når dataen faktisk trengs senere.

Dynamisk håndtering av iskremsmaker (2 poeng). Koden håndterer at vi ikke kjenner iskremsmakene på forhånd. Det ok å anta at alle iskremsmaker som finnes er nevnt i enten gammel varetelling eller i produksjonsloggen (linje 19, 34–35, 53–54, 69).

Utregning av hvor mange iskrem som mangler (2 poeng). I eksempelløsning gjøres dette på linje 32–39 og 20–22.

Til slutt gjøres en helhetsvurdering verdt 2 poeng. Sensor vurderer her helheten av besvarelsen og om delene fungerer i sammenheng. Sensor skal ikke trekke for syntaksfeil dersom intensjonen er tydelig og algoritmen er presist beskrevet, men ser på om programmet som helhet henger sammen og er logisk strukturert.