在JavaScript中将数字转换为罗马数字

96

我该如何将整数转换为罗马数字

function romanNumeralGenerator (int) {

}
例如,看下面的样例输入和输出:
1 = "I"
5 = "V"
10 = "X"
20 = "XX"
3999 = "MMMCMXCIX"

注意:仅支持1到3999之间的数字。

96个回答

134

我在Google上发现了这篇博客,里面有一个不错的罗马数字转换器:

http://blog.stevenlevithan.com/archives/javascript-roman-numeral-converter

function romanize (num) {
    if (isNaN(num))
        return NaN;
    var digits = String(+num).split(""),
        key = ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM",
               "","X","XX","XXX","XL","L","LX","LXX","LXXX","XC",
               "","I","II","III","IV","V","VI","VII","VIII","IX"],
        roman = "",
        i = 3;
    while (i--)
        roman = (key[+digits.pop() + (i * 10)] || "") + roman;
    return Array(+digits.join("") + 1).join("M") + roman;
}

2
提醒:正如在那篇文章中讨论的那样,最好返回 NaN 或抛出异常,而不是返回 false - Onur Yıldırım
1
我刚刚发现,这个函数能处理的最大数字是 715799999999(715,799,999,999)。更大的数字要么没有返回值,要么(对于非常大的数字)会输出 RangeError: Invalid array length 错误。除此之外,它完美地工作。谢谢! - tukusejssirs
3
@tukusejssirs 幸运的是罗马数字是用于年份的,否则就会有比1000更大的数字了。但知道这个也是好的! - Rene Pot

113

4
这很好,似乎在理解这里发生的事情方面更容易。从最大的数字开始,继续从查找表中减去并追加,只要余数大于查找值就可以。 - Alex C
23
对象没有顺序!您应该使用数组并避免使用 for...in - Oriol
4
我不是在谈论性能,而是说迭代顺序不能保证,因此结果可能完全错误。 - Oriol
13
我无法提供一个不起作用的示例,因为顺序取决于实现。请给我提供规范的哪个部分确保它将按所需顺序进行迭代。 哦,您无法这样做 - Oriol
3
[更新,2021年6月]:自ES2020以来,即使在for...in循环中,对象的迭代顺序也保证与对象的属性顺序相同。回应@Oriol可能有点啰嗦,对象确实有“顺序”,自ES2015以来就是如此,但使用for...in时它们的_迭代顺序_直到最近才被保证与属性顺序相同。 - Timothy J. Aveni
显示剩余8条评论

78
我不明白为什么每个人的解决方案都这么长并且使用了多个for循环。
function convertToRoman(num) {
  var roman = {
    M: 1000,
    CM: 900,
    D: 500,
    CD: 400,
    C: 100,
    XC: 90,
    L: 50,
    XL: 40,
    X: 10,
    IX: 9,
    V: 5,
    IV: 4,
    I: 1
  };
  var str = '';

  for (var i of Object.keys(roman)) {
    var q = Math.floor(num / roman[i]);
    num -= q * roman[i];
    str += i.repeat(q);
  }

  return str;
}

我认为我终于理解了这个解决方案。添加 'if (num===0){return str}' 是否会使您循环次数更少?您可以将其放在循环的末尾并删除 'return str'。 - kite
是的@kite,发现得好!这会使循环次数减少,但考虑到我们只循环了13个项目,这不会有太大的影响 :) - August

33
我开发了下面的递归解决方案。该函数返回一个字母,然后调用自身返回下一个字母,直到传递给函数的数字为0,这意味着所有字母都已找到,我们可以退出递归。
var romanMatrix = [
  [1000, 'M'],
  [900, 'CM'],
  [500, 'D'],
  [400, 'CD'],
  [100, 'C'],
  [90, 'XC'],
  [50, 'L'],
  [40, 'XL'],
  [10, 'X'],
  [9, 'IX'],
  [5, 'V'],
  [4, 'IV'],
  [1, 'I']
];

function convertToRoman(num) {
  if (num === 0) {
    return '';
  }
  for (var i = 0; i < romanMatrix.length; i++) {
    if (num >= romanMatrix[i][0]) {
      return romanMatrix[i][1] + convertToRoman(num - romanMatrix[i][0]);
    }
  }
}

6
我非常喜欢你的解决方案。它易读易懂,非常简单。很棒! - Sony ThePony
有太多不必要的重复发生了。 - am guru prasath
到目前为止最佳解决方案。 - Ali Mustafa

17

这些函数将任何正整数转换为相应的罗马数字字符串;并将任何罗马数字转换为数字。

