Hoppa till huvudinnehåll

Rövarspråksgenerator

· 10 min att läsa
Filip Tammergård
Programmerare på Frilans Finans

Rövarspråket sägs ha uppfunnits av Astrid Lindgrens make Sture Lindgren när han lekte med sina kompisar som liten. Astrid Lindgrens böcker om Kalle Blomkvist gjorde språket populärt i Sverige.

Rövarspråket är ett talbaserat språk där varje konsonant ersätts med konsonanten + o + konsonanten igen. Utifrån den enkla regeln skulle det vara busenkelt att göra en rövarspråksgenerator. När det kommer till rövarspråket i skrift – hädanefter kallat rövarskrift – finns det dock några fler aspekter att beakta. I det här inlägget är det alltså egentligen inte en rövarspråksgenerator som byggs, utan snarare en rövarskriftsgenerator.

TL;DR

  • Den enkla talregeln (konsonant + "o" + konsonant) behöver några tillägg för att fungera i skrift – framförallt hantering av "x" och versaler.
  • Det här inlägget går igenom två strategier för att bygga en generator i JavaScript: att loopa över konsonanterna, och att loopa över texten som ska översättas.
  • En regex med en callback kan korta ner alltihop till två rader, som visas som det slutgiltiga alternativet.

Testa den färdiga rövarskriftsgeneratorn här:

Rorövovarorsospoproråkoketot

Rövarspråksmodellen

Den mest grundläggande beskrivningen av rövarspråket skulle kunna vara enligt följande:

Efter varje konsonant läggs bokstaven "o" och samma konsonant igen till.

Ordet "hej" blir alltså "hohejoj" och "rövarspråket" blir "rorövovarorsospoproråkoketot". Denna enkla regel räcker när det gäller talspråk. När det gäller skrift krävs dock några tilläggsregler. Hur fungerar det exempelvis med versaler och gemener i rövarskrift? Utgående från beskrivningen ovan ska samma konsonant skrivas igen. Betyder det att "Hej" blir "HoHejoj"? Jag utgår ifrån att regler kring versaler och gemener ska fungera på samma sätt som svenska språket i övrigt – inga versaler mitt i ord. "Hej" borde alltså bli "Hohejoj".

Talspråk får anses vara rövarspråkets bas. Rövarskrift utgår alltså från att det ska bli rätt när skriften uttalas. Bokstaven "x" är en konsonant och borde enligt modellen översättas till "xox". Men eftersom "x" uttalas "ks" funkar det utgående från talspråket bättre om "x" översätts till "koksos". Ordet "yxa" blir alltså "ykoksosa" snarare än "yxoxa".

Reglerna kan nu sammanställas enligt följande:

  1. Bokstaven "x" byts ut till "ks".
  2. Efter varje konsonant läggs bokstaven "o" och samma konsonant igen till.
  3. Vid versala konsonanter är det bara konsonanten innan "o" som är versal, den andra är gemen.

Ordningen spelar roll: att köra regel 1 först låter konsonantregeln behandla alla konsonanter likadant efteråt, istället för att huvudloopen ska behöva bära med sig ett specialfall. Den här förbehandlingsmodellen är vad de flesta funktionsbaserade exempel nedan utgår från – exempel 10 visar inline-alternativet.

Bygga en rövarskriftsgenerator

Låt oss fundera kring hur en rövarskriftsgenerator skulle kunna byggas i JavaScript. Först och främst kan vi påminna oss om den enkla regeln för rövarspråket i tal:

Efter varje konsonant läggs bokstaven "o" och samma konsonant igen till.

Utgå från konsonanterna

Den allra enklaste approachen till problemet som jag kan komma på är att loopa igenom en variabel med alla konsonanter och i varje varv byta ut den aktuella konsonanten i meningen som ska översättas med konsonanten + "o" + samma konsonant igen. Så här:

Exempel 1
const consonants = "bcdfghjklmnpqrstvwxz"

let sentence = "Min mening"

consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
})

// sentence = "Minon momenoninongog"

Minon momenoninongog

Om du testar att skriva in något i rutan ovan ser du att generatorn faktiskt funkar – ibland. Eftersom konsonanterna i variabeln är gemena och JavaScript gör skillnad på gemener och versaler kommer generatorn inte att göra någonting med versala konsonanter som skrivs in. Varför inte lägga till alla konsonanter som versaler i variabeln också då?

Exempel 2
const consonants = "bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ"

let sentence = "Min mening"

consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
})

// sentence = "MoMinon momenoninongog"

MoMinon momenoninongog

Jo visst, då hanteras versalerna också. Men det känns inte särskilt snyggt att behöva ange alla konsonanter två gånger. Att ordet "Min" blir "MoMinon" strider också mot regel 3 som säger att versaler bara ska förekomma för konsonanten före "o" i en översättning. Hur kan vi förbättra generatorns kod? I JavaScript finns de inbyggda funktionerna toUpperCase() och toLowerCase() som kan användas på strängar. Med hjälp av dessa kan problemet lösas på många olika sätt – exempelvis så här:

