Created
February 4, 2026 01:39
-
-
Save komakai/e4c11ef1c49a99cd9e1cf8a7fececd28 to your computer and use it in GitHub Desktop.
Kotlin conversions to and from RomanNumerals
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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