发布于 2026-01-06 0 阅读
0

古老的计算机科学:让我们从零开始构建一个罗马数字转换器🏺📜

古老的计算机科学:让我们从零开始构建一个罗马数字转换器🏺📜

今天,我们要穿越时空!让我们回到公元217年,也就是CCXVII年,一直到铁器时代:罗马帝国时期。

但今天我们既不去探索罗马斗兽场,也不去参观万神殿,更不会与罗马军团士兵交谈,也不会漫步于公共大道。相反,我们将学习一个支撑罗马经济很大一部分以及一些最宏伟建筑杰作的概念。今天的主题是罗马数字。

等等,CCXVII 怎么会是 217 呢?

问得好!我们来分析一下。

(简短插曲:) 如果您还不知道的话,我们常用的数字(0-9)被称为“阿拉伯数字”,因为它们起源于阿拉伯半岛西部和北非地区。你知道吗,我划掉的那句话其实是错的?正如@youngdad33在评论里指出的,我们熟知的十进制数字起源于印度,后来传入阿拉伯地区,在十字军东征期间被欧洲人发现,因此被错误地称为“阿拉伯数字”。今天才知道。😀)

首先,C、X、V 和 I 分别代表什么?

下表概述了罗马数字及其值:

罗马数字 价值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

与十进制数字一样,罗马数字也由数字组成。然而,罗马数字中每个数字在不同位置并不对应完全相同的值(例如,217 可以是 2 * 100、1 * 10 和 7 * 1),而是通过组合不同的数字来表示。相同数字的个数决定了该数字的值。因此,我们可以将 217 改写为 2 * 100。CCXVII根据C + C + X + V + I + I上表,这可以转化为 217 * 100 100 + 100 + 10 + 5 + 1 + 1 = 217

例如,数字 4 可以写成 4 IIII,对吧?差不多!虽然这似乎是最直观的答案,但发明者们认为这不是最佳方案。相反,所有不能用最多三个相同数字相加得到的数,都用下一个更大的数减去它来表示。所以,1 + 1 + 1 + 1 = 4我们不写 4,而是5 - 1 = 4用罗马数字写成 4 V - I,或者直接写成 4 IV

总之,这意味着如果数字 A(位于数字 B 的左边)小于数字 B,则减去数字 A;否则,加上数字 B。举例来说:

IV --> I < V --> V - I
But:
VI --> V > I --> V + I
Enter fullscreen mode Exit fullscreen mode

这适用于任何数字:

CDXLIV 
--> (D - C) + (L - X) + (V - I) 
= (500 - 100) + (50 - 10) + (5 - 1) = 444

XC = (100 - 10) = 90
Enter fullscreen mode Exit fullscreen mode

然而,99 不是写成100 - 1,而是写成(100 - 10) + (10 - 1)

总之,以下是将一位数(以 10 为基数)转换N为罗马数字的规则:

  • 如果 N <= 3,则重复I1 到 3 次。
  • 如果 N === 4,则结果为 5 - 1,所以VI
  • 如果 N === 5,则V
  • 如果 N < 9,则执行 5 次,并重复I1 到 3 次。
  • 如果 N === 9,那么就是 10 - 1,所以IX

如果我们看一下上面的表格,我们会注意到,对于 10 的每一个幂(1 到 1000,即 1、10、100、1000),都有个位数(1、10 等)和个位数(5、50、500)——因此,我们可以对 10 的每一个幂重复上述步骤,并相应地改变我们使用的数字集。

从十进制到罗马数字的编码

首先,我们将常用的十进制数字转换为罗马数字。

我们需要一个简单的罗马数字到数字的映射表:

