更多罗马数字和Bash

作者:Dave Taylor

在罗马时:完成罗马数字转换器脚本。

在我的上一篇文章中,我开始深入研究一个经典的计算机科学难题:将罗马数字转换为阿拉伯数字。首先,更准确的说法应该是印度-阿拉伯数字,值得一提的是,它被认为是在公元一世纪到四世纪之间发明的——一种基于 0..9 值的计数系统。

上次我完成的脚本提供了分析指定罗马数字的基础知识,并通过这个简单的函数将每个值转换为其十进制等价值


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
}

然后我演示了一种巧妙的方法,使用未被充分利用的 seq 命令逐字符解析字符串,但遗憾的是,您将无法将其用于最终的罗马数字到阿拉伯数字转换器。为什么?因为根据情况,脚本有时需要向前跳两个位置,而不仅仅是线性地从左到右,一次一个字符。

相反,您可以将主循环构建为 while 循环


while [ $index -lt $length ] ; do

    our code

    index=$(( $index + 1 ))
done

在解决这个算法难题时,需要考虑两种基本情况:后续值大于当前值,或者不是——例如,IX 与 II。第一个是 9(字面意思是 10 减去 1),第二个是 2。这并不奇怪;您需要知道脚本中的当前值和下一个值。

敏锐的读者已经意识到序列中的最后一个字符是一个特殊情况,因为不会有下一个值可用。我将首先忽略特殊情况,并在稍后的代码开发中解决它。敬请关注,敏锐的朋友们!

由于 Bash shell 脚本没有优雅的内联函数,因此获取当前值和下一个值的代码不会是 value=mapit(romanchar),但它会有点笨拙,因为它使用了全局变量 value


mapit ${romanvalue:index-1:1}
currentval=$value

mapit ${romanvalue:index:1}
nextval=$value

关键是要意识到,在下一个值不大于当前值的情况下(例如,MC),您不能自动断定下一个值不会是复杂的双值序列的一部分。像这样:MCM。您不能只说 M=1000 和 C=500,所以我们将其转换为 1500,并在稍后处理第二个 M。MCM=1900,而不是 2500!

基本逻辑结果非常简单明了


if [ $nextval -gt $currentval ] ; then
  sum=$(( $sum + $nextval - $currentval ))
else
  sum=$(( $sum + currentval ))
fi

完成!

或者,嗯,不是。上面条件代码的问题在于,在您引用了当前值和下一个值的情况下,您需要确保在下次循环中不再处理下一个值。

换句话说,当序列“CM”被转换时,M 不应在第二次循环中再次被转换。

这正是我放弃 for 循环的原因,这样您就可以使某些循环迭代为 +1 迭代,而其他循环迭代为 +2 迭代(视情况而定)。

考虑到这一点,让我们在条件语句中添加必要的行


if [ $nextval -gt $currentval ] ; then
  sum=$(( $sum + $nextval - $currentval ))
  index=$(( $index + 1 ))
else
  sum=$(( $sum + currentval ))
fi

请记住,while 循环的最底部仍然有索引值增量 +1。上面添加到条件语句中的内容基本上是,当遇到下一个值 > 当前值的情况时,脚本将处理这两个值并向前跳过一个额外的字符。

这意味着对于任何给定的罗马数字,循环的次数将等于或小于序列中字符的总数。

这意味着现在问题已经解决,除了序列中的最后一个值。如果它不是下一个-当前对的一部分会发生什么?最简单的情况,您如何解析“X”?

这需要大量的代码来绕过从字符串转换 nextval(这将失败,因为它超出了字符串的长度)以及对 nextval 的任何测试引用。

这暗示了一个简单的解决方案:将整个 if-then-else 代码块包装在一个测试最后一个字符的条件中


if [ $index -lt $length ] ; then
  if-then-else code block
else
  sum=$(( $sum + $currentval ))
fi

就是这样。天哪,我想你明白了!这是完整的 while 语句,以便您了解这如何融入整体程序逻辑


while [ $index -le $length ] ; do

  mapit ${romanvalue:index-1:1}
  currentval=$value

  if [ $index -lt $length ] ; then
    mapit ${romanvalue:index:1}
    nextval=$value

    if [ $nextval -gt $currentval ] ; then
      sum=$(( $sum + $nextval - $currentval ))
      index=$(( $index + 1 ))
    else
      sum=$(( $sum + $currentval ))
    fi
  else
    sum=$(( $sum + $currentval ))
  fi

  index=$(( $index + 1 ))

done

结果证明它根本不是特别复杂。关键是要认识到您需要以相当集中的方式解析罗马数字,而不是逐个字母解析。

让我们快速测试一下这个脚本


$ sh roman.sh DXXV
Roman number DXXV converts to Arabic value 525
$ sh roman.sh CMXCIX
Roman number CMXCIX converts to Arabic value 999
$ sh roman.sh MCMLXXII
Roman number MCMLXXII converts to Arabic value 1972

任务完成。

在我的下一篇文章中,我计划研究这个编码挑战的反面,将阿拉伯数字转换为罗马数字序列。换句话说,您输入 99,它返回 XCIX。为什么不在等待的时候自己尝试编写代码呢?

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

加载 Disqus 评论