Rzymskie na dziesiętne
Shreq
Jak chyba większość maniaków programowania najlepiej odpoczywam po tej pięknej i fascynującej pracy... programując różne pierdoły ;) Natknąłem się na kilka sposobów przekodowywania liczby dziesiętnej na rzymską, ale jakoś nie wpadł mi w oko algorytm przeciwny. Tymczasem potrzeba takiej konwersji zachodzi częściej - choćby, jak w moim przypadku - do zamiany roku produkcji podanego w czołówce filmu w systemie, a jakże, rzymskim, na jakiś przyswajalny mojej wyobraźni, czyli dziesiętny :) Oto owoc kilku chwil mojego relaksu. Dla tych, którzy zrzynają gotowce mechanicznie podaję najpierw źródło :D
unit Roman2Dec;
{ 'private' }
interface
function RomanToDec( const roman: string ): integer;
implementation
uses
Sysutils, { funkcja UpperCase }
Strutils; { funkcje AnsiEndsStr, LeftStr }
const
numbers_max = 21;
type
liczby = record
rom: string;
dec: integer;
end;
function RomanToDecRec( const roman: string ): integer;
const
numbers: array[ 1..numbers_max ] of liczby =
( ( rom: 'III'; dec: 3 ),
( rom: 'II'; dec: 2 ),
( rom: 'IV'; dec: 4 ),
( rom: 'IX'; dec: 9 ),
( rom: 'I'; dec: 1 ),
( rom: 'V'; dec: 5 ),
( rom: 'XXX'; dec: 30 ),
( rom: 'XX'; dec: 20 ),
( rom: 'XL'; dec: 40 ),
( rom: 'XC'; dec: 90 ),
( rom: 'X'; dec: 10 ),
( rom: 'L'; dec: 50 ),
( rom: 'CCC'; dec: 300 ),
( rom: 'CC'; dec: 200 ),
( rom: 'CD'; dec: 400 ),
( rom: 'CM'; dec: 900 ),
( rom: 'C'; dec: 100 ),
( rom: 'D'; dec: 500 ),
( rom: 'MMM'; dec: 3000 ),
( rom: 'MM'; dec: 2000 ),
( rom: 'M'; dec: 1000 ) );
var
roman_temp: string;
i: integer;
decimal_temp: integer;
success: boolean;
begin
{ Przypisanie wartosci poczatkowych }
result := 0; { Wazne przypisanie, pozwala uproscic obsluge ew. bledow! }
decimal_temp := 0; { zmienna sluzaca do przechowywania sumy czastkowej }
{ Przepisanie liczby rzymskiej do zmiennej tymczasowej, zmiana na duze litery }
roman_temp := UpperCase( roman );
{ Wlasciwa analiza }
while Length( roman_temp ) > 0 do
begin
{ Zmienna success bedzie ustawiona na true jesli ktorys z elementow tablicy
numbers bedzie KONCZYL lancuch roman_temp. Na razie ustawiamy ja na false,
co pozwala wylapac blad w przypadku liczb zawierajacych znaki inne niz
cyfry rzymskie, na przyklad 'A'. }
success := false;
{ przeszukiwanie tablicy numbers }
for i := 1 to numbers_max do
begin
if AnsiEndsStr( numbers[ i ].rom, roman_temp ) then
{ jeden z elementow numbers znajduje sie na koncu lancucha roman_temp }
begin
{ kontrola poprawnosci - dziesietny odpowiednik nie moze byc mniejszy
niz uzyskana juz suma tymczasowa. Poniewaz result jest na razie
ustawiony na 0 - mozemy spokojnie opuscic funkcje bez dodatkowych
czynnosci }
if ( numbers[ i ].dec <= decimal_temp ) then
exit;
{ Zwiekszamy sume tymczasowa }
decimal_temp := decimal_temp + numbers[ i ].dec;
{ Obcinamy lancuch tymczasowy od konca o dlugosc znalezionego wlasnie
elementu liczby rzymskiej }
roman_temp := LeftStr( roman_temp, Length( roman_temp ) - Length( numbers[ i ].rom ) );
{ Ustawiamy zmienna success i konczymy przeszukiwanie }
success := true;
break;
end;
end;
if not success then
begin
{ jesli liczba rzymska konczy sie elementem, ktorego nie znaleziono
w tablicy numbers - na przyklad zawiera 'A' - opuszczamy funkcje.
Result dalej jest ustawiony na zero :) }
exit;
end;
end; { lancuch roman_temp pusty, koniec przeszukiwania }
result := decimal_temp;
end; { function RomanToDec }
end.
Teraz kilka słów o użytym algorytmie:
Konwersja oparta jest na prostym fakcie: zapis cyfr rzymskich jest skomplikowany jedynie dlatego, że najstarsi Rzymianie nie znali... SPACJI :) Oraz nie pofatygowali się wprowadzić jednoznakowych odpowiedników dla wszystkich cyfr z zakresu 0..9, co również nie dziwi zważywszy że zera też nie znali :) Tak więc jakaś liczba rzymska, na przykład
MMCCCLXXXVIII (2388 dziesiętnie)
co wygląda dość koszmarnie - rozbiera się jako:
MM CCC L XXX V III
Czyli 2000 + 300 + 50 + 30 + 5 + 3.
Można było stosować logiczny rozbiór liczby, sprawdzając czy 'I' to jedynka czy część 'II', 'III', 'IV', 'VI' lub 'IX', ale powstałby logiczny potworek :) . Można było zapisać odpowiedniki cyfr rzymskich 1..9, 10..90, 100..900, 1000..3000 - ale wtedy tablica numbers miałaby 30 elementów. Wybrałem metodę IMHO najprostszą, polegającą na wyliczeniu listy możliwych tokenów (patrz tablica numbers) i wyszukiwaniu jej elementów w liczbie rzymskiej. Wyjaśnienia wymaga jedynie jej (tablicy :P) uporządkowanie. Jest tak posortowana, żeby podczas przeszukiwania liczby rzymskiej najpierw wychwytywać 'III' a potem 'II'. Zauważcie, że w przypadku wystąpienia niedozwolonej sekwencji 'XXXX' najpierw zostanie odnaleziony token 'XXX', a następnie 'X', co spowoduje wyjście z funkcji z wynikiem zero :) Wyjaśnia się tu dlaczego nie redukowałem rozmiarów tablicy numbers poprzez usunięcie tokenów 'III', 'II' i pozwolenie programowi by zliczał to jako odpowiednią ilość jedynek. Otóż... musiałbym wtedy znacznie rozbudować kontrolę poprawności wprowadzanych liczb rzymskich! Obecnie opiera się ona na prostym fakcie: wartość żadnego kolejnego tokenu nie może być mniejsza niż już obliczona suma cząstkowa (ret_val). Zapis 'VII' jest poprawny, zapis 'IIV' - już nie. Gdybym zostawił tylko 'I', przy konwersji liczby 'III' funkcja zwróciłaby zero :) Dla szczególnie dociekliwych - podpowiem, że kontrola nie jest zupełnie szczelna i mimo wszystko można przemycić niewłaściwy zapis ;)
Konwersja wyłącznie dla liczb z przedziału 1..3999. Na moje potrzeby (zmiana roku filmu) - to aż nadto.
Na koniec (o ile ktoś do tego miejsca doczytał :P) kilka ogólnych uwag o sposobie napisania funkcji. Miałem do wyboru użycie dwóch tablic, jednej na tokeny rzymskie i drugiej na odpowiedniki dziesiętne - lub tablicy rekordów. Pierwszy wariant mimo iż teoretycznie upraszczał strukturę programu - MÓGŁ spowodować problemy i przy pisaniu (łatwo się pomylić w odpowiednikach rom-dec) i przy EWENTUALNYM późniejszym rozszerzaniu funkcji na przykład poprzez zwiększanie zakresu konwertowanych liczb. Z tych samych względów zamiast kodować rozmiar tablicy numbers "na sztywno" - użyłem stałej numbers_max. A życie szybko uczy każdego programistę, że jedynie małą część kodu pisze się raz-na-zawsze :)
Ale sposoby pisania przejrzystego i łatwego do modyfikacji kodu - to już temat na zupełnie inne opowiadanie... :)
Trochę może nie wczas, ale można to rozwiązać dużo prościej w C.
Wystarczą 2 Edity i 1 Button i można też np. tak:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Edit2: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
i:integer;
roman:string;
dzies:integer;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
roman:='';
dzies:=0;
roman:=Edit1.Text;
i:=length(roman);
// Można dodać jeszcze zamianą znaków małych na duże ale to kwestia estetyki i poprawnego działania programu w każdych warunkach.
For i:=1 to i do
Begin
if (roman[i]='i') then roman[i]:='I';
if (roman[i]='v') then roman[i]:='V';
if (roman[i]='x') then roman[i]:='X';
if (roman[i]='l') then roman[i]:='L';
if (roman[i]='c') then roman[i]:='C';
if (roman[i]='d') then roman[i]:='D';
if (roman[i]='m') then roman[i]:='M';
end;
// -> zamiana z małych na duże :P
Edit1.Text:=roman; // Podmiana textu w Edit1 by zlikwidować w nim małe i dać duże. Czysta estetyka :)
for i:=1 to i do
Begin
if (roman[i]='I') then dzies:=dzies+1;
if (roman[i]='I')and(roman[i+1]='V') then dzies:=dzies-2;
if (roman[i]='V') then dzies:=dzies+5;
if (roman[i]='I')and(roman[i+1]='X') then dzies:=dzies-2;
if (roman[i]='X') then dzies:=dzies+10;
if (roman[i]='X')and(roman[i+1]='L') then dzies:=dzies-20;
if (roman[i]='L') then dzies:=dzies+50;
if (roman[i]='X')and(roman[i+1]='C') then dzies:=dzies-20;
if (roman[i]='C') then dzies:=dzies+100;
if (roman[i]='C')and(roman[i+1]='D') then dzies:=dzies-200;
if (roman[i]='D') then dzies:=dzies+500;
if (roman[i]='C')and(roman[i+1]='M') then dzies:=dzies-200;
if (roman[i]='M') then dzies:=dzies+1000;
end;
Edit2.Text:=inttostr(dzies);
end;
end.
Sposób myślę w miarę prosty i zrozumiały :) Myślę że rozważyłem wszystkie opcje w razie czego pisać a poprawię :)
po pierwsze piwo widziałem o wiele prostszy algorytm podobny do przedstawionego przez Tomuslaw'a (pierwszy komentaż)
po drugi piwo rosjanie zapisują rzymska 4 jako IIII a nie IV
(pewnie czepicie się ze rosjanie wszystko inaczej robia X-( ale jesteś programista i musisz przewidzieć wszystkie ruchy użytkownika)
To chyba jest troszke prostsze: http://4programmers.net/article.php?id=197 Warto czasem poczytać co już jest w gotowacach :)
kod bedzie w c, sorx ;>
moze na delphi tez da sie przelozyc...
Pewnie że prostszy, tylko robi konwersję w drugą stronę, z arabskich na rzymskie, nieprawdaż? :D
A nie lepiej zrobić program, który będzie odejmował? :) Tzn.:
...
rz[1]:='I';
rz[2]:='IV';
rz[3]:='V';
rz[4]:='IX';
rz[5]:='X';
rz[6]:='XL';
rz[7]:='L';
rz[8]:='XC';
rz[9]:='C';
rz[10]:='CD';
rz[11]:='D';
rz[12]:='CM';
rz[13]:='M';
ar[1]:=1;
ar[2]:=4;
ar[3]:=5;
ar[4]:=9;
ar[5]:=10;
ar[6]:=40;
ar[7]:=50;
ar[8]:=90;
ar[9]:=100;
ar[10]:=400;
ar[11]:=500;
ar[12]:=900;
ar[13]:=1000;
...
x:=StrToInt(edit1.text); // liczba arabska
i:=13;
repeat
if x>=ar[i] then
begin
x:=x-ar[i];
edit2.text:=edit2.text+rz[i];
end
else
Dec(i);
until x=0;
Prostrzy, nieprawdaż ? :)
-> Gynvael Coldwind
Wbrew pozorom - po wycięciu komentarzy mój kod jest tylko o kilka (dwie?) linijki dłuższy, co wziąwszy pod uwagę iż to Delphi vs C - nie jest złym wynikiem :) Natomiast mój nie przepuści potworków typu:
"IIIIIII"
"IXIXIXIXIX"
"XCAAACX"
że już o "iiiiii" nie wspomnę :)
Idea mi się podoba jako zaczątek logicznego rozbioru zapisu, ale dorzuć jeszcze jakieś sprawdzanie poprawności danych :)
Pozdrawiam
Shreq - rzeczywiście :)