Numbers and Math
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Visual Basic arithmetic has four habits that surprise programmers coming from C-family languages: / always produces a floating-point number (except for Decimal), \ is the integer division, ^ is an exponentiation that returns a
Double, and integer overflow throws an OverflowException instead of wrapping around. On top of the .NET Math class the language adds the VB6 helpers Int, Fix, Rnd, Val, IsNumeric and a module of financial functions. This page covers the numeric operations, rounding rules,
Decimal versus Double, big integers, random numbers, the financial functions, overflow, floating-point special values and the parsing helpers, and ends with what VB cannot do with the generic-math interfaces of .NET 7 and later. It was written against Visual Basic 17.13 on .NET 10
(SDK 10.0.402) and checked on 2026-10-19; every program was built with Option Strict On and TreatWarningsAsErrors and run in a Linux container, unless it is marked otherwise. The numeric types, their ranges and literals are on
Data Types and Literals; casts and the C... conversion functions on Type Conversions; operator precedence on
Operators and Expressions. Formatting numbers as text (ToString("N2"), Format) is on Dates, Times and Formatting. For the C# side see
Operators and Expressions in the C# reference.
The Math class, rounding and the integer operators
System.Math is a static class (VB code calls it as Math.Sqrt(2); no Imports is needed because System is imported). Besides the usual functions it has Clamp, DivRem, FusedMultiplyAdd, BitIncrement, IEEERemainder, Truncate, Cbrt, Log2 and the
trigonometric functions in radians; MathF is the Single twin. The VB6 functions Abs, Sgn, Sqr, Sin, Atan and the like do not exist in Visual Basic .NET (a program that calls them gets BC30451, "is not declared"); use Math.Abs, Math.Sign, Math.Sqrt, Math.Sin and Math.Atan. Only Int, Fix and the random-number functions Rnd and Randomize survive from that generation.
Option Strict On
Module Ex01
Sub Main()
Console.WriteLine("-- Math.Round: ties go to the even neighbour unless you say otherwise")
For Each x In {0.5, 1.5, 2.5, 3.5, -0.5, -2.5}
Console.WriteLine($"{x,5}: Round = {Math.Round(x),3} AwayFromZero = {Math.Round(x, MidpointRounding.AwayFromZero),3} ToZero = {Math.Round(x, MidpointRounding.ToZero),3} ToNegativeInfinity = {Math.Round(x, MidpointRounding.ToNegativeInfinity),3} CInt = {CInt(x),3}")
Next
Console.WriteLine($"Math.Round(2.675D, 2) = {Math.Round(2.675D, 2)}, AwayFromZero = {Math.Round(2.675D, 2, MidpointRounding.AwayFromZero)}")
Console.WriteLine($"Rounding to hundreds (Math.Round has no negative digits): {Math.Round(1234.5678D / 100D) * 100D}")
Console.WriteLine("-- Int, Fix, Floor, Ceiling, Truncate")
For Each x In {2.7, -2.7}
Console.WriteLine($"{x,5}: Int = {Int(x),3} Fix = {Fix(x),3} Floor = {Math.Floor(x),3} Ceiling = {Math.Ceiling(x),3} Truncate = {Math.Truncate(x),3}")
Next
Console.WriteLine($"Int(2.7D) is {Int(2.7D).GetType().Name}; Int(7) is {Int(7).GetType().Name}; Math.Floor(2.7) returns {Math.Floor(2.7).GetType().Name}")
Console.WriteLine("-- Integer division and remainder: \ truncates toward zero, Mod keeps the sign of the dividend")
For Each pair In {(7, 2), (-7, 2), (7, -2), (-7, -2)}
Console.WriteLine($"{pair.Item1,3} \ {pair.Item2,2} = {pair.Item1 \ pair.Item2,2} {pair.Item1,3} Mod {pair.Item2,2} = {pair.Item1 Mod pair.Item2,2} Math.DivRem = {Math.DivRem(pair.Item1, pair.Item2)}")
Next
Console.WriteLine($"7.5 Mod 2 = {7.5 Mod 2}; -7.5 Mod 2 = {-7.5 Mod 2}; Math.IEEERemainder(7.5, 2) = {Math.IEEERemainder(7.5, 2)}")
Dim positive = ((-7 Mod 5) + 5) Mod 5
Console.WriteLine($"Non-negative remainder of -7 modulo 5: {positive}")
Console.WriteLine("-- ^ is Math.Pow and returns Double")
Console.WriteLine($"2 ^ 10 = {2 ^ 10} ({(2 ^ 10).GetType().Name}); Math.Pow(2, 0.5) = {Math.Pow(2, 0.5):F6}; -2 ^ 2 = {-2 ^ 2}; (-2) ^ 2 = {(-2) ^ 2}")
Dim squared As Integer = CInt(2 ^ 10)
Console.WriteLine($"As Integer: {squared}")
Console.WriteLine("-- More of the Math class")
Console.WriteLine($"Abs {Math.Abs(-3.5)}, Sign {Math.Sign(-3.5)}, Max {Math.Max(3, 9)}, Min {Math.Min(3, 9)}, Clamp {Math.Clamp(15, 0, 10)}")
Console.WriteLine($"Sqrt {Math.Sqrt(2):F6}, Exp {Math.Exp(1):F6}, Log {Math.Log(Math.E):F1}, Log10 {Math.Log10(1000)}, Log2 {Math.Log2(8)}, Log(8, 2) {Math.Log(8, 2)}")
Console.WriteLine($"PI {Math.PI}, Tau {Math.Tau}, E {Math.E}")
Console.WriteLine($"Sin(PI/2) {Math.Sin(Math.PI / 2)}, Cos(PI) {Math.Cos(Math.PI)}, Atan2(1, 1) * 4 {Math.Atan2(1, 1) * 4}, Tan(PI/4) {Math.Tan(Math.PI / 4):F6}")
Console.WriteLine($"Degrees to radians: {90 * Math.PI / 180:F6}; Math.Sin(Math.PI) = {Math.Sin(Math.PI)}")
Console.WriteLine($"MathF.Sqrt(2) = {MathF.Sqrt(2)} ({MathF.Sqrt(2).GetType().Name}); Math.FusedMultiplyAdd(2, 3, 4) = {Math.FusedMultiplyAdd(2, 3, 4)}")
End Sub
End Module
-- Math.Round: ties go to the even neighbour unless you say otherwise
0.5: Round = 0 AwayFromZero = 1 ToZero = 0 ToNegativeInfinity = 0 CInt = 0
1.5: Round = 2 AwayFromZero = 2 ToZero = 1 ToNegativeInfinity = 1 CInt = 2
2.5: Round = 2 AwayFromZero = 3 ToZero = 2 ToNegativeInfinity = 2 CInt = 2
3.5: Round = 4 AwayFromZero = 4 ToZero = 3 ToNegativeInfinity = 3 CInt = 4
-0.5: Round = -0 AwayFromZero = -1 ToZero = -0 ToNegativeInfinity = -1 CInt = 0
-2.5: Round = -2 AwayFromZero = -3 ToZero = -2 ToNegativeInfinity = -3 CInt = -2
Math.Round(2.675D, 2) = 2.68, AwayFromZero = 2.68
Rounding to hundreds (Math.Round has no negative digits): 1200
-- Int, Fix, Floor, Ceiling, Truncate
2.7: Int = 2 Fix = 2 Floor = 2 Ceiling = 3 Truncate = 2
-2.7: Int = -3 Fix = -2 Floor = -3 Ceiling = -2 Truncate = -2
Int(2.7D) is Decimal; Int(7) is Int32; Math.Floor(2.7) returns Double
-- Integer division and remainder: \ truncates toward zero, Mod keeps the sign of the dividend
7 \ 2 = 3 7 Mod 2 = 1 Math.DivRem = (3, 1)
-7 \ 2 = -3 -7 Mod 2 = -1 Math.DivRem = (-3, -1)
7 \ -2 = -3 7 Mod -2 = 1 Math.DivRem = (-3, 1)
-7 \ -2 = 3 -7 Mod -2 = -1 Math.DivRem = (3, -1)
7.5 Mod 2 = 1.5; -7.5 Mod 2 = -1.5; Math.IEEERemainder(7.5, 2) = -0.5
Non-negative remainder of -7 modulo 5: 3
-- ^ is Math.Pow and returns Double
2 ^ 10 = 1024 (Double); Math.Pow(2, 0.5) = 1.414214; -2 ^ 2 = -4; (-2) ^ 2 = 4
As Integer: 1024
-- More of the Math class
Abs 3.5, Sign -1, Max 9, Min 3, Clamp 10
Sqrt 1.414214, Exp 2.718282, Log 1.0, Log10 3, Log2 3, Log(8, 2) 3
PI 3.141592653589793, Tau 6.283185307179586, E 2.718281828459045
Sin(PI/2) 1, Cos(PI) -1, Atan2(1, 1) * 4 3.141592653589793, Tan(PI/4) 1.000000
Degrees to radians: 1.570796; Math.Sin(Math.PI) = 1.2246467991473532E-16
MathF.Sqrt(2) = 1.4142135 (Single); Math.FusedMultiplyAdd(2, 3, 4) = 10
Source: Math Class
Rounding
Three different roundings are in play, which is a favourite source of off-by-a-cent bugs:
-
Math.Round(x)andCInt(x)andCLng(x)round a tie (a value exactly halfway between two integers) to the nearest even integer, "banker’s rounding": 0.5 becomes 0, 1.5 and 2.5 become 2, and 3.5 becomes 4. The result for-0.5is theDoublenegative zero, printed-0. VB6’sCIntused the same rule, so this is not new, but C# programmers expectMath.Round(2.5)to give 3 because of school arithmetic. -
Math.Round(x, MidpointRounding.AwayFromZero)is the "school" rounding. The other modes areToEven(the default),ToZero,ToNegativeInfinityandToPositiveInfinity; the digits overload isMath.Round(x, digits, mode), with 0 to 15 digits forDoubleand 0 to 28 forDecimal. There is no negative-digits overload: divide, round and multiply, as the program does for hundreds. -
Formatting rounds too, and not always the same way (see the dates-and-formatting page):
2.5D.ToString("F0")is3but2.5.ToString("F0")is2. If a rule is part of the requirements, round explicitly with the mode you want and format the result.
Int(x) is the floor (the largest integer not greater than x: Int(-2.7) is -3) and Fix(x) truncates toward zero (Fix(-2.7) is -2). They return the type of their argument, so Int(2.7D) is a Decimal and Int(7) an Integer, not an Integer conversion. The Math counterparts are
Floor/Ceiling/Truncate, which exist for Double and Decimal only (Math.Floor(2.7) returns a Double).
Division, remainder and power
-
a / breturns aDoublewhen the operands are integers:7 / 2is3.5, andDim ratio As Integer = 7 / 2is BC30512 underOption Strict On. If both operands areDecimalthe result isDecimal. Dividing by zero givesInfinityorNaNforDouble(no exception) and throwsDivideByZeroExceptionforDecimal. -
a \ bis integer division and truncates toward zero:-7 \ 2is -3. It works on integer operands (7.9 \ 2is BC30512 underOption Strict On, because aDoubledoes not convert implicitly toLong), throwsDivideByZeroExceptionfor a zero divisor, andInteger.MinValue \ -1throwsOverflowException. -
a Mod breturns the remainder with the sign of the dividend (-7 Mod 2is -1), the same as C#'s%. It works on floating point too (7.5 Mod 2is 1.5) and throws for an integer zero divisor. For a result that is never negative use((a Mod n) + n) Mod n;Math.IEEERemainderis a different (round-to-nearest) definition. -
a ^ bconverts its operands toDouble, callsMath.Powand returns aDouble: assigning it to anIntegerneedsCInt(2 ^ 10)(BC30512 otherwise). Its precedence is higher than unary minus, so-2 ^ 2is -4.^is not "exclusive or" (that isXor), and it is undefined forBigInteger. -
The shift operators
<<and>>mask the shift count to the type’s width (a shift of 32 on anIntegershifts by 0),>>is arithmetic for signed types, and there is no unsigned right shift operator (>>>is a syntax error, BC30201).
Decimal and Double
Double is a 64-bit IEEE 754 binary number: fast, with 15 to 17 significant decimal digits, and unable to represent most decimal fractions exactly. Decimal is a 128-bit decimal number with 28 to 29 significant digits and a stored scale (the number of digits after the decimal point), which is exact for
the amounts that people write in decimal, which makes it the type for money, rates and anything that has to match a calculator. It is slower (software arithmetic), has a smaller range (about 7.9 x 1028) and no NaN or Infinity.
Option Strict On
Module Ex02
Function LateFee(daysLate As Integer, centsPerDay As Decimal) As Decimal
Dim fee = daysLate * centsPerDay / 100D
Return Math.Round(fee, 2, MidpointRounding.AwayFromZero)
End Function
Sub Main()
Console.WriteLine("-- Double is binary floating point")
Dim sumD As Double = 0.1 + 0.2
Console.WriteLine($"0.1 + 0.2 = {sumD} equal to 0.3: {sumD = 0.3} difference: {sumD - 0.3:E3}")
Console.WriteLine($"Within tolerance: {Math.Abs(sumD - 0.3) < 0.000000001}")
Console.WriteLine($"Single: {0.1F + 0.2F} ({(0.1F + 0.2F) = 0.3F})")
Console.WriteLine("-- Decimal is base-10 and keeps its scale")
Dim sumM As Decimal = 0.1D + 0.2D
Console.WriteLine($"0.1D + 0.2D = {sumM} equal to 0.3D: {sumM = 0.3D}")
Console.WriteLine($"1.10D * 2 = {1.10D * 2} 1.10D * 2D = {1.10D * 2D} 1.50D + 1.5D = {1.5D + 1.50D} 1.0D / 3D = {1D / 3D}")
Console.WriteLine($"Decimal.MaxValue = {Decimal.MaxValue}")
Console.WriteLine($"Scale of 1.10D: {Decimal.GetBits(1.10D)(3) >> 16 And 255}, of 1.1D: {Decimal.GetBits(1.1D)(3) >> 16 And 255}, of 110D: {Decimal.GetBits(110D)(3) >> 16 And 255}")
Console.WriteLine($"Decimal.Round(0.345D, 2) = {Decimal.Round(0.345D, 2)}, away from zero = {Decimal.Round(0.345D, 2, MidpointRounding.AwayFromZero)}")
Console.WriteLine($"Convert: CDec(0.1) = {CDec(0.1)} vs CDec(0.1 + 0.2) = {CDec(0.1 + 0.2)} Double to Decimal loses the noise: {CDec(sumD) = 0.3D}")
Console.WriteLine($"Late fee for 7 days at 25 cents a day: {LateFee(7, 25D):F2}; for 3 days at 12.5 cents: {LateFee(3, 12.5D):F2}")
Console.WriteLine("-- Division by zero depends on the type")
Dim zeroI As Integer = 0
Dim zeroD As Double = 0
Dim zeroM As Decimal = 0
Console.WriteLine($"10 / zeroI (Double division) = {10 / zeroI}")
Console.WriteLine($"-10 / zeroD = {-10 / zeroD}, 0 / zeroD = {0 / zeroD}")
Try
Console.WriteLine(10 \ zeroI)
Catch ex As DivideByZeroException
Console.WriteLine("10 \ 0 throws " & ex.GetType().Name)
End Try
Try
Console.WriteLine(10 Mod zeroI)
Catch ex As DivideByZeroException
Console.WriteLine("10 Mod 0 throws " & ex.GetType().Name)
End Try
Try
Console.WriteLine(10D / zeroM)
Catch ex As DivideByZeroException
Console.WriteLine("10D / 0D throws " & ex.GetType().Name)
End Try
Console.WriteLine($"10.5 Mod zeroD = {10.5 Mod zeroD}")
End Sub
End Module
-- Double is binary floating point
0.1 + 0.2 = 0.30000000000000004 equal to 0.3: False difference: 5.551E-017
Within tolerance: True
Single: 0.3 (True)
-- Decimal is base-10 and keeps its scale
0.1D + 0.2D = 0.3 equal to 0.3D: True
1.10D * 2 = 2.20 1.10D * 2D = 2.20 1.50D + 1.5D = 3.00 1.0D / 3D = 0.3333333333333333333333333333
Decimal.MaxValue = 79228162514264337593543950335
Scale of 1.10D: 2, of 1.1D: 1, of 110D: 0
Decimal.Round(0.345D, 2) = 0.34, away from zero = 0.35
Convert: CDec(0.1) = 0.1 vs CDec(0.1 + 0.2) = 0.3 Double to Decimal loses the noise: True
Late fee for 7 days at 25 cents a day: 1.75; for 3 days at 12.5 cents: 0.38
-- Division by zero depends on the type
10 / zeroI (Double division) = Infinity
-10 / zeroD = -Infinity, 0 / zeroD = NaN
10 \ 0 throws DivideByZeroException
10 Mod 0 throws DivideByZeroException
10D / 0D throws DivideByZeroException
10.5 Mod zeroD = NaN
Source: Decimal Struct
Use these rules:
-
Money, quantities that are counted in fractions of a unit, and anything that is typed by a person in decimal:
Decimal. Literals need theDsuffix (0.1D) or the@type character; a bare0.1is aDouble,1D + 0.1is aDoubleand assigning it to aDecimalis BC30512 underOption Strict On(write1D + 0.1D, orCDec(0.1)). -
Measurements, geometry, statistics, anything with
Math.Sqrtor trigonometry:Double. -
Decimalkeeps trailing zeros that you wrote or computed:1.10D * 2is2.20, andDecimal.GetBitsshows the scale. This is useful for money and surprising in comparisons of text;=ignores scale. -
CDec(0.1 + 0.2)is0.3because the conversion fromDoublerounds to 15 significant digits; that hides, not repairs, the noise in theDoubleresult. -
The late-fee function rounds once, at the end, away from zero, as a billing rule would; do not round every intermediate step.
BigInteger and the 128-bit integers
System.Numerics.BigInteger is an arbitrary-precision signed integer: it grows as needed, supports the arithmetic operators and Mod, compares with < and =, and converts from any integer type implicitly. It has no \ or ^ in VB (both are BC30452, "Operator is not defined for types
BigInteger and Integer`"): use `/, which is integer division for BigInteger, and BigInteger.Pow. Conversions to fixed-size types are explicit (CInt, CLng, CDbl) and throw OverflowException when the value does not fit. Int128 and UInt128 (.NET 7) are fixed-size 128-bit
integers with the usual arithmetic and overflow checking.
Option Strict On
Imports System.Numerics
Module Ex03
Function Factorial(n As Integer) As BigInteger
Dim result As BigInteger = BigInteger.One
For i = 2 To n
result *= i
Next
Return result
End Function
Sub Main()
Dim big As BigInteger = BigInteger.Pow(2, 100)
Console.WriteLine($"2^100 = {big}")
Console.WriteLine($"30! = {Factorial(30)}")
Console.WriteLine($"Digits in 100!: {Factorial(100).ToString().Length}")
Dim a As BigInteger = BigInteger.Parse("123456789012345678901234567890")
Dim b As BigInteger = 987654321
Console.WriteLine($"a * b = {a * b}")
Console.WriteLine($"a / b = {a / b} a Mod b = {a Mod b} Remainder = {BigInteger.Remainder(a, b)}")
Console.WriteLine($"DivRem: {BigInteger.DivRem(a, b)}")
Console.WriteLine($"GCD = {BigInteger.GreatestCommonDivisor(a, b)} ModPow(7, 128, 13) = {BigInteger.ModPow(7, 128, 13)}")
Console.WriteLine($"Compare: {a > b}, sign {a.Sign}, is even {a.IsEven}, bit length {a.GetBitLength()}")
Console.WriteLine("-- Conversions are explicit and checked")
Try
Dim tooBig As Integer = CInt(big)
Console.WriteLine(tooBig)
Catch ex As OverflowException
Console.WriteLine("CInt(2^100) throws " & ex.GetType().Name)
End Try
Dim small As BigInteger = 42
Console.WriteLine($"CInt(42) = {CInt(small)}, CLng = {CLng(small)}, CDbl(2^100) = {CDbl(big)}, implicit from Integer ok: {small + 1}")
Console.WriteLine("-- Int128 and UInt128 arrive in the same family")
Dim i128 As Int128 = Int128.MaxValue
Console.WriteLine($"Int128.MaxValue = {i128}")
Console.WriteLine($"UInt128.MaxValue = {UInt128.MaxValue}")
Console.WriteLine($"Long.MaxValue * 2 as Int128 = {CType(Long.MaxValue, Int128) * 2}")
Console.WriteLine($"Integer sizes: Short {Short.MaxValue}, Integer {Integer.MaxValue}, Long {Long.MaxValue}, ULong {ULong.MaxValue}")
End Sub
End Module
2^100 = 1267650600228229401496703205376
30! = 265252859812191058636308480000000
Digits in 100!: 158
a * b = 121932631124828532112482853211126352690
a / b = 124999998873437499901 a Mod b = 574845669 Remainder = 574845669
DivRem: (124999998873437499901, 574845669)
GCD = 9 ModPow(7, 128, 13) = 3
Compare: True, sign 1, is even True, bit length 97
-- Conversions are explicit and checked
CInt(2^100) throws OverflowException
CInt(42) = 42, CLng = 42, CDbl(2^100) = 1.2676506002282294E+30, implicit from Integer ok: 43
-- Int128 and UInt128 arrive in the same family
Int128.MaxValue = 170141183460469231731687303715884105727
UInt128.MaxValue = 340282366920938463463374607431768211455
Long.MaxValue * 2 as Int128 = 18446744073709551614
Integer sizes: Short 32767, Integer 2147483647, Long 9223372036854775807, ULong 18446744073709551615
Source: BigInteger Struct
Random numbers
System.Random is a pseudo-random generator for simulations, games, sampling and tests. Create one instance and reuse it rather than a new one per call, or use the thread-safe shared instance Random.Shared (.NET 6). Next(min, max)
has an exclusive upper bound; NextDouble() returns a value in [0, 1); Shuffle (.NET 8) and GetItems (.NET 8) cover the common array tasks. A seeded generator is reproducible, which is why tests seed it, but the algorithm and therefore the sequence are not guaranteed to stay the same across .NET versions.
Random is not suitable for secrets: tokens, passwords, keys and nonces come from System.Security.Cryptography.RandomNumberGenerator.
The legacy VB generator is Rnd() (a Single in [0, 1)) with Randomize() to seed it from the clock and Randomize(seed) to seed it deterministically. Rnd(-1) followed by Randomize(n) restarts the same sequence for a given n, Rnd(0) repeats the last number, and the classic die formula is Int((upper - lower + 1) * Rnd() + lower).
Use Random in new code.
Option Strict On
Imports System.Security.Cryptography
Module Ex04
Sub Main()
' A seeded Random is reproducible; its exact sequence is not a promise across runtime versions.
Dim seeded As New Random(42)
Console.WriteLine("Seeded Next(1, 7): " & String.Join(" ", Enumerable.Range(0, 8).Select(Function(i) seeded.Next(1, 7))))
Console.WriteLine($"Seeded NextDouble in [0, 1): {seeded.NextDouble() >= 0 AndAlso seeded.NextDouble() < 1}")
' The shared instance is thread-safe and needs no seed.
Dim roll = Random.Shared.Next(1, 7)
Console.WriteLine($"Random.Shared.Next(1, 7) is between 1 and 6: {roll >= 1 AndAlso roll <= 6} (upper bound is exclusive)")
Dim titles As String() = {"Dune", "Emma", "Ulysses", "Hamlet"}
Dim shuffled = CType(titles.Clone(), String())
Random.Shared.Shuffle(shuffled)
Console.WriteLine($"Shuffle keeps the elements: {shuffled.OrderBy(Function(t) t).SequenceEqual(titles.OrderBy(Function(t) t))}")
Console.WriteLine($"GetItems picks with replacement: {Random.Shared.GetItems(titles, 6).Length} items")
' Legacy VB generator: Rnd and Randomize
Rnd(-1)
Randomize(42)
Dim first = Rnd()
Dim second = Rnd()
Console.WriteLine($"Rnd after Rnd(-1) / Randomize(42): {first:F6} {second:F6}")
Rnd(-1)
Randomize(42)
Console.WriteLine($"Same seed again repeats the sequence: {Rnd() = first AndAlso Rnd() = second}")
Console.WriteLine($"Rnd returns {Rnd().GetType().Name} in [0, 1)")
Dim lower = 1
Dim upper = 6
Dim legacyRoll = CInt(Int((upper - lower + 1) * Rnd() + lower))
Console.WriteLine($"Classic die roll formula gives {If(legacyRoll >= 1 AndAlso legacyRoll <= 6, "1 to 6", "out of range")}")
Console.WriteLine($"Rnd(0) repeats the last number: {Rnd(0) = Rnd(0)}")
' Anything security-related uses the cryptographic generator.
Dim token = RandomNumberGenerator.GetInt32(100000, 1000000)
Console.WriteLine($"Six-digit code from RandomNumberGenerator: {token.ToString().Length} digits")
Dim bytes = RandomNumberGenerator.GetBytes(16)
Console.WriteLine($"16 random bytes -> hex length {Convert.ToHexString(bytes).Length}")
End Sub
End Module
Seeded Next(1, 7): 5 1 1 4 2 2 5 4
Seeded NextDouble in [0, 1): True
Random.Shared.Next(1, 7) is between 1 and 6: True (upper bound is exclusive)
Shuffle keeps the elements: True
GetItems picks with replacement: 6 items
Rnd after Rnd(-1) / Randomize(42): 0.907809 0.611087
Same seed again repeats the sequence: True
Rnd returns Single in [0, 1)
Classic die roll formula gives 1 to 6
Rnd(0) repeats the last number: True
Six-digit code from RandomNumberGenerator: 6 digits
16 random bytes -> hex length 32
Source: Random Class
The Financial functions
Microsoft.VisualBasic.Financial is a VB-only convenience library that every VB project can call without a package: Pmt, IPmt, PPmt, PV, FV, NPer, Rate, NPV, IRR, MIRR, and the depreciation functions SLN, SYD, DDB. The sign convention is the spreadsheet one: money you pay out is negative, money you receive is positive, and the rate is per period,
so a 6 percent annual loan paid monthly uses 0.06 / 12 and a number of months. IRR takes a Double() of cash flows and an optional guess, and throws ArgumentException ("Arguments are not valid") if the flows do not change sign, for example IRR({100.0, 200.0}). NPV discounts the first value by one period too (it does not treat it as "time zero", unlike spreadsheet usage where the
initial outlay is added separately). Everything is Double: for money that must be exact, use the results for estimates and do the booking arithmetic in Decimal.
Option Strict On
Module Ex05
Sub Main()
' Cash you pay out is negative, cash you receive is positive.
Dim loanAmount As Double = 24000 ' new shelving, borrowed
Dim annualRate As Double = 0.06
Dim months As Integer = 36
Dim monthlyRate = annualRate / 12
Dim payment = Pmt(monthlyRate, months, -loanAmount)
Console.WriteLine($"Monthly payment: {payment:F2}")
Console.WriteLine($"Total paid: {payment * months:F2}, interest: {payment * months - loanAmount:F2}")
Console.WriteLine($"Interest in payment 1: {-IPmt(monthlyRate, 1, months, loanAmount):F2}, principal in payment 1: {-PPmt(monthlyRate, 1, months, loanAmount):F2}")
Console.WriteLine($"Balance check, PV of the payments: {PV(monthlyRate, months, -payment):F2}")
' Saving for a new branch: 500 a month at 4 percent a year for 5 years
Dim saved = FV(0.04 / 12, 60, -500)
Console.WriteLine($"Future value of the savings: {saved:F2}")
Console.WriteLine($"Months needed to reach 40,000 with 600 a month at 3 percent: {NPer(0.03 / 12, -600, 0, 40000):F1}")
Console.WriteLine($"Rate for 36 payments of {payment:F2} on 24,000: {Rate(36, -payment, 24000) * 12:P2} a year")
' Project cash flows: outlay first, then the yearly savings
Dim flows As Double() = {-10000, 3000, 4200, 6800}
Console.WriteLine($"NPV at 8 percent (every value is discounted at least one period, the outlay too): {NPV(0.08, flows):F2}")
Console.WriteLine($"IRR: {IRR(flows):P2}")
' Depreciation of a scanner worth 3,000 with 300 salvage over 5 years
Console.WriteLine($"SLN {SLN(3000, 300, 5):F2}, year 1 SYD {SYD(3000, 300, 5, 1):F2}, year 1 DDB {DDB(3000, 300, 5, 1):F2}")
End Sub
End Module
Monthly payment: 730.13
Total paid: 26284.55, interest: 2284.55
Interest in payment 1: 120.00, principal in payment 1: 610.13
Balance check, PV of the payments: 24000.00
Future value of the savings: 33149.49
Months needed to reach 40,000 with 600 a month at 3 percent: 61.7
Rate for 36 payments of 730.13 on 24,000: 6.00 % a year
NPV at 8 percent (every value is discounted at least one period, the outlay too): 1645.06
IRR: 16.34 %
SLN 540.00, year 1 SYD 900.00, year 1 DDB 1200.00
Source: Financial Class
Integer overflow
The VB compiler emits checked arithmetic for +, -, *, unary - and the narrowing conversions of Byte, SByte, Short, UShort, Integer, UInteger, Long and ULong: a result that does not fit throws OverflowException. This is the opposite of C#'s default (unchecked), and means that a VB program notices overflow where a C# one silently wraps. Decimal always checks. A constant expression that overflows is a compile error
(Integer.MaxValue + 1 is BC30439, "Constant expression not representable in type 'Integer'"). Some operations check regardless of settings: Math.Abs(Integer.MinValue), Integer.MinValue \ -1, Decimal arithmetic and CInt of a value outside the range of Integer, which in the checked mode throws.
Option Strict On
Module Ex06
Function Attempt(label As String, work As Func(Of Object)) As String
Try
Return $"{label,-30} -> {work()}"
Catch ex As OverflowException
Return $"{label,-30} -> OverflowException"
Catch ex As DivideByZeroException
Return $"{label,-30} -> DivideByZeroException"
End Try
End Function
Sub Main()
Dim maxInt As Integer = Integer.MaxValue
Dim minInt As Integer = Integer.MinValue
Dim b As Byte = 250
Dim s As Short = Short.MaxValue
Dim minusOne As Integer = -1
Dim big As Long = 3000000000L
Dim maxLong As Long = Long.MaxValue
Dim maxDecimal As Decimal = Decimal.MaxValue
Dim nan As Double = Double.NaN
Dim shift As Integer = 31
Console.WriteLine(Attempt("Integer.MaxValue + 1", Function() maxInt + 1))
Console.WriteLine(Attempt("Integer.MinValue - 1", Function() minInt - 1))
Console.WriteLine(Attempt("Integer.MaxValue * 2", Function() maxInt * 2))
Console.WriteLine(Attempt("Byte 250 + 10", Function() CByte(b + 10)))
Console.WriteLine(Attempt("Short.MaxValue + 1", Function() CShort(s + 1)))
Console.WriteLine(Attempt("-Integer.MinValue", Function() -minInt))
Console.WriteLine(Attempt("Long.MaxValue + 1", Function() maxLong + 1L))
Console.WriteLine(Attempt("Integer.MaxValue + 1L", Function() maxInt + 1L))
Console.WriteLine(Attempt("CInt(3000000000L)", Function() CInt(big)))
Console.WriteLine(Attempt("CByte(300L)", Function() CByte(big \ 10000000L)))
Console.WriteLine(Attempt("Math.Abs(Integer.MinValue)", Function() Math.Abs(minInt)))
Console.WriteLine(Attempt("Integer.MinValue \ -1", Function() minInt \ minusOne))
Console.WriteLine(Attempt("Decimal.MaxValue + 1D", Function() maxDecimal + 1D))
Console.WriteLine(Attempt("CInt(Double.NaN)", Function() CInt(nan)))
Console.WriteLine(Attempt("1 << 31", Function() 1 << shift))
Console.WriteLine(Attempt("1 << 32 (count is masked)", Function() 1 << (shift + 1)))
End Sub
End Module
Integer.MaxValue + 1 -> OverflowException
Integer.MinValue - 1 -> OverflowException
Integer.MaxValue * 2 -> OverflowException
Byte 250 + 10 -> OverflowException
Short.MaxValue + 1 -> OverflowException
-Integer.MinValue -> OverflowException
Long.MaxValue + 1 -> OverflowException
Integer.MaxValue + 1L -> 2147483648
CInt(3000000000L) -> OverflowException
CByte(300L) -> OverflowException
Math.Abs(Integer.MinValue) -> OverflowException
Integer.MinValue \ -1 -> OverflowException
Decimal.MaxValue + 1D -> OverflowException
CInt(Double.NaN) -> OverflowException
1 << 31 -> -2147483648
1 << 32 (count is masked) -> 1
Source: -removeintchecks
The compiler option -removeintchecks+ switches the integer checks off for the whole project. With MSBuild it is the property:
<PropertyGroup>
<RemoveIntegerChecks>true</RemoveIntegerChecks>
</PropertyGroup>
Running exactly the same program with that property gives:
Integer.MaxValue + 1 -> -2147483648
Integer.MinValue - 1 -> 2147483647
Integer.MaxValue * 2 -> -2
Byte 250 + 10 -> 4
Short.MaxValue + 1 -> -32768
-Integer.MinValue -> -2147483648
Long.MaxValue + 1 -> -9223372036854775808
Integer.MaxValue + 1L -> 2147483648
CInt(3000000000L) -> -1294967296
CByte(300L) -> 44
Math.Abs(Integer.MinValue) -> OverflowException
Integer.MinValue \ -1 -> OverflowException
Decimal.MaxValue + 1D -> OverflowException
CInt(Double.NaN) -> 0
1 << 31 -> -2147483648
1 << 32 (count is masked) -> 1
Notes. The setting is project-wide, there is no checked/unchecked block or keyword, and Math.Abs, \ -1, Decimal and Math methods still throw. Do not turn it on to make an overflow bug disappear: the wrapped number is wrong too. The two uses that justify it are hashing and checksum code written for wrap-around, and (rarely) a hot loop that has been measured. The
better way to get wrap-around is to keep the project checked and write the arithmetic explicitly, as in the following FNV-1a hash, which widens to Long and masks, and reinterprets bits with BitConverter:
Option Strict On
Imports System.Text
Imports System.Numerics
Module Ex11
' 32-bit FNV-1a: the algorithm relies on wrap-around multiplication.
' Visual Basic checks integer overflow, so the arithmetic is done in Long and masked to 32 bits.
Function Fnv1a(text As String) As UInteger
Dim hash As Long = &H811C9DC5L
For Each b In Encoding.UTF8.GetBytes(text)
hash = (hash Xor b) * 16777619L And &HFFFFFFFFL
Next
Return CUInt(hash)
End Function
' Reinterpreting the bits of an Integer as a UInteger (and back) without an overflow check.
Function ToSigned(value As UInteger) As Integer
Return BitConverter.ToInt32(BitConverter.GetBytes(value), 0)
End Function
Function Rotate(value As UInteger, count As Integer) As UInteger
Return BitOperations.RotateLeft(value, count)
End Function
Sub Main()
Console.WriteLine($"FNV-1a(""a"") = {Fnv1a("a"):X8}")
Console.WriteLine($"FNV-1a(""shelf"") = {Fnv1a("shelf"):X8}")
Console.WriteLine($"As signed Integer: {ToSigned(Fnv1a("a"))}")
Console.WriteLine($"Rotate left by 8: {Rotate(&H12345678UI, 8):X8}")
Dim a As Integer = Integer.MaxValue
Try
Console.WriteLine(a * 16777619)
Catch ex As OverflowException
Console.WriteLine("Plain Integer multiplication overflows: " & ex.GetType().Name)
End Try
' Shifts and bit operators never overflow
Dim flags As Integer = &HF0
Console.WriteLine($"{flags >> 4:X} {flags << 4:X} {flags And &H3C:X} {flags Or 1:X} {flags Xor &HFF:X} {Not flags} {-16 >> 2} {&H80000000UI >> 31}")
Console.WriteLine($"PopCount {BitOperations.PopCount(&HFFUI)}, LeadingZeroCount {BitOperations.LeadingZeroCount(1UI)}, IsPow2 {BitOperations.IsPow2(64)}")
End Sub
End Module
FNV-1a("a") = E40C292C
FNV-1a("shelf") = 07E1EC43
As signed Integer: -468965076
Rotate left by 8: 34567812
Plain Integer multiplication overflows: OverflowException
F F00 30 F1 F -241 -4 1
PopCount 8, LeadingZeroCount 31, IsPow2 True
Source: BitOperations Class
Floating-point special values and comparisons
Double and Single implement IEEE 754, so besides the numbers they have Infinity, -Infinity, NaN and a negative zero. Visual Basic does not turn these into exceptions: 1 / 0.0 is Infinity, 0 / 0.0 is NaN, Math.Sqrt(-1) is NaN, and Math.Log(0) is -Infinity. Test with Double.IsNaN, IsInfinity and IsFinite,
not with =: NaN = NaN is False and NaN <> NaN is True, while NaN.Equals(NaN) is True (which is what sorting uses, and why a sorted array has the NaN first). Math.Max(1, NaN) is NaN. Never compare computed doubles with =; use a tolerance that is relative to the magnitude, as the NearlyEqual function does, because Double.Epsilon
is the smallest positive value (about 5E-324), not a rounding tolerance.
Option Strict On
Module Ex07
Function NearlyEqual(a As Double, b As Double, Optional relative As Double = 0.000000001) As Boolean
If a = b Then Return True
Return Math.Abs(a - b) <= relative * Math.Max(Math.Abs(a), Math.Abs(b))
End Function
Sub Main()
Dim zero As Double = 0
Dim posInf = 1 / zero
Dim negInf = -1 / zero
Dim nan = zero / zero
Console.WriteLine($"1/0 = {posInf}, -1/0 = {negInf}, 0/0 = {nan}")
Console.WriteLine($"IsInfinity {Double.IsInfinity(posInf)}, IsPositiveInfinity {Double.IsPositiveInfinity(posInf)}, IsNaN {Double.IsNaN(nan)}, IsFinite(1) {Double.IsFinite(1.0)}")
Console.WriteLine($"NaN = NaN: {nan = nan}; NaN <> NaN: {nan <> nan}; NaN < 1: {nan < 1}; NaN.Equals(NaN): {nan.Equals(nan)}; CompareTo: {nan.CompareTo(nan)}")
Console.WriteLine($"Infinity - Infinity = {posInf - posInf}; Infinity * 0 = {posInf * zero}; 1 / Infinity = {1 / posInf}")
Console.WriteLine($"Math.Sqrt(-1) = {Math.Sqrt(-1)}; Math.Log(0) = {Math.Log(0)}; Math.Log(-1) = {Math.Log(-1)}")
Dim negativeZero = -zero
Console.WriteLine($"-0.0 = 0.0: {negativeZero = zero}; 1 / -0.0 = {1 / negativeZero}; -0.0 prints as {negativeZero}")
Console.WriteLine("-- Comparing floating-point results")
Dim total As Double = 0
For i = 1 To 10
total += 0.1
Next
Console.WriteLine($"Ten times 0.1 = {total:R}; = 1.0: {total = 1.0}; NearlyEqual: {NearlyEqual(total, 1.0)}")
Console.WriteLine($"Double.Epsilon = {Double.Epsilon} (smallest positive, not a tolerance); Math.BitIncrement(1.0) - 1.0 = {Math.BitIncrement(1.0) - 1.0}")
Console.WriteLine($"Double.MaxValue = {Double.MaxValue}; MaxValue * 10 = {Double.MaxValue * 10}")
Console.WriteLine($"Single.MaxValue = {Single.MaxValue}; 16777217 as Single = {CSng(16777217)}")
Console.WriteLine($"Double has 15-17 significant digits: 9007199254740993 as Double = {CDbl(9007199254740993L):F0}")
Console.WriteLine("-- Sorting with NaN")
Dim values As Double() = {3.5, Double.NaN, -1, 10, Double.NegativeInfinity}
Array.Sort(values)
Console.WriteLine(String.Join(", ", values))
Console.WriteLine($"Max ignoring NaN: {values.Where(Function(v) Not Double.IsNaN(v)).Max()}; Math.Max(1, NaN) = {Math.Max(1.0, Double.NaN)}")
Console.WriteLine("-- Converting to text and back")
Console.WriteLine($"{0.1:R} {1 / 3.0} {100000000000000000000.0} {1.0E-7} {1234567.0F}")
Console.WriteLine($"Double.Parse(""0.30000000000000004"") = {Double.Parse("0.30000000000000004", Globalization.CultureInfo.InvariantCulture)}")
End Sub
End Module
1/0 = Infinity, -1/0 = -Infinity, 0/0 = NaN
IsInfinity True, IsPositiveInfinity True, IsNaN True, IsFinite(1) True
NaN = NaN: False; NaN <> NaN: True; NaN < 1: False; NaN.Equals(NaN): True; CompareTo: 0
Infinity - Infinity = NaN; Infinity * 0 = NaN; 1 / Infinity = 0
Math.Sqrt(-1) = NaN; Math.Log(0) = -Infinity; Math.Log(-1) = NaN
-0.0 = 0.0: True; 1 / -0.0 = -Infinity; -0.0 prints as -0
-- Comparing floating-point results
Ten times 0.1 = 0.9999999999999999; = 1.0: False; NearlyEqual: True
Double.Epsilon = 5E-324 (smallest positive, not a tolerance); Math.BitIncrement(1.0) - 1.0 = 2.220446049250313E-16
Double.MaxValue = 1.7976931348623157E+308; MaxValue * 10 = Infinity
Single.MaxValue = 3.4028235E+38; 16777217 as Single = 16777216
Double has 15-17 significant digits: 9007199254740993 as Double = 9007199254740992
-- Sorting with NaN
NaN, -Infinity, -1, 3.5, 10
Max ignoring NaN: 10; Math.Max(1, NaN) = NaN
-- Converting to text and back
0.1 0.3333333333333333 1E+20 1E-07 1234567
Double.Parse("0.30000000000000004") = 0.30000000000000004
Source: Double Struct
Also visible in the output: Single has about 7 significant digits (16777217 is not representable), Double about 15 to 17 (253 + 1 is not representable), and .NET prints the shortest string that round-trips (0.1, 0.30000000000000004), which is why the output of Double calculations looks longer than in VB6. The format R is the same as the default for round-tripping.
Parsing numbers: Val, IsNumeric and TryParse
Visual Basic keeps its VB6 parsing helpers. Val(text) reads the leading numeric part of a string and ignores the rest (and spaces), returns 0 if there is none, always treats the point as the decimal separator regardless of culture, understands the prefixes &H and &O, and returns a Double. IsNumeric is culture-sensitive and accepts what CDbl accepts, including
leading and trailing white space, group separators, and the &H... and &O... forms. Both are convenient in throwaway code and misleading in anything that handles data: Val("1,5") is 1 while CDbl("1,5") is 1.5 under de-DE. The reliable tools are TryParse with an explicit NumberStyles and CultureInfo, and Parse when
failure should be an exception (FormatException for bad text, OverflowException for text that does not fit). CInt, CDbl and friends throw InvalidCastException for text that does not parse.
Option Strict On
Imports System.Globalization
Imports System.Threading
Module Ex08
Sub Main()
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("de-DE")
Console.WriteLine("-- Val: legacy, forgiving, always uses the point as decimal separator")
Console.WriteLine($"Val(""12abc"") = {Val("12abc")}, Val("" 3.5kg"") = {Val(" 3.5kg")}, Val(""abc"") = {Val("abc")}, Val(""1,5"") = {Val("1,5")}")
Console.WriteLine($"Val(""&HFF"") = {Val("&HFF")}, Val(""&O17"") = {Val("&O17")}, Val(""1e3"") = {Val("1e3")}, Val(""-.5"") = {Val("-.5")}")
Console.WriteLine("-- IsNumeric and CInt follow the current culture")
For Each sample In {"42", "1,5", "1.5", "1.234,56", "$5", "&HFF", "1e3", " 7 ", "", "twelve"}
Console.WriteLine($"{sample,-10} IsNumeric = {IsNumeric(sample),-5} Double.TryParse(current) = {Double.TryParse(sample, Nothing)}")
Next
Console.WriteLine($"CDbl(""1,5"") under de-DE = {CDbl("1,5")}; CInt(""1.234"") = {CInt("1.234")}")
Console.WriteLine("-- The .NET way: TryParse with an explicit culture and style")
Dim n As Integer
Dim d As Decimal
Console.WriteLine($"Integer.TryParse(""42"", n) = {Integer.TryParse("42", n)}, n = {n}")
Console.WriteLine($"Integer.TryParse(""4 2"", n) = {Integer.TryParse("4 2", n)}")
Console.WriteLine($"Integer.TryParse("" 42 "", n) = {Integer.TryParse(" 42 ", n)} (white space is allowed)")
Console.WriteLine($"Integer.TryParse(""1,000"", n) = {Integer.TryParse("1,000", n)} (de-DE: comma is the decimal separator)")
Console.WriteLine($"Integer.TryParse(""1,000"", NumberStyles.AllowThousands, en-US, n) = {Integer.TryParse("1,000", NumberStyles.AllowThousands, CultureInfo.GetCultureInfo("en-US"), n)}, n = {n}")
Console.WriteLine($"Decimal.TryParse(""1.234,56"", Number, de-DE) = {Decimal.TryParse("1.234,56", NumberStyles.Number, CultureInfo.CurrentCulture, d)}, d = {d}")
Console.WriteLine($"Integer.TryParse(""FF"", HexNumber, invariant, n) = {Integer.TryParse("FF", NumberStyles.HexNumber, CultureInfo.InvariantCulture, n)}, n = {n}")
Try
Console.WriteLine(Integer.Parse("12x"))
Catch ex As FormatException
Console.WriteLine("Integer.Parse(""12x"") throws " & ex.GetType().Name)
End Try
Try
Console.WriteLine(Integer.Parse("99999999999"))
Catch ex As OverflowException
Console.WriteLine("Integer.Parse(""99999999999"") throws " & ex.GetType().Name)
End Try
Try
Console.WriteLine(CInt("12x"))
Catch ex As InvalidCastException
Console.WriteLine("CInt(""12x"") throws " & ex.GetType().Name)
End Try
Console.WriteLine("-- Hex, Oct and radix literals")
Console.WriteLine($"Hex(255) = {Hex(255)}, Hex(-1) = {Hex(-1)}, Oct(8) = {Oct(8)}, Convert.ToString(10, 2) = {Convert.ToString(10, 2)}, Convert.ToInt32(""1010"", 2) = {Convert.ToInt32("1010", 2)}")
Console.WriteLine($"&HFF = {&HFF}, &O17 = {&O17}, &B1010 = {&B1010}, 1_000_000 = {1_000_000}, &H7FFFFFFF = {&H7FFFFFFF}, &H80000000 = {&H80000000}")
Console.WriteLine($"Number type chars: 5I {5I.GetType().Name}, 5L {5L.GetType().Name}, 5S {5S.GetType().Name}, 5.0F {5.0F.GetType().Name}, 5.0R {5.0R.GetType().Name}, 5D {5D.GetType().Name}, 5UI {5UI.GetType().Name}")
End Sub
End Module
-- Val: legacy, forgiving, always uses the point as decimal separator
Val("12abc") = 12, Val(" 3.5kg") = 3,5, Val("abc") = 0, Val("1,5") = 1
Val("&HFF") = 255, Val("&O17") = 15, Val("1e3") = 1000, Val("-.5") = -0,5
-- IsNumeric and CInt follow the current culture
42 IsNumeric = True Double.TryParse(current) = True
1,5 IsNumeric = True Double.TryParse(current) = True
1.5 IsNumeric = True Double.TryParse(current) = True
1.234,56 IsNumeric = True Double.TryParse(current) = True
$5 IsNumeric = False Double.TryParse(current) = False
&HFF IsNumeric = True Double.TryParse(current) = False
1e3 IsNumeric = True Double.TryParse(current) = True
7 IsNumeric = True Double.TryParse(current) = True
IsNumeric = False Double.TryParse(current) = False
twelve IsNumeric = False Double.TryParse(current) = False
CDbl("1,5") under de-DE = 1,5; CInt("1.234") = 1234
-- The .NET way: TryParse with an explicit culture and style
Integer.TryParse("42", n) = True, n = 42
Integer.TryParse("4 2", n) = False
Integer.TryParse(" 42 ", n) = True (white space is allowed)
Integer.TryParse("1,000", n) = False (de-DE: comma is the decimal separator)
Integer.TryParse("1,000", NumberStyles.AllowThousands, en-US, n) = True, n = 1000
Decimal.TryParse("1.234,56", Number, de-DE) = True, d = 1234,56
Integer.TryParse("FF", HexNumber, invariant, n) = True, n = 255
Integer.Parse("12x") throws FormatException
Integer.Parse("99999999999") throws OverflowException
CInt("12x") throws InvalidCastException
-- Hex, Oct and radix literals
Hex(255) = FF, Hex(-1) = FFFFFFFF, Oct(8) = 10, Convert.ToString(10, 2) = 1010, Convert.ToInt32("1010", 2) = 10
&HFF = 255, &O17 = 15, &B1010 = 10, 1_000_000 = 1000000, &H7FFFFFFF = 2147483647, &H80000000 = -2147483648
Number type chars: 5I Int32, 5L Int64, 5S Int16, 5.0F Single, 5.0R Double, 5D Decimal, 5UI UInt32
Source: Conversion Class
Radix and literal notes visible above: &HFF (hexadecimal), &O17 (octal), &B1010 (binary), digit separators 1_000_000 and the binary literal (VB 15), and the type characters and suffixes I, L, S, UI, UL, US, F, R, D. An &H literal without a suffix is an Integer when it has up to 8 hex digits, so &H80000000 is -2147483648, not 2147483648;
write &H80000000UI or &H80000000L for the unsigned or long value. Hex(-1) returns the two’s-complement digits FFFFFFFF.
Generic math from Visual Basic
INumber(Of T), IAdditionOperators, IBinaryInteger, IFloatingPoint and others) whose members are static abstract: operators and constants such as T.Zero. Visual Basic 17.13 can consume the interfaces but cannot call their static abstract members through a type parameter, and it cannot declare such interfaces. Thepractical limits, all of them verified by the build below:
-
A method may constrain a type parameter to
INumber(Of T), and may call the instance members that the interfaces expose (CompareTo,Equals,ToString). -
T.Zerois BC32098 ("Type parameters cannot be used as qualifiers"), anda + bon twoTvalues is BC30452, so no generic sum, average or dot product can be written over anINumber. -
The static methods of the concrete types remain callable by name:
Integer.CreateSaturating(3000000000L),Byte.CreateChecked(300),Double.Clamp,Integer.IsEvenInteger,Double.Parse. -
The usual workaround is to pass the operations in (a
Func(Of T, T, T)and a zero value), to use LINQ’sSum,AverageandAggregate, or to put the generic algorithm in a C# library and call it from VB.
Option Strict On
Imports System.Numerics
Module Ex09
' The constraint is accepted, and so are the instance members the interfaces expose.
Function Largest(Of T As INumber(Of T))(first As T, second As T) As T
Return If(first.CompareTo(second) >= 0, first, second)
End Function
' Operators and static members of T are out of reach in VB, so pass in what you need.
Function SumOf(Of T)(items As IEnumerable(Of T), zero As T, add As Func(Of T, T, T)) As T
Dim total = zero
For Each item In items
total = add(total, item)
Next
Return total
End Function
Sub Main()
Console.WriteLine($"Largest(3, 9) = {Largest(3, 9)}, Largest(2.5, -1.0) = {Largest(2.5, -1.0)}, Largest(BigInteger.One, BigInteger.Zero) = {Largest(BigInteger.One, BigInteger.Zero)}")
Console.WriteLine($"SumOf Integer: {SumOf(New Integer() {1, 2, 3}, 0, Function(a, b) a + b)}")
Console.WriteLine($"SumOf Decimal: {SumOf(New Decimal() {0.1D, 0.2D}, 0D, Function(a, b) a + b)}")
Console.WriteLine($"SumOf BigInteger: {SumOf(New BigInteger() {BigInteger.Pow(2, 70), 1}, BigInteger.Zero, Function(a, b) a + b)}")
Console.WriteLine($"LINQ Sum: {New Integer() {1, 2, 3}.Sum()}, Average: {New Integer() {1, 2, 4}.Average():F3}, BigInteger Aggregate: {New BigInteger() {2, 3, 4}.Aggregate(BigInteger.One, Function(a, b) a * b)}")
' Static methods of the numeric types are ordinary shared members and can be called by name.
Console.WriteLine($"Integer.CreateSaturating(3000000000L) = {Integer.CreateSaturating(3000000000L)}")
Console.WriteLine($"Integer.CreateTruncating(3000000000L) = {Integer.CreateTruncating(3000000000L)}")
Console.WriteLine($"Double.Clamp(5.5, 0, 1) = {Double.Clamp(5.5, 0.0, 1.0)}, Integer.Abs(-4) = {Integer.Abs(-4)}, Integer.IsEvenInteger(7) = {Integer.IsEvenInteger(7)}")
Console.WriteLine($"Byte.CreateChecked(200) = {Byte.CreateChecked(200)}")
Try
Console.WriteLine(Byte.CreateChecked(300))
Catch ex As OverflowException
Console.WriteLine("Byte.CreateChecked(300) throws " & ex.GetType().Name)
End Try
Console.WriteLine($"Double.Parse via IParsable: {Double.Parse("2.5", Globalization.CultureInfo.InvariantCulture)}")
Console.WriteLine($"Integer.MaxValue (IMinMaxValue): {Integer.MaxValue}, BigInteger.One: {BigInteger.One}, Double.E: {Double.E:F4}")
End Sub
End Module
Largest(3, 9) = 9, Largest(2.5, -1.0) = 2.5, Largest(BigInteger.One, BigInteger.Zero) = 1
SumOf Integer: 6
SumOf Decimal: 0.3
SumOf BigInteger: 1180591620717411303425
LINQ Sum: 6, Average: 2.333, BigInteger Aggregate: 24
Integer.CreateSaturating(3000000000L) = 2147483647
Integer.CreateTruncating(3000000000L) = -1294967296
Double.Clamp(5.5, 0, 1) = 1, Integer.Abs(-4) = 4, Integer.IsEvenInteger(7) = False
Byte.CreateChecked(200) = 200
Byte.CreateChecked(300) throws OverflowException
Double.Parse via IParsable: 2.5
Integer.MaxValue (IMinMaxValue): 2147483647, BigInteger.One: 1, Double.E: 2.7183
Source: Generic math in .NET
The following program does not compile on purpose: it collects the errors of the preceding and a few related mistakes. The text shows the compiler output.
Option Strict On
Imports System.Numerics
Module Ex10
Function SumOf(Of T As INumber(Of T))(items As IEnumerable(Of T)) As T
Dim total As T = T.Zero
For Each item In items
total = total + item
Next
Return total
End Function
Sub Main()
Dim big As BigInteger = BigInteger.Pow(2, 100)
Dim half = big \ 2
Dim square = big ^ 2
Dim power As Integer = 2 ^ 10
Dim ratio As Integer = 7 / 2
Console.WriteLine(SumOf(New Integer() {1, 2, 3}))
End Sub
End Module
Ex10.vb(6,26): error BC32098: Type parameters cannot be used as qualifiers.
Ex10.vb(8,21): error BC30452: Operator '+' is not defined for types 'T' and 'T'.
Ex10.vb(15,20): error BC30452: Operator '\' is not defined for types 'BigInteger' and 'Integer'.
Ex10.vb(16,22): error BC30452: Operator '^' is not defined for types 'BigInteger' and 'Integer'.
Ex10.vb(17,32): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Ex10.vb(18,32): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Comparison with C# and the older books
| Topic | Visual Basic | C# |
|---|---|---|
|
|
|
Remainder |
|
|
Power |
|
no operator; |
Overflow of |
|
wraps by default; |
Constant overflow |
Compile error BC30439 |
Compile error CS0220 |
Rounding |
|
|
Division by zero |
|
Same |
Random |
|
|
Financial functions |
|
none (packages) |
Generic math |
can constrain to |
full support |
Where the books are outdated. Older material, including the two books that this section was planned from, says that dividing by zero throws DivideByZeroException. That is true for \, Mod and Decimal division, and false for / on integers or Double: 10 / 0 is Infinity. They show Dim q As Integer = a / b and a ^ b assigned to an Integer, which
do not compile under Option Strict On (both operators return Double). They teach Rnd and Randomize for random numbers, where Random and Random.Shared are current, and Val and IsNumeric for input validation, where TryParse with a culture is current. Banker’s rounding in Math.Round and CInt is often presented as a surprise or omitted. They predate
BigInteger, Int128, Decimal formatting changes and generic math.
Pitfalls checklist
-
/returns aDouble;\is integer division;^returns aDoubleand is not Xor;-2 ^ 2is -4. -
Math.Round,CIntandDecimal.Roundround ties to even;ToString("F0")follows a different rule forDoublethan forDecimal. Choose theMidpointRoundingexplicitly. -
Intfloors,Fixtruncates;Math.FloorreturnsDouble. -
Integer overflow throws, constants that overflow do not compile;
RemoveIntegerChecksis project-wide and hides bugs. -
Math.Abs(Integer.MinValue)andInteger.MinValue \ -1throw. -
0.1 + 0.2 = 0.3isFalseforDouble,TrueforDecimal. Compare doubles with a relative tolerance;NaNnever equals anything. -
Decimaldivision by zero throws;Doubledivision by zero does not. -
Modkeeps the sign of the dividend. -
A literal like
0.1isDouble; money literals needD.&H80000000is negative. -
Random.Next(1, 7)never returns 7; do not useRandomfor secrets; do not create a newRandomin a loop. -
Valignores the culture,IsNumericandCDbldo not; useTryParsewith a culture. -
NPVdiscounts the first cash flow;Pmtneeds the signs right. -
T.Zeroand+on genericTdo not compile.