Exempel 3
const consonants = "bcdfghjklmnpqrstvwxz"

let sentence = "Min mening"

consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
sentence = sentence.replaceAll(
consonant.toUpperCase(),
consonant.toUpperCase() + "o" + consonant,
)
})

// sentence = "Mominon momenoninongog"

Mominon momenoninongog

Eller så här:

Exempel 4
const consonants = "bcdfghjklmnpqrstvwxz"
const allConsonants = consonants + consonants.toUpperCase()

let sentence = "Min mening"

allConsonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(
consonant,
consonant + "o" + consonant.toLowerCase(),
)
})

// sentence = "Mominon momenoninongog"

Mominon momenoninongog

Av exempel 3 och 4 tycker jag att 3 är snyggast eftersom det är lättare att förstå vad den gör vid en snabb anblick – och exempel 4 är i praktiken samma lösning som exempel 2, fast krångligare. Jag utgår därför från exempel 3 i nästa exempel.

Men hur ska vi hantera regel 1? Regeln säger att bokstaven "x" ska bytas ut till "ks". Enklast är att helt enkelt göra det bytet innan resten av översättningen görs:

Exempel 5
const consonants = "bcdfghjklmnpqrstvwxz"

let sentence = "Xylofoner är fina"

sentence = sentence.replaceAll("x", "ks").replaceAll("X", "Ks")
consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
sentence = sentence.replaceAll(
consonant.toUpperCase(),
consonant.toUpperCase() + "o" + consonant,
)
})

// sentence = "Koksosylolofofononeror äror fofinona"

Koksosylolofofononeror äror fofinona

Observera att det inte räcker med att bara byta "x" mot "ks", versala "X" behöver också bytas ut, vilket blir till "Ks" för att reglerna ska stämma.

Nu följs alla tre regler och generatorn funkar som den ska. Men det finns mycket kvar att önska av koden i sig. Eftersom den inte innehåller några egna funktioner med deskriptiva namn måste man själv lista ut vad koden åstadkommer rad för rad. Att bryta ut saker i egna funktioner gör koden längre men lättare att förstå – och att lyfta ut förbehandlingen av "x" i en egen funktion låter huvudsteget fokusera på en sak i taget:

Exempel 6
const consonants = "bcdfghjklmnpqrstvwxz"

function expandX(sentence) {
return sentence.replaceAll("x", "ks").replaceAll("X", "Ks")
}

function toRobber(sentence) {
sentence = expandX(sentence)
consonants.split("").forEach((consonant) => {
sentence = sentence.replaceAll(consonant, consonant + "o" + consonant)
sentence = sentence.replaceAll(
consonant.toUpperCase(),
consonant.toUpperCase() + "o" + consonant,
)
})
return sentence
}

const sentence = "Xylofoner är fina"

const robberSentence = toRobber(sentence)

// robberSentence = "Koksosylolofofononeror äror fofinona"

Koksosylolofofononeror äror fofinona

Utgå från meningen som ska översättas

Även om exempel 6 funkar och har en ganska lättläst kod är strategin att utgå från konsonanterna bakvänd av två anledningar: loopen itererar över konsonanterna snarare än över texten vi faktiskt vill översätta, och sentence måste muteras i varje iteration eftersom forEach inte returnerar något. Vi testar därför att åstadkomma samma generator genom att börja från andra hållet.

Exempel 7
const consonants = "bcdfghjklmnpqrstvwxz"

const sentence = "Xylofoner är fina"

const sentenceWithExpandedX = sentence
.replaceAll("x", "ks")
.replaceAll("X", "Ks")

let robberSentence = ""

sentenceWithExpandedX.split("").forEach((letter) => {
if (consonants.split("").includes(letter)) {
robberSentence += letter + "o" + letter
} else if (consonants.toUpperCase().split("").includes(letter)) {
robberSentence += letter + "o" + letter.toLowerCase()
} else {
robberSentence += letter
}
})

// robberSentence = "Koksosylolofofononeror äror fofinona"

Koksosylolofofononeror äror fofinona

Exempel 7 genererar rövarskrift enligt reglerna, men det finns ett par problem med koden: robberSentence muteras i varje iteration och det tar en stund att förstå vad koden faktiskt gör. Den inbyggda funktionen map löser båda: den returnerar en ny array istället för att mutera en, så vi kan släppa den tomma robberSentence-ackumulatorn och låta kedjan producera resultatet direkt:

Exempel 8
const consonants = "bcdfghjklmnpqrstvwxz"

const sentence = "Xylofoner är fina"

const robberSentence = sentence
.replaceAll("x", "ks")
.replaceAll("X", "Ks")
.split("")
.map((letter) =>
consonants.split("").includes(letter.toLowerCase())
? letter + "o" + letter.toLowerCase()
: letter,
)
.join("")

// robberSentence = "Koksosylolofofononeror äror fofinona"

Koksosylolofofononeror äror fofinona

Det här tycker jag är en snygg lösning. Men även den här koden skulle må bra av att brytas ut i några deskriptiva funktioner:

