File Coverage

blib/lib/Math/Roman.pm
Criterion Covered Total %
statement 102 119 85.7
branch 35 48 72.9
condition 6 8 75.0
subroutine 12 14 85.7
pod 6 6 100.0
total 161 195 82.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2              
3             # todo: could be faster,storing values of tokes as BigInt instead integer
4             # makes it slower (due to $k < $last)
5             # Roman.pm uses 4.2s for 1...4000 compared to our 6.5s (new())
6             # and 5.7s (roman()), so actually we are pretty fast (we construct a
7             # bigint on-the-fly, too!)
8             #
9             # maybe: make 'use Roman qw(badd); print badd("M","X"),"\n";' work:
10             # just define the following and allow of export badd:
11             # sub badd
12             # {
13             # if ($_[0] eq $class)
14             # {
15             # shift;
16             # }
17             # $class->SUPER::badd(@_);
18             # }
19             # The problem is the additional overhead (about 2%) and the problem to write
20             # the above for _all_ functions of BigInt. That's rather long. AUTOLOAD does
21             # not work, since it steps in _after_ inheritance. Do we really need this?
22             # 2001-11-08: Don't think we need it, othe subclasses don't do it, either. Tels
23              
24             package Math::Roman;
25 1     1   4070 use vars qw($VERSION);
  1         1  
  1         46  
26             $VERSION = 1.08; # Current version of this package
27             require 5.005; # requires this Perl version or later
28              
29             require Exporter;
30 1     1   898 use Math::BigInt;
  1         15517  
  1         4  
31             @ISA = qw(Exporter Math::BigInt);
32             @EXPORT_OK = qw( as_number tokens roman error);
33 1     1   9708 use strict;
  1         5  
  1         20  
34              
35 1     1   3 use overload; # inherit from MBI
  1         1  
  1         6  
36              
37             #############################################################################
38             # global variables
39              
40             my $sh; # hash of roman symbols (symbol => value)
41             my $sm; # hash of roman symbols (value => symbol)
42             my $ss; # a list sorted by value
43             my $re; # compiled regexps matching tokens
44             my $err; # error message
45             my $bt; # biggest token
46             my $bv; # biggest value
47              
48             # some shortcuts for easier life
49             sub roman
50             {
51             # exportable version of new
52 2     2 1 31 my $c = 'Math::Roman';
53 2 50       3 my $value = shift; $value = 0 if !defined $value;
  2         5  
54             # try construct a number (if we got '1999')
55 2         4 my $self = Math::BigInt->new($value);
56             # if first failed, so check for Roman
57 2 100       46 _initialize($self,$value) if $self->{sign} eq 'NaN';
58 2         3 bless $self, $c; # rebless
59 2         3 $self;
60             }
61              
62             sub error
63             {
64             # return last error message in case of NaN
65 0     0 1 0 return $err;
66             }
67              
68             sub new
69             {
70 2098     2098 1 268231 my $c = shift;
71 2098   50     6732 $c = ref($c) || __PACKAGE__;
72 2098 50       1970 my $value = shift; $value = 0 if !defined $value;
  2098         3028  
73             # try construct a number (if we got '1999')
74 2098         4529 my $self = Math::BigInt->new($value);
75             # if first failed, so check for Roman
76 2098 100       45340 _initialize($self,$value) if $self->{sign} eq 'NaN';
77 2098         1961 bless $self, $c; # rebless
78 2098         10854 $self;
79             }
80              
81             #############################################################################
82             # self initalization
83              
84             sub tokens
85             {
86             # set/return list of valid/invalid tokens
87             # sorted by length to favour 'IM' over 'I' when matching
88             # create hash and length sorted array
89 2     2 1 648 my @sym = @_;
90             # return current token set
91 2 50       11 return map { $_, $sh->{$_} } keys %$sh if (@_ == 0);
  0         0  
92 2         3 my $sl = []; # a list sorted by name-length
93 2         3 $ss = [];
94 2         4 $sh = {}; $sm = {};
  2         8  
95 2         5 $bv = -1; $bt = ''; $re = "";
  2         2  
  2         2  
96 2         2 my $i;
97 2         6 for ($i = 0; $i<@sym;$i += 2)
98             {
99             #print "token $sym[$i] => $sym[$i+1]\n";
100 36         35 push @$sl,$sym[$i]; # store all tokens in a tmp list
101 36         60 $sh->{$sym[$i]} = int($sym[$i+1]); # contain all token=>value
102 36 100       44 if (int($sym[$i+1]) != -1) # only valid ones
103             {
104 26         26 push @$ss,int($sym[$i+1]); # for regexp compiler
105 26         43 $sm->{$sym[$i+1]} = $sym[$i]; # generate hash for value=>token
106 26 100       69 ($bt,$bv) = ($sym[$i],int($sym[$i+1])) if (int($sym[$i+1]) > $bv);
107             }
108             }
109             # sort symbols by name length (and if equal, by value)
110 2 50       6 @$sl = sort { length $b <=> length $a || $sh->{$b} <=> $sh->{$a} } @$sl;
  103         123  
111             # compile a big regexp for token parsing
112 2         6 $re = join('|', @$sl);
113             # print "regexp '$re'\n";
114             # for converting Arabic => Roman
115 2         6 @$ss = sort { $b <=> $a } @$ss;
  65         41  
116             # return current token set
117 2 50       18 return map { $_, $sh->{$_} } keys %$sh if (@_ == 0);
  0         0  
118             }
119              
120             BEGIN
121             {
122 1     1   411 tokens( qw(
123             IIII -1
124             XXXX -1
125             CCCC -1
126             DD -1
127             LL -1
128             VV -1
129             C[MD][CDM] -1
130             X[LC][XLCDM] -1
131             I[VX][IVXLCDM] -1
132             LXL -1
133             III 3
134             XXX 30
135             CCC 300
136             II 2
137             XX 20
138             CC 200
139             IV 4
140             IX 9
141             XL 40
142             XC 90
143             CD 400
144             CM 900
145             I 1
146             V 5
147             X 10
148             L 50
149             C 100
150             D 500
151             M 1000
152             ) );
153 1         475 undef $err;
154             }
155              
156             # check for illegal sequences (simple return, we are already NaN)
157             # the following are not valid tokens according to rules:
158             # IIII
159             # XXXX
160             # CCCC
161             # only ICX as precede:
162             # VX 5
163             # VL 45
164             # VC 95
165             # VD 495
166             # LM 995
167             # LC 50
168             # LD 450
169             # LM 950
170             # not smaller then 10 preceding:
171             # IL 49
172             # IC 99
173             # ID 499
174             # IM 999
175             # XD 490
176             # XM 990
177             # illegal ones, smaller then following (several cases are already caught
178             # by rule: token0 < token1)
179             # CDD (C < D)
180             # CDC (C = C)
181             # XCD (X < D)
182             # LXL (L = L)
183             # They need to be checked separetely, the following regexps take care
184             # of that:
185             # C[MD][CDM]
186             # X[LC][XLCDM]
187             # I[VX][IVXLCDM]
188              
189             sub _initialize
190             {
191             # set yourself to the value represented by the given string
192 1067     1067   819 my $self = shift;
193 1067         822 my $value = shift;
194              
195 1067         1499 $self->bzero(); # start with 0
196              
197             # this is probably very inefficient...
198 1067         10555 my $k; my $e = 0; my $last = -1; undef $err;
  1067         898  
  1067         707  
  1067         924  
199 1067   100     3421 while ((length($value) > 0) && ($e == 0))
200             {
201             # can't use /o since tokens might redefine $re
202 3716         16368 $value =~ s/^($re)//;
203 3716 100       6653 if (defined $1)
204             {
205 3714         5273 _symb($self,$1,\$e,\$last);
206             }
207             else
208             {
209 2         4 $err = "Math::Roman: Invalid part '$value' encountered.";
210 2         8 $e = 4;
211             }
212             }
213 1067 100       1478 $self->bnan() if ($e != 0);
214 1067         1394 return;
215             }
216              
217             sub _symb
218             {
219             # current symbol, last symbole, error
220 3714     3714   4475 my ($self,$s,$error,$last) = @_;
221             #print "$s => ";
222 3714         3660 my $k = $sh->{$s}; # get value of token
223             #print "$k" if defined $k;
224 3714 100       4209 if (!defined $k)
225             {
226 5         8 $err = "Math::Roman: Undefined token '$s' encountered.";
227 5         6 $$error = 1;
228             }
229             else
230             {
231 3709 100       4841 if ($k == -1)
232             {
233 7         11 $err = "Math::Roman: Invalid token '$s' encountered.";
234 7         7 $$error = 2;
235             }
236 3709 100       4701 $$last = $k if $$last == -1;
237             # next symbol must always be smaller then previous
238 3709 100       4898 if ($k > $$last)
239             {
240 17         42 $err = "Math::Roman: Token '$s' ($k) is greater than last ('$$last').";
241 17         21 $$error = 3;
242             }
243             }
244 3714 100       4700 return if $$error != 0;
245 3685         5661 $self->badd($k); $$last = $k;
  3685         242423  
246 3685         13294 return;
247             }
248              
249             sub bstr
250             {
251 1035     1035 1 5263 my ($x) = @_;
252 1035 50       1700 return $x if !ref($x);
253 1035 100       1847 return '' if $x->is_zero();
254 1034 50       10031 return 'NaN' if $x->is_nan;
255              
256             # make sure that we calculate with BigInt's, otherwise objectify() will
257             # try to make copies of us via bstr(), resulting in deep recursion
258 1034         4453 my $rem = $x->as_number(); $rem->babs();
  1034         31500  
259             ## get the biggest symbol
260             #return $bt if $rem == $bv;
261              
262 1034         4690 my $es = ''; my $cnt;
  1034         759  
263 1034         772 my $level = -1; # for all tokens
264 1034   66     2689 while (($level < scalar @$ss) && (!$rem->is_zero()))
265             {
266 16701         1003821 $level++;
267 16701 100       24741 next if $ss->[$level] > $rem; # this wont fit
268             # calculate number of biggest token
269 3654         177153 ($cnt,$rem) = $rem->bdiv($ss->[$level]);
270 3654 50       368616 if ($rem->sign() eq 'NaN')
271             {
272 0         0 warn ("Something went wrong at token $ss->[$level].");
273 0         0 return 'NaN';
274             }
275             # this limits $cnt to be < 65536, anyway 65536 Ms is impressive though)
276 3654         16905 $cnt = int ($cnt);
277 3654 50       37304 $es .= $sm->{$ss->[$level]} x $cnt if $cnt != 0;
278             }
279 1034         102298 return $es;
280             # remove biggest token(s) so that only reminder is left
281             #my $es = '';
282             #my $cnt;
283             #if ($rem > $bv)
284             # {
285             # # calculate number of biggest token
286             # ($cnt,$rem) = $rem->bdiv($bv);
287             # if ($rem->sign() eq 'NaN')
288             # {
289             # warn ("Something went wrong with bt='$bt' and bv='$bv'");
290             ## return 'NaN';
291             # }
292             # # this limits $cnt to be < 65536, anyway 65536 Ms is impressive though)
293             # $es = $bt x $cnt;
294             # }
295             #return $es if $rem->is_zero();
296             # find combination of tokens (with decreasing value) that matches reminder
297             # restricted knappsack problem with symbols in @sym, sum 1...999
298             #my $stack = []; my $value = 0;
299             #_recurse(0,\$value,$stack,int($rem));
300             #print "done $value $rem\n";
301             # found valid combination? (should never fail if system is consistent!)
302             #if ($value == $rem)
303             # {
304             # map { $es .= $_ } @$stack;
305             # # {
306             # # $es .= $_;
307             ## # }
308             # # $es .= join //,@$stack; # faster but gives error!?
309             # return $es;
310             # }
311             #return 'NaN';
312             }
313              
314             sub _recurse
315             {
316 0     0   0 my ($level,$value,$stack,$rem) = @_;
317             #print "level $level cur $$value target $rem ",scalar @$ss,"\n";
318              
319 0 0       0 return if $$value >= $rem; # early out, can not get smaller
320 0         0 while ($level < scalar @$ss)
321             {
322             # get current value according to level
323 0         0 my $s = $ss->[$level];
324             # and try it
325 0         0 push @$stack,$sm->{$s}; # get symbol from value
326             #print " "x$level."Trying $s $sm->{$s} at level $level\n";
327 0         0 $$value += $s; # add to test value
328 0         0 _recurse($level,$value,$stack,$rem); # try to add more symbols
329             #print " "x$level."back w/ $$value $rem\n";
330 0 0       0 last if $$value == $rem; # keep this try
331 0         0 $$value -= $s; # reverse try
332 0         0 pop @$stack;
333 0         0 $level ++;
334             }
335 0         0 return;
336             }
337              
338             sub as_number
339             {
340 2097     2097 1 1572 my $self = shift;
341              
342 2097         3694 Math::BigInt->new($self->SUPER::bstr());
343             }
344              
345             1;
346              
347             __END__