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) and CInt(x) and CLng(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.5 is the Double negative zero, printed -0. VB6’s CInt used the same rule, so this is not new, but C# programmers expect Math.Round(2.5) to give 3 because of school arithmetic.

  • Math.Round(x, MidpointRounding.AwayFromZero) is the "school" rounding. The other modes are ToEven (the default), ToZero, ToNegativeInfinity and ToPositiveInfinity; the digits overload is Math.Round(x, digits, mode), with 0 to 15 digits for Double and 0 to 28 for Decimal. 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") is 3 but 2.5.ToString("F0") is 2. 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 / b returns a Double when the operands are integers: 7 / 2 is 3.5, and Dim ratio As Integer = 7 / 2 is BC30512 under Option Strict On. If both operands are Decimal the result is Decimal. Dividing by zero gives Infinity or NaN for Double (no exception) and throws DivideByZeroException for Decimal.

  • a \ b is integer division and truncates toward zero: -7 \ 2 is -3. It works on integer operands (7.9 \ 2 is BC30512 under Option Strict On, because a Double does not convert implicitly to Long), throws DivideByZeroException for a zero divisor, and Integer.MinValue \ -1 throws OverflowException.

  • a Mod b returns the remainder with the sign of the dividend (-7 Mod 2 is -1), the same as C#'s %. It works on floating point too (7.5 Mod 2 is 1.5) and throws for an integer zero divisor. For a result that is never negative use ((a Mod n) + n) Mod n; Math.IEEERemainder is a different (round-to-nearest) definition.

  • a ^ b converts its operands to Double, calls Math.Pow and returns a Double: assigning it to an Integer needs CInt(2 ^ 10) (BC30512 otherwise). Its precedence is higher than unary minus, so -2 ^ 2 is -4. ^ is not "exclusive or" (that is Xor), and it is undefined for BigInteger.

  • The shift operators << and >> mask the shift count to the type’s width (a shift of 32 on an Integer shifts 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 the D suffix (0.1D) or the @ type character; a bare 0.1 is a Double, 1D + 0.1 is a Double and assigning it to a Decimal is BC30512 under Option Strict On (write 1D + 0.1D, or CDec(0.1)).

  • Measurements, geometry, statistics, anything with Math.Sqrt or trigonometry: Double.

  • Decimal keeps trailing zeros that you wrote or computed: 1.10D * 2 is 2.20, and Decimal.GetBits shows the scale. This is useful for money and surprising in comparisons of text; = ignores scale.

  • CDec(0.1 + 0.2) is 0.3 because the conversion from Double rounds to 15 significant digits; that hides, not repairs, the noise in the Double result.

  • 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

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

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

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

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

NET 7 turned the numeric types into a hierarchy of generic interfaces (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. The

practical 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.Zero is BC32098 ("Type parameters cannot be used as qualifiers"), and a + b on two T values is BC30452, so no generic sum, average or dot product can be written over an INumber.

  • 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’s Sum, Average and Aggregate, 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

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#

7 / 2

3.5 (a Double); \ for integer division

3 for two integers

Remainder

Mod (also for floating point)

%

Power

^ returns Double (Math.Pow)

no operator; ^ is exclusive-or

Overflow of +, *

OverflowException by default; project-wide RemoveIntegerChecks

wraps by default; checked/unchecked blocks

Constant overflow

Compile error BC30439

Compile error CS0220

Rounding

Math.Round ties to even; CInt ties to even; Int/Fix

Math.Round ties to even; casts truncate

Division by zero

Double: Infinity/NaN; integer and Decimal: exception

Same

Random

Random, Random.Shared, legacy Rnd/Randomize

Random, Random.Shared

Financial functions

Financial module built in

none (packages)

Generic math

can constrain to INumber(Of T), cannot use T.Zero or + on T

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 a Double; \ is integer division; ^ returns a Double and is not Xor; -2 ^ 2 is -4.

  • Math.Round, CInt and Decimal.Round round ties to even; ToString("F0") follows a different rule for Double than for Decimal. Choose the MidpointRounding explicitly.

  • Int floors, Fix truncates; Math.Floor returns Double.

  • Integer overflow throws, constants that overflow do not compile; RemoveIntegerChecks is project-wide and hides bugs.

  • Math.Abs(Integer.MinValue) and Integer.MinValue \ -1 throw.

  • 0.1 + 0.2 = 0.3 is False for Double, True for Decimal. Compare doubles with a relative tolerance; NaN never equals anything.

  • Decimal division by zero throws; Double division by zero does not.

  • Mod keeps the sign of the dividend.

  • A literal like 0.1 is Double; money literals need D. &H80000000 is negative.

  • Random.Next(1, 7) never returns 7; do not use Random for secrets; do not create a new Random in a loop.

  • Val ignores the culture, IsNumeric and CDbl do not; use TryParse with a culture.

  • NPV discounts the first cash flow; Pmt needs the signs right.

  • T.Zero and + on generic T do not compile.