数字转罗马数字:

Number.prototype.toRoman= function () {
    var num = Math.floor(this), 
        val, s= '', i= 0, 
        v = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], 
        r = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']; 

    function toBigRoman(n) {
        var ret = '', n1 = '', rem = n;
        while (rem > 1000) {
            var prefix = '', suffix = '', n = rem, s = '' + rem, magnitude = 1;
            while (n > 1000) {
                n /= 1000;
                magnitude *= 1000;
                prefix += '(';
                suffix += ')';
            }
            n1 = Math.floor(n);
            rem = s - (n1 * magnitude);
            ret += prefix + n1.toRoman() + suffix;
        }
        return ret + rem.toRoman();
    }

    if (this - num || num < 1) num = 0;
    if (num > 3999) return toBigRoman(num);

    while (num) {
        val = v[i];
        while (num >= val) {
            num -= val;
            s += r[i];
        }
        ++i;
    }
    return s;
};

罗马数字字符串转换为数字:

Number.fromRoman = function (roman, accept) {
    var s = roman.toUpperCase().replace(/ +/g, ''), 
        L = s.length, sum = 0, i = 0, next, val, 
        R = { M: 1000, D: 500, C: 100, L: 50, X: 10, V: 5, I: 1 };

    function fromBigRoman(rn) {
        var n = 0, x, n1, S, rx =/(\(*)([MDCLXVI]+)/g;

        while ((S = rx.exec(rn)) != null) {
            x = S[1].length;
            n1 = Number.fromRoman(S[2])
            if (isNaN(n1)) return NaN;
            if (x) n1 *= Math.pow(1000, x);
            n += n1;
        }
        return n;
    }

    if (/^[MDCLXVI)(]+$/.test(s)) {
        if (s.indexOf('(') == 0) return fromBigRoman(s);

        while (i < L) {
            val = R[s.charAt(i++)];
            next = R[s.charAt(i)] || 0;
            if (next - val > 0) val *= -1;
            sum += val;
        }
        if (accept || sum.toRoman() === s) return sum;
    }
    return NaN;
};

17

我个人认为最简洁的方法(不一定是最快的)是使用递归。

function convert(num) { 
  if(num < 1){ return "";}
  if(num >= 40){ return "XL" + convert(num - 40);}
  if(num >= 10){ return "X" + convert(num - 10);}
  if(num >= 9){ return "IX" + convert(num - 9);}
  if(num >= 5){ return "V" + convert(num - 5);}
  if(num >= 4){ return "IV" + convert(num - 4);}
  if(num >= 1){ return "I" + convert(num - 1);}  
}
console.log(convert(39)); 
//Output: XXXIX

这只支持1-40的数字,但是按照这种模式可以轻松扩展。


1
简单,我喜欢它。 - Kate Kiatsiri
这绝对是我最喜欢的方法。我知道递归不太高效,但这些计算并不难,而且代码非常易读。 - Steve

13

这个版本不需要像其他版本那样为边缘情况(如4(IV),9(IX),40(XL),900(CM)等)硬编码逻辑。

我已经对这段代码进行了1-3999的数据集测试,它可以正常工作。

简而言之;

这也意味着这个解决方案可以处理比罗马数字最大规模(3999)更大的数字。

看起来有一个交替规则来决定下一个主要的罗马数字字符。从I开始,乘以5得到下一个数字V,然后乘以2得到X,再乘以5得到L,然后乘以2得到C,以此类推,得到比例尺中的下一个主要数字字符。在这种情况下,假设“T”被添加到比例尺中以允许处理比3999更大的数字。为了保持相同的算法,“T”将代表5000。

I = 1
V = I * 5
X = V * 2
L = X * 5
C = L * 2
D = C * 5
M = D * 2
T = M * 5

这将使我们能够表示4000到5000之间的数字;例如,MT = 4000。


代码:

function convertToRoman(num) {
  //create key:value pairs
  var romanLookup = {M:1000, D:500, C:100, L:50, X:10, V:5, I:1};
  var roman = [];
  var romanKeys = Object.keys(romanLookup);
  var curValue;
  var index;
  var count = 1;
  
  for(var numeral in romanLookup){
    curValue = romanLookup[numeral];
    index = romanKeys.indexOf(numeral);
    
    while(num >= curValue){
      
      if(count < 4){
        //push up to 3 of the same numeral
        roman.push(numeral);
      } else {
        //else we had to push four, so we need to convert the numerals 
        //to the next highest denomination "coloring-up in poker speak"
        
        //Note: We need to check previous index because it might be part of the current number.
        //Example:(9) would attempt (VIIII) so we would need to remove the V as well as the I's
        //otherwise removing just the last three III would be incorrect, because the swap 
        //would give us (VIX) instead of the correct answer (IX)
        if(roman.indexOf(romanKeys[index - 1]) > -1){
          //remove the previous numeral we worked with 
          //and everything after it since we will replace them
          roman.splice(roman.indexOf(romanKeys[index - 1]));
          //push the current numeral and the one that appeared two iterations ago; 
          //think (IX) where we skip (V)
          roman.push(romanKeys[index], romanKeys[index - 2]);
        } else {
          //else Example:(4) would attemt (IIII) so remove three I's and replace with a V 
          //to get the correct answer of (IV)
          
          //remove the last 3 numerals which are all the same
          roman.splice(-3);
          //push the current numeral and the one that appeared right before it; think (IV)
          roman.push(romanKeys[index], romanKeys[index - 1]);
        }
      }
      //reduce our number by the value we already converted to a numeral
      num -= curValue;
      count++;
    }
    count = 1;
  }
  return roman.join("");
}

convertToRoman(36);

5
为什么这篇文章会被踩?它是一个完整的例子,解答了问题,并且文档非常详细。 - Shawn
欢迎来到互联网。 - ximo

9

我知道这是一个老问题,但我对这个解决方案感到非常自豪 :) 它只处理小于1000的数字,但可以通过添加到“numeralCodes”二维数组来轻松扩展以包括任意大的数字。