Exempel 9
const consonants = "bcdfghjklmnpqrstvwxz"

function isConsonant(letter) {
return consonants.split("").includes(letter.toLowerCase())
}

function handleConsonant(letter) {
return letter + "o" + letter.toLowerCase()
}

function handleLetter(letter) {
return isConsonant(letter) ? handleConsonant(letter) : letter
}

function expandX(sentence) {
return sentence.replaceAll("x", "ks").replaceAll("X", "Ks")
}

function toRobber(sentence) {
return expandX(sentence).split("").map(handleLetter).join("")
}

const sentence = "Xylofoner är fina"

const robberSentence = toRobber(sentence)

// robberSentence = "Koksosylolofofononeror äror fofinona"

Koksosylolofofononeror äror fofinona

Eller så hoppar vi över förbehandlingssteget helt. Istället för att byta ut "x" mot "ks" innan huvudloopen kan vi behålla "x" i konsonantlistan och låta loopen hantera det via en specialfunktion:

Exempel 10
const consonants = "bcdfghjklmnpqrstvwxz"

function isConsonant(letter) {
return consonants.split("").includes(letter.toLowerCase())
}

function isX(letter) {
return letter.toLowerCase() === "x"
}

function isUpperCase(letter) {
return letter === letter.toUpperCase()
}

function consonantToRobber(letter) {
return letter + "o" + letter.toLowerCase()
}

function handleX(letter) {
return isUpperCase(letter)
? consonantToRobber("K") + consonantToRobber("s")
: consonantToRobber("k") + consonantToRobber("s")
}

function handleConsonant(letter) {
return isX(letter) ? handleX(letter) : consonantToRobber(letter)
}

function handleLetter(letter) {
return isConsonant(letter) ? handleConsonant(letter) : letter
}

function toRobber(sentence) {
return sentence.split("").map(handleLetter).join("")
}

const sentence = "Xylofoner är fina"

const robberSentence = toRobber(sentence)

// robberSentence = "Koksosylolofofononeror äror fofinona"

Koksosylolofofononeror äror fofinona

Vid en första anblick ser exempel 10 ut som mycket kod för en liten uppgift. Men eftersom varje funktion bara har en liten uppgift är det enkelt att ge funktionerna meningsfulla namn, vilket i sin tur leder till att det blir enklare att förstå vad som händer.

Eller så kompakterar vi exempel 10 genom att fälla in fallet för "x" direkt i consonantToRobber:

Exempel 11
const consonants = "bcdfghjklmnpqrstvwxz"

function isConsonant(letter) {
return consonants.split("").includes(letter.toLowerCase())
}

function isUpperCase(letter) {
return letter === letter.toUpperCase()
}

function consonantToRobber(letter) {
if (letter.toLowerCase() === "x") {
const k = isUpperCase(letter) ? "K" : "k"
return `${k}oksos`
}
return letter + "o" + letter.toLowerCase()
}

function toRobber(sentence) {
return sentence
.split("")
.map((letter) => (isConsonant(letter) ? consonantToRobber(letter) : letter))
.join("")
}

const sentence = "Xylofoner är fina"

const robberSentence = toRobber(sentence)

// robberSentence = "Koksosylolofofononeror äror fofinona"

Koksosylolofofononeror äror fofinona

Den största vinsten med exempel 11 är ärlig namngivning. I exempel 10 hanterar consonantToRobber faktiskt inte "x" – det dispatchas till handleX – så funktionen ljuger tyst om vad den gör. I exempel 11 hanterar consonantToRobber alla konsonanter inklusive "x". Avvägningen med exempel 11 är att consonantToRobber nu har två grenar inuti, så den är inte lika strikt single-purpose – men i utbyte matchar abstraktionen sitt namn. Som följd har exempel 11 också färre funktioner.

Sammantaget läser sentence-first-strategin (exempel 7–11) mer naturligt eftersom den itererar över texten vi faktiskt vill översätta, inte konsonanterna. Consonant-first-strategin (exempel 1–6) är snabbare att slänga ihop men tvingar in mutation i varje varv. När det kommer till att skriva snygg kod handlar det till slut alltid om avvägningar.

Bättre alternativ

Alla elva exempel ovan kan kortas ner dramatiskt med regex. Med en enda teckenklass och en callback ryms samma översättning på två rader:

const sentence = "Xylofoner är fina"

const robberSentence = sentence
.replaceAll(/x/gi, (m) => (m === "X" ? "Ks" : "ks"))
.replaceAll(/[bcdfghjklmnpqrstvwz]/gi, (c) => `${c}o${c.toLowerCase()}`)

// robberSentence = "Koksosylolofofononeror äror fofinona"

Om du vill ha det här i produktionskod utan att skriva det själv så finns logiken paketerad som @tammergard/robber på npm, och det finns ett JSON-API på rovarspraket.tammergard.se – se uppföljningsinlägget Ett API för rövarspråket för detaljerna.

Där har du det – nu vet du allt om rövarspråket!

Referenser