使用 Bash 将十进制数转换为罗马数字

作者:Dave Taylor

十进制数到罗马数字——在这里我们触及了 Bash shell 脚本的所有局限性。

我最近的几篇文章让我有机会重温我的大学计算机科学学位,并编写一个罗马数字到十进制转换器。当您观看老电影时(MCMLVII 是什么时候?),这非常方便,而且基本的编码算法也相当简单。(请参阅 Dave 的“罗马数字和 Bash”“更多罗马数字和 Bash”。)

然而,罗马数字的诀窍在于它被称为减法记数法。换句话说,它不是位置 → 值,甚至不是符号 → 值记数法,而是一种混合形式。MM = 2000,C = 100,但 MMC 和 MCM 完全不同:前者是 2100,后者是 1000 + (–100 + 1000) = 1900。

这意味着转换不像映射表那么简单,这使其成为年轻计算机科学学生的良好家庭作业!

让我们编写一些代码

在罗马数字到十进制的转换中,很多关键工作都是由这个简单的函数完成的


mapit() {
   case $1 in
     I|i) value=1 ;;
     V|v) value=5 ;;
     X|x) value=10 ;;
     L|l) value=50 ;;
     C|c) value=100 ;;
     D|d) value=500 ;;
     M|m) value=1000 ;;
      * ) echo "Error: Value $1 unknown" >&2 ; exit 2 ;;
   esac
}

您需要此函数才能继续,但作为一个级联的条件语句集。实际上,以其简单的形式,您可以像这样编写一个十进制到罗马数字的转换器


while [ $decvalue -gt 0 ] ; do

  if [ $decvalue -gt 1000 ] ; then
    romanvalue="$romanvalue M"
    decvalue=$(( $decvalue - 1000 ))
  elif [ $decvalue -gt 500 ] ; then
    romanvalue="$romanvalue D"
    decvalue=$(( $decvalue - 500 ))
  elif [ $decvalue -gt 100 ] ; then
    romanvalue="$romanvalue C"
    decvalue=$(( $decvalue - 100 ))
  elif [ $decvalue -gt 50 ] ; then
    romanvalue="$romanvalue L"
    decvalue=$(( $decvalue - 50 ))
  elif [ $decvalue -gt 10 ] ; then
    romanvalue="$romanvalue X"
    decvalue=$(( $decvalue - 10 ))
  elif [ $decvalue -gt 5 ] ; then
    romanvalue="$romanvalue V"
    decvalue=$(( $decvalue - 5 ))
  elif [ $decvalue -ge 1 ] ; then
    romanvalue="$romanvalue I"
    decvalue=$(( $decvalue - 1 ))
  fi

done

这实际上是可行的,尽管结果有点笨拙


$ sh 2roman.sh 25
converts to roman numeral  X X I I I I I

或者,更令人难以接受


$ sh 2roman.sh 1900
converts to roman numeral  M D C C C L X X X X V I I I I I

我想后者有一定的魅力,但也有更好、更简单的方法来简化这一点。您可以进行所有数学运算,但由于我的编码方法通常是“偷懒,完成它,继续前进”,让我们认识到只有非常少量的特殊情况数值


900 = CM
400 = CD
90  = XC
40  = XL
9   = IX
4   = IV

真的就这些了。该记数法只允许一个字符从另一个字符中减去,所以你不能有 CCM 或 IIX(后者正确写法是 VIII),以及其他一些可能的双字符记数法没有意义。例如,当 V 是相同的值时,为什么要使用 VX 呢?

因此,鉴于此,您真正需要做的就是扩展 if-elseif 代码块以添加上述五个可能的值,这会形成一个非常长的代码块。但在我分享它之前,您是否发现了上面代码中的错误?

实际上,这是导致生成的罗马数字如此之长的另一个原因。让我们只看一下第一个条件语句


if [ $decvalue -gt 1000 ] ; then
  romanvalue="$romanvalue M"
  decvalue=$(( $decvalue - 1000 ))

这是要问自己的问题:如果 $decvalue 正好是 1000 会发生什么?那不是“M”吗?是的,是这样。这意味着所有这些条件都是错误的;它们应该使用 -ge 而不是 -gt

修复了这个错误,这是大的代码块


