Skip to content

Instantly share code, notes, and snippets.

@komakai
Created February 4, 2026 01:39
Show Gist options
  • Select an option

  • Save komakai/e4c11ef1c49a99cd9e1cf8a7fececd28 to your computer and use it in GitHub Desktop.

Select an option

Save komakai/e4c11ef1c49a99cd9e1cf8a7fececd28 to your computer and use it in GitHub Desktop.
Kotlin conversions to and from RomanNumerals
package org.example
import kotlin.math.min
val symbols = listOf('I', 'V', 'X', 'L', 'C', 'D', 'M')
val digitEncodings = listOf("", "1", "11", "111", "15", "5", "51", "511", "5111", "1A")
fun pow10(n: Int): Int {
var res = 1
for (i in 1..n) {
res *= 10
}
return res
}
val maxEncodable = ((symbols.size - 1) / 2).let { power ->
pow10(power).let {
(if ((symbols.size - 1) % 2 == 0) 3 else 8) * it + it - 1
}
}
fun encodeDigit(d: Int, power: Int): String {
assert(d in 0..9)
val startOffset = power * 2
val has5 = digitEncodings[d].contains('5')
val hasA = digitEncodings[d].contains('A')
assert(startOffset < digitEncodings.size)
assert(startOffset + 1 < digitEncodings.size || !has5)
assert(startOffset + 2 < digitEncodings.size || !hasA)
return digitEncodings[d]
.replace('1', symbols[startOffset])
.let {
if (has5) it.replace('5', symbols[startOffset + 1]) else it
}
.let {
if (hasA) it.replace('A', symbols[startOffset + 2]) else it
}
}
fun decodeDigit(s: String, power: Int): Int {
assert(power <= symbols.size / 2)
val char1 = symbols[power * 2]
val have5 = power * 2 + 1 < symbols.size
val char5 = if (have5) symbols[power * 2 + 1] else null
val haveA = power * 2 + 2 < symbols.size
val charA = if (haveA) symbols[power * 2 + 2] else null
return when {
s == "" -> 0
s == "$char1" -> 1
s == "$char1$char1" -> 2
s == "$char1$char1$char1" -> 3
have5 && s == "$char1$char5" -> 4
have5 && s == "$char5" -> 5
have5 && s == "$char5$char1" -> 6
have5 && s == "$char5$char1$char1" -> 7
have5 && s == "$char5$char1$char1$char1" -> 8
haveA && s == "$char1$charA" -> 9
else -> throw NumberFormatException()
} * pow10(power)
}
class RomanNumeral(val value: Int) {
init {
assert(value in 0..maxEncodable)
}
fun encode(): String {
var remaining = value
var result = ""
var power = 0
while (remaining > 0) {
val digit = remaining % 10
result = encodeDigit(digit, power) + result
power++
remaining /= 10
}
return result
}
override fun toString() = "$value"
companion object {
fun decode(input: String): RomanNumeral {
var result = 0
var power = 0
var endPosition = input.length
while (endPosition > 0) {
if (power * 2 >= symbols.size) {
throw NumberFormatException()
}
val char1 = symbols[power * 2]
val char5 = if (power * 2 + 1 < symbols.size) symbols[power * 2 + 1] else null
val remaining = input.substring(0..<endPosition)
val pos1 = remaining.indexOf(char1).let { if (it == -1) endPosition else it }
val pos5 = char5?.let { ch ->
remaining.indexOf(ch).let { if (it == -1) endPosition else it }
} ?: endPosition
val newEndPosition = min(pos1, pos5)
result += decodeDigit(input.substring(newEndPosition..<endPosition), power)
endPosition = newEndPosition
power++
}
return RomanNumeral(result)
}
}
}
fun main() {
try {
for (i in 1..<maxEncodable) {
assert(RomanNumeral.decode(RomanNumeral(i).encode()).value == i)
}
println("Success")
} catch (t: Throwable) {
println(t.printStackTrace())
println("Failure")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment