Visual Basic is now completely on a par with C# and other
languages in its scientific, engineering, and financial number-crunching
capabilities. This chapter demonstrates how easy it is to develop very
fast and powerful, yet easy-to-read code for advanced number-crunching
applications. Some of the recipes will appeal to almost all developers,
such as those demonstrating rounding, the new unsigned integers, and the
new Decimal
numbers that are suitable
for the most demanding financial calculations. Other recipes will appeal
to the many scientist and engineer types searching for 21st century
updates for FORTRAN, programmable calculators, and Excel.
You want to write compact, efficient code using the latest syntax available for assignment operators.
Sample code folder: Chapter 06CompactOperators
Visual Basic 2005 now lets you use the same compact assignment notation for some math operations that has been used in the C and C# languages for many years.
There are several compact assignment operators, and they all work the same way. The variable to the left of the operator is used both as a source value and as a destination for the results of the operation. The operators are listed in Table 6-1.
Consider the following program statement, which increments the
variable count
:
count = count + 1
The variable count
is
repeated twice in this simple line of code, once to retrieve its value
and once to assign the results of adding 1 to the value. The new, more
efficient compact assignment syntax uses the variable’s name just
once:
count += 1
The compact assignment operator +=
causes the variable to be used both as
the source of the value to be operated on and as the destination for
the result.
The following sample code demonstrates all of the operators listed in Table 6-1:
Dim result As New System.Text.StringBuilder Dim testDouble As Double = Math.PI result.Append("Double ").AppendLine(testDouble) testDouble += Math.PI result.Append("+= ").AppendLine(testDouble) testDouble *= Math.PI result.Append("*= ").AppendLine(testDouble) testDouble -= Math.PI result.Append("-= ").AppendLine(testDouble) testDouble /= Math.PI result.Append("/= ").AppendLine(testDouble) testDouble ^= Math.PI result.Append("^= ").AppendLine(testDouble) result.AppendLine() Dim testInteger As Integer = 17 result.Append("Integer ").AppendLine(testInteger) testInteger = 2 result.Append("= 2 … ").AppendLine(testInteger) testInteger += 1 result.Append("+= 1 … ").AppendLine(testInteger) testInteger <<= 1 result.Append("<<= 1 … ").AppendLine(testInteger) testInteger >>= 3 result.Append(">>= 3 … ").AppendLine(testInteger) result.AppendLine() Dim testString As String = "Abcdef" result.Append("String ").AppendLine(testString) testString &= "ghi" result.Append("&= ghi … ").AppendLine(testString) testString += "jkl" result.Append("+= jkl … ").AppendLine(testString) MsgBox(result.ToString())
Figure 6-1 shows the results displayed by this block of code. While many of the operators work on double-precision variables, some work only on integers of various sizes, and the concatenation operator works only on strings.
Although the +=
(addition)
operator is overloaded to operate on either numerical variables or
strings, your code will be clearer if you use the addition operator
only for mathematical operations. For string concatenation, use the
&=
operator instead. This rule
can also help you avoid hidden errors when working with numbers
formatted as strings. For instance, consider the following code, which
updates an Integer
value with
numbers stored in strings:
Dim numberFromStrings As Integer numberFromStrings = "4" numberFromStrings += "3" MsgBox(numberFromStrings)
When you run this code, it displays “7” in the message box. This
works because Visual Basic is “helping you out,” automatically
converting the strings to Integer
values before performing the assignment or addition. If you replace
the +=
operator in that code with the &=
operator, the code behaves
differently:
Dim numberFromStrings As Integer
numberFromStrings = "4"
numberFromStrings &= "3"
MsgBox(numberFromStrings)
This time, the message box displays “43,” the concatenation of
the two strings. Some of the documentation for the +
= and &=
operators claims that the two are
functionally equivalent when working with strings, but this example
shows that care should be exercised when using them in mixed
string/number situations.
Search for “operator procedures” in Visual Studio Help for more information.
Sample code folder: Chapter 06UsingIntegers
Visual Basic 2005 now has signed and unsigned integer variable types that range in size from 8 bits to 64 bits (1 byte to 8 bytes). Using the right size and type of integer can save memory, generate more efficient code, and provide ranges of integer values suitable to a variety of needs.
Visual Basic 2005 is the first version of Visual Basic to support signed byte values and unsigned integer values in a variety of sizes. Here’s a list of all the integer types now supported:
Byte
Eight-bit (1-byte) values ranging from 0 to 255.
Equivalent to System.Byte
.
SByte
A signed type that is 8 bits (1 byte) in size and holds
values ranging from −128 to +127. Equivalent to System.SByte
.
Short
Sixteen-bit (2-byte) values ranging from −32,768 to
+32,767. Equivalent to System.Int16
.
UInt16
An unsigned type that is 16 bits (2 bytes) in
size and holds values ranging from 0 to 65,535.
Equivalent to System.UInt16
.
Integer
Thirty-two-bit (4-byte) values ranging from −2,147,483,648
to +2,147,483,647. Equivalent to System.Int32
.
UInteger
An unsigned type that is 32 bits (4 bytes) in size and holds values ranging from 0 to
4,294,967,295. Equivalent to System.UInt32
.
Long
Sixty-four-bit (8-byte) values ranging from
−9,223,372,036,854,775,808 to +9,223,372,036,854,775,807 (−9 to
+9 quintillion). Equivalent to System.Int64
.
ULong
An unsigned type that is 64 bits (8 bytes) in size and
holds values ranging from 0 to 18,446,744,073,709,551,615 (18
quintillion). Equivalent to System.UInt64
.
The following code demonstrates each of these integer types by displaying the largest possible value for each:
Dim result As New System.Text.StringBuilder() result.AppendLine("MaxValue…") result.AppendLine() Dim maxByte As Byte = Byte.MaxValue Dim maxSByte As SByte = SByte.MaxValue Dim maxShort As Short = Short.MaxValue Dim maxUShort As UShort = UShort.MaxValue Dim maxInteger As Integer = Integer.MaxValue Dim maxUInteger As UInteger = UInteger.MaxValue Dim maxLong As Long = Long.MaxValue Dim maxULong As ULong = ULong.MaxValue result.Append("Byte ").AppendLine(maxByte) result.Append("SByte ").AppendLine(maxSByte) result.Append("Short ").AppendLine(maxShort) result.Append("UShort = ").AppendLine(maxUShort) result.Append("Integer = ").AppendLine(maxInteger) result.Append("UInteger = ").AppendLine(maxUInteger) result.Append("Long = ").AppendLine(maxLong) result.Append("ULong = ").AppendLine(maxULong) MsgBox(result.ToString())
For all unsigned variable types, the minimum possible value is
zero. For all signed types, to find the minimum value add one to the
maximum value, and change the sign. For example, the maximum value for
signed bytes is 127, and the minimum value is −128. As
shown above, the MaxValue
property
of each integer type provides a straightforward way to access the
largest possible value. Similarly, you can get the smallest possible
value by accessing each type’s MinValue
property.
Figure 6-2 shows the maximum values for each type of integer, as displayed by the message box in the example code.
One other variable type is worth considering for extremely large
integer values. Although not true integers,
Decimal
variables can hold integer values up
to 79,228,162,514,264,337,593,543,950,335 (79 octillion). The rule for
determining the minimum value for a Decimal
-type variable is slightly different
than for the true integer types: in this case, just reverse the sign of
the maximum value, don’t add 1. The MinValue
for Decimal
variables is thus
−79,228,162,514,264,337,593,543,950,335.
Decimal
values are signed
128-bit (16-byte) numbers, and they may have a decimal point. If you
appropriately round off or truncate values, the Decimal
type can accurately hold extremely
large integer values. However, even on 64-bit machines, this data type
can slow down calculations somewhat because the processor must perform
calculations using multiple steps to process each value.
You want to work with nonnegative integers while minimizing the memory requirements of variables in your code.
Use the smallest unsigned integer variable types that will hold the desired range of nonnegative values.
As mentioned in the previous recipe, the unsigned integer variable types provide many new options for working with nonnegative integers in Visual Basic 2005. The following code provides a specific example to help clarify the concept:
Dim testUShort As UShort Do Until (testUShort > CUShort(33000)) testUShort += CUShort(1) Loop MsgBox("UShort result: " & testUShort.ToString())
The standard Visual Basic Short
variable type holds signed integers in
the range −32,768 to +32,767 and uses only two bytes of memory. If the
previous code used signed integers, an exception would be generated
during the looping because values up to 33,001 are not allowed. The
unsigned testUShort
integer stores
values up to 65,535, so the program runs successfully, and the
variable still requires only two bytes of memory. Figure 6-3 shows a two-byte
unsigned variable displaying a number too big for a standard signed
two-byte integer.
Search for “UInteger” in Visual Studio Help for more information.
You want to swap the values of two integer variables without creating a third.
Sample code folder: Chapter 06IntegerSwap
Use the exclusive-or bit manipulation function to do the trick.
Nowadays efforts to save the space of a single variable in memory seem kind of silly, but this recipe nevertheless demonstrates an interesting technique for swapping two numbers without creating a third variable. More importantly, it demonstrates how bit-manipulation functions can be quite useful in Visual Basic 2005. Here’s the sample code:
Dim result As String Dim firstValue As Integer Dim secondValue As Integer ' ----- Set the initial test values. firstValue = 17 secondValue = 123 result = String.Format("Before swap: {0}, {1}", _ firstValue, secondValue) result &= vbNewLine ' ----- Swap the values at the bit level. firstValue = firstValue Xor secondValue secondValue = firstValue Xor secondValue firstValue = firstValue Xor secondValue result &= String.Format("After swap: {0}, {1}", _ firstValue, secondValue) MsgBox(result)
The above code loads values into integers firstValue
and secondValue
, then swaps their values by
applying three successive Xor
operators on them. The Xor
operator
combines the two integers on a bit-by-bit basis, resulting in a 1 bit
whenever the original bits are different and a 0 when they are the
same. Once these three Xor
operations have been performed, the original contents of the two
integers will have migrated to the opposite locations in memory. Figure 6-4 shows the results
displayed by the sample code.
Search for “Xor operator” in Visual Studio Help for more information.
Sample code folder: Chapter 06SingleDouble
Choose the most appropriate variable type based on the range and precision of numbers it can hold and on its memory footprint.
To help you understand the capabilities of Single
and Double
variables, the following sample code
uses several useful properties and functions to display information
about them:
Dim result As New System.Text.StringBuilder Dim maxSingle As Single = Single.MaxValue Dim maxDouble As Double = Double.MaxValue Dim sizeOfSingle As Integer = _ Runtime.InteropServices.Marshal.SizeOf(maxSingle.GetType) Dim sizeOfDouble As Integer = _ Runtime.InteropServices.Marshal.SizeOf(maxDouble.GetType) result.Append("Memory size of a Single (bytes): ") result.AppendLine(sizeOfSingle) result.Append("Maximum value of a Single: ") result.AppendLine(maxSingle) result.AppendLine() result.Append("Memory size of a Double (bytes): ") result.AppendLine(sizeOfDouble) result.Append("Maximum value of a Double: ") result.AppendLine(maxDouble) MsgBox(result.ToString())
The MaxValue
constant
provided by each type provides the largest possible value for
variables of that type. The Marshal.SizeOf()
function returns the
unmanaged size, in bytes, of any class, which in this case is the
class returned by the GetType()
method of our Single
and Double
variables. Figure 6-5 shows the
results.
If you’re working with large arrays of numbers and memory issues
are of concern, the Single
type
might be appropriate. If you need greater precision, and using twice
the memory per occurrence is not a problem, Doubles
might work best.
Many mathematical functions, such as those provided by the
Math
class, operate on Doubles
only. Generally this is not a
problem, as conversion between Single
and Double
types in memory is efficient. On the
other hand, the GDI+ Graphics
object operates on Single
values,
so it’s best to work with these where possible when creating graphics.
For example, many of the graphics functions and methods accept
PointF
objects passed as
parameters, and a PointF
is
comprised of a pair of Single
numbers, X and Y.
The “PointF” topic in Visual Studio Help describes how Singles
are used for many graphics
methods.
The “Math Class” subject lists many useful functions that
operate on Doubles
.
You want to manipulate numbers with many significant digits of accuracy.
Sample code folder: Chapter 06SingleDouble
The Decimal
number type holds
numbers with up to 29 digits of accuracy and is well suited to tasks
in which rounding errors are to be kept to a minimum, as in financial
calculations.
For really big numbers where you want many digits of accuracy,
the Decimal
number type is ideal.
Numbers of this type are stored in 16 bytes (128 bits) of memory each,
with up to 29 significant digits. These numbers can be positive or
negative, and a decimal point can be included anywhere within the
number. The following code demonstrates Decimal
variables in action:
Dim result As New System.Text.StringBuilder Dim maxDecimal As Decimal = Decimal.MaxValue Dim sizeOfDecimal As Integer = _ Runtime.InteropServices.Marshal.SizeOf(maxDecimal.GetType) result.Append("Memory size of a Decimal (bytes): ") result.AppendLine(sizeOfDecimal) result.Append("Maximum value of a Decimal: ") result.AppendLine(maxDecimal) result.Append("Divided by one million: ") result.AppendLine(maxDecimal / 1000000D) result.Append("1D / 3D: ") result.AppendLine(1D / 3D) MsgBox(result.ToString())
Figure 6-6 shows
the display created by this code. The Marshal.SizeOf()
function determines the
number of bytes of memory the Decimal
variable uses, and the MaxValue
constant gets the largest possible
numerical value it can hold. To demonstrate how the decimal point can
be anywhere in the number, the maximum value is divided by one
million. The decimal point shifts six digits in from the right as a
result. To demonstrate that the math operators are overloaded to
accurately take advantage of the Decimal
’s full precision, the quantity 1/3
is calculated and displayed in the last line of the message box. An
uppercase “D” is appended to the constants 1 and 3 in the code to tell
the compiler that they are Decimal
values.
See “Decimal data type” in Visual Studio Help for more information.
You want to explicitly convert numeric variables and calculation results between the various number types.
Sample code folder: Chapter
06ConvertNumber
It’s always a good idea to make sure your project’s Option Explicit
and Option Strict
settings are on, but this
often forces you to apply explicit conversions when working with more than one type of
numeric variable. The solution is to apply one of the many standalone
conversion functions provided by Visual Basic or to use one of the
many methods of the Convert
object.
The following code sample demonstrates a simple conversion of
Double
numeric values to Byte
values, using both the standalone
CByte()
function and the Convert.ToByte()
method. Some people prefer
to use the Convert
object
exclusively, which may be easier to remember because all the
conversion methods have names beginning with “To”. Others prefer the
standalone conversion functions, because many of these have been
around in previous versions of Visual Basic for some time now. We look
at both approaches here:
Dim result As New System.Text.StringBuilder Dim b1 As Byte = CByte(3.1416) + CByte(314.16 / 2) Dim b2 As Byte = Convert.ToByte(3.1416) + _ Convert.ToByte(314.16 / 2) result.AppendLine("Example conversions to Byte…") result.AppendLine() result.AppendLine("Dim b1 As Byte = CByte(3.1416) + " & _ "CByte(314.16 / 2)") result.Append("b1 = ") result.AppendLine(b1.ToString) result.AppendLine() result.Append("Dim b2 As Byte = Convert.ToByte(3.1416) + ") result.AppendLine("Convert.ToByte(314.16 / 2)") result.Append("b2 = ") result.AppendLine(b2.ToString) result.AppendLine() result.AppendLine("Numeric Conversions…") result.AppendLine() result.AppendLine("CByte(expression)") result.AppendLine("CSByte(expression)") result.AppendLine("CShort(expression)") result.AppendLine("CUShort(expression)") result.AppendLine("CInt(expression)") result.AppendLine("CUInt(expression)") result.AppendLine("CLng(expression)") result.AppendLine("CULng(expression)") result.AppendLine("CSng(expression)") result.AppendLine("CDbl(expression)") result.AppendLine("CDec(expression)") MsgBox(result.ToString())
The Double
value 314.16 will
not convert to a Byte
because it is
out of range for byte values. Attempting this conversion causes an
exception. However, dividing this value by 2 results in a Double
value that does convert. The point is
that the decimal digits don’t cause a problem when converting to a
Byte
(they are simply rounded to
the nearest byte value), but the number must be in the range 0 to 255
to allow the conversion.
Figure 6-7 shows the results of the above demonstration code in action. A sample conversion is shown using both techniques, and a list of the standalone conversion functions is displayed for easy review.
The signed byte and unsigned integer data types are new with this latest version of Visual Basic, and so are the functions to convert values to them.
See “conversion functions” in Visual Studio Help for more information on these functions.
You need to round off double-precision numbers in a standard, accurate way.
Sample code folder: Chapter 06Rounding
Use the Math.Round()
function
to round numbers to the desired precision.
The Math.Round()
function is
overloaded to accept several different sets of parameters. If you pass
just a Double
or Decimal number to
it, the number is rounded to the nearest whole number. By passing a
second parameter, you control the number of digits after the decimal
point where the rounding is to occur. For example, the following code
rounds off the value of pi (π) using zero through five as the number
of digits for the rounding:
Dim outputFormat As String = _ "Rounding value: {0} Results: {1}" Dim oneTry As String Dim result As New System.Text.StringBuilder Dim piRounded As Double Dim digits As Integer For digits = 0 To 5 piRounded = Math.Round(Math.PI, digits) oneTry = String.Format(outputFormat, digits, piRounded) result.AppendLine(oneTry) Next digits MsgBox(result.ToString())
Figure 6-8 shows the results of these rounding actions.
A third optional parameter lets you fine-tune the way a number is rounded when the number is exactly halfway between two values at the point where the number is to be rounded. The choices are to have the number rounded to an even digit, or away from zero. The default is to round to an even digit.
See “Math.Round” in Visual Studio Help for more information.
You want to create a variable to hold the loop counter in a
For…Next
loop, but you want the
variable to exist only within the body of the loop.
Declare the variable type directly using the optional syntax for
doing this in the For… Next
loop
command.
If you include As
Type
immediately after the variable name used in the For…Next
statement, Visual Basic 2005
creates this variable on the spot, and its scope is limited to the
body of the For…Next
loop. If you
declare the variable elsewhere, don’t add the As
Type
clause in
the loop statement; doing so triggers an exception.
This sample code creates nested For…Next
loops, with the outer loop counter
variable declared outside the loop and the inner loop variable
declared just for the body of the loop. Study the lines starting with
For
to see the difference:
Dim formatString As String = "outerLoop: {0} innerLoop: {1} "Dim result As String = "" Dim outerLoopAs Integer
For outerLoop = 1 To 2 For innerLoopAs Integer
= 1 To 2 result &= String.Format(formatString, _ outerLoop, innerLoop) result &= vbNewLine Next innerLoop Next outerLoop MsgBox(result)
These two loops are nearly the same. Their counter variable
values are displayed each time through the inner loop, as shown in
Figure 6-9. The variable
outerLoop
can be referenced past
the end of the sample lines of code, but referencing innerLoop
will causes an exception. innerLoop
exists only within the For…Next
loop where it is declared.
See “For…Next statements” in Visual Studio Help for more information.
You want a simple, consistent, easy-to-read, and easy-to-use way to convert angles between radians and degrees.
Define two constants, RadPerDeg
and DegPerRad
, and multiply by degrees or
radians, respectively, to convert to the other units.
You can create standalone functions to perform these conversions, but these constants are straightforward definitions, and your code will compile to inline conversions that are compact and fast. The following code defines the constants and uses them to convert a few sample angular values. It’s generally best to define your constants at the top of your source-code files or in a global module, but here they are shown close to the code where they are used for easy reference:
Const RadPerDeg As Double = Math.PI / 180# Const DegPerRad As Double = 180# / Math.PI Dim radians As Double Dim degrees As Double radians = Math.PI / 4# degrees = radians * DegPerRad radians = degrees * RadPerDeg MsgBox("Radians: " & radians.ToString & _ vbNewLine & "Degrees: " & degrees.ToString)
This code rather redundantly converts radians to degrees and then immediately converts degrees right back to radians. You wouldn’t want to do this normally, but it shows both conversions side by side for easy comparison.
Figure 6-10 shows the same angle (45 degrees, or π/4 radians) expressed in the calculated units after conversion using the constants.
Both constants are defined using a division calculation. The
Visual Basic 2005 compiler converts this math statement to a single
constant by doing the division at compile time rather than at runtime,
so there is no inefficiency in expressing the constants this way. The
value of π is defined as a constant in the Math
object with full double-precision
accuracy, so the constants defined here are also accurate with
Double
values.
See “Derived Math Functions” in Visual Studio Help for additional derived functions, many of which assume radian units.
You want to shift intermediate angular calculation results into a range such as 0° to 360°, −180° to 180°, 0 to 2π radians, or –π to π radians.
Sample code folder: Chapter 06AngleRange
Create a function that handles all these range conversions efficiently.
Some scientific calculations produce angular results that are beyond normal ranges, requiring adjustment to bring them into the standard range of values. For example, in astronomical calculations a variety of polynomials are used to compute highly accurate positions of the planets and stars, but the polynomials often return angles representing many revolutions of the various orbs. You might say the angles are astronomical in size before they are adjusted into a normalized range such as 0° to 360°. The following function handles these range adjustments efficiently, bringing the values back down to earth:
Public Function FixRange(ByVal origValue As Double, _ ByVal rangeMin As Double, ByVal rangeMax As Double) _ As Double ' ----- Adjust a value to within a specified range. ' Use the range size as the adjustment factor. Dim shiftedValue As Double Dim delta As Double shiftedValue = origValue - rangeMin delta = rangeMax - rangeMin Return (((shiftedValue Mod delta) + delta) Mod delta) + _ rangeMin End Function
The FixRange()
function
accepts an out-of-range angular value expressed in either degrees or
radians (or any range-limited system), followed by the minimum and
maximum limits of the desired normalized range. All three parameters
must use the same measurement system, such as radians, for the results
to make sense.
The function uses a double application of the Mod
operator plus some additions and
subtractions to bring the value into the desired range. This
calculation is more straightforward and efficient than adding or
subtracting values in a loop until the value is brought into range,
which is the technique sometimes shown in astronomical calculation
books.
The following code demonstrates the use of the Range()
function on a variety of positive
and negative angular values as they are brought into a number of
desired ranges:
Dim result As New System.Text.StringBuilder Dim formatDegrees As String = _ "Degrees: {0} Range: {1},{2} Value: {3}" Dim formatRadians As String = _ "Radians: {0} Range: {1},{2} Value: {3}" Dim degrees As Double Dim radians As Double Dim ranged As Double ' ----- Degrees over the range. degrees = 367.75 ranged = FixRange(degrees, 0, 360) result.AppendLine(String.Format(formatDegrees, _ degrees, 0, 360, ranged)) ' ----- Degress under the range. degrees = -97.5 ranged = FixRange(degrees, 0, 360) result.AppendLine(String.Format(formatDegrees, _ degrees, 0, 360, ranged)) ' ----- Degrees in range. degrees = -97.5 ranged = FixRange(degrees, -180, 180) result.AppendLine(String.Format(formatDegrees, _ degrees, -180, 180, ranged)) ' ----- Radians over the range. radians = Math.PI * 3.33 ranged = FixRange(radians, -Math.PI, Math.PI) result.AppendLine(String.Format(formatRadians, _ radians, -Math.PI, Math.PI, ranged)) MsgBox(result.ToString())
Figure 6-11 shows the results produced by this sample code.
Search for information on the Mod
operator in Visual Studio Help.
The PointF
structure used in many graphics and
other methods is defined to hold single-precision X
and Y
values, but you need greater precision.
Sample code folder: Chapter 06DoublePoint
Create your own Point2D
class
with double-precision X
and
Y
values.
The following simple class provides a blueprint for creating
Point2D
objects containing
double-precision X
and Y
values:
Public Class Point2D Public X As Double Public Y As Double Public Sub New(ByVal xPoint As Double, _ ByVal yPoint As Double) Me.X = xPoint Me.Y = yPoint End Sub Public Overrides Function Tostring() As String Return "{X=" & X & ",Y=" & Y & "}" End Function End Class
As shown in the sample class code, the ToString()
function overrides the default
ToString()
and returns a string
formatted in a way that’s similar to the PointF
class in the .NET Framework.
The following code demonstrates the creation of both the
PointF
and new Point2D
objects. Both types of objects have
the same “look and feel” in that they allow access directly to the
X
and Y
values, they both can be populated with a
pair of X, Y
values at the moment
of creation, and they both return similar strings via their respective
ToString()
functions:
Dim result As New System.Text.StringBuilder ' ----- Original PointF version. Dim singlePoint As New PointF(1 / 17, Math.PI) result.AppendLine("PointF: " & singlePoint.ToString() result.AppendLine("X: " & singlePoint.X) result.AppendLine() ' ----- New Point2D version. Dim doublePoint As New Point2D(1 / 17, Math.PI) result.AppendLine("Point2D: " & doublePoint.ToString()) result.AppendLine("X: " & doublePoint.X) result.AppendLine() MsgBox(result.ToString())
Figure 6-12 shows the results displayed by the message box in this sample code.
See “Graphics” in Visual Studio Help for more information about the use of two-dimensional points.
You want to convert between two-dimensional coordinates expressed in either rectangular or polar notation.
Sample code folder: Chapter 06ConvertPolar
Create two functions for the two conversions: ToPolar()
and ToRectangular()
.
The PointF
structure provides
a natural way to handle two-dimensional coordinates because each
X, Y
pair is handled as a single
unit. A straightforward way to handle conversions between coordinates
expressed in either rectangular (X,
Y
) or polar (radius, radians) notation is to simply pass and
return PointF
objects. This
requires you, the programmer, to keep track of the current notation of
each PointF
object, but this is
generally easy to do. Here are the two functions for making the
conversions:
Public Function ToPolar(ByVal sourcePoint As PointF) _ As PointF ' ----- Convert rectangular coordinates to polar. Dim magnitude As Single Dim radians As Single magnitude = CSng(Math.Sqrt(sourcePoint.X ^ 2 + _ sourcePoint.Y ^ 2)) radians = CSng(Math.Atan2(sourcePoint.Y, sourcePoint.X)) Return New PointF(magnitude, radians) End Function Public Function ToRectangular(ByVal sourcePoint As PointF) _ As PointF ' ----- Convert polar coordinates to rectangular. Dim X As Single Dim Y As Single X = CSng(sourcePoint.X * Math.Cos(sourcePoint.Y)) Y = CSng(sourcePoint.X * Math.Sin(sourcePoint.Y)) Return New PointF(X, Y) End Function
Both functions assume angles will be expressed in radians, which is consistent with the way angles are expressed in Visual Basic. You can convert angles to and from degrees using the constants presented in Recipe 6.10.
The following block of code demonstrates the use of the ToPolar()
and
ToRectangular()
functions:
Dim result As New System.Text.StringBuilder Dim pointA As PointF Dim pointB As PointF Dim pointC As PointF pointA = New PointF(3, 4) pointB = ToPolar(pointA) pointC = ToRectangular(pointB) result.AppendLine("Rectangular: " & pointA.ToString()) result.AppendLine("Polar: " & pointB.ToString()) result.AppendLine("Rectangular: " & pointC.ToString()) MsgBox(result.ToString())
The ToString()
function
presents the X
and Y
values of the PointF
data using “X=” and “Y=” labels,
which can be misleading when the PointF
is holding a coordinate in polar
mode. Be sure to keep track of the state of the data as you work with
it.
Figure 6-13 shows
the formatted string results of the ToRectangular()
and ToPolar()
functions in action.
You want to work with three-dimensional coordinates as single entities.
Sample code folder: Chapter 06ThreePoint
Create a Point3D
class that
works like the PointF
class except
that it contains a Z
property in
addition to X
and Y
.
The following class definition is similar to the Point2D
class presented in Recipe 6.12:
Public Class Point3D Public X As Double Public Y As Double Public Z As Double Public Sub New(ByVal xPoint As Double, _ ByVal yPoint As Double, ByVal zPoint As Double) Me.X = xPoint Me.Y = yPoint Me.Z = zPoint End Sub Public Overrides Function Tostring() As String Return "{X=" & X & ",Y=" & Y & ",Z=" & Z & "}" End Function End Class
The most important modification is the addition of a public
Z
value for the third dimension. As
presented here, the X, Y
, and
Z
properties are all Double
precision, but you can easily
redefine these to Single
if that
provides sufficient precision for your calculations, and if you want
to save memory when you create large arrays of this data type.
The following code demonstrates the use of some Point3D
objects. Notice how the New()
function lets you create a Point3D
variable with nonzero X, Y
, and Z
values:
Dim result As New System.Text.StringBuilder Dim distance As Double Dim point1 As Point3D Dim point2 As Point3D Dim deltaX As Double Dim deltaY As Double Dim deltaZ As Double point1 = New Point3D(3, 4, 5) point2 = New Point3D(7, 2, 3) deltaX = point1.X - point2.X deltaY = point1.Y - point2.Y deltaZ = point1.Z - point2.Z distance = Math.Sqrt(deltaX ^ 2 + deltaY ^ 2 + deltaZ ^ 2) result.AppendLine("3D Point 1: " & point1.ToString()) result.AppendLine("3D Point 2: " & point2.ToString()) result.AppendLine("Distance: " & distance.ToString()) MsgBox(result.ToString())
Figure 6-14 shows the results of calculating the distance in space between these two coordinates.
Search for “basic 3D math” on the Web for a variety of explanations and further information about this subject.
You need to convert three-dimensional coordinates between rectangular, spherical, and cylindrical notation.
Sample code folder: Chapter 06Convert3D
Create a set of six functions to convert Point3D
variables to and from each
coordinate notation.
The following six functions convert from any one of the three
types of three-dimensional coordinates to any of the others. All these functions
accept a Point3D
argument and
return a Point3D
value. It is up to
you to keep track of the current type of coordinate notation in each
Point3D
variable. Note that in all
cases the Point3D
value passed in
to any of these functions is not altered; a new Point3D
instance is returned instead. Here
are the six functions:
Public Function RectToCylinder(ByVal pointA As Point3D) _ As Point3D ' ----- Convert rectangular 3D coordinates to ' cylindrical coordinates. Dim rho As Double Dim theta As Double rho = Math.Sqrt(pointA.X ^ 2 + pointA.Y ^ 2) theta = Math.Atan2(pointA.Y, pointA.X) Return New Point3D(rho, theta, pointA.Z) End Function Public Function CylinderToRect(ByVal pointA As Point3D) _ As Point3D ' ----- Convert cylindrical coordinates to ' rectangular 3D coordinates. Dim x As Double Dim y As Double x = pointA.X * Math.Cos(pointA.Y) y = pointA.X * Math.Sin(pointA.Y) Return New Point3D(x, y, pointA.Z) End Function Public Function RectToSphere(ByVal pointA As Point3D) _ As Point3D ' ----- Convert rectangular 3D coordinates to ' spherical coordinates. Dim rho As Double Dim theta As Double Dim phi As Double rho = Math.Sqrt(pointA.X ^ 2 + pointA.Y ^ 2 + _ pointA.Z ^ 2) theta = Math.Atan2(pointA.Y, pointA.X) phi = Math.Acos(pointA.Z / Math.Sqrt( _ pointA.X ^ 2 + pointA.Y ^ 2 + pointA.Z ^ 2)) Return New Point3D(rho, theta, phi) End Function Public Function SphereToRect(ByVal pointA As Point3D) _ As Point3D ' ----- Convert spherical coordinates to ' rectangular 3D coordinates. Dim x As Double Dim y As Double Dim z As Double x = pointA.X * Math.Cos(pointA.Y) * Math.Sin(pointA.Z) y = pointA.X * Math.Sin(pointA.Y) * Math.Sin(pointA.Z) z = pointA.X * Math.Cos(pointA.Z) Return New Point3D(x, y, z) End Function Public Function CylinderToSphere(ByVal pointA As Point3D) _ As Point3D ' ----- Convert cylindrical coordinates to ' spherical coordinates. Dim rho As Double Dim theta As Double Dim phi As Double rho = Math.Sqrt(pointA.X ^ 2 + pointA.Z ^ 2) theta = pointA.Y phi = Math.Acos(pointA.Z / _ Math.Sqrt(pointA.X ^ 2 + pointA.Z ^ 2)) Return New Point3D(rho, theta, phi) End Function Public Function SphereToCylinder(ByVal pointA As Point3D) _ As Point3D ' ----- Convert spherical coordinates to ' cylindrical coordinates. Dim rho As Double Dim theta As Double Dim z As Double rho = pointA.X * Math.Sin(pointA.Z) theta = pointA.Y z = pointA.X * Math.Cos(pointA.Z) Return New Point3D(rho, theta, z) End Function
The following code creates several Point3D
variables using names that indicate
the types of coordinates they contain. For example, pointCyl
is a Point3D
variable containing
three-dimensional cylindrical coordinates. The various conversion
functions are used to populate the variables, and the results are
shown in Figure
6-15:
Dim result As New System.Text.StringBuilder Dim pointRec As New Point3D(3, 4, 5) Dim pointCyl As Point3D = RectToCylinder(pointRec) Dim pointSph As Point3D = RectToSphere(pointRec) Dim pointRecToCyl As Point3D = RectToCylinder(pointRec) Dim pointRecToSph As Point3D = RectToSphere(pointRec) Dim pointCylToRec As Point3D = CylinderToRect(pointCyl) Dim pointCylToSph As Point3D = CylinderToSphere(pointCyl) Dim pointSphToRec As Point3D = SphereToRect(pointSph) Dim pointSphToCyl As Point3D = SphereToCylinder(pointSph) result.AppendLine("Rec: " & pointRec.ToString()) result.AppendLine("Cyl: " & pointCyl.ToString()) result.AppendLine("Sph: " & pointSph.ToString()) result.AppendLine() result.AppendLine("Rec to Cyl: " & pointRecToCyl.ToString()) result.AppendLine("Rec to Sph: " & pointRecToSph.ToString()) result.AppendLine("Cyl to Rec: " & pointCylToRec.ToString()) result.AppendLine("Cyl to Sph: " & pointCylToSph.ToString()) result.AppendLine("Sph to Rec: " & pointSphToRec.ToString()) result.AppendLine("Sph to Cyl: " & pointSphToCyl.ToString()) MsgBox(result.ToString())
Sample code folder: Chapter 06ComplexNumbers
Create a ComplexNumber
structure. Overload the standard mathematical operators so that using
complex number variables is easy and natural.
This recipe provides a great way to see how overloading standard
operators can enhance the usability of your classes and structures. In
this case, we’ve created a ComplexNumber
structure. Structures are
similar to classes, except that they exist as value types rather than
reference types. This allows complex number instances to act the same
as other simple variables, such as standard numerical
variables.
The following code defines the ComplexNumber
number structure. Place this
code in its own file named ComplexNumber.vb for easy inclusion in any
application that requires complex numbers:
Structure ComplexNumber Public Real As Double Public Imaginary As Double Public Sub New(ByVal realPart As Double, _ ByVal imaginaryPart As Double) Me.Real = realPart Me.Imaginary = imaginaryPart End Sub Public Sub New(ByVal sourceNumber As ComplexNumber) Me.Real = sourceNumber.Real Me.Imaginary = sourceNumber.Imaginary End Sub Public Overrides Function ToString() As String Return Real & "+" & Imaginary & "i" End Function Public Shared Operator +(ByVal a As ComplexNumber, _ ByVal b As ComplexNumber) As ComplexNumber ' ----- Add two complex numbers together. Return New ComplexNumber(a.Real + b.Real, _ a.Imaginary + b.Imaginary) End Operator Public Shared Operator -(ByVal a As ComplexNumber, _ ByVal b As ComplexNumber) As ComplexNumber ' ----- Subtract one complex number from another. Return New ComplexNumber(a.Real - b.Real, _ a.Imaginary - b.Imaginary) End Operator Public Shared Operator *(ByVal a As ComplexNumber, _ ByVal b As ComplexNumber) As ComplexNumber ' ----- Multiply two complex numbers together. Return New ComplexNumber(a.Real * b.Real - _ a.Imaginary * b.Imaginary, _ a.Real * b.Imaginary + a.Imaginary * b.Real) End Operator Public Shared Operator /(ByVal a As ComplexNumber, _ ByVal b As ComplexNumber) As ComplexNumber ' ----- Divide one complex number by another. Return a * Reciprocal(b) End Operator Public Shared Function Reciprocal( _ ByVal a As ComplexNumber) As ComplexNumber ' ----- Calculate the reciprocal of a complex number; ' that is, the 1/x calculation. Dim divisor As Double ' ----- Check for divide-by-zero possibility. divisor = a.Real * a.Real + a.Imaginary * a.Imaginary If (divisor = 0.0#) Then Throw New DivideByZeroException ' ----- Perform the operation. Return New ComplexNumber(a.Real / divisor, _ -a.Imaginary / divisor) End Function End Structure
The overloaded New()
function lets you instantiate a
ComplexNumber
number using either a
pair of numbers (the real and imaginary parts) or another ComplexNumber
number.
The following code demonstrates how complex numbers are created and how standard operators
allow mathematical operations such as addition and subtraction in a
natural way. The overloaded + operator also impacts the += assignment
operator. The last example in the code demonstrates this by adding
complex number b
to complex number
a
using the new assignment-operator
syntax:
Dim result As New System.Text.StringBuilder Dim a As ComplexNumber Dim b As ComplexNumber Dim c As ComplexNumber a = New ComplexNumber(3, 4) b = New ComplexNumber(5, -2) c = a + b result.AppendLine(" Complex Numbers") result.AppendLine("a = " & a.ToString()) result.AppendLine("b = " & b.ToString()) ' ----- Addition. c = a + b result.AppendLine("a + b = " & c.ToString()) ' ----- Subtraction. c = a - b result.AppendLine("a - b = " & c.ToString()) ' ----- Multiplication. c = a * b result.AppendLine("a * b = " & c.ToString()) ' ----- Division. c = a / b result.AppendLine("a / b = " & c.ToString()) ' ----- Addition as assignment. a += b result.AppendLine("a += b … a = " & a.ToString()) MsgBox(result.ToString())
The ToString()
function is
overridden in the ComplexNumber
structure to format the real and imaginary parts. Figure 6-16 shows the output
from the sample code.
You want to calculate all the remaining sides and angles of a right triangle given two known parts of the triangle.
Sample code folder: Chapter 06RightTriangle
Create a RightTriangle
class
that calculates all parts of a right triangle given any two of its
parts.
The parts of a right triangle we are concerned with are the two sides A and B adjacent to the right angle, the hypotenuse (the side opposite the right angle), and the two angles formed where the hypotenuse meets sides A and B. If you know any two of these values, all the rest can be determined.
There are many ways to set up the RightTriangle
class, and the technique
chosen here is not the only reasonable approach to the problem. We
chose to use the initializing function New()
to define the triangle by passing in
nonzero numbers for the known parts and a value of zero for the
unknowns. The IntelliSense pop-up prompt makes it easy to remember
what parts of the triangle are passed in at each parameter position.
It’s as easy as filling in the blanks. The code for the RightTriangle
class is as follows:
Public Class RightTriangle Private StoredSideA As Double Private StoredSideB As Double Private StoredHypotenuse As Double Private StoredAngleA As Double Private StoredAngleB As Double Public Sub New(ByVal hypotenuse As Double, _ ByVal sideA As Double, ByVal sideB As Double, _ ByVal angleA As Double, ByVal angleB As Double) Me.StoredHypotenuse = hypotenuse Me.StoredSideA = sideA Me.StoredSideB = sideB Me.StoredAngleA = angleA Me.StoredAngleB = angleB Me.Resolve() End Sub Public ReadOnly Property SideA() As Double Get Return StoredSideA End Get End Property Public ReadOnly Property SideB() As Double Get Return StoredSideB End Get End Property Public ReadOnly Property AngleA() As Double Get Return StoredAngleA End Get End Property Public ReadOnly Property AngleB() As Double Get Return StoredAngleB End Get End Property Public ReadOnly Property Hypotenuse() As Double Get Return StoredHypotenuse End Get End Property Private Sub Resolve() ' ----- Figure out the missing (zero) parts of the ' triangle. Start with the angles. If (StoredAngleA = 0.0#) And _ (StoredAngleB <> 0.0#) Then _ StoredAngleA = Math.PI / 2 - StoredAngleB If (StoredAngleB = 0.0#) And _ (StoredAngleA <> 0.0#) Then _ StoredAngleB = Math.PI / 2 - StoredAngleA If (StoredAngleA <> 0.0#) And _ (StoredHypotenuse <> 0.0#) Then _ StoredSideB = StoredHypotenuse * _ Math.Cos(StoredAngleA) If (StoredAngleB <> 0.0#) And _ (StoredHypotenuse <> 0.0#) Then _ StoredSideA = StoredHypotenuse * _ Math.Cos(StoredAngleB) If (StoredAngleA <> 0.0#) And _ (StoredSideA <> 0.0#) Then _ StoredHypotenuse = StoredSideA / _ Math.Sin(StoredAngleA) If (StoredAngleB <> 0.0#) And _ (StoredSideB <> 0.0#) Then _ StoredHypotenuse = StoredSideB / _ Math.Sin(StoredAngleB) If (StoredAngleA <> 0.0#) And _ (StoredSideB <> 0.0#) Then _ StoredHypotenuse = StoredSideB / _ Math.Cos(StoredAngleA) If (StoredAngleB <> 0.0#) And _ (StoredSideA <> 0.0#) Then _ StoredHypotenuse = StoredSideA / _ Math.Cos(StoredAngleB) ' ----- Now calculate the sides. If (StoredSideA <> 0.0#) And _ (StoredSideB <> 0.0#) Then _ StoredHypotenuse = Math.Sqrt(StoredSideA ^ 2 + _ StoredSideB ^ 2) If (StoredSideA <> 0.0#) And _ (StoredHypotenuse <> 0.0#) Then _ StoredSideB = Math.Sqrt(StoredHypotenuse ^ 2 - _ StoredSideA ^ 2) If (StoredSideB <> 0.0#) And _ (StoredHypotenuse <> 0.0#) Then _ StoredSideA = Math.Sqrt(StoredHypotenuse ^ 2 - _ StoredSideB ^ 2) If (StoredAngleA = 0.0#) Then StoredAngleA = _ Math.Asin(StoredSideA / StoredHypotenuse) If (StoredAngleB = 0.0#) Then StoredAngleB = _ Math.Asin(StoredSideB / StoredHypotenuse) End Sub Public Overrides Function Tostring() As String ' ----- Display all values of the triangle. Dim result As New System.Text.StringBuilder result.AppendLine(" Right Triangle:") result.AppendLine("Hypotenuse=" & _ StoredHypotenuse.ToString) result.AppendLine("Side A=" & StoredSideA.ToString) result.AppendLine("Side B=" & StoredSideB.ToString) result.AppendLine("Angle A=" & StoredAngleA.ToString) result.Append("Angle B=" & StoredAngleB.ToString) Return result.ToString() End Function End Class
The core calculations of this class are performed in the private
Resolve()
function. There, the
various triangle parts are tested to see if they are nonzero, and the
appropriate calculations are performed to start filling in the blanks
for the unknowns. Resolve()
is
called just once, at the moment when the RightTriangle
object is instantiated. All
the parts of the right triangle are later returned as required via
read-only properties.
Visual Basic internally always assumes angles to be in radians, even though degrees are the most commonly used units for angles among the general population. It’s tempting to use degrees in user-defined classes and procedures, but for consistency this book will assume radians throughout.
The following sample code creates an instance of the RightTriangle
object and uses it to
calculate a typical right triangle. In this example, the lengths of
sides A and B are known. All other parts of the triangle are passed as
zero when the RightTriangle
is
instantiated:
Dim testTriangle As RightTriangle Dim area As Double testTriangle = New RightTriangle(0, 3, 4, 0, 0) area = (testTriangle.SideA * testTriangle.SideB) / 2 MsgBox(testTriangle.Tostring & vbNewLine & _ "Area = " & area.ToString)
Figure 6-17 shows the results of calculating the missing parts of a right triangle with sides A and B of lengths 3 and 4.
Search for “right triangle” on the Web for more information about this subject (see, for example, http://mathworld.wolfram.com/RightTriangle.html).
You want to solve any triangle given any three known parts. Examples might include the lengths of any two sides and the measure of the angle between them, or the measures of two angles and the length of the side between them.
Sample code folder: Chapter 06AnyTriangle
Create a Triangle
class to
handle the details of calculating all the remaining parts of a
triangle given any combination of three of its parts. Also create a
separate utility function to calculate any triangle’s area given the
lengths of its three sides.
The Triangle
class, presented
below, allows the remaining elements of any triangle to be calculated
given the measures of any three of its sides and angles. The only
combination that won’t work, of course, is when three angles are
given, as these pin down the shape of a triangle but not its size.
Here is the code for the Triangle
class:
Imports System.Math Public Class Triangle Private StoredSideA As Double Private StoredSideB As Double Private StoredSideC As Double Private StoredAngleA As Double Private StoredAngleB As Double Private StoredAngleC As Double ' ----- The GivenParts variable indicates which parts ' the user has already supplied. Uppercase letters ' (A, B, C) indicate sides; lowercase letters ' (a, b, c) are angles. Private GivenParts As String = "" Public Overrides Function ToString() As String ' ----- Show the details of the triangle. Return String.Format( _ "SideA={0}, SideB={1}, SideC={2}, " & _ "AngleA={3}, AngleB={4}, AngleC={5}", _ StoredSideA, StoredSideB, StoredSideC, _ StoredAngleA, StoredAngleB, StoredAngleC) End Function Public Property SideA() As Double Get If (GivenParts.Length >= 3) Then _ Return StoredSideA Else NotYet() End Get Set(ByVal Value As Double) If (Value < 0) Then _ Throw New ArgumentOutOfRangeException( _ "Negative side length (A) not allowed.") CheckIt("A") StoredSideA = Value Resolve() End Set End Property Public Property SideB() As Double Get If (GivenParts.Length >= 3) Then _ Return StoredSideB Else NotYet() End Get Set(ByVal Value As Double) If (Value < 0) Then _ Throw New ArgumentOutOfRangeException( _ "Negative side length (B) not allowed.") CheckIt("B") StoredSideB = Value Resolve() End Set End Property Public Property SideC() As Double Get If (GivenParts.Length >= 3) Then _ Return StoredSideC Else NotYet() End Get Set(ByVal Value As Double) If (Value < 0) Then _ Throw New ArgumentOutOfRangeException( _ "Negative side length (C) not allowed.") CheckIt("C") StoredSideC = Value Resolve() End Set End Property Public Property AngleA() As Double Get If (GivenParts.Length >= 3) Then _ Return StoredAngleA Else NotYet() End Get Set(ByVal Value As Double) If (Value < 0) Or (Value > Math.PI) Then _ Throw New Exception( _ "Angle (A) must range from 0 to PI.") CheckIt("a") StoredAngleA = Value Resolve() End Set End Property Public Property AngleB() As Double Get If (GivenParts.Length >= 3) Then _ Return StoredAngleB Else NotYet() End Get Set(ByVal Value As Double) If (Value < 0) Or (Value > Math.PI) Then _ Throw New Exception( _ "Angle (B) must range from 0 to PI.") CheckIt("b") StoredAngleB = Value Resolve() End Set End Property Public Property AngleC() As Double Get If (GivenParts.Length >= 3) Then _ Return StoredAngleC Else NotYet() End Get Set(ByVal Value As Double) If (Value < 0) Or (Value > Math.PI) Then _ Throw New Exception( _ "Angle (C) must range from 0 to PI.") CheckIt("c") StoredAngleC = Value Resolve() End Set End Property Private Sub CheckIt(ByVal whatToCheck As String) ' ----- Make sure it is OK to adjust a component. If (GivenParts.Length >= 3) Then Throw New Exception( _ "Triangle is immutable once defined by three parts.") If (GivenParts.IndexOf(whatToCheck) >= 0) Then _ Throw New Exception( _ "Triangle component cannot be modified once set.") ' ---- Mark this part as modified. GivenParts &= whatToCheck End Sub Private Sub NotYet() ' ----- The user tried to access components before ' anything was calculated. Throw New Exception( _ "Triangle has not yet been completely defined.") End Sub Private Sub Resolve() ' ----- Calculate the missing angles and sides of ' the triangle. Dim sinRatio As Double Dim inSort() As Char ' ----- Wait for the triangle to be completely defined. If (GivenParts.Length < 3) Then Return ' ----- Sort the known parts list. inSort = GivenParts.ToCharArray() Array.Sort(inSort) GivenParts = New String(inSort) ' ----- Time to resolve. In all cases, the goal is to ' get three known sides. Then, the ResolveABC() ' method can work on getting the missing angles. Select Case GivenParts Case "ABC" ResolveABC() Case "ABa" sinRatio = Sin(StoredAngleA) / StoredSideA StoredAngleB = Asin(StoredSideB * sinRatio) StoredAngleC = PI - StoredAngleA - StoredAngleB StoredSideC = Sin(StoredAngleC) / sinRatio Case "ABb" sinRatio = Sin(StoredAngleB) / StoredSideB StoredAngleA = Asin(StoredSideA * sinRatio) StoredAngleC = PI - StoredAngleA - StoredAngleB StoredSideC = Sin(StoredAngleC) / sinRatio Case "ABc" StoredSideC = Sqrt(StoredSideA ^ 2 + _ StoredSideB ^ 2 - 2 * StoredSideA * _ StoredSideB * Cos(StoredAngleC)) Case "ACa" sinRatio = Sin(StoredAngleA) / StoredSideA StoredAngleC = Asin(StoredSideC * sinRatio) StoredAngleB = PI - StoredAngleA - StoredAngleC StoredSideB = Sin(StoredAngleB) / sinRatio Case "ACb" StoredSideB = Sqrt(StoredSideA ^ 2 + _ StoredSideC ^ 2 - 2 * StoredSideA * _ StoredSideC * Cos(StoredAngleB)) Case "ACc" sinRatio = Sin(StoredAngleC) / StoredSideC StoredAngleA = Asin(StoredSideA * sinRatio) StoredAngleB = PI - StoredAngleA - StoredAngleC StoredSideB = Sin(StoredAngleB) / sinRatio Case "Aab" sinRatio = Sin(StoredAngleA) / StoredSideA StoredSideB = Sin(StoredAngleB) / sinRatio StoredAngleC = PI - StoredAngleA - StoredAngleB StoredSideC = Sin(StoredAngleC) / sinRatio Case "Aac" sinRatio = Sin(StoredAngleA) / StoredSideA StoredSideC = Sin(StoredAngleC) / sinRatio StoredAngleB = PI - StoredAngleA - StoredAngleC StoredSideB = Sin(StoredAngleB) / sinRatio Case "Abc" StoredAngleA = PI - StoredAngleB - StoredAngleC sinRatio = Sin(StoredAngleA) / StoredSideA StoredSideB = Sin(StoredAngleB) / sinRatio StoredSideC = Sin(StoredAngleC) / sinRatio Case "BCa" StoredSideA = Sqrt(StoredSideB ^ 2 + _ StoredSideC ^ 2 - 2 * StoredSideB * _ StoredSideC * Cos(StoredAngleA)) Case "BCb" sinRatio = Sin(StoredAngleB) / StoredSideB StoredAngleC = Asin(StoredSideC * sinRatio) StoredAngleA = PI - StoredAngleB - StoredAngleC StoredSideA = Sin(StoredAngleA) / sinRatio Case "BCc" sinRatio = Sin(StoredAngleC) / StoredSideC StoredAngleB = Asin(StoredSideB * sinRatio) StoredAngleA = PI - StoredAngleB - StoredAngleC StoredSideA = Sin(StoredAngleA) / sinRatio Case "Bab" StoredAngleC = PI - StoredAngleA - StoredAngleB sinRatio = Sin(StoredAngleB) / StoredSideB StoredSideA = Sin(StoredAngleA) / sinRatio StoredSideC = Sin(StoredAngleC) / sinRatio Case "Bac" StoredAngleB = PI - StoredAngleA - StoredAngleC sinRatio = Sin(StoredAngleB) / StoredSideB StoredSideA = Sin(StoredAngleA) / sinRatio StoredSideC = Sin(StoredAngleC) / sinRatio Case "Bbc" StoredAngleA = PI - StoredAngleB - StoredAngleC sinRatio = Sin(StoredAngleB) / StoredSideB StoredSideA = Sin(StoredAngleA) / sinRatio StoredSideC = Sin(StoredAngleC) / sinRatio Case "Cab" StoredAngleC = PI - StoredAngleA - StoredAngleB sinRatio = Sin(StoredAngleC) / StoredSideC StoredSideA = Sin(StoredAngleA) / sinRatio StoredSideB = Sin(StoredAngleB) / sinRatio Case "Cac" StoredAngleB = PI - StoredAngleA - StoredAngleC sinRatio = Sin(StoredAngleC) / StoredSideC StoredSideA = Sin(StoredAngleA) / sinRatio StoredSideB = Sin(StoredAngleB) / sinRatio Case "Cbc" StoredAngleA = PI - StoredAngleB - StoredAngleC sinRatio = Sin(StoredAngleC) / StoredSideC StoredSideA = Sin(StoredAngleA) / sinRatio StoredSideB = Sin(StoredAngleB) / sinRatio Case "abc" Throw New Exception("Cannot resolve " & _ "triangle with only angles specified.") Case Else Throw New Exception( _ "Undefined combination of triangle parts.") End Select ResolveABC() End Sub Private Sub ResolveABC() ' ----- All three sides are known. Calculate the angles. LengthCheck(StoredSideA, StoredSideB, StoredSideC) StoredAngleC = Acos((StoredSideA ^ 2 + _ StoredSideB ^ 2 - StoredSideC ^ 2) / _ (2 * StoredSideA * StoredSideB)) StoredAngleB = Acos((StoredSideA ^ 2 + _ StoredSideC ^ 2 - StoredSideB ^ 2) / _ (2 * StoredSideA * StoredSideC)) StoredAngleA = PI - StoredAngleB - StoredAngleC End Sub Private Sub LengthCheck(ByVal A As Double, _ ByVal B As Double, ByVal C As Double) ' ----- Make sure that one of the sides isn't ' too long for the other two. If (A >= B) AndAlso (A >= C) AndAlso _ (A <= (B + C)) Then Return If (B >= A) AndAlso (B >= C) AndAlso _ (B <= (A + C)) Then Return If (C >= A) AndAlso (C >= B) AndAlso _ (C <= (A + B)) Then Return Throw New Exception( _ "One side is too long for the others.") End Sub End Class
Exceptions are thrown if the triangle “doesn’t make sense.” For example, if the sum of two sides is less than the length of the third, or if three angles are given, the triangle is impossible, or at least the data is insufficient to completely define the triangle.
To find the area of any triangle, you could include a shared
function within the Triangle
class,
but for the sake of demonstration (and because it can be useful in a
wider variety of computational situations) we’ve chosen to create a
TriangleArea()
function separate
from the class. This makes it easy to find the area of any triangle
given the lengths of its three sides, whether or not you’re solving
triangles using the Triangle
class:
Public Function TriangleArea(ByVal sideA As Double, _ ByVal sideB As Double, _ ByVal sideC As Double) As Double ' ----- Calculate the area of a triangle. Dim sumHalfSides As Double Dim deltaA As Double Dim deltaB As Double Dim deltaC As Double sumHalfSides = (sideA + sideB + sideC) / 2 deltaA = sumHalfSides - sideA deltaB = sumHalfSides - sideB deltaC = sumHalfSides - sideC Return Math.Sqrt(sumHalfSides * deltaA * deltaB * deltaC) End Function
The following code demonstrates the use of the Triangle
class by solving for a triangle
that has two sides of length 4 and 5, with a 75°; angle between the
two sides. The RadPerDeg
constant
(see Recipe 6.10)
converts 75°to radians at compile time rather than at runtime (to be
consistent with all other angular measurements in Visual Basic 2005,
radians are always assumed in all the procedures in this book that
involve angles):
Const RadPerDeg As Double = Math.PI / 180 Dim testTriangle As New Triangle Dim area As Double ' ----- Build a triangle with sides of 4 and 5, and an ' angle between them of 75 degrees. testTriangle.SideA = 4 testTriangle.SideB = 5 testTriangle.AngleC = 75 * RadPerDeg ' ----- The triangle is already resolved. Calculate area. area = TriangleArea(testTriangle.SideA, _ testTriangle.SideB, testTriangle.SideC) MsgBox(testTriangle.ToString & vbNewLine & _ "Area = " & area.ToString)
A ToString()
function is
included in the Triangle
class to
provide a default format for presenting the triangle’s parts in a
single string. The solved triangle for our example is shown in Figure 6-18.
You want to verify that a user-entered string contains a valid number.
Visual Basic 2005 provides a function named IsNumeric()
that checks the content
of any string, returning a Boolean True
if the string contains a valid number
representation and False
if it
doesn’t:
Dim result As New System.Text.StringBuilder Dim testString As String testString = "2.E3" result.Append(testString).Append(vbTab) result.AppendLine(IsNumeric(testString).ToString) testString = "2.D3" result.Append(testString).Append(vbTab) result.AppendLine(IsNumeric(testString).ToString) testString = "-123" result.Append(testString).Append(vbTab) result.AppendLine(IsNumeric(testString).ToString) testString = "-1 2 3" result.Append(testString).Append(vbTab) result.AppendLine(IsNumeric(testString).ToString) testString = "$54.32" result.Append(testString).Append(vbTab) result.AppendLine(IsNumeric(testString).ToString) MsgBox(result.ToString())
Currency values are valid numbers, even with the currency symbol
included. The IsNumeric()
function
expects a single number in the string, so extra spaces, such as those
shown in the next-to-last string in the example, cause IsNumeric()
to return False. If you want to
determine how many valid numbers are in a string, and be able to grab
them all, consider using regular expressions instead.
Figure 6-19 shows
the strings used in this example and the results returned by IsNumeric()
for each.
You want to convert numbers to integers, or perhaps truncate or round values to integer values, and you want to understand the various ways to do this.
As always, use the best tool for the job. If you want to remove
decimal parts of a number, consider using Int(), Floor()
, or the
Round()
function. But if you want to convert
a numeric value to an Integer
data
type, use CInt()
or
Convert.ToInteger()
instead.
The following code demonstrates differences between the CInt()
and Int()
functions. Once you gain a good
understanding of these two functions, you’ll be well on your way to
understanding similar functions such as Round(), Convert.ToInteger()
, and so
on.
One important difference between CInt()
and Int()
is that Int()
is overloaded to work with a wide
variety of numeric data types. For example, you can pass a Double
, such as the value of π, to Int()
, and it will return another Double
value that no longer has any
post-decimal digits (i.e., it will round to a whole number). This is
entirely different from converting a number to an Integer
. The Int()
function works on numbers that are way
out of the legal range for an Integer
. Using CInt()
on similar numbers would throw an
exception.
The two functions are demonstrated in the following code:
Dim result As New System.Text.StringBuilder Dim number As Double ' ----- Positive decimal value. number = 3.14 result.Append(number) result.Append(" CInt(): ") result.Append(CInt(number).ToString) result.Append(" Int(): ") result.AppendLine(Int(number).ToString) ' ----- Negative decimal value. number = -3.14 result.Append(number) result.Append(" CInt(): ") result.Append(CInt(number).ToString) result.Append(" Int(): ") result.AppendLine(Int(number).ToString) ' ----- Number that won't fit in an Integer. number = 3000000000.0 result.Append(number) result.Append(" CInt(): ") Try result.Append(CInt(number).ToString) Catch result.Append("(error)") End Try result.Append(" Int(): ") result.Append(Int(number).ToString) MsgBox(result.ToString())
There are some other functions in the Math
object that provide similar
functionality to Int()
. For
example, the Math.Floor()
and
Math.Ceiling()
functions also
operate on numbers that might be out of the range of Integers. Floor()
returns the largest whole
number less than or equal to a given number, and Ceiling()
returns the smallest whole number
that’s greater than or equal to a given number. See Figure 6-20.
You want to impress people by showing how quickly Visual Basic 2005 can calculate π to a thousand or more decimal places. While you’re at it, you might want to discover how to create multidigit mathematical functions using integer arrays of digits.
Sample code folder: Chapter 06 CalculatePi
Create functions for basic mathematical operations (+, -, *, /) that operate on integer arrays of any reasonable size. Then demonstrate these functions by calculating π to many digits using one of the standard π-calculation algorithms.
This recipe includes a module called PiCalculator
that contains the functions
needed to perform multidigit math, along with one to calculate π to
any number of digits. The four main multidigit functions are named
ArrayMult(), ArrayDivide(),
ArrayAdd()
, and ArraySub()
. These are declared as Private
to the module because they serve
only as support routines to the FindPi()
function, but you can change them
to Public
to experiment with them
for other purposes. Other supporting functions include ArrayZero()
, which sets all “digits” in an
array to zeros, and ArcTangent()
,
which calls the other functions to calculate the arctangent of a
multi-digit number.
The way the basic math functions work is similar to the way math is performed on paper by grade-schoolers: when two digits are added, any overflow is added into the next pair of digits, and so on. Calculating π to 500 decimal places requires a huge number of these small repetitive calculations, but that’s what computers are really good at doing.
Here is the code to calculate π. It is based on the following calculation for π:
π/4 = (arctan 1/2)+ (arctan 1/3) |
Each part of the algorithm is performed manually, including the arctangent calculation:
Module PiCalculator Private NumberDigits As Integer Public Function FindPi(ByVal digits As Integer) As String ' ----- Calculate Pi to the specified number of digits, ' based on the formula: ' Pi/4 = arctan(1/2) + arctan(1/3) Dim result As New System.Text.StringBuilder("PI=3.") Dim digitIndex As Integer Dim divFactor As Integer ' ----- Build an array that will hold manual calculations. NumberDigits = digits + 2 Dim targetValue(NumberDigits) As Integer Dim sourceValue(NumberDigits) As Integer ' ---- Perform the calculation. divFactor = 2 ArcTangent(targetValue, sourceValue, divFactor) divFactor = 3 ArcTangent(targetValue, sourceValue, divFactor) ArrayMult(targetValue, 4) ' ----- Return a string version of the calculation. For digitIndex = 1 To NumberDigits - 3 result.Append(Chr(targetValue(digitIndex) + Asc("0"c))) Next digitIndex Return result.ToString End Function Private Sub ArrayMult(ByRef baseNumber() As Integer, _ ByRef multiplier As Integer) ' ----- Multiply an array number by another number by hand. ' The product remains in the array number. Dim carry As Integer Dim position As Integer Dim holdDigit As Integer ' ----- Multiple each base digit, from right to left. For position = NumberDigits To 0 Step -1 ' ----- If the multiplication went past 9, carry the ' tens value to the next column. holdDigit = (baseNumber(position) * multiplier) + carry carry = holdDigit 10 baseNumber(position) = holdDigit Mod 10 Next position End Sub Private Sub ArrayDivide(ByRef dividend() As Integer, ByRef divisor As Integer) ' ----- Divide an array number by another number by hand. ' The quotient remains in the array number. Dim borrow As Integer Dim position As Integer Dim holdDigit As Integer ' ----- Process division for each digit. For position = 0 To NumberDigits ' ----- If the division can't happen directly, borrow from ' the previous position. holdDigit = dividend(position) + borrow * 10 dividend(position) = holdDigit divisor borrow = holdDigit Mod divisor Next position End Sub Private Sub ArrayAdd(ByRef baseNumber() As Integer, ByRef addend() As Integer) ' ----- Add two array numbers together. ' The sum remains in the first array number. Dim carry As Integer Dim position As Integer Dim holdDigit As Integer ' ----- Add each digit from right to left. For position = NumberDigits To 0 Step -1 ' ----- If the sum goes beyond 9, carry the tens ' value to the next column. holdDigit = baseNumber(position) + addend(position) + carry carry = holdDigit 10 baseNumber(position) = holdDigit Mod 10 Next position End Sub Private Sub ArraySub(ByRef minuend() As Integer, ByRef subtrahend() As Integer) ' ----- Subtract one array number from another. ' The difference remains in the first array number. Dim borrow As Integer Dim position As Integer Dim holdDigit As Integer ' ---- Subtract the digits from right to left. For position = NumberDigits To 0 Step -1 ' ----- If the subtraction would give a negative value ' for a column, we will have to borrow. holdDigit = minuend(position) - subtrahend(position) + 10 borrow = holdDigit 10 minuend(position) = holdDigit Mod 10 If (borrow = 0) Then minuend(position - 1) -= 1 Next position End Sub Private Function ArrayZero(ByRef baseNumber() As Integer) As Boolean ' ----- Report whether an array number is all zero. Dim position As Integer ' ----- Examine each digit. For position = 0 To NumberDigits If (baseNumber(position) <> 0) Then ' ----- The number is nonzero. Return False End If Next position ' ----- The number is zero. Return True End Function Private Sub ArcTangent(ByRef targetValue() As Integer, _ ByRef sourceValue() As Integer, _ ByVal divFactor As Integer) ' ----- Calculate an arctangent of a fraction, ' 1/divFactor. This routine performs a modified ' Maclaurin series to calculate the arctangent. ' The base formula is: ' arctan(x) = x - x^3/3 + x^5/5 -' x^7/7 + x^9/9 - … ' where -1 < x < 1 (1/divFactor in this case). Dim workingFactor As Integer Dim incremental As Integer ' ----- Figure out the "x" part, 1/divFactor. sourceValue(0) = 1 incremental = 1 workingFactor = divFactor ArrayDivide(sourceValue, workingFactor) ' ----- Add "x" to the total. ArrayAdd(targetValue, sourceValue) Do ' ----- Perform the "- (x^y)/y" part. ArrayMult(sourceValue, incremental) workingFactor = divFactor * divFactor ArrayDivide(sourceValue, workingFactor) incremental += 2 workingFactor = incremental ArrayDivide(sourceValue, workingFactor) ArraySub(targetValue, sourceValue) ' ----- Perform the "+ (x^y)/y" part. ArrayMult(sourceValue, incremental) workingFactor = divFactor * divFactor ArrayDivide(sourceValue, workingFactor) incremental += 2 workingFactor = incremental ArrayDivide(sourceValue, workingFactor) ArrayAdd(targetValue, sourceValue) Loop Until ArrayZero(sourceValue) End Sub End Module
To exercise these procedures, the following statement uses the
FindPi()
function to calculate π to 500 digits:
MsgBox(FindPi(500))
You can change the 500
argument to obtain a different number of digits. However, even though
the time required to calculate π to 500 or even 1,000 digits is fairly
negligible, every time you double the count, the FindPi()
function requires around four times
as long to return the results. Try smaller counts first, moving up to
larger counts when you have a good feel for just how long the
calculation will take on your computer.
Figure 6-21 shows
the first 500 digits of π as formatted by the
FindPi()
function. If you prefer to format
the digits differently, say in groups of 10 digits or with occasional
end-of-line characters, you might want to change FindPi()
to return an array of digits. The
calling code can then format the digits as desired. The “digits” in
the array have values in the range 0 to 9, and they need to be
converted to ASCII digits by adding the ASCII equivalent of “0” (zero)
to their value before applying the Chr()
conversion function.
There are many places on the Web to see many digits of π and to learn of the different algorithms used for calculating π to even millions of decimal places. See, for example, http://www.exploratorium.edu/pi/Pi10-6.html.
You need to determine all prime factors of a given number, perhaps for demonstrating cryptographic algorithms or for some other purpose.
Sample code folder: Chapter 06PrimeFactor
Create a function called PrimeFactors()
that analyzes any Long
integer and returns a string listing
all the number’s prime factors in a clear format.
The algorithm used here is fairly straightforward, suitable for
reasonably sized Long
integers. The
prime factors are found by checking for even divisibility by numbers
from 2 to the square root of the number being checked. Whenever a
factor is found, it is extracted, and the divisibility check is
repeated. Tallies for the factors are converted to string format
during this process, and the string is returned when all the checks
are completed:
Private Function PrimeFactors( _ ByVal numberToFactor As Long) As String ' ----- Calculate the prime factors of a starting number. Dim result As New System.Text.StringBuilder Dim testFactor As Long Dim workNumber As Long Dim factorCount As Long ' ----- Scan through all numbers up to ' Sqrt(numberToFactor). workNumber = numberToFactor testFactor = 1 Do While (testFactor < Math.Sqrt(CType(workNumber, _ Double))) testFactor += 1 factorCount = 0 Do While (workNumber / testFactor) = _ (workNumber testFactor) ' ----- Found a factor. factorCount += 1 workNumber = testFactor Loop Select Case factorCount Case 1 ' ----- Show a prime factor. result.AppendLine(testFactor) Case Is > 1 ' ----- Show a prime factor as a power. result.Append(testFactor) result.Append("^") result.AppendLine(factorCount) End Select Loop ' ----- Include the final prime factor, if available. If (workNumber > 1) Then result.Append(workNumber) Return result.ToString End Function
Here’s the code that drives the example, which finds and displays the prime factors for the number 7999848:
Dim result As New System.Text.StringBuilder Dim number As Long = 7999848 result.AppendLine("PrimeFactors(" & number & ")… ") result.AppendLine() result.Append(PrimeFactors(number)) MsgBox(result.ToString())
Figure 6-22 shows the results of calculating that number’s prime factors.
There are many good resources on the Web for learning about prime numbers and prime factors. See, for example, http://primes.utm.edu/largest.html.
You want to study a sample of Visual Basic’s ability to define recursive functions, or you need a factorial function for smaller integers.
Sample code folder: Chapter 06Factorial
Create a Factorial()
function
that recursively calls itself.
The code in this recipe does not represent the most efficient
way to calculate factorials for larger integers. You’ll want to use a
standard For…Next
loop or similar
process when working with larger numbers, simply because each
recursive function call uses up stack space and adds a little
overhead. However, recursive functions can be quite useful in some
programming situations. A simple recursive function that calculates
the factorial of a number is a great way to understand
recursion.
The factorial of a number N is the product of all numbers from 1
to N. For example, the factorial of 3 is calculated as 3 x 2 x 1,
which results in a value of 6. The Factorial()
function returns the value 1 if
it is passed a value of zero; otherwise, it returns the passed value
times the factorial of the next smaller integer. Study the Select Case
lines of code in the function to
see how this is accomplished:
Public Function Factorial(ByVal number As Decimal) As Decimal Select Case number Case Is < 0 Throw New Exception("Factorial: Bad argument") Case Is = 0 Return 1 Case Else Return number * Factorial(number - 1) End Select End Function
Calling the Factorial()
function from inside its own code is what recursion is all about. All
pending returns are literally stacked up until the value of the passed
number finally reaches zero, at which time the pending multiplications
all happen in a hurry. As a result of the way this recursion works, if
you request the Factorial()
of a
large number, you run the risk of running out of stack memory or of
numeric over-flow. With Decimal
variables, as shown in the previous code, the largest value you can
pass to the function without overflow is just 27. Of course, the
factorial of 27 is a huge number, and the answer is exact when using
Decimal
values. You might consider
switching the algorithm to use Double
values to find approximations of even
larger factorials.
The following lines demonstrate the Factorial()
function by calculating and
displaying the factorial of 7:
Dim result As New System.Text.StringBuilder Dim number As Decimal = 7 result.AppendLine("Factorial(" & number & ")… ") result.AppendLine() result.Append(Factorial(number)) MsgBox(result.ToString())
Figure 6-23 shows the results of calculating the factorial of 7.
Search for “Factorial” on the Web to learn more about factorials (see, for example, http://mathworld.wolfram.com/Factorial.html).
You need to shift, mask, and perform other bitwise manipulations on integers.
Visual Basic 2005 has functions for all the major bit-manipulation techniques, and it’s easy to combine these to perform more complicated bitwise calculations as required.
There are several operators that are most often thought of as
Boolean operators, working with and returning True
and False
(Boolean) values. However, these
operators also accept and return integer values of various sizes, and
this is where they can be of value for bit manipulations. These
bitwise operators include the following:
And
Bits are combined to 1 only if they are both 1.
Not
Bits are inverted, 0 to 1 and 1 to 0.
Xor
Bits are combined to 1 only if the two bits are not the same.
Or
Bits are combined to 1 if either bit is a 1.
<<
Bits are all shifted left a given number of bit positions.
>>
Bits are all shifted right a given number of bit positions.
The two bit-shift operators can be used as assignment operators. That is, the following two lines of code provide identical results:
a = a << 3 a <<= 3
In both cases the bits in integer variable a are shifted to
the left three positions. The And, Or,
Not
, and Xor
operators
don’t support assignment notation.
The following code demonstrates a sampling of these bit manipulations. You can change the program to experiment with the various operators:
Dim result As New System.Text.StringBuilder Dim number As Integer = 7 result.Append(number) result.Append(" <<= 3 … ") number <<= 3 result.AppendLine(number) result.Append(number) result.Append(" Xor 17 … ") number = number Xor 17 result.AppendLine(number) MsgBox(result.ToString())
Figure 6-24 shows the output displayed by this sample code.
You want to store and retrieve a lot of bits without wasting memory and without sacrificing speed of operation.
Sample code folder: Chapter 06GetPrimes
Use a BitArray
to store and
access individual bits in memory efficiently.
The BitArray
object lets you
access bits by indexed position, and all the details of decoding which
bit position of which byte the bit is stored in are taken care of
transparently behind the scenes. A BitArray
of 80 bits is actually stored in 10
bytes of memory.
To demonstrate using a BitArray
, we’ve created a module named
Eratosthenes.vb that contains code to find all
prime numbers between 2 and 8,000,000 very quickly. The 8 million bits
are stored in 1 million bytes of memory, and the individual bits are
accessed using indexes in the range 0 to 8,000,000.
The Sieve of Eratosthenes (http://en.wikipedia.org/wiki/Sieve_of_Eratosthenes)
works by first setting all bits to 1, or True
.
The BitArray
can be instantiated
with a count and an optional second Boolean parameter that presets all
bits to True
or False
. In this case, True
sets them all to 1. Starting with 2,
each prime number, or bit that is set, clears all bits that are exact
multiples of that number. So, for instance, bit 2 is kept at True
, but bits 4, 6, 8, and so on, are all
set to False
. This marks all even
numbers except for 2 as nonprime. Similarly, bit 3 is left True
, and bits 6, 9, 12, 15, etc., are set
to False
to mark all multiples of 3
as nonprime. This looping technique very quickly sets all bits in the
BitArray
that appear in prime
number positions to True and all other bits to False
.
The Eratosthenes module contains the BitArray
itself, a Sieve()
method to set all the prime number
bits as described earlier, and a GetBit()
function to retrieve the bit at any
location, converting the bit’s True
or False
Boolean value to a 1 or 0
integer value:
Module Eratosthenes Private Const MaxNumber As Integer = 8000000 Private PrimeStorage As New BitArray(MaxNumber, True) Public Sub Sieve() ' ----- Get all the prime numbers from 1 to MaxNumber. Dim index As Integer = 1 Dim counter As Integer ' ----- Scan through all primes. Do While (index < (MaxNumber - 1)) index += 1 If (PrimeStorage(index) = True) Then ' ----- Found a prime. Set all of its multiples ' to non-prime. For counter = index * 2 To MaxNumber - 1 _ Step index PrimeStorage(counter) = False Next counter End If Loop End Sub Public Function GetBit(ByVal index As Integer) As Integer ' ----- Retrieve the status of a single prime bit. If (PrimeStorage(index) = True) Then _ Return 1 Else Return 0 End Function End Module
The following block of code demonstrates the BitArray
in action, displaying the prime
numbers up to the size of the BitArray
. To prevent information overload,
only the first and last few numbers in the desired range are formatted
into a string for display, as there are a lot of prime numbers between
0 and 8,000,000:
Dim result As New System.Text.StringBuilder Dim counter As Integer Dim needBreak As Boolean = True result.AppendLine( _ "Prime numbers using the ""Sieve of Eratosthenes""") ' ----- Generate the primes. Sieve() ' ----- Report each prime. For counter = 2 To 7999999 If (GetBit(counter) = 1) Then If (counter < 50) Or (counter > 7999800) Then ' ----- Only show a limited number of primes. result.AppendLine(counter) ElseIf (needBreak = True) Then ' ----- Show that we are leaving something out. result.AppendLine("…") needBreak = False End If End If Next counter MsgBox(result.ToString())
Figure 6-25 shows
the partial list of all the prime numbers as determined by the
bits in the BitArray
. On your system there could be less
than a second’s delay during the computation and display of these
prime numbers!
Search for “prime numbers” on the Web for more information. See also “Logical and Bitwise Operators in Visual Basic” in the Visual Studio online help.
Sample code folder: Chapter 06RepeatRandom
You can use the RNGCryptoServiceProvider
class to generate
cryptographically strong random numbers, or you can use the technique
presented here to greatly extend the cycle length of the standard
pseudorandom number generator and make it easier to use.
The BetterRandom
class
presented here uses the standard Rnd()
function and the Randomize()
initialization method, but it
enhances them in several ways. Contrary to what some people claim, it
is possible to initialize the random number generator to a unique but
repeatable sequence, but the technique is far from obvious. You have
to call the Randomize()
method
immediately after calling the Rnd()
function, but only after passing Rnd()
a negative numerical value. So, one
advantage of this BetterRandom
class is the encapsulation of this technique into something that makes
a lot more sense. If you instantiate a BetterRandom
object by passing any string to
it, each unique string initializes the generator to a unique but
repeatable state. If you instantiate a BetterRandom
object with no string, the
system clock generates a unique sequence for every system tick, which
means it is always unique.
The cycle length of the generator is greatly enhanced by
maintaining a table of pseudorandom Double
numbers in the normalized range 0 to
1. Rolling indexes are used to add table entries together along with
the next value returned by Rnd()
,
and the result is brought back into the range 0 to 1 using the
Mod
operator. The GetNextDouble()
function forms the core of
this algorithm, as shown here:
Public Function GetNextDouble() As Double ' ----- Return the next pseudorandom number as a Double. ' ----- Move to the next index positions. Index1 = (Index1 + 1) Mod TableSize Index2 = (Index2 + 1) Mod TableSize ' ----- Update the random numbers at those positions. RandomTable(Index1) += RandomTable(Index2) + Rnd() RandomTable(Index1) = RandomTable(Index1) Mod 1.0 ' ----- Return the newest random table value. Return RandomTable(Index1) End Function
This table keeps the pseudorandom values well mixed while
providing a nice flat distribution of the values with excellent
statistical results. When the Rnd()
function cycles back around to its starting point, the table will be
in a completely different state, which means the cycle length of the
values returned from this table will be some off-the-chart
astronomical value. It simply won’t repeat in the amount of time there
is in this universe to exercise the algorithm.
The table size is set to 32, but feel free to make the table larger or smaller as desired. A larger table will be slightly slower to initialize, but subsequent pseudorandom numbers will be calculated and returned just as fast.
Another advantage of this class is that it can be used to return
several types of pseudorandom numbers. The GetNextDouble()
function, which is
demonstrated in this recipe, returns a double-precision value between
0 and 1. The next few recipes in this chapter will demonstrate how the
BetterRandom
class can be used to
return several other types of pseudorandom numbers. The code for the
class is presented here in its entirety for easy review:
Public Class BetterRandom Private Const TableSize As Integer = 32 Private RandomTable(TableSize - 1) As Double Private Index1 As Integer Private Index2 As Integer Public Sub New() ' ----- Generate truly pseudorandom numbers. InitRandom(Now.Ticks.ToString) End Sub Public Sub New(ByVal Key As String) ' ----- Generate a repeatable random sequence. InitRandom(Key) End Sub Private Sub InitRandom(ByVal repeatKey As String) ' ----- Prepare the random number generator. Dim stringIndex As Integer Dim workNumber As Double Dim counter As Integer ' ----- All sequences start with the same base sequence. Randomize(Rnd(-1)) ' ----- Initialize the table using the key string. For counter = 0 To TableSize - 1 stringIndex = counter Mod repeatKey.Length workNumber = Math.PI / _ Asc(repeatKey.Substring(stringIndex, 1)) RandomTable(counter) = (Rnd() + workNumber) Mod 1.0 Next counter ' ----- Set the starting state for the table. Index1 = TableSize 2 Index2 = TableSize 3 ' ----- Cycle through a bunch of values to get a good ' starting mix. For counter = 0 To TableSize * 5 GetNextDouble() Next counter ' ----- Reset the random sequence based on our ' preparations. Randomize(Rnd(-GetNextSingle())) End Sub Public Function GetNextDouble() As Double ' ----- Return the next pseudorandom number as ' a Double. ' ----- Move to the next index positions. Index1 = (Index1 + 1) Mod TableSize Index2 = (Index2 + 1) Mod TableSize ' ----- Update the random numbers at those positions. RandomTable(Index1) += RandomTable(Index2) + Rnd() RandomTable(Index1) = RandomTable(Index1) Mod 1.0 ' ----- Return the newest random table value. Return RandomTable(Index1) End Function Public Function GetNextSingle() As Single ' ----- Return the next pseudorandom number as ' a Single. Return CSng(GetNextDouble()) End Function Public Function GetNextInteger(ByVal minInt As Integer, _ ByVal maxInt As Integer) As Integer ' ----- Return the next pseudorandom number within an ' Integer range. Return CInt(Int(GetNextDouble() * _ (maxInt - minInt + 1.0) + minInt)) End Function Public Function GetNextReal(ByVal minReal As Double, _ ByVal maxReal As Double) As Double ' ----- Return the next pseudorandom number within a ' floating-point range. Return GetNextDouble() * (maxReal - minReal) + minReal End Function Public Function GetNextNormal(ByVal mean As Double, _ ByVal stdDev As Double) As Double ' ----- Return the next pseudorandom number adjusted ' to a normal distribution curve. Dim x As Double Dim y As Double Dim factor As Double Dim radiusSquared As Double Do x = GetNextReal(-1, 1) y = GetNextReal(-1, 1) radiusSquared = x * x + y * y Loop Until radiusSquared <= 1.0 factor = Math.Sqrt(-2.0 * Math.Log(radiusSquared) / _ radiusSquared) Return x * factor * stdDev + mean End Function Public Function GetNextExp(ByVal mean As Double) As Double ' ----- Return the next pseudorandom number adjusted ' for exponential distribution. Return -Math.Log(GetNextDouble) * mean End Function End Class
The following code demonstrates the BetterRandom
class by generating two short
sequences of pseudorandom Double
numbers in the range 0 to 1. The first sequence is generated uniquely
each time by not passing a string during initialization of the
BetterRandom
object. The second
sequence uses the same string each time for initialization, and
therefore the sequence is always repeated:
Dim result As New System.Text.StringBuilder Dim generator As BetterRandom result.AppendLine("Never the same sequence:") generator = New BetterRandom result.AppendLine(generator.GetNextDouble.ToString) result.AppendLine(generator.GetNextDouble.ToString) result.AppendLine(generator.GetNextDouble.ToString) result.AppendLine() result.AppendLine("Always the same sequence:") generator = New BetterRandom( _ "Every string creates a unique, repeatable sequence") result.AppendLine(generator.GetNextDouble.ToString) result.AppendLine(generator.GetNextDouble.ToString) result.AppendLine(generator.GetNextDouble.ToString) MsgBox(result.ToString())
Figure 6-26 shows the never-and always-repeating sequences generated by this demonstration code.
You need to generate a sequence of pseudorandom integers with a flat distribution over a given range.
Sample code folder: Chapter 06RepeatRandom
The BetterRandom
class (see
Recipe 6.26) sports a
GetNextInteger()
function. Two
parameters define the range limits for the returned pseudorandom
integer, as shown here:
newRnd.GetNextInteger(minInt, maxInt)
The returned integer has a statistically flat distribution across the given range.
The following code creates a new instance of the BetterRandom
object, which it then uses to
generate 200 pseudorandom integers in the range −10 to +10. The
results are collected and then displayed for review. As a programming
exercise, you might consider changing this code to display the average
and perhaps the standard deviation for these returned values.
The generator
object is created without passing a
string to initialize the generator, so a unique sequence is created
every time this program is run:
Dim result As New System.Text.StringBuilder Dim generator As New BetterRandom Dim minInt As Integer = -10 Dim maxInt As Integer = 10 Dim counter As Integer result.Append("Random integers in range ") result.AppendLine(minInt & " to " & maxInt) For counter = 1 To 200 ' ----- Add one random number. result.Append(generator.GetNextInteger(-10, 10)) If ((counter Mod 40) = 0) Then ' ----- Group on distinct lines periodically. result.AppendLine() Else result.Append(",") End If Next counter MsgBox(result.ToString())
Figure 6-27 shows the results of generating the 200 pseudorandom integers.
Recipe 6.26
shows the full code for the BetterRandom
class.
Search Visual Studio Help for “Random Class” and “RNGCryptoServiceProvider Class” for information about other ways to generate pseudorandom numbers in Visual Basic.
You need to generate a sequence of pseudorandom real numbers with a flat distribution over a given range.
Sample code folder: Chapter 06RepeatRandom
The BetterRandom
class (see
Recipe 6.26) sports a
GetNextReal()
function. Two
parameters define the range limits for the returned pseudorandom real
values, and the returned value has a statistically flat distribution
across the given range:
GetNextReal(minReal, maxReal)
The following code creates a new instance of the BetterRandom
object, which it then uses to
generate 20 pseudorandom double-precision real numbers in the range
−10.0 to +10.0. The results are collected and then displayed for
review. As a programming exercise, you might consider changing this
code to display the average and perhaps the standard deviation for
these returned values.
The generator
object is
created without passing a string to initialize the generator, so a
unique sequence will be created every time this program is run:
Dim result As New System.Text.StringBuilder Dim generator As New BetterRandom Dim minReal As Integer = -10 Dim maxReal As Integer = 10 Dim counter As Integer result.Append("Random reals in range ") result.AppendLine(minReal & " to " & maxReal) result.AppendLine() For counter = 1 To 20 ' ----- Add one random number. result.Append(generator.GetNextReal(minReal, maxReal)) If ((counter Mod 5) = 0) Then ' ----- Group on distinct lines periodically. result.AppendLine() Else result.Append(", ") End If Next counter MsgBox(result.ToString())
Figure 6-28 shows the results of generating the 20 pseudorandom double-precision real values.
Recipe 6.26
shows the full code for the BetterRandom
class.
There are many good references on the Web to learn more about random number generation (see, for example, http://random.mat.sbg.ac.at).
You need to generate a sequence of pseudorandom numbers with a normal distribution, given the distribution’s mean and standard deviation.
Sample code folder: Chapter 06RepeatRandom
The BetterRandom
class (see
Recipe 6.26) sports a
GetNextNormal()
function. Two
parameters passed to this function define the mean and standard
deviation for the distribution of the generated values:
GetNextNormal(mean, stdDev)
The following code creates a new instance of the BetterRandom
object, which it then uses to
generate 20 pseudorandom double-precision numbers with the desired
normal distribution. As a programming exercise you might consider
changing this code to display the mean and standard deviation for the
returned values, to compare the results with the goal.
The generator
object is
created without passing a string to initialize the generator, so a
unique sequence will be created every time this program is run:
Dim result As New System.Text.StringBuilder Dim generator As New BetterRandom Dim mean As Double = 100 Dim stdDev As Double = 10 Dim counter As Integer result.Append("Normal distribution randoms with mean ") result.AppendLine(mean & " and standard deviation " & stdDev) result.AppendLine() For counter = 1 To 20 ' ----- Add one random number. result.Append(generator.GetNextNormal(mean, stdDev)) If ((counter Mod 3) = 0) Then ' ----- Group on distinct lines periodically. result.AppendLine() Else result.Append(", ") End If Next counter MsgBox(result.ToString())
Figure 6-29 shows the results of generating the 20 pseudorandom double-precision normal-distribution numbers.
Recipe 6.26
shows the full code for the BetterRandom
class.
There are many good references on the Web to learn more about random number generation (see, for example, http://random.mat.sbg.ac.at).
You need to generate a sequence of pseudorandom numbers with an exponential distribution given the distribution’s mean.
Sample code folder: Chapter 06RepeatRandom
The BetterRandom
class (see
Recipe 6.26) sports a
GetNextExp()
function. One
parameter passed to this function defines the mean of the
exponentially distributed return values:
GetNextExp(mean)
The following code creates a new instance of the BetterRandom
object, which it then uses to
generate 20 pseudorandom double-precision numbers with the desired
exponential distribution. As a programming exercise you might consider
changing this code to display the mean of the returned values, to
compare the results with the goal.
The generator
object is
created without passing a string to initialize the generator, so a
unique sequence is created every time this program is run:
Dim result As New System.Text.StringBuilder Dim generator As New BetterRandom Dim mean As Double = 10 Dim counter As Integer result.Append("Exponential distribution randoms with mean ") result.AppendLine(mean) result.AppendLine() For counter = 1 To 20 ' ----- Add one random number. result.Append(generator.GetNextExp(mean)) If ((counter Mod 3) = 0) Then ' ----- Group on distinct lines periodically. result.AppendLine() Else result.Append(", ") End If Next counter MsgBox(result.ToString())
Figure 6-30 shows the results of generating the 20 pseudorandom double-precision exponential-distribution numbers.
Recipe 6.26
shows the full code for the BetterRandom
class.
There are many good references on the Web to learn more about random number generation (see, for example, http://random.mat.sbg.ac.at).
You want to declare a matrix, populate it with nonzero values, and perform several standard matrix calculations on it.
Sample code folder: Chapter 06Matrix
This recipe demonstrates how to declare and populate a matrix in a clear, readable way. A module of matrix functions is also included, although several of the functions it contains will be presented in follow-up recipes.
Nested braces containing comma-separated numbers can be used to fill arrays of one or more dimensions. In the case of a two-dimensional matrix, the braces can optionally be separated to show each row of numbers on its own line using the underscore (_) line-continuation character. Feel free to use whatever layout details work for you, but the following sample of a 3 x 3 matrix can provide a decent, visually appealing layout in your source code:
Dim matrixA(,) As Double = { _ {4, 5, 6}, _ {7, 8, 9}, _ {3, 2, 1}} MsgBox(MatrixHelper.MakeDisplayable(matrixA))
The last line of this code uses a function named MakeDisplayable()
to return a string
representation of a matrix suitable for display, as shown in Figure 6-31. This function is
one of several to be presented in the code module named MatrixHelper
.
The MatrixHelper
module
contains several functions to work with matrices, and the recipes that follow will describe
them further. A complete listing of
MatrixHelper.vb can be found at the end of this
chapter.
See the full MatrixHelper.vb listing in Recipe 6.35.
You want to invert a matrix.
Sample code folder: Chapter 06Matrix
Use the MatrixHelper. Inverse()
function presented here and
expanded upon in the MatrixHelper
module presented in Recipe
6.35.
The inverse of a matrix is another identically sized matrix that, when multiplied with the original matrix, gives the identity matrix. Only square matrices can be inverted. Matrix inversion is one of the basic matrix operations used for scientific, engineering, and computer graphics work. (A full description of matrices and their operations is beyond the scope of this book.)
Visual Basic 2005 is a good language for developing high-speed .NET Framework–based mathematical collections of number-crunching routines. It allows you to create fast-running classes, structures, and modules containing related functions or methods to meet many requirements. This recipe presents the code required to invert a matrix efficiently:
Dim matrixA(,) As Double = { _ {1, 3, 3}, _ {2, 4, 3}, _ {1, 3, 4}} Dim matrixB(,) As Double = MatrixHelper.Inverse(matrixA) MsgBox(MatrixHelper.MakeDisplayable(matrixA) & _ vbNewLine & vbNewLine & "Inverse: " & _ vbNewLine & MatrixHelper.MakeDisplayable(matrixB))
The MatrixHelper
module is
listed in its entirety in Recipe 6.35; it includes
the Inverse()
function and other
functions called by Inverse()
.
Figure 6-32 shows the result of finding the inverse of a 3 x 3 matrix.
To use the MatrixHelper.Inverse()
function in your own
applications, add the MatrixHelper
module to your project and call the function from anywhere within your
application.
See the full MatrixHelper.vb listing in Recipe 6.35.
You need to calculate the determinant of a matrix.
Sample code folder: Chapter 06Matrix
Add the MatrixHelper module to your
application, and pass your matrix to the MatrixHelper.Determinant()
function.
The determinant of a matrix is a single number derived from a matrix. It helps determine if a matrix is invertible, and it also comes into play when using matrices to solve simultaneous equations. (A full description of matrices and their operations is beyond the scope of this book.)
The following sample code creates a square matrix of
double-precision numbers and passes it to the MatrixHelper.Determinant()
function in the
MatrixHelper
module, which returns
the determinant of the matrix:
Dim matrixA(,) As Double = { _ {1, 2, 3}, _ {5, 4, 6}, _ {9, 7, 8}} Dim determinant As Double = MatrixHelper.Determinant(matrixA) MsgBox(MatrixHelper.MakeDisplayable(matrixA) & _ vbNewLine & vbNewLine & "Determinant: " & _ determinant.ToString)
The complete MatrixHelper
module is listed in Recipe
6.35. The Determinant()
function is listed here for easy reference:
Public Function Determinant(ByVal sourceMatrix(,) _ As Double) As Double ' ----- Calculate the determinant of a matrix. Dim result As Double Dim pivots As Integer Dim count As Integer ' ----- Only calculate the determinants of square matrices. If (UBound(sourceMatrix, 1) <> _ UBound(sourceMatrix, 2)) Then Throw New Exception("Determinant only " & _ "calculated for square matrices.") End If Dim rank As Integer = UBound(sourceMatrix, 1) ' ----- Make a copy of the matrix so we can work ' inside of it. Dim workMatrix(rank, rank) As Double Array.Copy(sourceMatrix, workMatrix, _ sourceMatrix.Length) ' ----- Use LU decomposition to form a ' triangular matrix. Dim rowPivots(rank) As Integer Dim colPivots(rank) As Integer workMatrix = FormLU(workMatrix, rowPivots, _ colPivots, count) ' ----- Get the product at each of the pivot points. result = 1 For pivots = 0 To rank result *= workMatrix(rowPivots(pivots), _ colPivots(pivots)) Next pivots ' ----- Determine the sign of the result using ' LaPlace's formula. result = (-1) ^ count * result Return result End Function
A very useful technique for copying one array into another is
shown in one of the program lines in the Determinant()
function. Consider the
following line of code:
Array.Copy(a, b, a.Length)
The Array
class sports a
shared Copy()
method that provides
a high-speed way to copy the binary data from one array into another.
There are several overloaded versions of this method, but as used
here, all bytes in array a are copied into array b
, starting at the first byte location in
each array. The transfer of these bytes from one location in memory to
another is highly efficient. You could loop through all of array
a
’s indexed variable locations and
copy them one at a time into corresponding locations within array
b
, but the Array.Copy()
method copies all the bytes
with one function call and no looping.
Figure 6-33 shows the calculated determinant of a 3 x 3 matrix.
See the full MatrixHelper.vb listing in Recipe 6.35.
Sample code folder: Chapter 06Matrix
Use the matrix operations presented in the MatrixHelper
module to solve the
equation.
Matrices are useful in solving simultaneous equations. The solution is defined in Cramer’s Rule, a theorem of linear algebra named after mathematician Gabriel Cramer. (A full description of matrices and their operations is beyond the scope of this book.)
The MatrixHelper
module
contains a special-purpose function that solves simultaneous equations
by calling several matrix-analysis functions. You pass a square matrix
of size n containing the coefficients of the
unknowns from the equations, along with a one-dimensional array
containing the equation constants. The MatrixHelper.SimultEq()
function then
returns a one-dimensional array containing the solution values for the
equation’s unknowns. Here is the code listing for the MatrixHelper.SimultEq()
function:
Public Function SimultEq( _ ByVal sourceEquations(,) As Double, _ ByVal sourceRHS() As Double) As Double() ' ----- Use matrices to solve simultaneous equations. Dim rowsAndCols As Integer ' ----- The matrix must be square and the array size ' must match. Dim rank As Integer = UBound(sourceEquations, 1) If (UBound(sourceEquations, 2) <> rank) Or _ (UBound(sourceRHS, 1) <> rank) Then Throw New Exception( _ "Size problem for simultaneous equations.") End If ' ----- Create some arrays for doing all of the work. Dim coefficientMatrix(rank, rank) As Double Dim rightHandSide(rank) As Double Dim solutions(rank) As Double Dim rowPivots(rank) As Integer Dim colPivots(rank) As Integer ' ----- Make copies of the original matrices so we don't ' mess them up. Array.Copy(sourceEquations, coefficientMatrix, _ sourceEquations.Length) Array.Copy(sourceRHS, rightHandSide, sourceRHS.Length) ' ----- Use LU decomposition to form a triangular matrix. coefficientMatrix = FormLU(coefficientMatrix, _ rowPivots, colPivots, rowsAndCols) ' ----- Find the unique solution for the upper-triangle. BackSolve(coefficientMatrix, rightHandSide, solutions, _ rowPivots, colPivots) ' ----- Return the simultaneous equations result in ' an array. Return solutions End Function
For example, say you have a pile of 18 coins comprised of pennies, nickels, dimes, and quarters totaling $2.23. The nickels and dimes total $.70, and the dimes and quarters total $2.00. The unknowns are the numbers of each of the four types of coins. The given information provides all you need to solve a set of four equations with four unknowns:
P + N + D + Q + = 18 |
P + 5N + 10D + 25Q = 223 |
0P + 5N + 10D + 0Q= 70 |
0P + 0N + 10D + 25Q= 200 |
The following code sets up the 4 x 4 matrix of coefficients and
the array of constants, then passes these two arrays to MatrixHelper.SimultEq()
to solve for the
four unknowns:
Dim matrixA(,) As Double = { _ {1, 1, 1, 1}, _ {1, 5, 10, 25}, _ {0, 5, 10, 0}, _ {0, 0, 10, 25}} Dim arrayB() As Double = {18, 223, 70, 200} Dim arrayC() As Double = _ MatrixHelper.SimultEq(matrixA, arrayB) MsgBox(MatrixHelper.MakeDisplayable(matrixA) & vbNewLine & _ vbNewLine & MatrixHelper.MakeDisplayable(arrayB) & _ vbNewLine & vbNewLine & _ "Simultaneous Equations Solution:" & _ vbNewLine & MatrixHelper.MakeDisplayable(arrayC))
As shown by the results displayed in Figure 6-34, there are three pennies, four nickels, five dimes, and six quarters in the pile.
The MatrixHelper.SimultEq()
function is listed in the MatrixHelper
module code, presented in the
next recipe.
See the full MatrixHelper.vb listing in Recipe 6.35.
Sample code folder: Chapter 06Matrix
Following is the full code for the MatrixHelper
class described in Recipes 6.31, 6.32, 6.33 through 6.34:
Module MatrixHelper Public Function MakeDisplayable( _ ByVal sourceMatrix(,) As Double) As String ' ----- Prepare a multi-line string that shows the ' contents of a matrix, a 2D array. Dim rows As Integer Dim cols As Integer Dim eachRow As Integer Dim eachCol As Integer Dim result As New System.Text.StringBuilder ' ----- Process all rows of the matrix, generating one ' output line per row. rows = UBound(sourceMatrix, 1) + 1 cols = UBound(sourceMatrix, 2) + 1 For eachRow = 0 To rows - 1 ' ----- Process each column of the matrix on a ' single row, separating values by commas. If (eachRow > 0) Then result.AppendLine() For eachCol = 0 To cols - 1 ' ----- Add a single matrix element to the output. If (eachCol > 0) Then result.Append(",") result.Append(sourceMatrix(eachRow, _ eachCol).ToString) Next eachCol Next eachRow ' ----- Finished. Return result.ToString End Function Public Function MakeDisplayable( _ ByVal sourceArray() As Double) As String ' ----- Present an array as multiple lines of output. Dim result As New System.Text.StringBuilder Dim scanValue As Double For Each scanValue In sourceArray result.AppendLine(scanValue.ToString) Next scanValue Return result.ToString End Function Public Function Inverse( _ ByVal sourceMatrix(,) As Double) As Double(,) ' ----- Build a new matrix that is the mathematical ' inverse of the supplied matrix. Multiplying ' a matrix and its inverse together will give ' the identity matrix. Dim eachCol As Integer Dim eachRow As Integer Dim rowsAndCols As Integer ' ----- Determine the size of each dimension of the ' matrix. Only square matrices can be inverted. If (UBound(sourceMatrix, 1) <> _ UBound(sourceMatrix, 2)) Then Throw New Exception("Matrix must be square.") End If Dim rank As Integer = UBound(sourceMatrix, 1) ' ----- Clone a copy of the matrix (not just a ' new reference). Dim workMatrix(,) As Double = _ CType(sourceMatrix.Clone, Double(,)) ' ----- Variables used for backsolving. Dim destMatrix(rank, rank) As Double Dim rightHandSide(rank) As Double Dim solutions(rank) As Double Dim rowPivots(rank) As Integer Dim colPivots(rank) As Integer ' ----- Use LU decomposition to form a ' triangular matrix. workMatrix = FormLU(workMatrix, rowPivots, _ colPivots, rowsAndCols) ' ----- Backsolve the triangular matrix to get the ' inverted value for each position in the ' final matrix. For eachCol = 0 To rank rightHandSide(eachCol) = 1 BackSolve(workMatrix, rightHandSide, solutions, _ rowPivots, colPivots) For eachRow = 0 To rank destMatrix(eachRow, eachCol) = solutions(eachRow) rightHandSide(eachRow) = 0 Next eachRow Next eachCol ' ----- Return the inverted matrix result. Return destMatrix End Function Public Function Determinant(ByVal sourceMatrix(,) _ As Double) As Double ' ----- Calculate the determinant of a matrix. Dim result As Double Dim pivots As Integer Dim count As Integer ' ----- Only calculate the determinants of square ' matrices. If (UBound(sourceMatrix, 1) <> _ UBound(sourceMatrix, 2)) Then Throw New Exception("Determinant only " & _ "calculated for square matrices.") End If Dim rank As Integer = UBound(sourceMatrix, 1) ' ----- Make a copy of the matrix so we can work ' inside of it. Dim workMatrix(rank, rank) As Double Array.Copy(sourceMatrix, workMatrix, _ sourceMatrix.Length) ' ----- Use LU decomposition to form a ' triangular matrix. Dim rowPivots(rank) As Integer Dim colPivots(rank) As Integer workMatrix = FormLU(workMatrix, rowPivots, _ colPivots, count) ' ----- Get the product at each of the pivot points. result = 1 For pivots = 0 To rank result *= workMatrix(rowPivots(pivots), _ colPivots(pivots)) Next pivots ' ----- Determine the sign of the result using ' LaPlace's formula. result = (-1) ^ count * result Return result End Function Public Function SimultEq( _ ByVal sourceEquations(,) As Double, _ ByVal sourceRHS() As Double) As Double() ' ----- Use matrices to solve simultaneous equations. Dim rowsAndCols As Integer ' ----- The matrix must be square and the array size ' must match. Dim rank As Integer = UBound(sourceEquations, 1) If (UBound(sourceEquations, 2) <> rank) Or _ (UBound(sourceRHS, 1) <> rank) Then Throw New Exception( _ "Size problem for simultaneous equations.") End If ' ----- Create some arrays for doing all of the work. Dim coefficientMatrix(rank, rank) As Double Dim rightHandSide(rank) As Double Dim solutions(rank) As Double Dim rowPivots(rank) As Integer Dim colPivots(rank) As Integer ' ----- Make copies of the original matrices so we don't ' mess them up. Array.Copy(sourceEquations, coefficientMatrix, _ sourceEquations.Length) Array.Copy(sourceRHS, rightHandSide, sourceRHS.Length) ' ----- Use LU decomposition to form a triangular matrix. coefficientMatrix = FormLU(coefficientMatrix, _ rowPivots, colPivots, rowsAndCols) ' ----- Find the unique solution for the upper-triangle. BackSolve(coefficientMatrix, rightHandSide, solutions, _ rowPivots, colPivots) ' ----- Return the simultaneous equations result in ' an array. Return solutions End Function Private Function FormLU(ByVal sourceMatrix(,) As Double, _ ByRef rowPivots() As Integer, _ ByRef colPivots() As Integer, _ ByRef rowsAndCols As Integer) As Double(,) ' ----- Perform an LU (lower and upper) decomposition ' of a matrix, a modified form of Gaussian ' elimination. Dim eachRow As Integer Dim eachCol As Integer Dim pivot As Integer Dim rowIndex As Integer Dim colIndex As Integer Dim bestRow As Integer Dim bestCol As Integer Dim rowToPivot As Integer Dim colToPivot As Integer Dim maxValue As Double Dim testValue As Double Dim oldMax As Double Const Deps As Double = 0.0000000000000001 ' ----- Determine the size of the array. Dim rank As Integer = UBound(sourceMatrix, 1) Dim destMatrix(rank, rank) As Double Dim rowNorm(rank) As Double ReDim rowPivots(rank) ReDim colPivots(rank) ' ----- Make a copy of the array so we don't mess it up. Array.Copy(sourceMatrix, destMatrix, _ sourceMatrix.Length) ' ----- Initialize row and column pivot arrays. For eachRow = 0 To rank rowPivots(eachRow) = eachRow colPivots(eachRow) = eachRow For eachCol = 0 To rank rowNorm(eachRow) += _ Math.Abs(destMatrix(eachRow, eachCol)) Next eachCol If (rowNorm(eachRow) = 0) Then Throw New Exception( _ "Cannot invert a singular matrix.") End If Next eachRow ' ----- Use Gauss-Jordan elimination on the matrix rows. For pivot = 0 To rank - 1 maxValue = 0 For eachRow = pivot To rank rowIndex = rowPivots(eachRow) For eachCol = pivot To rank colIndex = colPivots(eachCol) testValue = Math.Abs(destMatrix(rowIndex, _ colIndex)) / rowNorm(rowIndex) If (testValue > maxValue) Then maxValue = testValue bestRow = eachRow bestCol = eachCol End If Next eachCol Next eachRow ' ----- Detect a singular, or very nearly ' singular, matrix. If (maxValue = 0) Then Throw New Exception( _ "Singular matrix used for LU.") ElseIf (pivot > 1) Then If (maxValue < (Deps * oldMax)) Then Throw New Exception( _ "Non-invertible matrix used for LU.") End If End If oldMax = maxValue ' ----- Swap row pivot values for the best row. If (rowPivots(pivot) <> rowPivots(bestRow)) Then rowsAndCols += 1 Swap(rowPivots(pivot), rowPivots(bestRow)) End If ' ----- Swap column pivot values for the best column. If (colPivots(pivot) <> colPivots(bestCol)) Then rowsAndCols += 1 Swap(colPivots(pivot), colPivots(bestCol)) End If ' ----- Work with the current pivot points. rowToPivot = rowPivots(pivot) colToPivot = colPivots(pivot) ' ----- Modify the remaining rows from the ' pivot points. For eachRow = (pivot + 1) To rank rowIndex = rowPivots(eachRow) destMatrix(rowIndex, colToPivot) = _ -destMatrix(rowIndex, colToPivot) / _ destMatrix(rowToPivot, colToPivot) For eachCol = (pivot + 1) To rank colIndex = colPivots(eachCol) destMatrix(rowIndex, colIndex) += _ destMatrix(rowIndex, colToPivot) * _ destMatrix(rowToPivot, colIndex) Next eachCol Next eachRow Next pivot ' ----- Detect a non-invertible matrix. If (destMatrix(rowPivots(rank), _ colPivots(rank)) = 0) Then Throw New Exception( _ "Non-invertible matrix used for LU.") ElseIf (Math.Abs(destMatrix(rowPivots(rank), _ colPivots(rank))) / rowNorm(rowPivots(rank))) < _ (Deps * oldMax) Then Throw New Exception( _ "Non-invertible matrix used for LU.") End If ' ----- Success. Return the LU triangular matrix. Return destMatrix End Function Private Sub Swap(ByRef firstValue As Integer, _ ByRef secondValue As Integer) ' ----- Reverse the values of two reference integers. Dim holdValue As Integer holdValue = firstValue firstValue = secondValue secondValue = holdValue End Sub Private Sub BackSolve(ByVal sourceMatrix(,) As Double, _ ByVal rightHandSide() As Double, _ ByVal solutions() As Double, _ ByRef rowPivots() As Integer, _ ByRef colPivots() As Integer) ' ----- Solve an upper-right-triangle matrix. Dim pivot As Integer Dim rowToPivot As Integer Dim colToPivot As Integer Dim eachRow As Integer Dim eachCol As Integer Dim rank As Integer = UBound(sourceMatrix, 1) ' ----- Work through all pivot points. This section ' builds the "B" in the AX=B formula. For pivot = 0 To (rank - 1) colToPivot = colPivots(pivot) For eachRow = (pivot + 1) To rank rowToPivot = rowPivots(eachRow) rightHandSide(rowToPivot) += _ sourceMatrix(rowToPivot, colToPivot) _ * rightHandSide(rowPivots(pivot)) Next eachRow Next pivot ' ----- Now solve for each X using the general formula ' x(i) = (b(i) - summation(a(i,j)x(j)))/a(i,i) For eachRow = rank To 0 Step -1 colToPivot = colPivots(eachRow) rowToPivot = rowPivots(eachRow) solutions(colToPivot) = rightHandSide(rowToPivot) For eachCol = (eachRow + 1) To rank solutions(colToPivot) -= _ sourceMatrix(rowToPivot, colPivots(eachCol)) _ * solutions(colPivots(eachCol)) Next eachCol solutions(colToPivot) /= sourceMatrix(rowToPivot, _ colToPivot) Next eachRow End Sub End Module
18.225.234.28