使用 Bash 将十进制数转换为罗马数字
十进制数到罗马数字——在这里我们触及了 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,所以我们使用我们拥有的,对吧?
下次见。同时,您有什么有趣的编程难题吗?给我留言,我会看一看!