var numeralCodes = [["","I","II","III","IV","V","VI","VII","VIII","IX"],         // Ones
                    ["","X","XX","XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"],   // Tens
                    ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM"]];        // Hundreds

function convert(num) {
  var numeral = "";
  var digits = num.toString().split('').reverse();
  for (var i=0; i < digits.length; i++){
    numeral = numeralCodes[i][parseInt(digits[i])] + numeral;
  }
  return numeral;  
}
<input id="text-input" type="text">
<button id="convert-button" onClick="var n = parseInt(document.getElementById('text-input').value);document.getElementById('text-output').value = convert(n);">Convert!</button>
<input id="text-output" style="display:block" type="text">


1
啊,这很聪明。我之前也用了switch语句实现了类似的功能,但在找出模式并使代码更加优美后,这将是我的下一个重构步骤。 - Jason H

7

循环可能更优雅,但我觉得它们难以阅读。我想出了一个几乎硬编码的版本,这样看起来更加清晰易懂。只要你理解第一行,其余的就是小菜一碟。

function romanNumeralGenerator (int) {
  let roman = '';

  roman +=  'M'.repeat(int / 1000);  int %= 1000; 
  roman += 'CM'.repeat(int / 900);   int %= 900; 
  roman +=  'D'.repeat(int / 500);   int %= 500;  
  roman += 'CD'.repeat(int / 400);   int %= 400;
  roman +=  'C'.repeat(int / 100);   int %= 100;
  roman += 'XC'.repeat(int / 90);    int %= 90;
  roman +=  'L'.repeat(int / 50);    int %= 50;
  roman += 'XL'.repeat(int / 40);    int %= 40;
  roman +=  'X'.repeat(int / 10);    int %= 10;
  roman += 'IX'.repeat(int / 9);     int %= 9;
  roman +=  'V'.repeat(int / 5);     int %= 5;
  roman += 'IV'.repeat(int / 4);     int %= 4;
  roman +=  'I'.repeat(int);

  return roman;
}

7

我创建了两个转换函数。

第一个函数使用reduce方法将数字转换为罗马数字。 第二个函数与第一个函数非常相似,该函数使用相同的方式来转换值。

你需要更改的是_roman属性。因为你必须根据需要扩展这个常量的规模,我在那里放置了最大数量1000,但你可以放更多。

更大规模的罗马数字可以在此处找到https://www.tuomas.salste.net/doc/roman/numeri-romani.html

const _roman = { M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90, L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1 };

// 1903 => MCMIII
function toRoman(number = 0) {
    return Object.keys(_roman).reduce((acc, key) => {
        while (number >= _roman[key]) {
            acc += key;
            number -= _roman[key];
        }
        return acc;
    }, '');
}


// MCMIII => 1903
function fromRoman(roman = '') {
    return Object.keys(_roman).reduce((acc, key) => {
        while (roman.indexOf(key) === 0) {
            acc += _roman[key];
            roman = roman.substr(key.length);
        }
        return acc;
    }, 0);
}

console.log(toRoman(1903));  // should return 'MCMIII
console.log(fromRoman('MCMIII')); // should return 1903


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接