首页 > 解决方案 > 如何将罗马数字转换为十进制数字?

问题描述

这会将十进制数字转换为罗马数字:

select  to_char(515, 'RN') from dual;

回报:DXV

怎么做反向?这抛出ORA-01722: Invalid number

select to_number('DXV', 'RN') from dual;

标签: oracleplsql

解决方案


只是为了好玩,一种将字符串分成单个和相邻数字组的替代方法,允许标准减法表示法(谢谢,维基百科;将每个单个或相邻对或数字转换为其十进制等效项;然后对它们求和:

with t (str) as (select 'MCMLXXXIV' from dual)
select sum(
  case regexp_substr(str, '(CM|M|CD|D|XC|C|XL|L|IX|X|IV|V|I)', 1, level)
    when 'M'  then 1000
    when 'CM' then 900
    when 'D'  then 500
    when 'CD' then 400
    when 'C'  then 100
    when 'XC' then 90
    when 'L'  then 50
    when 'XL' then 40
    when 'X'  then 10
    when 'IX' then 9
    when 'V'  then 5
    when 'IV' then 4
    when 'I'  then 1
  end) as decimals
from t
connect by regexp_substr(str, '(CM|M|CD|D|XC|C|XL|L|IX|X|IV|V|I)', 1, level) is not null;

  DECIMALS
----------
      1984

请注意,正则表达式中搜索词的顺序与其等效的十进制顺序不同;由于减法符号,您需要在 M 之前匹配 CM。

如果这是您需要做的很多事情,那么可能值得创建一个确定性函数(当然,递归 CTE 方法也是如此)。在这种情况下,您可以切换到 PL/SQL 循环以减少上下文切换:

create or replace function roman_to_decimal(p_roman varchar2)
return number deterministic is
  l_decimal number := 0;
begin
  for i in 1..regexp_count(p_roman, '(CM|M|CD|D|XC|C|XL|L|IX|X|IV|V|I)') loop
    l_decimal := l_decimal +
      case regexp_substr(p_roman, '(CM|M|CD|D|XC|C|XL|L|IX|X|IV|V|I)', 1, i)
        when 'M'  then 1000
        when 'CM' then 900
        when 'D'  then 500
        when 'CD' then 400
        when 'C'  then 100
        when 'XC' then 90
        when 'L'  then 50
        when 'XL' then 40
        when 'X'  then 10
        when 'IX' then 9
        when 'V'  then 5
        when 'IV' then 4
        when 'I'  then 1
      end;
  end loop;

  return l_decimal;
end;
/

select roman_to_decimal('DXV'), roman_to_decimal('MCMLXXXIV')
from dual;

ROMAN_TO_DECIMAL('DXV') ROMAN_TO_DECIMAL('MCMLXXXIV')
----------------------- -----------------------------
                    515                          1984

您可以使用以下命令查看并检查所有转换(1-3999,因为这是 支持的范围to_char()):

with t (orig, roman) as (
  select level, to_char(level, 'RN') from dual connect by level < 4000
)
select orig, roman, roman_to_decimal(roman)
from t;

      ORIG ROMAN           ROMAN_TO_DECIMAL(ROMAN)
---------- --------------- -----------------------
         1               I                       1
         2              II                       2
         3             III                       3
         4              IV                       4
         5               V                       5
         6              VI                       6
         7             VII                       7
         8            VIII                       8
         9              IX                       9
        10               X                      10
        11              XI                      11
...
      3994       MMMCMXCIV                    3994
      3995        MMMCMXCV                    3995
      3996       MMMCMXCVI                    3996
      3997      MMMCMXCVII                    3997
      3998     MMMCMXCVIII                    3998
      3999       MMMCMXCIX                    3999

或者只是为了验证它们都转换回原来的值:

with t (original, roman) as (
  select level, to_char(level, 'RN') from dual connect by level < 4000
)
select original, roman, roman_to_decimal(roman)
from t
where roman_to_decimal(roman) != original;

no rows selected

这比我的递归 CTE 等价物要慢得多,但在其他版本和平台上可能会有所不同;就像我说的,只是为了好玩...


推荐阅读