This is an OOP example that focuses on Statistical Functions, with input via a NumericUpDown Control, and displaying output in a PropertyGrid
Control. The OOP part is simple. There's a Class for each of these...
Each Class has just one familiar Public member - getValue, which returns either a single Decimal or an Array of Decimal. There is also a common Class which hosts methods used by more than one of the getValue Functions.
This contains methods used in more than one of the OOP Core Classes
Public Class common Public Shared Function ocToArray(items As ListBox.ObjectCollection) As Decimal() Return items.Cast(Of Object).Select(Function(o) CDec(o.ToString)).OrderBy(Function(d) d).ToArray End Function Public Shared Function median(ByVal numbers() As Decimal) As Decimal Array.Sort(numbers) If numbers.Length = 0 Then Return 0 ElseIf numbers.Length = 1 Then Return numbers(0) End If If numbers.Length Mod 2 = 1 Then 'odd count Return numbers(CInt(Math.Floor(numbers.Length / 2))) Else 'even count Dim lower As Integer = CInt(numbers.Length / 2 - 1) Dim higher As Integer = lower + 1 Return (numbers(lower) + numbers(higher)) / 2 End If End Function End Class
Mean is just the average of an Array of numbers, which is easily calculated in .Net
Public Class mean Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal Dim numbers() As Decimal = common.ocToArray(items) Return numbers.Average End Function End Class
Median is the middle value in a sorted Array of numbers
Public Class median Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal Dim numbers() As Decimal = common.ocToArray(items) Return common.median(numbers) End Function End Class
Mode is the number with the highest occurence in an Array of numbers. This can be multi-modal which means more than one number
Public Class mode Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As String Dim numbers() As Decimal = common.ocToArray(items) Dim map As New Dictionary(Of Decimal, Integer) For Each f As Decimal In numbers Dim c As Integer = 0 For x As Integer = 0 To numbers.Length - 1 If f = numbers(x) Then c += 1 End If Next x If Not map.ContainsKey(f) Then map.Add(f, c) Else map(f) = c End If Next Dim highest As Integer = map.Values.Max Dim counter As Integer = map.Where(Function(kvp) kvp.Value = highest).Count Dim modeValues As List(Of Decimal) = map.Where(Function(kvp) kvp.Value = highest).Select(Function(kvp) kvp.Key).Distinct.ToList If counter = items.Count Then Return "...(All)" Else Dim result As String = "" For Each f As Decimal In modeValues result &= f.ToString() & ", " Next f Return result.Substring(0, result.Length - 2) End If End Function End Class
Range is the highest number minus the lowest number in an Array of numbers. In this example, min, max, and range are returned.
Public Class range Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal() Dim numbers() As Decimal = common.ocToArray(items) Return New Decimal() {numbers.Min, numbers.Max, numbers.Max - numbers.Min} End Function End Class
IQR is inter-quartile range, which is calculated like this.
Q1 (first quartile) is the median of the first half of a sorted Array of numbers.
Q3 (third quartile) is the median of the second half of a sorted Array of numbers.
IQR is Q3 - Q1.
In this example, Q1, Q3, and IQR are returned.
Public Class iqr Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal() Dim numbers() As Decimal = common.ocToArray(items) Array.Sort(numbers) Dim i As Integer = CInt(Math.Floor(numbers.Count / 2)) Dim firstHalf As New List(Of Decimal)(numbers.Take(i).ToArray) i = If(i = numbers.Count / 2, i, i + 1) Dim secondHalf As New List(Of Decimal)(numbers.Skip(i).Take(i).ToArray) Dim Q1 As Decimal = common.median(firstHalf.ToArray) Dim Q3 As Decimal = common.median(secondHalf.ToArray) Return New Decimal() {Q1, Q3, Q3 - Q1} End Function End Class
Standard Deviation is used to quantify the amount of variation or dispersion of an Array of numbers. In this example, sample and population StdDev is calculated.
Public Class sd Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal() Dim numbers() As Decimal = common.ocToArray(items) Dim mean As Decimal = numbers.Average Dim squaredDifference(numbers.Length - 1) As Decimal For x As Integer = 0 To numbers.Length - 1 squaredDifference(x) = CDec(Math.Pow(numbers(x) - mean, 2)) Next x Return New Decimal() {CDec(Math.Sqrt(minusOneAverage(squaredDifference))), CDec(Math.Sqrt(squaredDifference.Average))} End Function Private Shared Function minusOneAverage(ByVal a() As Decimal) As Decimal If a.Length > 1 Then Return a.Sum / (a.Length - 1) Else Return Nothing End If End Function End Class
In this example a PropertyGrid Control is used for output. To use a PropertyGrid Control, you bind a Class to the Control, which shows the Properties
contained in the class. This can be a read/write arrangement, but in this Class, the Properties are all ReadOnly.
This is a coordinating class for the OOP core as well as the Datasource for the PropertyGrid, as there is a Public method which, when invoked,
gathers all of the Statistical Function values and assigns them to the ReadOnly Properties in the same Class.
Imports System.ComponentModel <TypeConverter(GetType(PropertySorter))> Public Class datasource Public Sub updateValues(listbox As ListBox) Dim b As Boolean = listbox.Items.Count > 0 _mean = If(b, mean.getValue(listbox.Items).ToString, "") _median = If(b, median.getValue(listbox.Items).ToString, "") _mode = If(b, mode.getValue(listbox.Items).ToString, "") If b Then Dim values() As Decimal = range.getValue(listbox.Items) _min = values(0).ToString _max = values(1).ToString _range = values(2).ToString values = iqr.getValue(listbox.Items) _q1 = values(0).ToString _q3 = values(1).ToString _iqr = values(2).ToString values = sd.getValue(listbox.Items) _sample = values(0).ToString _population = values(1).ToString Else _min = "" _max = "" _range = "" _q1 = "" _q3 = "" _iqr = "" _sample = "" _population = "" End If End Sub Private _mean As String <SortedCategory("General -", 0, 4), DisplayName("Mean:"), PropertyOrder(0)> Public ReadOnly Property pmean As String Get Return _mean End Get End Property Private _median As String <SortedCategory("General -", 0, 4), DisplayName("Median:"), PropertyOrder(1)> Public ReadOnly Property pmedian As String Get Return _median End Get End Property Private _mode As String <SortedCategory("General -", 0, 4), DisplayName("Mode:"), PropertyOrder(2)> Public ReadOnly Property pmode As String Get Return _mode End Get End Property Private _min As String <SortedCategory("Range -", 1, 4), DisplayName("Min:"), PropertyOrder(3)> Public ReadOnly Property pmin As String Get Return _min End Get End Property Private _max As String <SortedCategory("Range -", 1, 4), DisplayName("Max:"), PropertyOrder(4)> Public ReadOnly Property pmax As String Get Return _max End Get End Property Private _range As String <SortedCategory("Range -", 1, 4), DisplayName("Range:"), PropertyOrder(5)> Public ReadOnly Property prange As String Get Return _range End Get End Property Private _q1 As String <SortedCategory("IQR -", 2, 4), DisplayName("Q1:"), PropertyOrder(6)> Public ReadOnly Property pq1 As String Get Return _q1 End Get End Property Private _q3 As String <SortedCategory("IQR -", 2, 4), DisplayName("Q3:"), PropertyOrder(7)> Public ReadOnly Property pq3 As String Get Return _q3 End Get End Property Private _iqr As String <SortedCategory("IQR -", 2, 4), DisplayName("IQR:"), PropertyOrder(8)> Public ReadOnly Property piqr As String Get Return _iqr End Get End Property Private _sample As String <SortedCategory("StdDev -", 3, 4), DisplayName("Sample:"), PropertyOrder(9)> Public ReadOnly Property psample As String Get Return _sample End Get End Property Private _population As String <SortedCategory("StdDev -", 3, 4), DisplayName("Population:"), PropertyOrder(10)> Public ReadOnly Property ppopulation As String Get Return _population End Get End Property End Class
There are two custom Attributes...
By default, the PropertyGrid sorts Categories alphabetically by name. The only way (in VB2017) to change that sort order is to trick the alphabetical sorter by prepending zero length unprintable characters to the Category display name. SubClassing the CategoryAttribute allows a custom CategoryAttribute to be used with three parameters instead of just the standard categoryKey. These parameters are: categoryKey (a string name), order (zero based) and categoryCount (which is the number of categories used in your class). The Protected GetLocalizedString Function prepends the correct number of zero length unprintable characters according to the input arguments you've provided, resulting in Categories displayed in the order you choose.
Imports System.ComponentModel Public Class SortedCategoryAttribute Inherits CategoryAttribute Const trickString As String = Chr(31) & Chr(32) Private order As Integer Private categoryCount As Integer Public Sub New(ByVal categoryKey As String, order As Integer, categoryCount As Integer) MyBase.New(categoryKey) Me.order = order Me.categoryCount = categoryCount End Sub Protected Overrides Function GetLocalizedString(ByVal value As String) As String Dim x As Integer = Me.categoryCount - Me.order Dim s As String = "" While x >= 1 s &= trickString x -= 1 End While Return s & value End Function End Class
This is a simple Attribute holding an order value
<AttributeUsage(AttributeTargets.Property)> Public Class PropertyOrderAttribute Inherits Attribute ' ' Simple attribute for a property to allow an order to be specified ' Public ReadOnly Property Order() As Integer Public Sub New(ByVal order As Integer) Me.Order = order End Sub End Class
Including a PropertySorter means the properties within the categories will retain the order you specify, even when the sort alphabetically button is pressed, and also when returning to a categorised view. Examples of this type of sorter are freely and widely available on the internet.
Imports System.ComponentModel Public Class PropertySorter Inherits ExpandableObjectConverter Public Overrides Function GetPropertiesSupported(ByVal context As ITypeDescriptorContext) As Boolean Return True End Function Public Overrides Function GetProperties(ByVal context As ITypeDescriptorContext, ByVal value As Object, ByVal attributes() As Attribute) As PropertyDescriptorCollection ' ' This override returns a list of properties in order ' Dim pdc As PropertyDescriptorCollection = TypeDescriptor.GetProperties(value, attributes) Dim dProperties As New Dictionary(Of String, Integer) For Each pd As PropertyDescriptor In pdc Dim attribute As PropertyOrderAttribute = TryCast(pd.Attributes(GetType(PropertyOrderAttribute)), PropertyOrderAttribute) dProperties.Add(pd.Name, If(Not attribute Is Nothing, attribute.Order, 0)) Next pd ' ' Perform the sorting using LINQ ' Dim myList As List(Of KeyValuePair(Of String, Integer)) = dProperties.ToList() myList.Sort(Function(x As KeyValuePair(Of String, Integer), y As KeyValuePair(Of String, Integer)) If x.Value = y.Value Then Return x.Key.CompareTo(y.Key) Else Return x.Value.CompareTo(y.Value) End If Return 0 End Function) ' ' Pass in the ordered names for the PropertyDescriptorCollection to sort by ' Return pdc.Sort(myList.Select(Function(kvp) kvp.Key).ToArray) End Function End Class
OOP Programming in VB.Net makes easily readable code. Where possible, re-using methods is efficient. Making controls work as you want them to, results in usable programs, assuming your idea of how your program should function is realistic.
This example has a PropertyGrid that doesn't use the default sort order. This is achieved with the Custom Attributes and the TypeConverter, which work together with the datasource Class, and the PropertyGrid, to attain the desired sorting.
This example is available for download here...