const romanNumerals = {
  1: 'I',
  5: 'V',
  10: 'X',
  50: 'L',
  100: 'C',
  500: 'D',
  1000: 'M'
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们需要实现个位数转换的规则。上述规则可以if直接转化为一组语句,我们只需要知道 10 的幂次方,因此我们选择了正确的罗马数字:

const romanNumerals = {
  1: 'I',
  5: 'V',
  10: 'X',
  50: 'L',
  100: 'C',
  500: 'D',
  1000: 'M'
}

/**
 * Translates a single digit in respect of the power of 10 into a Roman numeral.
 * @param n
 * @param powerOf10
 * @returns {*|string}
 */
const numDigitToRomDigits = (n, powerOf10) => {
  if (n <= 3) { // I, II, III, X, X, XXX, C, CC, CCC
    return romanNumerals[powerOf10].repeat(n)
  }

  if (n === 4) { // IV, XL, CD
    return romanNumerals[powerOf10] 
      + romanNumerals[powerOf10 * 5]
  }

  if (n === 5) { // V, L, D
    return romanNumerals[powerOf10 * 5]
  }

  if (n < 9) { // VI, VII, VIII, etc.
    return romanNumerals[powerOf10 * 5] 
      + romanNumerals[powerOf10].repeat(n - 5)
  }

  // MC, XC, IX
  return romanNumerals[powerOf10] 
    + romanNumerals[powerOf10 * 10]
}
Enter fullscreen mode Exit fullscreen mode

我们来试一试:

numDigitToRomDigits(7, 10) // "70", yields `LXX`
numDigitToRomDigits(5, 100) // "500", yields `D`
numDigitToRomDigits(3, 1) // "3", yields `III`
numDigitToRomDigits(4, 10) // "40", yields `XL`
Enter fullscreen mode Exit fullscreen mode

看起来不错!现在,我们可以用这个函数来转换更大的数字:

/**
 * Translates an entire number to Roman numerals.
 * @param x
 * @returns {string}
 */
const num2rom = x => {
  // Split number into digits and reverse, 
  // so figuring out the power of 10 is easier.
  const digits = x.toString()
    .split('')
    .map(n => parseInt(n))
    .reverse()

  // Larger numbers don't work, 5000 is written 
  // as V with a dash on top, we don't have that 
  // character...
  if (x > 3999) {
    throw new Error(
      'Numbers larger than 3999 cannot be converted'
    )
  }

  // Loop over all digits, convert them each
  let romanNum = ''
  for (let i = 0; i < digits.length; i++) {
    romanNum = 
      numDigitToRomDigits(digits[i], 10 ** i) 
      + romanNum // Attach to front of already converted
  }

  return romanNum
}
Enter fullscreen mode Exit fullscreen mode

我们来试试:

num2rom(3724) // yields `MMMDCCXXIV` - works!
Enter fullscreen mode Exit fullscreen mode

从罗马数字重新转换为十进制

另一种方法会稍微复杂一些——我们需要解析罗马数字,然后将它们转换回十进制。首先,我们需要翻转之前的映射。Stack Overflow上有相关教程。

const flipObject = obj => Object.entries(obj)
  .reduce((acc, [key, value]) => (acc[value] = key, acc), {})

const base10Numerals = flipObject(romanNumerals)

/* yields
{
  C: "100"
  D: "500"
  I: "1"
  L: "50"
  M: "1000"
  V: "5"
  X: "10"
}
*/
Enter fullscreen mode Exit fullscreen mode

接下来我们要实现的是加减运算。我们知道,左边较大的数字要加,左边较小的数字要减。所以,基本上是:VI = V + I,但是IV = V - I。由于不存在IIV,我们可以检查下一个数字来决定是加还是减当前数字。所以,类似这样:

From left to right,
If next number to the right is larger:
  Subtract current digit
Else
  Add current digit
Enter fullscreen mode Exit fullscreen mode

用代码表示如下:

/**
 * Converts a roman number to base10.
 * @param x
 * @returns {number}
 */
const rom2num = x => {
  // Split number and assign base10 
  // value to each digit.
  // parseInt is necessary, because the 
  // flip yields strings.
  const digits = x.split('')
    .map(d => parseInt(base10Numerals[d]))

  let sum = 0
  // Loop over every digit
  for (let i = 0; i < digits.length; i++) {
    // If number to the right larger than the 
    // current number
    if (digits[i + 1] > digits[i]) {
      sum -= digits[i]
    } else {
      sum += digits[i]
    }
  }

  return sum
}
Enter fullscreen mode Exit fullscreen mode

我们来测试一下,看看把 1 到 3999 之间的所有数字都来回转换一遍是否可行:

let result = true
for (let i = 0; i < 3999; i++) {
  result = result && rom2num(num2rom(i)) === i
}

console.log(result) // true, works!
Enter fullscreen mode Exit fullscreen mode

结果

现在我们需要一些输入框和按钮,瞧!

呼,古代的故事就到此为止吧,让我们回到21世纪。


希望您喜欢这篇文章,就像我喜欢写这篇文章一样!如果喜欢,请点个赞❤️🦄 !我会在空闲时间写一些科技文章,偶尔也喜欢喝杯咖啡。

如果你想支持我的工作, 可以请我喝杯咖啡 或者 在推特上关注我🐦 你也可以直接通过PayPal支持我!

给我买杯咖啡按钮

文章来源:https://dev.to/thormeier/ancient-computer-science-let-s-build-a-roman-numeral-converter-from-scratch-4ga7