roman numbers revisited
authorClaus Gittinger <cg@exept.de>
Wed, 31 Jul 2002 12:05:53 +0200
changeset 6673 34547f06c5c7
parent 6672 c5a5a9f9f4f5
child 6674 c383bfa3dc8c
roman numbers revisited
Integer.st
--- a/Integer.st	Wed Jul 31 10:39:56 2002 +0200
+++ b/Integer.st	Wed Jul 31 12:05:53 2002 +0200
@@ -402,8 +402,11 @@
     "Modified: / 14.4.1998 / 19:16:46 / cg"
 !
 
-readFromRomanString:aStringOrStream onError:exceptionalValue
-    "convert a string or stream containing a roman representation into an integer"
+readFromRomanString:aStringOrStream
+    "convert a string or stream containing a roman representation into an integer.
+     Raises an exception, if the inputs format is wrong.
+     Will read both real and pidgin roman numbers (see printRomanOn: vs. printRomanOn:naive:),
+     however, a proceedable exception is raised for pidgin numbers."
 
     |romanValues s c val digitVal prevDigitVal countSame delta 
      stopOnSeparator finish|
@@ -416,8 +419,7 @@
         s := aStringOrStream readStream.
     ].
     s atEnd ifTrue:[ 
-        "RomanException raiseErrorStirng:'empty string'"
-        ^ exceptionalValue value 
+        ^ RomanNumberFormatError raiseErrorString:'empty string'
     ].
     val := 0.
     prevDigitVal := 99999.
@@ -428,28 +430,31 @@
         c := s next asUppercase.
         c isSeparator ifTrue:[
             stopOnSeparator ifFalse:[
-                "RomanException raiseErrorStirng:'garbage at the end'"
-                ^ exceptionalValue value 
+                ^ RomanNumberFormatError raiseErrorString:'garbage at the end'
             ].
             finish := true.
         ] ifFalse:[
             digitVal := romanValues at:c ifAbsent:nil.
             digitVal isNil ifTrue:[
-                "RomanException raiseErrorStirng:'invalid character'"
-                ^ exceptionalValue value 
+                ^ RomanNumberFormatError raiseErrorString:'invalid character'
             ].
 
             digitVal = prevDigitVal ifTrue:[
                 ( #( 1 10 100 1000) includes:digitVal) ifFalse:[
-                    "RomanException raiseErrorStirng:'character may not be repeated'"
-                    ^ exceptionalValue value 
+                    ^ RomanNumberFormatError raiseErrorString:'character may not be repeated'
                 ].
                 val := val + digitVal.
                 countSame := countSame + 1.
-                countSame == 4 ifTrue:[
+                countSame >= 4 ifTrue:[
                     digitVal ~= 1000 ifTrue:[
-                        "RomanException raiseErrorStirng:'more than 3 occurrences of same character'"
-                        ^ exceptionalValue value 
+                        countSame > 4 ifTrue:[
+                            ^ RomanNumberFormatError raiseErrorString:'more than 4 occurrences of same character'
+                        ].
+                        "/ this is a naive roman number (such as VIIII);
+                        "/ Its not correct, but sometimes encountered (especially as page numbers).
+                        "/ If you do not want to be too picky,
+                        "/ provide a proceeding handler in order to proceed the parsing.
+                        NaiveRomanNumberFormatError raiseRequestErrorString:'more than 3 occurrences of same character'.
                     ]
                 ].
             ] ifFalse:[
@@ -457,13 +462,11 @@
                     val := val + digitVal.
                 ] ifFalse:[
                     countSame == 1 ifFalse:[
-                        "RomanException raiseErrorStirng:'invalid character combination'"
-                        ^ exceptionalValue value 
+                        ^ RomanNumberFormatError raiseErrorString:'invalid character combination'
                     ].
                     delta := digitVal - prevDigitVal.
                     ( #( 4 9 40 90 400 900) includes:delta) ifFalse:[
-                        "RomanException raiseErrorStirng:'invalid character combination'"
-                        ^ exceptionalValue value 
+                        ^ RomanNumberFormatError raiseErrorString:'invalid character combination'
                     ].
                     val := val - prevDigitVal.
                     val := val + delta.
@@ -475,21 +478,24 @@
         ].
     ].
 "/    val > 5000 ifTrue:[
-"/        "RomanException raiseErrorStirng:'number out of range (1..5000)'"
-"/        ^ exceptionalValue value 
+"/        ^ RomanNumberFormatError raiseErrorStirng:'number out of range (1..5000)'
 "/    ].    
     ^ val.
 
     "
-     self readFromRomanString:'I' onError:nil                                      
-     self readFromRomanString:'II' onError:nil  
-     self readFromRomanString:'III' onError:nil   
-     self readFromRomanString:'IV' onError:nil     
-     self readFromRomanString:'LC' onError:nil       
-     self readFromRomanString:'clix' onError:nil       
-     self readFromRomanString:'MCMXCIX' onError:nil       
+     Integer readFromRomanString:'I'                                      
+     Integer readFromRomanString:'II'   
+     Integer readFromRomanString:'III'        
+     Integer readFromRomanString:'IV'         
+     Integer readFromRomanString:'clix'     
+     Integer readFromRomanString:'MIX'       
+     Integer readFromRomanString:'MCMXCIX'       
+
+    Error case:
+     Integer readFromRomanString:'LC'      
     "
 
+
     "error cases:
       #( 
          ''   
@@ -503,7 +509,7 @@
         'LL'        
         'DD'        
      ) do:[:badString |
-        (self readFromRomanString:badString onError:nil) notNil ifTrue:[self halt].
+        (Integer readFromRomanString:badString onError:nil) notNil ifTrue:[self halt].
      ]
     "
 
@@ -542,7 +548,7 @@
         'MMMMCMXCIX'            4999    
         'MMMMMMMMMCMXCIX'       9999 
      ) pairWiseDo:[:goodString :expectedValue |
-        (self readFromRomanString:goodString onError:nil) ~= expectedValue ifTrue:[self halt].
+        (Integer readFromRomanString:goodString onError:nil) ~= expectedValue ifTrue:[self halt].
      ]
     "
 
@@ -551,7 +557,124 @@
         |romanString|
 
         romanString := String streamContents:[:stream | n printRomanOn:stream].
-        (self readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+        (Integer readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+     ]
+    "
+!
+
+readFromRomanString:aStringOrStream onError:exceptionalValue
+    "convert a string or stream containing a roman representation into an integer.
+     Raises an exception, if the inputs format is wrong. Does not allow naive roman numbers."
+
+    |val|
+
+    RomanNumberFormatError 
+        handle:[:ex |
+            ex signal == NaiveRomanNumberFormatError ifTrue:[
+                NaiveRomanNumberFormatError isHandled ifTrue:[
+                    ex reject
+                ]
+            ].
+            val := exceptionalValue value
+        ]
+        do:[
+            val := self readFromRomanString:aStringOrStream
+        ].
+    ^ val
+
+
+    "
+     Integer readFromRomanString:'I'    onError:nil                                      
+     Integer readFromRomanString:'II'   onError:nil  
+     Integer readFromRomanString:'III'  onError:nil   
+     Integer readFromRomanString:'IV'   onError:nil     
+     Integer readFromRomanString:'clix' onError:nil       
+     Integer readFromRomanString:'MCMXCIX' onError:nil       
+
+   Error cases:
+     Integer readFromRomanString:'LC'   onError:nil       
+     Integer readFromRomanString:'IIII' onError:nil       
+
+   However, the last one can be suppressed:
+     NaiveRomanNumberFormatError ignoreIn:[
+         Integer readFromRomanString:'IIII' onError:nil       
+     ]
+    "
+
+    "error cases:
+      #( 
+         ''   
+        'IIII'   
+        'XIIX'      
+        'VV'        
+        'VVV'        
+        'XXL'         
+        'XLX'        
+        'LC'        
+        'LL'        
+        'DD'        
+     ) do:[:badString |
+        (Integer readFromRomanString:badString onError:nil) notNil ifTrue:[self halt].
+     ]
+    "
+
+    "good cases:
+     #( 'I'     1
+        'II'    2
+        'III'   3
+        'IV'    4
+        'V'     5
+        'VI'    6
+        'VII'   7
+        'VIII'  8
+        'IX'    9
+        'X'     10
+        'XI'    11      
+        'XII'   12   
+        'XIII'  13    
+        'XIV'   14   
+        'XV'    15  
+        'XVI'   16   
+        'XVII'  17    
+        'XVIII' 18     
+        'XIX'   19   
+        'XX'    20      
+        'XXX'   30   
+        'L'     50   
+        'XL'    40    
+        'LX'    60    
+        'LXX'   70     
+        'LXXX'  80      
+        'CXL'   140    
+        'CL'    150    
+        'CLX'   160     
+        'MMM'                   3000      
+        'MMMM'                  4000      
+        'MMMMCMXCIX'            4999    
+        'MMMMMMMMMCMXCIX'       9999 
+     ) pairWiseDo:[:goodString :expectedValue |
+        (Integer readFromRomanString:goodString onError:nil) ~= expectedValue ifTrue:[self halt].
+     ]
+    "
+
+    "
+      1 to:9999 do:[:n |
+        |romanString|
+
+        romanString := String streamContents:[:stream | n printRomanOn:stream].
+        (Integer readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+     ]
+    "
+
+    "reading naive numbers:
+
+      1 to:9999 do:[:n |
+        |romanString|
+
+        romanString := String streamContents:[:stream | n printRomanOn:stream naive:true].
+        NaiveRomanNumberFormatError ignoreIn:[
+            (Integer readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+        ]
      ]
     "
 ! !
@@ -2440,31 +2563,13 @@
 !
 
 printRomanOn:aStream
-    "print the receiver as roman number to the receiver, aStream"
-
-    "convert a string or stream containing a roman representation into an integer"
-
-    |restValue|
-
-    restValue := self.
-    restValue > 0 ifFalse:[self error:'negative roman'].
-
-    [restValue >= 1000] whileTrue:[ aStream nextPutAll:'M'.  restValue := restValue - 1000. ].
-    (restValue >=  900) ifTrue:   [ aStream nextPutAll:'CM'. restValue := restValue -  900. ].
-    (restValue >=  500) ifTrue:   [ aStream nextPutAll:'D'.  restValue := restValue -  500. ].
-    (restValue >=  400) ifTrue:   [ aStream nextPutAll:'CD'. restValue := restValue -  400. ].
-    [restValue >=  100] whileTrue:[ aStream nextPutAll:'C'.  restValue := restValue -  100. ].
-    (restValue >=   90) ifTrue:   [ aStream nextPutAll:'XC'. restValue := restValue -   90. ].
-    (restValue >=   50) ifTrue:   [ aStream nextPutAll:'L'.  restValue := restValue -   50. ].
-    (restValue >=   40) ifTrue:   [ aStream nextPutAll:'XL'. restValue := restValue -   40. ].
-    [restValue >=   10] whileTrue:[ aStream nextPutAll:'X'.  restValue := restValue -   10. ].
-    (restValue >=    9) ifTrue:   [ aStream nextPutAll:'IX'. restValue := restValue -    9. ].
-    (restValue >=    5) ifTrue:   [ aStream nextPutAll:'V'.  restValue := restValue -    5. ].
-    (restValue >=    4) ifTrue:   [ aStream nextPutAll:'IV'. restValue := restValue -    4. ].
-    [restValue >=    1] whileTrue:[ aStream nextPutAll:'I'.  restValue := restValue -    1. ].
+    "print the receiver as roman number to the receiver, aStream.
+     This converts correct (i.e. prefix notation for 4,9,40,90, etc.)."
+
+    ^ self printRomanOn:aStream naive:false
 
     "
-     1 printRomanOn:Transcript. Transcript cr.
+     1 to:10 do:[:i | i printRomanOn:Transcript. Transcript cr.].
      1999 printRomanOn:Transcript. Transcript cr.
      Date today year printRomanOn:Transcript. Transcript cr.
     "
@@ -2474,7 +2579,90 @@
         |romanString|
 
         romanString := String streamContents:[:stream | n printRomanOn:stream].
-        (self readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+        (Integer readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+     ]
+    "
+!
+
+printRomanOn:aStream naive:naive
+    "print the receiver as roman number to the receiver, aStream.
+     The naive argument controls if the conversion is
+     correct (i.e. prefix notation for 4,9,40,90, etc.),
+     or naive (i.e. print 4 as IIII and 9 as VIIII).
+     The naive version is often used for page numbers in documents."
+
+    |restValue spec|
+
+    restValue := self.
+    restValue > 0 ifFalse:[self error:'negative roman'].
+
+    naive ifTrue:[
+        spec := #(
+                " value string repeat "    
+                   1000 'M'    true
+                    500 'D'    false
+                    100 'C'    true
+                     50 'L'    false
+                     10 'X'    true
+                      5 'V'    false
+                      1 'I'    true
+                 ).
+    ] ifFalse:[
+        spec := #(
+                " value string repeat "    
+                   1000 'M'    true
+                    900 'CM'   false
+                    500 'D'    false
+                    400 'CD'   false
+                    100 'C'    true
+                     90 'XC'   false
+                     50 'L'    false
+                     40 'XL'   false
+                     10 'X'    true
+                      9 'IX'   false
+                      5 'V'    false
+                      4 'IV'   false
+                      1 'I'    true
+                 ).
+    ].
+
+    spec 
+        inGroupsOf:3 
+        do:[:rValue :rString :repeatFlag |
+
+            [
+                (restValue >= rValue) ifTrue:[
+                    aStream nextPutAll:rString.
+                    restValue := restValue - rValue.
+                ].
+            ] doWhile:[ repeatFlag and:[ restValue >= rValue] ].
+        ].
+
+    "
+     1 to:10 do:[:i | i printRomanOn:Transcript naive:false. Transcript cr.].
+     1 to:10 do:[:i | i printRomanOn:Transcript naive:true. Transcript cr.].
+
+     1999 printRomanOn:Transcript. Transcript cr.
+     Date today year printRomanOn:Transcript. Transcript cr.
+    "
+
+    "test all between 1 and 9999:
+      1 to:9999 do:[:n |
+        |romanString|
+
+        romanString := String streamContents:[:stream | n printRomanOn:stream naive:false].
+        (Integer readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+     ]
+    "
+
+    "test naive all between 1 and 9999:
+      1 to:9999 do:[:n |
+        |romanString|
+
+        romanString := String streamContents:[:stream | n printRomanOn:stream naive:true].
+        NaiveRomanNumberFormatError ignoreIn:[
+            (Integer readFromRomanString:romanString onError:nil) ~= n ifTrue:[self halt].
+        ]
      ]
     "
 !
@@ -2971,6 +3159,6 @@
 !Integer class methodsFor:'documentation'!
 
 version
-    ^ '$Header: /cvs/stx/stx/libbasic/Integer.st,v 1.149 2002-07-16 13:21:47 cg Exp $'
+    ^ '$Header: /cvs/stx/stx/libbasic/Integer.st,v 1.150 2002-07-31 10:05:53 cg Exp $'
 ! !
 Integer initialize!