awkrs 0.4.12

Awk implementation in Rust with broad CLI compatibility, parallel records, and experimental Cranelift JIT
Documentation
# Roman numerals ↔ integers.
# Each input line is one token. If it's a positive integer it's converted to
# Roman; if it's a Roman string (case-insensitive) it's converted to integer.
# Subtractive form supported (IV, IX, XL, XC, CD, CM). Range 1..3999.

BEGIN {
  # Decimal -> Roman tables in descending order.
  n = 0
  n++; vals[n] = 1000; syms[n] = "M"
  n++; vals[n] = 900;  syms[n] = "CM"
  n++; vals[n] = 500;  syms[n] = "D"
  n++; vals[n] = 400;  syms[n] = "CD"
  n++; vals[n] = 100;  syms[n] = "C"
  n++; vals[n] = 90;   syms[n] = "XC"
  n++; vals[n] = 50;   syms[n] = "L"
  n++; vals[n] = 40;   syms[n] = "XL"
  n++; vals[n] = 10;   syms[n] = "X"
  n++; vals[n] = 9;    syms[n] = "IX"
  n++; vals[n] = 5;    syms[n] = "V"
  n++; vals[n] = 4;    syms[n] = "IV"
  n++; vals[n] = 1;    syms[n] = "I"
  nsyms = n

  # Single-letter Roman digit values for parsing.
  rv["I"] = 1;   rv["V"] = 5;    rv["X"] = 10;   rv["L"] = 50
  rv["C"] = 100; rv["D"] = 500;  rv["M"] = 1000
}

function to_roman(x,   i, out) {
  if (x < 1 || x > 3999) return "ERR(out of range 1..3999)"
  out = ""
  for (i = 1; i <= nsyms; i++) {
    while (x >= vals[i]) { out = out syms[i]; x -= vals[i] }
  }
  return out
}

function to_int(s,   i, total, ch, nx, vi, vnext) {
  s = toupper(s)
  total = 0
  for (i = 1; i <= length(s); i++) {
    ch = substr(s, i, 1)
    if (!(ch in rv)) return "ERR(bad char " ch ")"
    vi = rv[ch]
    nx = substr(s, i + 1, 1)
    vnext = (nx in rv) ? rv[nx] : 0
    if (vnext > vi) total -= vi
    else total += vi
  }
  return total
}

NF == 0 { next }

{
  t = $1
  if (t ~ /^[0-9]+$/) printf "%s -> %s\n", t, to_roman(t + 0)
  else                printf "%s -> %s\n", t, to_int(t)
}