Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Imports System.Text.RegularExpressions
- Imports System.Text
- Public Class RomanNumeral
- ' -----------------------------------------------------------------------------------
- ' Private Shared Readonly variables will be shared by all objects of this class.
- ' -----------------------------------------------------------------------------------
- ' Minimum value of signed Integer
- Private Shared ReadOnly pNumberMinimum As Integer = -2147483648
- Private Shared ReadOnly pNumeralMinimum As String = "-(((II)))((CXLVII))(CDLXXXIII)DCXLVIII"
- ' Valid characters used in numerals
- Private Shared ReadOnly pValidCharacters As String = "IVXLCDM()-"
- ' Dictionary of numeral values
- Private Shared ReadOnly pNumeralValues As New Dictionary(Of Integer, String) From { _
- {1, "I"}, {2, "II"}, {3, "III"}, {4, "IV"}, {5, "V"}, {6, "VI"}, {7, "VII"}, {8, "VIII"}, {9, "IX"}, {10, "X"}, _
- {20, "XX"}, {30, "XXX"}, {40, "XL"}, {50, "L"}, {60, "LX"}, {70, "LXX"}, {80, "LXXX"}, {90, "XC"}, {100, "C"}, _
- {400, "CD"}, {500, "D"}, {900, "CM"}, {1000, "M"}}
- ' Invalid groupings of characters
- Private Shared ReadOnly pInvalidCharacterGroups As New System.Collections.Generic.List(Of String) From { _
- "IIII", "IIX", "IXI", "IXX", "IXL", "IXC", "IIV", "IVI", "IL", "IC", "ID", "IM", "VV", "VX", "VL", _
- "VC", "VD", "VM", "VIV", "VIX", "XXXX", "XD", "XM", "XLX", "XCX", "XXC", "LL", _
- "LXL", "LC", "LD", "LM", "CCCC", "CCD", "CDC", "CCM", "CMC", "CMM", "CMD", "DD", _
- "DM", "MCMC", "MCMD"}
- ' Regular expression patterns to handle notation parsing : Index 0 = Billions, 1 = Millions, 2 = Thousands
- Private Shared ReadOnly pNotationRegex() As Regex = { _
- New Regex("^\(\(\((.*?)\)\)\)"), New Regex("^\(\((.*?)\)\)"), New Regex("^\((.*?)\)")}
- ' Indexes used to access pNotationRegex and m_NumeralParts arrays
- Private Shared ReadOnly Billions As Integer = 0
- Private Shared ReadOnly Millions As Integer = 1
- Private Shared ReadOnly Thousands As Integer = 2
- Private Shared ReadOnly Standard As Integer = 3
- ' -----------------------------------------------------------------------------------
- ' Private Shared Subs and Functions will be used by all objects of this class.
- ' -----------------------------------------------------------------------------------
- Private Shared Sub ps_ZeroAll(ByRef Obj As RomanNumeral)
- ' Zero out all values of the object. Called when zero is passed, a malformed numeral is passed
- ' or an overflow is being handled.
- Obj.m_Number = 0
- Obj.m_Numeral = "0"
- Obj.m_Negative = False
- Obj.m_Malformation = MalformationError.None
- ps_ZeroNumeralParts(Obj)
- End Sub
- Private Shared Sub ps_ZeroNumeralParts(ByRef Obj As RomanNumeral)
- ' Zero out m_NumeralParts array
- For index As Integer = Billions To Standard
- Obj.m_NumeralParts(index) = 0
- Next
- End Sub
- Private Shared Function ps_MalformedNumeral(ByRef Obj As RomanNumeral, ByVal numeral As String) As Boolean
- Obj.m_Malformation = MalformationError.None
- ' If the numeral is equal to pNumeralMinimum we have to flag an adjustment when we test for
- ' an overflow error further down in this procedure.
- Dim negativeOverflowHandler As Boolean = False
- If numeral = pNumeralMinimum Then negativeOverflowHandler = True
- ' Numeral contains only valid characters?
- For Each character In numeral
- If Not pValidCharacters.Contains(character) Then
- Obj.m_Malformation = MalformationError.CharacterInvalid
- Return True
- End If
- Next
- Dim negative As Char = "-"c
- If numeral.Contains(negative) Then ' Contains negative symbol?
- ' Too many negative symbols?
- If numeral.Split(negative).Count <> 2 Then
- Obj.m_Malformation = MalformationError.SignCount
- Return True
- ElseIf Not numeral(0) = negative Then
- ' or not at the beginning of string?
- Obj.m_Malformation = MalformationError.SignIndex
- Return True
- Else ' set the negative flag and trim off symbol
- Obj.m_Negative = True
- numeral = numeral.Trim(negative)
- End If
- Else ' set negative flag
- Obj.m_Negative = False
- End If
- ' Contains any invalid groups of characters?
- For Each group In pInvalidCharacterGroups
- If numeral.Contains(group) Then
- Obj.m_Malformation = MalformationError.CharacterGroupInvalid
- Return True
- End If
- Next
- Dim oParenthesis As Char = "("c
- Dim cParenthesis As Char = ")"c
- ' Contains parentheses?
- If numeral.Contains(oParenthesis) Or numeral.Contains(cParenthesis) Then
- Dim matches As MatchCollection
- For expression As Integer = Billions To Thousands
- ' Look for matches to the Regex notations
- matches = pNotationRegex(expression).Matches(numeral)
- If matches.Count > 0 Then
- If matches.Count = 1 Then
- ' Match found...
- Dim currentMatch As String = matches.Item(0).ToString
- Select Case expression ' Trim off the parentheses based on the current pattern
- Case Billions
- currentMatch = Mid(currentMatch, 4, currentMatch.Length - 6)
- Case Millions
- currentMatch = Mid(currentMatch, 3, currentMatch.Length - 4)
- Case Thousands
- currentMatch = Mid(currentMatch, 2, currentMatch.Length - 2)
- End Select
- If currentMatch.Contains(oParenthesis) Or currentMatch.Contains(cParenthesis) Then
- ' If any parentheses remain in the current match then the numeral is malformed
- Obj.m_Malformation = MalformationError.Parenthesis
- Return True
- Else
- ' Otherwise assign the number
- Obj.m_NumeralParts(expression) = ps_NumeralToNumber(currentMatch)
- End If
- ' Remove the pattern from the numeral
- numeral = Regex.Replace(numeral, pNotationRegex(expression).ToString, String.Empty)
- Else ' More than 1 match found, numeral is malformed
- Obj.m_Malformation = MalformationError.Parenthesis
- Return True
- End If
- End If
- Next
- If numeral.Contains(oParenthesis) Or numeral.Contains(cParenthesis) Then
- ' If any straggling parentheses are found, numeral is malformed
- Obj.m_Malformation = MalformationError.Parenthesis
- Return True
- End If
- End If
- ' Check if we flagged for overflow adjustment at the top of this procedure
- ' If so, we decrement the value by 1 for now (we'll increment it back after
- ' we do our overflow check)
- If negativeOverflowHandler Then
- Obj.m_NumeralParts(Standard) = ps_NumeralToNumber(numeral) - 1
- Else
- Obj.m_NumeralParts(Standard) = ps_NumeralToNumber(numeral)
- End If
- Try
- ' Overflow check
- Dim overflowTest As Integer = _
- (Obj.m_NumeralParts(Billions) * 1000000000) + _
- (Obj.m_NumeralParts(Millions) * 1000000) + _
- (Obj.m_NumeralParts(Thousands) * 1000) + _
- (Obj.m_NumeralParts(Standard))
- Catch ex As Exception When TypeOf ex Is System.OverflowException OrElse TypeOf ex Is System.StackOverflowException
- Obj.m_Malformation = MalformationError.ValueOverflow
- Return True
- End Try
- ' If we adjusted for overflow, increment the value back
- If negativeOverflowHandler Then
- Obj.m_NumeralParts(Standard) += 1
- End If
- Return False ' Success in all malformation checks
- End Function
- Private Shared Function ps_NumeralToNumber(ByVal numeral As String) As Integer
- Dim rv As Integer = 0
- numeral = numeral.ToUpper
- ' Loop through each character in the numeral and increment rv by the value
- ' of each numeral piece. C, X and I are tested further for subtractive notation
- For i As Integer = 0 To numeral.Length - 1
- Select Case numeral(i)
- Case "M"c : rv += 1000I
- Case "D"c : rv += 500I
- Case "C"c
- If Not i = numeral.Length - 1 Then
- Select Case numeral(i + 1)
- Case "M"c
- rv += 900I
- i += 1
- Case "D"c
- rv += 400I
- i += 1
- Case Else
- rv += 100I
- End Select
- Else
- rv += 100I
- End If
- Case "L"c : rv += 50I
- Case "X"c
- If Not i = numeral.Length - 1 Then
- Select Case numeral(i + 1)
- Case "C"c
- rv += 90I
- i += 1
- Case "L"c
- rv += 40I
- i += 1
- Case Else
- rv += 10I
- End Select
- Else
- rv += 10I
- End If
- Case "V"c : rv += 5I
- Case "I"c
- If Not i = numeral.Length - 1 Then
- Select Case numeral(i + 1)
- Case "X"c
- rv += 9I
- i += 1
- Case "V"c
- rv += 4I
- i += 1
- Case Else
- rv += 1I
- End Select
- Else
- rv += 1I
- End If
- End Select
- Next
- Return rv
- End Function
- Private Shared Function ps_BuildNumeralFromNumber(ByRef Obj As RomanNumeral, ByVal number As Integer) As String
- Dim rv As StringBuilder = New StringBuilder ' Used to build return value
- ps_ZeroNumeralParts(Obj) ' Clear m_NumeralParts array
- ' If number is less than 0 we flip it's sign while we build the numeral, but if
- ' the number is equal to pNumberMinimum we have to increment it by 1 temporarily
- ' to avoid an overflow error
- Dim negativeOverflowHandler As Boolean = False
- If number < 0 Then
- If number = pNumberMinimum Then
- negativeOverflowHandler = True
- number += 1
- End If
- number *= -1
- End If
- ' Here we break the number up into it's numeral parts [ billions, millions, thousands and standard ]
- ' and begin building the return string...
- If number >= 1000000000 Then
- ' Billions
- Obj.m_NumeralParts(Billions) = CInt(Math.Floor(number / 1000000000))
- rv.AppendFormat("((({0})))", ps_NumberToNumeral(Obj.m_NumeralParts(Billions)))
- number = number Mod 1000000000
- End If
- If number >= 1000000 Then
- ' Millions
- Obj.m_NumeralParts(Millions) = CInt(Math.Floor(number / 1000000))
- rv.AppendFormat("(({0}))", ps_NumberToNumeral(Obj.m_NumeralParts(Millions)))
- number = number Mod 1000000
- End If
- If number >= 4000 Then
- ' Thousands (NOTE: we are only putting values 4000+ into notation)
- Obj.m_NumeralParts(Thousands) = CInt(Math.Floor(number / 1000))
- rv.AppendFormat("({0})", ps_NumberToNumeral(Obj.m_NumeralParts(Thousands)))
- number = number Mod 1000
- End If
- ' If we adjusted the number to avoid an overflow, we can now adjust it back
- If negativeOverflowHandler Then number += 1
- Obj.m_NumeralParts(Standard) = number ' Store the standard numeral part
- rv.Append(ps_NumberToNumeral(number)) ' Add it to the return string
- If Obj.m_Negative Then rv.Insert(0, "-") ' Insert negative sign if needed
- Return rv.ToString
- End Function
- Private Shared Function ps_NumberToNumeral(ByVal number As Integer) As String
- Dim rv As StringBuilder = New StringBuilder ' Used to build a return value
- ' While the number is greater than 0 keep dividing and modding by values in the
- ' pNumeralValues dictionary building a string of numerals which represents
- ' the number.
- While number > 0
- If number >= 1000 Then
- For i As Integer = 1 To CInt(Math.Floor(number / 1000))
- rv.Append(pNumeralValues(1000))
- Next
- number = number Mod 1000
- ElseIf number >= 900 Then
- rv.Append(pNumeralValues(900))
- number = number Mod 900
- ElseIf number >= 500 Then
- rv.Append(pNumeralValues(500))
- number = number Mod 500
- ElseIf number >= 400 Then
- rv.Append(pNumeralValues(400))
- number = number Mod 400
- ElseIf number >= 100 Then
- For i As Integer = 1 To CInt(Math.Floor(number / 100))
- rv.Append(pNumeralValues(100))
- Next
- number = number Mod 100
- ElseIf number >= 10 Then
- rv.Append(pNumeralValues(number - (number Mod 10)))
- number = number Mod 10
- Else
- rv.Append(pNumeralValues(number))
- number = 0
- End If
- End While
- Return rv.ToString
- End Function
- ' -----------------------------------------------------------------------------------
- ' Public Enumerations
- ' -----------------------------------------------------------------------------------
- Public Enum MalformationError
- None = 0 ' Not malformed
- CharacterInvalid = 1 ' 1 or more invalid characters found
- CharacterGroupInvalid = 2 ' 1 or more invalid character groups found
- SignIndex = 3 ' Negative sign (-) detected outside the first index
- SignCount = 4 ' More than 1 negative sign found
- Parenthesis = 5 ' Problem found with notation parentheses, either order or count
- ValueOverflow = 6 ' Numeral will cause overflow when converted to Integer
- End Enum
- ' -----------------------------------------------------------------------------------
- ' Private member variables. Unique to each object of this class
- ' -----------------------------------------------------------------------------------
- Private m_Number As Integer = 0
- Private m_Numeral As String = String.Empty
- Private m_Negative As Boolean = False
- Private m_Malformation As MalformationError = MalformationError.None
- ' Numeral values broken into parts : Index 0 = Billions, 1 = Millions, 2 = Thousands, 3 = Standard
- Private m_NumeralParts() As Integer = {0, 0, 0, 0}
- ' -----------------------------------------------------------------------------------
- ' Public member accessor methods
- ' -----------------------------------------------------------------------------------
- Public Property Number As Integer
- Get
- Return m_Number ' return the current value
- End Get
- Set(ByVal value As Integer)
- ps_ZeroAll(Me)
- If value <> 0 Then
- ' If the number isn't 0...
- ' flag if the number is negative, assign the number and build the numeral
- m_Negative = If(value < 0, True, False)
- m_Number = value
- m_Numeral = ps_BuildNumeralFromNumber(Me, value)
- End If
- End Set
- End Property
- Public Property Numeral As String
- Get
- Return m_Numeral ' return the current value
- End Get
- Set(ByVal value As String)
- ps_ZeroAll(Me) ' Zero all instance variables
- value = value.ToUpper
- If value = "0" Then
- ' If 0 is passed as a numeral...
- ' Since we are already zeroed out we just need to show no Malformation
- m_Malformation = MalformationError.None
- Else
- ' Otherwise...
- If Not ps_MalformedNumeral(Me, value) Then
- ' If the value is not malformed...
- ' we can set the Numeral, Number and Negative variables
- m_Numeral = value
- m_Number = (m_NumeralParts(Billions) * 1000000000) + _
- (m_NumeralParts(Millions) * 1000000) + _
- (m_NumeralParts(Thousands) * 1000) + _
- m_NumeralParts(Standard)
- If m_Negative Then m_Number *= -1
- End If
- End If
- End Set
- End Property
- Public Property Negative As Boolean
- Get
- Return m_Negative ' return the current value
- End Get
- Set(ByVal value As Boolean)
- ' If number isn't 0 and we are not reaffirming the existing value...
- If m_Number <> 0 And value <> m_Negative Then
- ' If our current value is pNumberMinimum we cannot flip it to positive Integer
- ' without causing an overflow, so we have to flag it as malformed.
- If m_Negative And Number = pNumberMinimum Then
- ps_ZeroAll(Me)
- m_Malformation = MalformationError.ValueOverflow
- Else
- ' Otherwise set the value, flip the number and fix the numeral string to
- ' display a negative sign.
- m_Negative = value
- m_Number *= -1
- If value = True Then
- m_Numeral = String.Format("-{0}", m_Numeral)
- Else
- m_Numeral = m_Numeral.TrimStart("-"c)
- End If
- End If
- End If
- End Set
- End Property
- Public ReadOnly Property Length As Integer
- Get
- ' If the objects number value is not 0 (either malformed or special zero) return the length
- ' of the objects numeral string. Otherwise return a length of 0
- Return If(m_Number <> 0, m_Numeral.Length, 0)
- End Get
- End Property
- Public ReadOnly Property Malformed As Boolean
- Get
- ' If the objects malformation value is anything but None, then it is malformed (return True)
- ' Otherwise return False
- Return If(m_Malformation = MalformationError.None, False, True)
- End Get
- End Property
- Public ReadOnly Property Malformation As MalformationError
- Get
- ' Return the objects specific malformation value
- Return m_Malformation
- End Get
- End Property
- ' -----------------------------------------------------------------------------------
- ' Object constructor methods
- ' -----------------------------------------------------------------------------------
- Public Sub New() ' Default: set all values to 0
- ps_ZeroAll(Me)
- End Sub
- Public Sub New(ByVal numeral As String) ' Create new object based on numeral string
- numeral = numeral.ToUpper
- If numeral = "0" Then
- ' If 0 is passed as a numeral...
- ' zero out and show no Malformation
- ps_ZeroAll(Me)
- Else
- ' Otherwise...
- If Not ps_MalformedNumeral(Me, numeral) Then
- ' If the value is not malformed...
- ' we can set the Numeral, Number and Negative variables
- m_Numeral = numeral
- m_Number = (m_NumeralParts(Billions) * 1000000000) + _
- (m_NumeralParts(Millions) * 1000000) + _
- (m_NumeralParts(Thousands) * 1000) + _
- m_NumeralParts(Standard)
- If m_Negative Then m_Number *= -1
- Else
- ps_ZeroAll(Me)
- End If
- End If
- End Sub
- Public Sub New(ByVal number As Integer) ' Create a new object based on a number
- m_Number = number ' Set the number property
- If m_Number <> 0 Then
- ' If number isn't 0 then flag if negative and build a numeral
- If m_Number < 0 Then m_Negative = True
- m_Numeral = ps_BuildNumeralFromNumber(Me, number)
- Else
- ' Otherwise set as zero
- m_Numeral = "0"
- End If
- End Sub
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement