如何在PHP中将罗马数字转换为整数?

31

使用PHP,我想将包含罗马数字的字符串转换为其整数表示。我需要这样做是因为我需要对它们进行计算。

维基百科关于罗马数字

只需识别基本的罗马数字字符即可:

$roman_values=array(
    'I' => 1,
    'V' => 5,
    'X' => 10,
    'L' => 50,
    'C' => 100,
    'D' => 500,
    'M' => 1000,
);

这意味着最大的罗马数字是3999(MMMCMXCIX)。我将使用N来代表零,除此之外只支持正整数。

我不能使用PEAR库来处理罗马数字。

我在SO上找到了一个很好的问题,可以测试字符串是否包含有效的罗马数字:

如何使用正则表达式匹配有效的罗马数字?

编写这个功能的最佳方式是什么?


1
为什么你不能使用PEAR库呢?你至少可以看一下它的代码吧?它采用与PHP相同的许可证。 - Andrew Aylett
由于Pear并不普及,例如无法在PHP命令行环境中安装。同时出于安全原因也不被允许使用 :) - publikz.com
@stereofrog 服务器上没有安装PEAR包管理器,而且我没有安装它的权限。说实话,对于这个简单的任务来说,它并不是非常值得安装。 - kapa
14个回答

48

这样怎么样:

$romans = array(
    '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,
);

$roman = 'MMMCMXCIX';
$result = 0;

foreach ($romans as $key => $value) {
    while (strpos($roman, $key) === 0) {
        $result += $value;
        $roman = substr($roman, strlen($key));
    }
}
echo $result;

对于提供的$roman,应该输出3999。在我进行有限的测试时似乎有效。

MCMXC = 1990
MM = 2000
MMXI = 2011
MCMLXXV = 1975

您可能希望首先进行一些验证 :-)


1
我喜欢你的解决方案非常简短,但是你需要向$romans添加一些项目,因为例如MIMMDCCCCLXXXXVIIII都可以表示1999(因为对于什么构成有效的罗马数字没有共识)。 - akTed
@andyb 我在一个将会使用 MIT 许可证发布的项目中真的需要这个代码片段。你有没有可能将你的回答授权为 MIT 许可证?我不喜欢病毒式许可证,也不想使用 SE 默认的 cc by-sa 3.0 许可证。 - Schlaus
1
很高兴以MIT协议发布。最简单/最好的方法是什么? - andyb
2
@akTed 其实不行,因为:1. 十位字符(_I、X、C 和 M_)最多可以重复三次。在第四次时,需要从下一个更高的五位字符中减去。因此,“MDCCCCLXXXXVIIII”是无效的数字(_CCCC 应该替换为 CD_)。2. 更大的值后面不应跟随较小的值,因此“MIM”也是无效的。1999年写作“MCMXCIX”。 - Alexandru Guzinschi
@Alexandru Guzinschi 如我评论所述,对于XXXX是有效的目前没有共识。对罗马数字的书写方式没有单一的“正确”方法。 - akTed
显示剩余2条评论

10

我不确定你是否已经获得ZF,但是如果你(或者任何正在阅读此内容的人)获得了ZF,这是我的片段:

$number = new Zend_Measure_Number('MCMLXXV', Zend_Measure_Number::ROMAN);
$number->convertTo (Zend_Measure_Number::DECIMAL);
echo $number->getValue();

Zend 2已经更改,请参见NumberFormat - Peter Krauss

10

这是我想出来的一个方案,我还添加了合法性检查。

class RomanNumber {
    //array of roman values
    public static $roman_values=array(
        'I' => 1, 'V' => 5, 
        'X' => 10, 'L' => 50,
        'C' => 100, 'D' => 500,
        'M' => 1000,
    );
    //values that should evaluate as 0
    public static $roman_zero=array('N', 'nulla');
    //Regex - checking for valid Roman numerals
    public static $roman_regex='/^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/';

    //Roman numeral validation function - is the string a valid Roman Number?
    static function IsRomanNumber($roman) {
         return preg_match(self::$roman_regex, $roman) > 0;
    }

    //Conversion: Roman Numeral to Integer
    static function Roman2Int ($roman) {
        //checking for zero values
        if (in_array($roman, self::$roman_zero)) {
            return 0;
        }
        //validating string
        if (!self::IsRomanNumber($roman)) {
            return false;
        }

        $values=self::$roman_values;
        $result = 0;
        //iterating through characters LTR
        for ($i = 0, $length = strlen($roman); $i < $length; $i++) {
            //getting value of current char
            $value = $values[$roman[$i]];
            //getting value of next char - null if there is no next char
            $nextvalue = !isset($roman[$i + 1]) ? null : $values[$roman[$i + 1]];
            //adding/subtracting value from result based on $nextvalue
            $result += (!is_null($nextvalue) && $nextvalue > $value) ? -$value : $value;
        }
        return $result;
    }
}

4

快速的想法是,从右到左遍历罗马数字,如果更左侧的$current的值比$previous小,则从结果中减去它,如果更大,则加上它。

$romanValues=array(
    'I' => 1,
    'V' => 5,
    'X' => 10,
    'L' => 50,
    'C' => 100,
    'D' => 500,
    'M' => 1000,
);
$roman = 'MMMCMXCIX';

// RTL
$arabic = 0;
$prev = null;
for ( $n = strlen($roman) - 1; $n >= 0; --$n ) {
    $curr = $roman[$n];
    if ( is_null($prev) ) {
        $arabic += $romanValues[$roman[$n]];
    } else {
        $arabic += $romanValues[$prev] > $romanValues[$curr] ? -$romanValues[$curr] : +$romanValues[$curr];
    }
    $prev = $curr;
}
echo $arabic, "\n";

// LTR
$arabic = 0;
$romanLength = strlen($roman);
for ( $n = 0; $n < $romanLength; ++$n ) {
    if ( $n === $romanLength - 1 ) {
        $arabic += $romanValues[$roman[$n]];
    } else {
        $arabic += $romanValues[$roman[$n]] < $romanValues[$roman[$n+1]] ? -$romanValues[$roman[$n]] : +$romanValues[$roman[$n]];
    }
}
echo $arabic, "\n";

一些罗马数字的验证也应该被添加,尽管你已经知道如何做了。

是的,在这种情况下很重要,因为“当前字母”的含义取决于“下一个字母”的值——如果下一个字母比当前字母小或相同,则将当前字母添加到结果中;如果下一个字母较大,则从结果中减去当前字母。如果我们从右到左进行,我们将“下一个字母”存储在$prev变量中,因此它始终可以访问,除了第一个(最右边)字母,其中基本的is_null($prev)检查就足够了。如果我们从左到右进行,我们必须检查下一个字母的值以及下一个字母的存在。 - binaryLV
请记住,这也可能适用于无效的罗马字母,例如,IVL将被视为-1-5+50并导致44,应该写为XLIV。因此,应添加数字结构的验证,如答案中所述。 - binaryLV
@binaryLV,“如果我们采用LTR,我们必须检查下一个字母的值以及下一个字母的存在”,你是在使用RTL,但你仍然对下一个字母的值进行了检查(三元运算符)。那么LTR还需要什么? - kapa
那个检查只需要以不同的方式进行。在RTL中,您可以使用上一个循环迭代的值作为“当前”迭代中的值将成为“下一个”迭代中的“先前”值。在LTR中,在每次迭代中,您必须获取将在“下一个”迭代中成为“当前”值的值,因为它尚未存储在任何地方。我已经更新了这段代码的LTR版本的答案。 - binaryLV
@binaryLV 嗯,我明白了。你可以同时保存罗马值,这样就少了一个数组查找。不错的解决方案。 - kapa

3
版权归此博客所有(顺便说一句!) http://scriptsense.blogspot.com/2010/03/php-function-number-to-roman-and-roman.html
<?php

function roman2number($roman){
    $conv = array(
        array("letter" => 'I', "number" => 1),
        array("letter" => 'V', "number" => 5),
        array("letter" => 'X', "number" => 10),
        array("letter" => 'L', "number" => 50),
        array("letter" => 'C', "number" => 100),
        array("letter" => 'D', "number" => 500),
        array("letter" => 'M', "number" => 1000),
        array("letter" => 0, "number" => 0)
    );
    $arabic = 0;
    $state = 0;
    $sidx = 0;
    $len = strlen($roman);

    while ($len >= 0) {
        $i = 0;
        $sidx = $len;

        while ($conv[$i]['number'] > 0) {
            if (strtoupper(@$roman[$sidx]) == $conv[$i]['letter']) {
                if ($state > $conv[$i]['number']) {
                    $arabic -= $conv[$i]['number'];
                } else {
                    $arabic += $conv[$i]['number'];
                    $state = $conv[$i]['number'];
                }
            }
            $i++;
        }

        $len--;
    }

    return($arabic);
}


function number2roman($num,$isUpper=true) {
    $n = intval($num);
    $res = '';

    /*** roman_numerals array ***/
    $roman_numerals = array(
        '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
    );

    foreach ($roman_numerals as $roman => $number)
    {
        /*** divide to get matches ***/
        $matches = intval($n / $number);

        /*** assign the roman char * $matches ***/
        $res .= str_repeat($roman, $matches);

        /*** substract from the number ***/
        $n = $n % $number;
    }

    /*** return the res ***/
    if($isUpper) return $res;
    else return strtolower($res);
}

/* TEST */
echo $s=number2roman(1965,true);
echo "\n and bacK:\n";
echo roman2number($s);


?>

1
不要花太多时间试图理解算法,它似乎存在缺陷 - 将800写成CCM(虽然通常被认为是不好的风格)和DCCC都是有效的,但方法应该是任何数字后面跟着一个比它更高数值的数字时,应该从后者中减去前者而不是相加。 - symcbean

2

我来晚了,但这是我的回答。假设字符串中的数字是有效的,但不测试其是否是有效的罗马数字,因为似乎没有共识。这个函数可以处理像 VC (95)、 MIM (1999)或 MMMMMM (6000)这样的罗马数字。

