Advertisement
munkyeetr

RomanNumeral.vb

Dec 3rd, 2013
99
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
VB.NET 21.52 KB | None | 0 0
  1. Imports System.Text.RegularExpressions
  2. Imports System.Text
  3.  
  4. Public Class RomanNumeral
  5.     ' -----------------------------------------------------------------------------------
  6.     ' Private Shared Readonly variables will be shared by all objects of this class.
  7.     ' -----------------------------------------------------------------------------------
  8.     ' Minimum value of signed Integer
  9.     Private Shared ReadOnly pNumberMinimum As Integer = -2147483648
  10.     Private Shared ReadOnly pNumeralMinimum As String = "-(((II)))((CXLVII))(CDLXXXIII)DCXLVIII"
  11.  
  12.     ' Valid characters used in numerals
  13.     Private Shared ReadOnly pValidCharacters As String = "IVXLCDM()-"
  14.  
  15.     ' Dictionary of numeral values
  16.     Private Shared ReadOnly pNumeralValues As New Dictionary(Of Integer, String) From { _
  17.         {1, "I"}, {2, "II"}, {3, "III"}, {4, "IV"}, {5, "V"}, {6, "VI"}, {7, "VII"}, {8, "VIII"}, {9, "IX"}, {10, "X"}, _
  18.         {20, "XX"}, {30, "XXX"}, {40, "XL"}, {50, "L"}, {60, "LX"}, {70, "LXX"}, {80, "LXXX"}, {90, "XC"}, {100, "C"}, _
  19.         {400, "CD"}, {500, "D"}, {900, "CM"}, {1000, "M"}}
  20.  
  21.     ' Invalid groupings of characters
  22.     Private Shared ReadOnly pInvalidCharacterGroups As New System.Collections.Generic.List(Of String) From { _
  23.         "IIII", "IIX", "IXI", "IXX", "IXL", "IXC", "IIV", "IVI", "IL", "IC", "ID", "IM", "VV", "VX", "VL", _
  24.         "VC", "VD", "VM", "VIV", "VIX", "XXXX", "XD", "XM", "XLX", "XCX", "XXC", "LL", _
  25.         "LXL", "LC", "LD", "LM", "CCCC", "CCD", "CDC", "CCM", "CMC", "CMM", "CMD", "DD", _
  26.         "DM", "MCMC", "MCMD"}
  27.  
  28.     ' Regular expression patterns to handle notation parsing : Index 0 = Billions, 1 = Millions, 2 = Thousands
  29.     Private Shared ReadOnly pNotationRegex() As Regex = { _
  30.         New Regex("^\(\(\((.*?)\)\)\)"), New Regex("^\(\((.*?)\)\)"), New Regex("^\((.*?)\)")}
  31.  
  32.     ' Indexes used to access pNotationRegex and m_NumeralParts arrays
  33.     Private Shared ReadOnly Billions As Integer = 0
  34.     Private Shared ReadOnly Millions As Integer = 1
  35.     Private Shared ReadOnly Thousands As Integer = 2
  36.     Private Shared ReadOnly Standard As Integer = 3
  37.  
  38.     ' -----------------------------------------------------------------------------------
  39.     ' Private Shared Subs and Functions will be used by all objects of this class.
  40.     ' -----------------------------------------------------------------------------------
  41.     Private Shared Sub ps_ZeroAll(ByRef Obj As RomanNumeral)
  42.         ' Zero out all values of the object. Called when zero is passed, a malformed numeral is passed
  43.         ' or an overflow is being handled.
  44.         Obj.m_Number = 0
  45.         Obj.m_Numeral = "0"
  46.         Obj.m_Negative = False
  47.         Obj.m_Malformation = MalformationError.None
  48.         ps_ZeroNumeralParts(Obj)
  49.     End Sub
  50.  
  51.     Private Shared Sub ps_ZeroNumeralParts(ByRef Obj As RomanNumeral)
  52.         ' Zero out m_NumeralParts array
  53.         For index As Integer = Billions To Standard
  54.             Obj.m_NumeralParts(index) = 0
  55.         Next
  56.     End Sub
  57.  
  58.     Private Shared Function ps_MalformedNumeral(ByRef Obj As RomanNumeral, ByVal numeral As String) As Boolean
  59.         Obj.m_Malformation = MalformationError.None
  60.  
  61.         ' If the numeral is equal to pNumeralMinimum we have to flag an adjustment when we test for
  62.         ' an overflow error further down in this procedure.
  63.         Dim negativeOverflowHandler As Boolean = False
  64.         If numeral = pNumeralMinimum Then negativeOverflowHandler = True
  65.  
  66.         ' Numeral contains only valid characters?
  67.         For Each character In numeral
  68.             If Not pValidCharacters.Contains(character) Then
  69.                 Obj.m_Malformation = MalformationError.CharacterInvalid
  70.                 Return True
  71.             End If
  72.         Next
  73.  
  74.         Dim negative As Char = "-"c
  75.         If numeral.Contains(negative) Then ' Contains negative symbol?
  76.             ' Too many negative symbols?
  77.             If numeral.Split(negative).Count <> 2 Then
  78.                 Obj.m_Malformation = MalformationError.SignCount
  79.                 Return True
  80.             ElseIf Not numeral(0) = negative Then
  81.                 ' or not at the beginning of string?
  82.                 Obj.m_Malformation = MalformationError.SignIndex
  83.                 Return True
  84.             Else ' set the negative flag and trim off symbol
  85.                 Obj.m_Negative = True
  86.                 numeral = numeral.Trim(negative)
  87.             End If
  88.         Else ' set negative flag
  89.             Obj.m_Negative = False
  90.         End If
  91.  
  92.         ' Contains any invalid groups of characters?
  93.         For Each group In pInvalidCharacterGroups
  94.             If numeral.Contains(group) Then
  95.                 Obj.m_Malformation = MalformationError.CharacterGroupInvalid
  96.                 Return True
  97.             End If
  98.         Next
  99.  
  100.         Dim oParenthesis As Char = "("c
  101.         Dim cParenthesis As Char = ")"c
  102.         ' Contains parentheses?
  103.         If numeral.Contains(oParenthesis) Or numeral.Contains(cParenthesis) Then
  104.             Dim matches As MatchCollection
  105.             For expression As Integer = Billions To Thousands
  106.                 ' Look for matches to the Regex notations
  107.                 matches = pNotationRegex(expression).Matches(numeral)
  108.                 If matches.Count > 0 Then
  109.                     If matches.Count = 1 Then
  110.                         ' Match found...
  111.                         Dim currentMatch As String = matches.Item(0).ToString
  112.                         Select Case expression ' Trim off the parentheses based on the current pattern
  113.                             Case Billions
  114.                                 currentMatch = Mid(currentMatch, 4, currentMatch.Length - 6)
  115.                             Case Millions
  116.                                 currentMatch = Mid(currentMatch, 3, currentMatch.Length - 4)
  117.                             Case Thousands
  118.                                 currentMatch = Mid(currentMatch, 2, currentMatch.Length - 2)
  119.                         End Select
  120.  
  121.                         If currentMatch.Contains(oParenthesis) Or currentMatch.Contains(cParenthesis) Then
  122.                             ' If any parentheses remain in the current match then the numeral is malformed
  123.                             Obj.m_Malformation = MalformationError.Parenthesis
  124.                             Return True
  125.                         Else
  126.                             ' Otherwise assign the number
  127.                             Obj.m_NumeralParts(expression) = ps_NumeralToNumber(currentMatch)
  128.                         End If
  129.                         ' Remove the pattern from the numeral
  130.                         numeral = Regex.Replace(numeral, pNotationRegex(expression).ToString, String.Empty)
  131.  
  132.                     Else ' More than 1 match found, numeral is malformed
  133.                         Obj.m_Malformation = MalformationError.Parenthesis
  134.                         Return True
  135.                     End If
  136.                 End If
  137.             Next
  138.  
  139.             If numeral.Contains(oParenthesis) Or numeral.Contains(cParenthesis) Then
  140.                 ' If any straggling parentheses are found, numeral is malformed
  141.                 Obj.m_Malformation = MalformationError.Parenthesis
  142.                 Return True
  143.             End If
  144.         End If
  145.  
  146.         ' Check if we flagged for overflow adjustment at the top of this procedure
  147.         ' If so, we decrement the value by 1 for now (we'll increment it back after
  148.         ' we do our overflow check)
  149.         If negativeOverflowHandler Then
  150.             Obj.m_NumeralParts(Standard) = ps_NumeralToNumber(numeral) - 1
  151.         Else
  152.             Obj.m_NumeralParts(Standard) = ps_NumeralToNumber(numeral)
  153.         End If
  154.  
  155.         Try
  156.             ' Overflow check
  157.             Dim overflowTest As Integer = _
  158.                 (Obj.m_NumeralParts(Billions) * 1000000000) + _
  159.                 (Obj.m_NumeralParts(Millions) * 1000000) + _
  160.                 (Obj.m_NumeralParts(Thousands) * 1000) + _
  161.                 (Obj.m_NumeralParts(Standard))
  162.         Catch ex As Exception When TypeOf ex Is System.OverflowException OrElse TypeOf ex Is System.StackOverflowException
  163.             Obj.m_Malformation = MalformationError.ValueOverflow
  164.             Return True
  165.         End Try
  166.  
  167.         ' If we adjusted for overflow, increment the value back
  168.         If negativeOverflowHandler Then
  169.             Obj.m_NumeralParts(Standard) += 1
  170.         End If
  171.  
  172.         Return False ' Success in all malformation checks
  173.     End Function
  174.  
  175.     Private Shared Function ps_NumeralToNumber(ByVal numeral As String) As Integer
  176.         Dim rv As Integer = 0
  177.         numeral = numeral.ToUpper
  178.         ' Loop through each character in the numeral and increment rv by the value
  179.         ' of each numeral piece. C, X and I are tested further for subtractive notation
  180.         For i As Integer = 0 To numeral.Length - 1
  181.             Select Case numeral(i)
  182.                 Case "M"c : rv += 1000I
  183.                 Case "D"c : rv += 500I
  184.                 Case "C"c
  185.                     If Not i = numeral.Length - 1 Then
  186.                         Select Case numeral(i + 1)
  187.                             Case "M"c
  188.                                 rv += 900I
  189.                                 i += 1
  190.                             Case "D"c
  191.                                 rv += 400I
  192.                                 i += 1
  193.                             Case Else
  194.                                 rv += 100I
  195.                         End Select
  196.                     Else
  197.                         rv += 100I
  198.                     End If
  199.                 Case "L"c : rv += 50I
  200.                 Case "X"c
  201.                     If Not i = numeral.Length - 1 Then
  202.                         Select Case numeral(i + 1)
  203.                             Case "C"c
  204.                                 rv += 90I
  205.                                 i += 1
  206.                             Case "L"c
  207.                                 rv += 40I
  208.                                 i += 1
  209.                             Case Else
  210.                                 rv += 10I
  211.                         End Select
  212.                     Else
  213.                         rv += 10I
  214.                     End If
  215.                 Case "V"c : rv += 5I
  216.                 Case "I"c
  217.                     If Not i = numeral.Length - 1 Then
  218.                         Select Case numeral(i + 1)
  219.                             Case "X"c
  220.                                 rv += 9I
  221.                                 i += 1
  222.                             Case "V"c
  223.                                 rv += 4I
  224.                                 i += 1
  225.                             Case Else
  226.                                 rv += 1I
  227.                         End Select
  228.                     Else
  229.                         rv += 1I
  230.                     End If
  231.             End Select
  232.         Next
  233.         Return rv
  234.     End Function
  235.  
  236.     Private Shared Function ps_BuildNumeralFromNumber(ByRef Obj As RomanNumeral, ByVal number As Integer) As String
  237.  
  238.         Dim rv As StringBuilder = New StringBuilder     ' Used to build return value
  239.         ps_ZeroNumeralParts(Obj)                        ' Clear m_NumeralParts array
  240.  
  241.         ' If number is less than 0 we flip it's sign while we build the numeral, but if
  242.         ' the number is equal to pNumberMinimum we have to increment it by 1 temporarily
  243.         ' to avoid an overflow error
  244.         Dim negativeOverflowHandler As Boolean = False
  245.         If number < 0 Then
  246.             If number = pNumberMinimum Then
  247.                 negativeOverflowHandler = True
  248.                 number += 1
  249.             End If
  250.             number *= -1
  251.         End If
  252.  
  253.         ' Here we break the number up into it's numeral parts [ billions, millions, thousands and standard ]
  254.         ' and begin building the return string...
  255.         If number >= 1000000000 Then
  256.             ' Billions
  257.             Obj.m_NumeralParts(Billions) = CInt(Math.Floor(number / 1000000000))
  258.             rv.AppendFormat("((({0})))", ps_NumberToNumeral(Obj.m_NumeralParts(Billions)))
  259.             number = number Mod 1000000000
  260.         End If
  261.  
  262.         If number >= 1000000 Then
  263.             ' Millions
  264.             Obj.m_NumeralParts(Millions) = CInt(Math.Floor(number / 1000000))
  265.             rv.AppendFormat("(({0}))", ps_NumberToNumeral(Obj.m_NumeralParts(Millions)))
  266.             number = number Mod 1000000
  267.         End If
  268.  
  269.         If number >= 4000 Then
  270.             ' Thousands (NOTE: we are only putting values 4000+ into notation)
  271.             Obj.m_NumeralParts(Thousands) = CInt(Math.Floor(number / 1000))
  272.             rv.AppendFormat("({0})", ps_NumberToNumeral(Obj.m_NumeralParts(Thousands)))
  273.             number = number Mod 1000
  274.         End If
  275.  
  276.         ' If we adjusted the number to avoid an overflow, we can now adjust it back
  277.         If negativeOverflowHandler Then number += 1
  278.  
  279.         Obj.m_NumeralParts(Standard) = number    ' Store the standard numeral part
  280.         rv.Append(ps_NumberToNumeral(number))    ' Add it to the return string
  281.  
  282.         If Obj.m_Negative Then rv.Insert(0, "-") ' Insert negative sign if needed
  283.  
  284.         Return rv.ToString
  285.     End Function
  286.  
  287.     Private Shared Function ps_NumberToNumeral(ByVal number As Integer) As String
  288.         Dim rv As StringBuilder = New StringBuilder     ' Used to build a return value
  289.  
  290.         ' While the number is greater than 0 keep dividing and modding by values in the
  291.         ' pNumeralValues dictionary building a string of numerals which represents
  292.         ' the number.
  293.         While number > 0
  294.             If number >= 1000 Then
  295.                 For i As Integer = 1 To CInt(Math.Floor(number / 1000))
  296.                     rv.Append(pNumeralValues(1000))
  297.                 Next
  298.                 number = number Mod 1000
  299.             ElseIf number >= 900 Then
  300.                 rv.Append(pNumeralValues(900))
  301.                 number = number Mod 900
  302.             ElseIf number >= 500 Then
  303.                 rv.Append(pNumeralValues(500))
  304.                 number = number Mod 500
  305.             ElseIf number >= 400 Then
  306.                 rv.Append(pNumeralValues(400))
  307.                 number = number Mod 400
  308.             ElseIf number >= 100 Then
  309.                 For i As Integer = 1 To CInt(Math.Floor(number / 100))
  310.                     rv.Append(pNumeralValues(100))
  311.                 Next
  312.                 number = number Mod 100
  313.             ElseIf number >= 10 Then
  314.                 rv.Append(pNumeralValues(number - (number Mod 10)))
  315.                 number = number Mod 10
  316.             Else
  317.                 rv.Append(pNumeralValues(number))
  318.                 number = 0
  319.             End If
  320.         End While
  321.         Return rv.ToString
  322.     End Function
  323.  
  324.     ' -----------------------------------------------------------------------------------
  325.     ' Public Enumerations
  326.     ' -----------------------------------------------------------------------------------
  327.     Public Enum MalformationError
  328.         None = 0                        ' Not malformed
  329.         CharacterInvalid = 1            ' 1 or more invalid characters found
  330.         CharacterGroupInvalid = 2       ' 1 or more invalid character groups found
  331.         SignIndex = 3                   ' Negative sign (-) detected outside the first index
  332.         SignCount = 4                   ' More than 1 negative sign found
  333.         Parenthesis = 5                 ' Problem found with notation parentheses, either order or count
  334.         ValueOverflow = 6               ' Numeral will cause overflow when converted to Integer
  335.     End Enum
  336.  
  337.     ' -----------------------------------------------------------------------------------
  338.     ' Private member variables. Unique to each object of this class
  339.     ' -----------------------------------------------------------------------------------
  340.     Private m_Number As Integer = 0
  341.     Private m_Numeral As String = String.Empty
  342.     Private m_Negative As Boolean = False
  343.     Private m_Malformation As MalformationError = MalformationError.None
  344.  
  345.     ' Numeral values broken into parts : Index 0 = Billions, 1 = Millions, 2 = Thousands, 3 = Standard
  346.     Private m_NumeralParts() As Integer = {0, 0, 0, 0}
  347.  
  348.     ' -----------------------------------------------------------------------------------
  349.     ' Public member accessor methods
  350.     ' -----------------------------------------------------------------------------------
  351.     Public Property Number As Integer
  352.         Get
  353.             Return m_Number     ' return the current value
  354.         End Get
  355.         Set(ByVal value As Integer)
  356.             ps_ZeroAll(Me)
  357.             If value <> 0 Then
  358.                 ' If the number isn't 0...
  359.                 ' flag if the number is negative, assign the number and build the numeral
  360.                 m_Negative = If(value < 0, True, False)
  361.                 m_Number = value
  362.                 m_Numeral = ps_BuildNumeralFromNumber(Me, value)
  363.             End If
  364.         End Set
  365.     End Property
  366.  
  367.     Public Property Numeral As String
  368.         Get
  369.             Return m_Numeral    ' return the current value
  370.         End Get
  371.         Set(ByVal value As String)
  372.             ps_ZeroAll(Me)    ' Zero all instance variables
  373.  
  374.             value = value.ToUpper
  375.  
  376.             If value = "0" Then
  377.                 ' If 0 is passed as a numeral...
  378.                 ' Since we are already zeroed out we just need to show no Malformation
  379.                 m_Malformation = MalformationError.None
  380.             Else
  381.                 ' Otherwise...
  382.                 If Not ps_MalformedNumeral(Me, value) Then
  383.                     ' If the value is not malformed...
  384.                     ' we can set the Numeral, Number and Negative variables
  385.                     m_Numeral = value
  386.                     m_Number = (m_NumeralParts(Billions) * 1000000000) + _
  387.                     (m_NumeralParts(Millions) * 1000000) + _
  388.                     (m_NumeralParts(Thousands) * 1000) + _
  389.                     m_NumeralParts(Standard)
  390.                     If m_Negative Then m_Number *= -1
  391.                 End If
  392.             End If
  393.         End Set
  394.     End Property
  395.  
  396.     Public Property Negative As Boolean
  397.         Get
  398.             Return m_Negative   ' return the current value
  399.         End Get
  400.         Set(ByVal value As Boolean)
  401.             ' If number isn't 0 and we are not reaffirming the existing value...
  402.             If m_Number <> 0 And value <> m_Negative Then
  403.                 ' If our current value is pNumberMinimum we cannot flip it to positive Integer
  404.                 ' without causing an overflow, so we have to flag it as malformed.
  405.                 If m_Negative And Number = pNumberMinimum Then
  406.                     ps_ZeroAll(Me)
  407.                     m_Malformation = MalformationError.ValueOverflow
  408.                 Else
  409.                     ' Otherwise set the value, flip the number and fix the numeral string to
  410.                     ' display a negative sign.
  411.                     m_Negative = value
  412.                     m_Number *= -1
  413.                     If value = True Then
  414.                         m_Numeral = String.Format("-{0}", m_Numeral)
  415.                     Else
  416.                         m_Numeral = m_Numeral.TrimStart("-"c)
  417.                     End If
  418.                 End If
  419.             End If
  420.         End Set
  421.     End Property
  422.  
  423.     Public ReadOnly Property Length As Integer
  424.         Get
  425.             ' If the objects number value is not 0 (either malformed or special zero) return the length
  426.             ' of the objects numeral string. Otherwise return a length of 0
  427.             Return If(m_Number <> 0, m_Numeral.Length, 0)
  428.         End Get
  429.     End Property
  430.  
  431.     Public ReadOnly Property Malformed As Boolean
  432.         Get
  433.             ' If the objects malformation value is anything but None, then it is malformed (return True)
  434.             ' Otherwise return False
  435.             Return If(m_Malformation = MalformationError.None, False, True)
  436.         End Get
  437.     End Property
  438.  
  439.     Public ReadOnly Property Malformation As MalformationError
  440.         Get
  441.             ' Return the objects specific malformation value
  442.             Return m_Malformation
  443.         End Get
  444.     End Property
  445.  
  446.     ' -----------------------------------------------------------------------------------
  447.     ' Object constructor methods
  448.     ' -----------------------------------------------------------------------------------
  449.     Public Sub New() ' Default: set all values to 0
  450.         ps_ZeroAll(Me)
  451.     End Sub
  452.  
  453.     Public Sub New(ByVal numeral As String) ' Create new object based on numeral string
  454.         numeral = numeral.ToUpper
  455.  
  456.         If numeral = "0" Then
  457.             ' If 0 is passed as a numeral...
  458.             ' zero out and show no Malformation
  459.             ps_ZeroAll(Me)
  460.         Else
  461.             ' Otherwise...
  462.             If Not ps_MalformedNumeral(Me, numeral) Then
  463.                 ' If the value is not malformed...
  464.                 ' we can set the Numeral, Number and Negative variables
  465.                 m_Numeral = numeral
  466.                 m_Number = (m_NumeralParts(Billions) * 1000000000) + _
  467.                 (m_NumeralParts(Millions) * 1000000) + _
  468.                 (m_NumeralParts(Thousands) * 1000) + _
  469.                 m_NumeralParts(Standard)
  470.                 If m_Negative Then m_Number *= -1
  471.             Else
  472.                 ps_ZeroAll(Me)
  473.             End If
  474.         End If
  475.     End Sub
  476.  
  477.     Public Sub New(ByVal number As Integer) ' Create a new object based on a number
  478.         m_Number = number ' Set the number property
  479.         If m_Number <> 0 Then
  480.             ' If number isn't 0 then flag if negative and build a numeral
  481.             If m_Number < 0 Then m_Negative = True
  482.             m_Numeral = ps_BuildNumeralFromNumber(Me, number)
  483.         Else
  484.             ' Otherwise set as zero
  485.             m_Numeral = "0"
  486.         End If
  487.     End Sub
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement