Kalenderluke 11: Hvem er input?


torsdag 12. desember 2013 Julekalender Ruby

Luke 11 i utviklernes julekalender 2013 inneholdt litt ruby-kode, navnet til en hemmelig valgt deltager, og et output generert fra dette navnet. Oppgaven var å finne hvilket navn som var brukt, og da har man i grunnen to ulike fremgangsmåter.

Forsøke alle navnene

Funksjonen i oppgaven går kun én vei; man kan ikke starte med output og få ut hva input var. Derimot kan man forsøke å kalle funksjonen med alle gyldige input – dvs. navnene til alle deltagerne i julekalenderen – og se hvilke navn som gir det riktige output. Dette er en brukbar strategi om man har Ruby installert, og jeg antar det var dette de fleste gjorde.

Men om du ønsker å forstå hva funksjonen gjør så kan du også komme frem til navnet med litt hjernekraft...

Analysere koden og output

def process input
  input.
    downcase.
    split('').
    sort.
    chunk {|c| c.ord }.
    inject('') {|a,x| "#{a}#{x[0]}#{x[1].size}" }
end

Funksjonen process sender input gjennom en pipeline av transformerende funksjonskall. La oss begynne med et tenkt input "Janne", og se hva som skjer i hvert steg.

"Janne".downcase

downcase endrer alle store bokstaver til små bokstaver. Resultatet er "janne".

"janne".split('')

split med en tom streng som argument deler opp strengen bokstav for bokstav. Resultatet er ["j", "a", "n", "n", "e"].

["j", "a", "n", "n", "e"].sort

Her sorterer jeg arrayet, som gir ["a", "e", "j", "n", "n"].

["a", "e", "j", "n", "n"].chunk {|c| c.ord }

chunk brukes til å gruppere elementene i arrayet. Jeg bestemmer at tegnene (c) skal grupperes på integer/ASCII-verdien (c.ord). Resultatet er en enumerasjon som inneholder [[97, ["a"]], [101, ["e"]], [106, ["j"]], [110, ["n", "n"]]].

[[97, ["a"]], 
 [101, ["e"]], 
 [106, ["j"]], 
 [110, ["n", "n"]]].inject('') {|a,x| 
   "#{a}#{x[0]}#{x[1].size}" 
}

inject er Rubys funksjon for å "slå sammen" en enumerasjon til én verdi - det samme som Aggregate i C#, fold i språk som Haskell / ML / F#, reduce i Clojure, Accumulate og Compress andre steder. Jeg har snakket mye om bruk av denne typen funksjoner her på bloggen.

I dette tilfellet bygger jeg opp en streng av ASCII-verdiene konkatenert med antall bokstaver i hver gruppe. Resultatet blir dermed 971101110611102.

Når vi forstår dette kan vi forsøke å bryte opp output fra oppgaven, som var 32297310121031104110611071108111011111114111511161. Vi kan anta at vi holder oss innenfor en range av verdier fra 32 (space) til rundt et par hundre om navnet inkluderer noen sære tegn.

32 2   #=> To space, navnet består av tre deler! 
97 3   #=> Tre a
101 2  #=> To  e
103 1  #=> En  g
104 1  #=> En  h
106 1  #=> En  j
107 1  #=> En  k
108 1  #=> En  l
110 1  #=> En  n
111 1  #=> En  o
114 1  #=> En  r
115 1  #=> En  s
116 1  #=> En  t

Og så gjenstår det å bruke hjernens mønstergjenkjennings-egenskaper til å sette sammen bokstavene til et fornuftig navn. Eller å endelig innse at det er lurere å kjøre funksjonen på alle navnene likevel :P

Hvem klarte det raskest?

Hurtigst til å svare riktig på oppgaven i luke 11 var Magnar Sveen, som begynner å få en solid ledelse sammenlagt. Han klarte oppgaven på 5 minutter og 46 sekunder.

Her er en oversikt over poengutviklingen for noen utvalgte spillere:

luke11_tabell

Og de samme tallene presentert grafisk (klikk for full størrelse):

luke11_chart


comments powered by Disqus