function roman2dec( $roman ) {
    $numbers = array(
        'I' => 1,
        'V' => 5,
        'X' => 10,
        'L' => 50,
        'C' => 100,
        'D' => 500,
        'M' => 1000,
    );

    $roman = strtoupper( $roman );
    $length = strlen( $roman );
    $counter = 0;
    $dec = 0;
    while ( $counter < $length ) {
        if ( ( $counter + 1 < $length ) && ( $numbers[$roman[$counter]] < $numbers[$roman[$counter + 1]] ) ) {
            $dec += $numbers[$roman[$counter + 1]] - $numbers[$roman[$counter]];
            $counter += 2;
        } else {
            $dec += $numbers[$roman[$counter]];
            $counter++;
        }
    }
    return $dec;
}

1
function romanToInt($s) {
    $array = ["I"=>1,"V"=>5,"X"=>10,"L"=>50,"C"=>100,"D"=>500,"M"=>1000];
    $sum = 0;
    for ($i = 0; $i < strlen($s); $i++){
        $curr = $s[$i];
        $next = $s[$i+1];
        if ($array[$curr] < $array[$next]) {
            $sum += $array[$next] - $array[$curr];
            $i++;
        } else {
            $sum += $array[$curr];
        }
    }
    return $sum;
}

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

1

哇! 这是相当多的答案,并且其中很多都是代码重复! 在我给出答案之前,我们先定义一个算法怎么样?

基础知识

  • 不要在数组中存储多位数字的罗马数字,例如'CM' => 900或类似的内容。 如果您知道M-C1000-100)等于900,那么最终,您只应存储1000100的值。 您不会有像CMI这样的多位罗马数字表示901,对吧?任何这样做的答案都将比理解罗马语法的答案效率低。

算法

示例:LIX59

  • 在数字上进行for循环,从罗马数字字符串的末尾开始。在我们的示例中:我们从“X”开始。
  • 大于等于情况 - 如果我们正在查看的值与上一个值相同或更大,则将其简单地添加到累计结果中。在我们的示例中:$result += $numeral_values["X"]
  • 小于情况 - 如果我们要减去的值小于前一个数字,则从累计结果中减去它。在我们的示例中IXI1X10,因此,由于1小于10,我们将其减去:得到9。

演示

完整的在线演示

代码

function RomanNumeralValues() {
    return [
        'I'=>1,
        'V'=>5,
        'X'=>10,
        'L'=>50,
        'C'=>100,
        'D'=>500,
        'M'=>1000,
    ];
}

function ConvertRomanNumeralToArabic($input_roman){
    $input_length = strlen($input_roman);
    if($input_length === 0) {
        return $result;
    }
    
    $roman_numerals = RomanNumeralValues();
    
    $current_pointer = 1;
    $result = 0;
    
    for($i = $input_length - 1; $i > -1; $i--){ 
        $letter = $input_roman[$i];
        $letter_value = $roman_numerals[$letter];
        
        if($letter_value === $current_pointer) {
            $result += $letter_value;
        } elseif ($letter_value < $current_pointer) {
            $result -= $letter_value;
        } else {
            $result += $letter_value;
            $current_pointer = $letter_value;
        }
    }
    
    return $result;
}

print ConvertRomanNumeralToArabic("LIX");

这里有几个问题:1)在定义$result之前就返回它;2)罗马数字的值是静态的,所以在方法中定义它们会更有意义(我个人的观点)。 - Nathanael McDaniel
这里有几个问题:1)在定义$result之前就返回它;2)罗马数字的值是静态的,所以在方法中定义它们会更有意义(个人观点)。 - undefined

0
function Romannumeraltonumber($input_roman){
  $di=array('I'=>1,
            'V'=>5,
            'X'=>10,
            'L'=>50,
            'C'=>100,
            'D'=>500,
            'M'=>1000);
  $result=0;
  if($input_roman=='') return $result;
  //LTR
  for($i=0;$i<strlen($input_roman);$i++){ 
    $result=(($i+1)<strlen($input_roman) and 
          $di[$input_roman[$i]]<$di[$input_roman[$i+1]])?($result-$di[$input_roman[$i]]) 
                                                        :($result+$di[$input_roman[$i]]);
   }
 return $result;
}

你应该添加解释。 - HoldOffHunger

0

定义您自己的架构!(可选)

function rom2arab($rom,$letters=array()){
    if(empty($letters)){
        $letters=array('M'=>1000,
                       'D'=>500,
                       'C'=>100,
                       'L'=>50,
                       'X'=>10,
                       'V'=>5,
                       'I'=>1);
    }else{
        arsort($letters);
    }
    $arab=0;
    foreach($letters as $L=>$V){
        while(strpos($rom,$L)!==false){
            $l=$rom[0];
            $rom=substr($rom,1);
            $m=$l==$L?1:-1;
            $arab += $letters[$l]*$m;
        }
    }
    return $arab;
}

受 andyb 答案的启发


这个$rom期望什么样的输入?我完全搞不清楚。 - HoldOffHunger

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