while [ $decvalue -gt 0 ] ; do

  if [ $decvalue -ge 1000 ] ; then
    romanvalue="$romanvalue M"
    decvalue=$(( $decvalue - 1000 ))
  elif [ $decvalue -ge 900 ] ; then
    romanvalue="$romanvalue CM"
    decvalue=$(( $decvalue - 900 ))
  elif [ $decvalue -ge 500 ] ; then
    romanvalue="$romanvalue D"
    decvalue=$(( $decvalue - 500 ))
  elif [ $decvalue -ge 400 ] ; then
    romanvalue="$romanvalue CD"
    decvalue=$(( $decvalue - 400 ))
  elif [ $decvalue -ge 100 ] ; then
    romanvalue="$romanvalue C"
    decvalue=$(( $decvalue - 100 ))
  elif [ $decvalue -ge 90 ] ; then
    romanvalue="$romanvalue XC"
    decvalue=$(( $decvalue - 90 ))
  elif [ $decvalue -ge 50 ] ; then
    romanvalue="$romanvalue L"
    decvalue=$(( $decvalue - 50 ))
  elif [ $decvalue -ge 40 ] ; then
    romanvalue="$romanvalue XL"
    decvalue=$(( $decvalue - 40 ))
  elif [ $decvalue -ge 10 ] ; then
    romanvalue="$romanvalue X"
    decvalue=$(( $decvalue - 10 ))
  elif [ $decvalue -ge 9 ] ; then
    romanvalue="$romanvalue IX"
    decvalue=$(( $decvalue - 9 ))
  elif [ $decvalue -ge 5 ] ; then
    romanvalue="$romanvalue V"
    decvalue=$(( $decvalue - 5 ))
  elif [ $decvalue -ge 4 ] ; then
    romanvalue="$romanvalue IV"
    decvalue=$(( $decvalue - 4 ))
  elif [ $decvalue -ge 1 ] ; then
    romanvalue="$romanvalue I"
    decvalue=$(( $decvalue - 1 ))
  fi

done

它适用于一些基本的数字测试(尽管有一些容易删除的空格)


$ sh 2roman.sh 71
converts to roman numeral  L X X I
$ sh 2roman.sh 1997
converts to roman numeral  M CM XC V I I
$ sh 2roman.sh 666
converts to roman numeral  D C L X V I

问题是,即使它确实解决了问题,这也是一个冗长且笨拙的代码块。

使代码更简洁

显然,每个代码块都具有相同的格式


elif [ $decvalue -ge VALUE ] ; then
  romanvalue="$romanvalue NOTATION-FOR-VALUE"
  decvalue=$(( $decvalue - VALUE ))

如突出显示的那样,只有两个值需要考虑:数值 VALUE 和一个或两个字符的 NOTATION-FOR-VALUE。例如,VALUE=90, NOTATION=XC。那么逻辑函数是


SubIfValue()
{
  # if $decvalue >= $2 then add $3 to romanvalue
  # and subtract $2 from decvalue

  if [ $decvalue -ge $1 ] ; then
    romanvalue="${romanvalue}$2"
    decvalue=$(( $decvalue - $1 ))
  fi
}

这将产生一系列像这样的调用


SubIfValue  500 "D"
SubIfValue  400 "CD"
SubIfValue  100 "C"
SubIfValue   90 "XC"

但这有一个问题。循环必须迭代并通过每次迭代减去最大可能的值;否则,您会得到非常奇怪的结果。

因此,在算法上,您仍然需要有 if-then-elif 循环


if ( SubIfValue 500 "D" fails ) then

问题是,这真的很难干净地完成,因为您实际上无法从 Bash shell 函数返回值。因此,为了不至于太笨拙,我将在我清理代码的探索中找到一个折衷方案。我可以让函数基本保持不变


SubValue()
{
  # add $3 to romanvalue and subtract $2 from decvalue

  romanvalue="${romanvalue}$2"
  decvalue=$(( $decvalue - $1 ))

}

然而,调用序列看起来会更简洁


if [ $decvalue -ge 1000 ] ; then
  SubValue 1000 "M"
elif [ $decvalue -ge 900 ] ; then
  SubValue 900 "CM"
elif [ $decvalue -ge 500 ] ; then
  SubValue 500 "D"
elif [ $decvalue -ge 400 ] ; then
  SubValue 400 "CD"
elif [ $decvalue -ge 100 ] ; then
  SubValue 100 "C"
elif [ $decvalue -ge 90 ] ; then
  SubValue 90 "XC"
elif [ $decvalue -ge 50 ] ; then
  SubValue 50 "L"
elif [ $decvalue -ge 40 ] ; then
  SubValue 40 "XL"
elif [ $decvalue -ge 10 ] ; then
  SubValue 10 "X"
elif [ $decvalue -ge 9 ] ; then
  SubValue 9 "IX"
elif [ $decvalue -ge 5 ] ; then
  SubValue 5 "V"
elif [ $decvalue -ge 4 ] ; then
  SubValue 4 "IV"
elif [ $decvalue -ge 1 ] ; then
  SubValue 1 "I"
fi

并且,一旦您将其包装在 while;do / done 循环中,这就是功能齐全的脚本。

一些测试


$ sh 2roman.sh 1991
converts to roman numeral MCMXCI
$ sh 2roman.sh 2222
converts to roman numeral MMCCXXII
$ sh 2roman.sh 1234
converts to roman numeral MCCXXXIV

就这样。解决了。现在我想不起来为什么我在大学时觉得它如此令人生畏。我会注意到,在更复杂的编程语言中,您可以提出一个相当简短的解决方案,特别是如果您可以利用数字/字符对的二维数组。但是,这不是 Bash shell,所以我们使用我们拥有的,对吧?

下次见。同时,您有什么有趣的编程难题吗?给我留言,我会看一看!

Dave Taylor 在 UNIX 和 Linux 系统上编写 shell 脚本已经很长时间了。他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。您可以在 Twitter 上通过 @DaveTaylor 找到他,您可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论