Dates, Times and Formatting

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.

Dates and numbers are the two kinds of value whose text form depends on who is looking: the same DateTime is 1/5/2026 in the United States, 05.01.2026 in Germany and 2026/01/05 in Japan, and the same Decimal has a comma or a point as its decimal separator. Visual Basic adds its own layer to the .NET types: the Date keyword with its #...# literals, the DateAndTime module that carries the VB6 date functions, and the Format family of functions. This page explains the .NET date and time types as a VB programmer sees them, the VB functions and where they differ from the methods, the culture rules for parsing and formatting, and the format strings for both dates and numbers. 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, where the current culture is the invariant culture and the time zone is UTC; programs that depend on a culture set it explicitly, and programs with dates avoid Now unless the output does not depend on it. The page on Numbers and Math covers the arithmetic, rounding and the Math class, and Strings and Text covers composite formatting, interpolation basics and culture-sensitive comparison.

The Date type and date literals

Date is the Visual Basic keyword for System.DateTime: a value type holding the number of 100-nanosecond ticks since midnight on 1 January 0001 in the proleptic Gregorian calendar, up to 31 December 9999, plus a Kind that records whether the value is meant as local time, UTC, or unspecified. There is no time-zone information in a Date, and comparisons and arithmetic ignore Kind. The default value, Nothing or Date.MinValue, is 1 January 0001; a missing date needs Date? (see Nothing and Nullable Value Types).

A date literal is enclosed in number signs and is always written in a culture-independent form, so a program compiles the same on every machine: the US form #1/15/2026# (month first) or the ISO-like form #2026-01-15#, optionally followed by a time in 12-hour (3:45:30 PM) or 24-hour (15:45:30) notation. The literal has no fractional seconds (#2026-01-05 15:04:09.123# is BC30201 plus BC31085, "Date constant is not valid"): for milliseconds, call New Date(2026, 1, 5, 15, 4, 9, 123). A literal is a Date only: there is no literal for DateOnly, TimeOnly or DateTimeOffset (Dim o As DateOnly = #1/15/2026# is BC30311), which are built with constructors or conversions.

Option Strict On
Imports System.Globalization

Module Ex01
    Sub Main()
        ' Date is an alias of System.DateTime. Literals are always written in the US format.
        Dim dueOn As Date = #1/15/2026#
        Dim afternoon As Date = #1/15/2026 3:45:30 PM#
        Dim isoLike As Date = #2026-01-15 15:45:30#
        Console.WriteLine($"{dueOn:yyyy-MM-dd HH:mm:ss}  {afternoon:yyyy-MM-dd HH:mm:ss}  {isoLike = afternoon}")
        Console.WriteLine($"GetType: {dueOn.GetType().FullName}, Kind: {dueOn.Kind}")

        ' Components: properties on the value, or the VB functions of the same names.
        Console.WriteLine($"Year/Month/Day: {dueOn.Year}/{dueOn.Month}/{dueOn.Day}, DayOfWeek: {dueOn.DayOfWeek}, DayOfYear: {dueOn.DayOfYear}")
        Console.WriteLine($"Functions: {Year(dueOn)} {Month(dueOn)} {Day(dueOn)} {Hour(afternoon)} {Minute(afternoon)} {Second(afternoon)} Weekday={Weekday(dueOn)}")

        ' Building dates
        Console.WriteLine($"DateSerial(2026, 2, 31) = {DateSerial(2026, 2, 31):yyyy-MM-dd}  (overflows into March)")
        Console.WriteLine($"New Date(2026, 2, 28)   = {New Date(2026, 2, 28):yyyy-MM-dd}")
        Console.WriteLine($"TimeSerial(25, 0, 0)    = {TimeSerial(25, 0, 0):yyyy-MM-dd HH:mm}  (date part is 0001-01-01 plus the overflow)")
        Try
            Console.WriteLine(New Date(2026, 2, 31))
        Catch ex As ArgumentOutOfRangeException
            Console.WriteLine("New Date(2026, 2, 31) throws " & ex.GetType().Name)
        End Try

        ' Arithmetic returns TimeSpan or Date
        Dim loanedOn As Date = #1/1/2026#
        Dim span As TimeSpan = dueOn - loanedOn
        Console.WriteLine($"Loan length: {span.TotalDays} days; {dueOn.AddDays(14):yyyy-MM-dd} is two weeks later")
        Console.WriteLine($"Comparisons: {loanedOn < dueOn}, {Date.Compare(loanedOn, dueOn)}, {dueOn.Equals(isoLike.Date)}")

        ' Defaults and limits
        Dim nothingDate As Date = Nothing
        Console.WriteLine($"Default: {nothingDate:yyyy-MM-dd}, MinValue = {Date.MinValue:yyyy-MM-dd}, MaxValue year = {Date.MaxValue.Year}")
        Console.WriteLine($"Leap year 2028: {Date.IsLeapYear(2028)}, days in Feb 2028: {Date.DaysInMonth(2028, 2)}")
        Console.WriteLine($"Ticks of 1 s: {TimeSpan.FromSeconds(1).Ticks}; DateTime.Now.Kind: {Date.Now.Kind}; UtcNow.Kind: {Date.UtcNow.Kind}; Today.TimeOfDay: {Date.Today.TimeOfDay}")
        Dim parsedLater As Date? = Nothing
        Console.WriteLine($"Date? default has value: {parsedLater.HasValue}")
    End Sub
End Module
2026-01-15 00:00:00  2026-01-15 15:45:30  True
GetType: System.DateTime, Kind: Unspecified
Year/Month/Day: 2026/1/15, DayOfWeek: Thursday, DayOfYear: 15
Functions: 2026 1 15 15 45 30 Weekday=5
DateSerial(2026, 2, 31) = 2026-03-03  (overflows into March)
New Date(2026, 2, 28)   = 2026-02-28
TimeSerial(25, 0, 0)    = 0001-01-02 01:00  (date part is 0001-01-01 plus the overflow)
New Date(2026, 2, 31) throws ArgumentOutOfRangeException
Loan length: 14 days; 2026-01-29 is two weeks later
Comparisons: True, -1, True
Default: 0001-01-01, MinValue = 0001-01-01, MaxValue year = 9999
Leap year 2028: True, days in Feb 2028: 29
Ticks of 1 s: 10000000; DateTime.Now.Kind: Local; UtcNow.Kind: Utc; Today.TimeOfDay: 00:00:00
Date? default has value: False

Things the program shows, and some related rules:

  • New Date(y, m, d) throws ArgumentOutOfRangeException for an impossible date; DateSerial(y, m, d) is lenient and rolls over (DateSerial(2026, 2, 31) is 3 March) and TimeSerial(25, 0, 0) returns a Date on 1 January 0001 plus one day and one hour. Use the constructor when the input should be valid.

  • Year(d), Month(d), Day(d), Hour(d), Minute(d), Second(d) and Weekday(d) are the VB functions, and d.Year, d.Month, d.DayOfWeek…​ the properties. Weekday returns 1 for Sunday (and 5 for Thursday) by default, DayOfWeek is the 0-based DayOfWeek enumeration (Sunday = 0): the numbers differ by one.

  • The - operator on two dates returns a TimeSpan; there is no + between Date and a number. Visual Basic 6 stored dates as a Double and let you write d + 1, .NET does not: d + 1 is BC30452 ("Operator + is not defined for types Date and Integer`"), and `CDbl(d) is BC30532 ("requires calling the Date.ToOADate method"). Use d.AddDays(1), DateAdd(DateInterval.Day, 1, d) or d + TimeSpan.FromDays(1). ToOADate and Date.FromOADate convert to and from the VB6/VBA/Excel serial number when you need it.

  • Now (the property of the DateAndTime module), Date.Now and DateTime.Now return the local time (Kind = Local); Today and Date.Today the local date at midnight; Date.UtcNow the current UTC instant. TimeOfDay, TimeString, DateString and Timer (seconds since midnight, as a Double) are legacy VB properties. Prefer DateTime.UtcNow or an injected TimeProvider for anything stored or compared.

  • A date has 100 ns resolution (TimeSpan.TicksPerSecond = 10,000,000) but the system clock ticks coarsely, and Now is not a stopwatch: use Stopwatch or TimeProvider.GetTimestamp to measure durations.

  • Two-digit years follow the calendar’s TwoDigitYearMax, 2049 for the default Gregorian calendar: Date.Parse("1/1/49") is 2049 and Date.Parse("1/1/50") is 1950 (checked). Never accept two-digit years from an external source.

The DateAndTime module and the DateTime members

The VB6 date functions live in Microsoft.VisualBasic.DateAndTime and are always in scope. Each has a .NET counterpart; the table shows how they differ.

VB function .NET counterpart Notes

DateAdd(interval, n, d)

d.AddYears, AddMonths, AddDays, AddHours…​

DateInterval has Year, Quarter, Month, DayOfYear, Day, Weekday, WeekOfYear, Hour, Minute, Second. Weekday adds calendar days, not working days. Month arithmetic clamps to the end of the shorter month, as AddMonths does: 31 January plus one month is 28 February

DateDiff(interval, d1, d2)

(d2 - d1).TotalDays…​

Not simply the elapsed time: see below

DatePart(interval, d)

d.Year, Calendar.GetWeekOfYear…​

DatePart(DateInterval.WeekOfYear, d) depends on FirstDayOfWeek and FirstWeekOfYear arguments (the defaults are Sunday and the week that contains 1 January); ISOWeek.GetWeekOfYear gives the ISO 8601 week

DateSerial(y, m, d), TimeSerial(h, m, s)

New Date(...), New TimeSpan(...)

Lenient: out-of-range values roll over

DateValue(text), TimeValue(text), CDate(text)

Date.Parse, DateOnly.Parse, TimeOnly.Parse

Parse with the current culture of the thread; see the next sections

IsDate(text)

Date.TryParse

True for text that the current culture parses; IsDate("2026-02-30") is False

Weekday(d), WeekdayName(n), MonthName(n)

d.DayOfWeek, d.ToString("dddd"), CultureInfo.DateTimeFormat.GetMonthName

Names come from the current culture; the week of Weekday starts on Sunday unless FirstDayOfWeek says otherwise

Year, Month, Day, Hour, Minute, Second

d.Year…​

Inside a class that has a member with one of these names (such as the CatalogItem.Year property), the member hides the function; qualify it as DateAndTime.Year(d)

Now, Today, TimeOfDay, Timer

Date.Now, Date.Today, d.TimeOfDay, Stopwatch

Same values; Timer is a Double number of seconds since midnight

Option Strict On
Imports System.Globalization

Module Ex02
    Sub Main()
        Dim loaned As Date = #12/31/2025 11:59:00 PM#
        Dim returned As Date = #1/1/2026 12:01:00 AM#

        Console.WriteLine("-- DateDiff: calendar boundaries for Year, Quarter and Month; whole elapsed units for the others")
        Console.WriteLine($"DateDiff(Year,   ...) = {DateDiff(DateInterval.Year, loaned, returned)}")
        Console.WriteLine($"DateDiff(Month,  ...) = {DateDiff(DateInterval.Month, loaned, returned)}")
        Console.WriteLine($"DateDiff(Day,    ...) = {DateDiff(DateInterval.Day, loaned, returned)}")
        Console.WriteLine($"DateDiff(Minute, ...) = {DateDiff(DateInterval.Minute, loaned, returned)}")
        Console.WriteLine($"DateDiff(Quarter, Mar 31 -> Apr 1) = {DateDiff(DateInterval.Quarter, #3/31/2026#, #4/1/2026#)}")
        Console.WriteLine($"DateDiff(WeekOfYear, Sat 23:59 -> Sun 00:01) = {DateDiff(DateInterval.WeekOfYear, #1/3/2026 11:59:00 PM#, #1/4/2026 12:01:00 AM#)}")
        Console.WriteLine($"DateDiff(Day, Mon 08:00 -> Wed 07:00) = {DateDiff(DateInterval.Day, #1/12/2026 8:00:00 AM#, #1/14/2026 7:00:00 AM#)}")
        Console.WriteLine($"(returned - loaned).TotalMinutes = {(returned - loaned).TotalMinutes}, TotalDays = {(returned - loaned).TotalDays:F4}")

        Console.WriteLine("-- DateAdd versus the Add methods")
        Dim jan31 As Date = #1/31/2026#
        Console.WriteLine($"DateAdd(Month, 1, Jan 31)  = {DateAdd(DateInterval.Month, 1, jan31):yyyy-MM-dd}")
        Console.WriteLine($"Jan 31 .AddMonths(1)       = {jan31.AddMonths(1):yyyy-MM-dd}")
        Console.WriteLine($"Jan 31 .AddMonths(13)      = {jan31.AddMonths(13):yyyy-MM-dd}")
        Console.WriteLine($"DateAdd(Day, -40, Jan 31)  = {DateAdd(DateInterval.Day, -40, jan31):yyyy-MM-dd}")
        Console.WriteLine($"DateAdd(Weekday, 3, Fri)   = {DateAdd(DateInterval.Weekday, 3, #1/16/2026#):yyyy-MM-dd}  (Weekday adds plain days)")
        Console.WriteLine($"DateAdd(Quarter, 1, Jan 31)= {DateAdd(DateInterval.Quarter, 1, jan31):yyyy-MM-dd}")

        Console.WriteLine("-- DatePart, Weekday and names")
        Dim d As Date = #1/15/2026#
        Console.WriteLine($"DatePart(Quarter) = {DatePart(DateInterval.Quarter, d)}, DatePart(WeekOfYear) = {DatePart(DateInterval.WeekOfYear, d)}")
        Console.WriteLine($"Weekday(d) = {Weekday(d)} (Sunday = 1); Weekday(d, FirstDayOfWeek.Monday) = {Weekday(d, FirstDayOfWeek.Monday)}; DayOfWeek = {CInt(d.DayOfWeek)}")
        Console.WriteLine($"WeekdayName = {WeekdayName(Weekday(d))}, MonthName = {MonthName(Month(d))}")
        Dim german = CultureInfo.GetCultureInfo("de-DE")
        Console.WriteLine($"German day name via ToString: {d.ToString("dddd", german)}, ISO week: {ISOWeek.GetWeekOfYear(d)}")

        Console.WriteLine("-- Truncating and comparing")
        Dim stamp As Date = #1/15/2026 3:45:30 PM#
        Console.WriteLine($"stamp.Date = {stamp.Date:yyyy-MM-dd HH:mm}, TimeOfDay = {stamp.TimeOfDay}")
        Console.WriteLine($"stamp = stamp.Date : {stamp = stamp.Date}")
        Console.WriteLine($"Start of month: {New Date(stamp.Year, stamp.Month, 1):yyyy-MM-dd}, end of month: {New Date(stamp.Year, stamp.Month, Date.DaysInMonth(stamp.Year, stamp.Month)):yyyy-MM-dd}")
        Console.WriteLine($"Age on 2026-03-01 for a birth date of 2000-03-02: {Age(#3/2/2000#, #3/1/2026#)}")
    End Sub

    Function Age(born As Date, asOf As Date) As Integer
        Dim years = asOf.Year - born.Year
        If asOf < born.AddYears(years) Then years -= 1
        Return years
    End Function
End Module
-- DateDiff: calendar boundaries for Year, Quarter and Month; whole elapsed units for the others
DateDiff(Year,   ...) = 1
DateDiff(Month,  ...) = 1
DateDiff(Day,    ...) = 0
DateDiff(Minute, ...) = 2
DateDiff(Quarter, Mar 31 -> Apr 1) = 1
DateDiff(WeekOfYear, Sat 23:59 -> Sun 00:01) = 0
DateDiff(Day, Mon 08:00 -> Wed 07:00) = 1
(returned - loaned).TotalMinutes = 2, TotalDays = 0.0014
-- DateAdd versus the Add methods
DateAdd(Month, 1, Jan 31)  = 2026-02-28
Jan 31 .AddMonths(1)       = 2026-02-28
Jan 31 .AddMonths(13)      = 2027-02-28
DateAdd(Day, -40, Jan 31)  = 2025-12-22
DateAdd(Weekday, 3, Fri)   = 2026-01-19  (Weekday adds plain days)
DateAdd(Quarter, 1, Jan 31)= 2026-04-30
-- DatePart, Weekday and names
DatePart(Quarter) = 1, DatePart(WeekOfYear) = 3
Weekday(d) = 5 (Sunday = 1); Weekday(d, FirstDayOfWeek.Monday) = 4; DayOfWeek = 4
WeekdayName = Thursday, MonthName = January
German day name via ToString: Donnerstag, ISO week: 3
-- Truncating and comparing
stamp.Date = 2026-01-15 00:00, TimeOfDay = 15:45:30
stamp = stamp.Date : False
Start of month: 2026-01-01, end of month: 2026-01-31
Age on 2026-03-01 for a birth date of 2000-03-02: 25

DateDiff is not elapsed time. The Language Reference describes DateDiff as counting the interval boundaries crossed. On .NET 10 the program shows that this is only true for Year, Quarter and Month: from 31 December 23:59 to 1 January 00:01 the result is 1 for years and months. The other intervals return the number of whole elapsed units (truncated), so the same two moments are 0 days apart, and a Saturday-to-Sunday step shows 0 WeekOfYear boundaries. Know which semantics you need: for "how many calendar months are between these dates" use DateDiff(DateInterval.Month, ...) or (d2.Year - d1.Year) * 12 + d2.Month - d1.Month; for "how long" use d2 - d1. Age is not a DateDiff(Year, ...): it is a year count corrected for whether the birthday has happened (Age in the program).

Beyond Date: DateTimeOffset, DateOnly, TimeOnly, TimeSpan and time zones

Date is a fine type for a local timestamp and a poor one for an instant, a calendar day or a time of day. .NET offers purpose-built types, and VB uses them like any other value type (without literals):

Type Represents Use for

DateTimeOffset

An instant plus its UTC offset

Anything that crosses machines or time zones: audit stamps, due times stored in a database, API payloads

DateOnly (.NET 6)

A calendar day, no time

Birth dates, due dates, holidays. Its DayNumber makes day counts exact

TimeOnly (.NET 6)

A time of day on a 24-hour clock, with wrap-around arithmetic

Opening hours, alarm times

TimeSpan

A duration (ticks)

Differences, timeouts, intervals. TotalDays is a Double, Days the whole days component

TimeZoneInfo

A time zone with its rules

Converting an instant to local time, finding DST gaps and overlaps

TimeProvider (.NET 8)

A source of "now" and of timestamps

Injecting a clock into a class so it can be tested

Option Strict On
Imports System.Globalization

Module Ex03
    Sub Main()
        ' DateTime.Kind says what an instance means; arithmetic ignores it.
        Dim utc As Date = DateTime.SpecifyKind(#2026-07-01 12:00:00#, DateTimeKind.Utc)
        Dim unspecified As Date = #2026-07-01 12:00:00#
        Console.WriteLine($"Kinds: {utc.Kind}, {unspecified.Kind}; equal: {utc = unspecified}")

        Dim madrid = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid")
        Dim local = TimeZoneInfo.ConvertTimeFromUtc(utc, madrid)
        Console.WriteLine($"UTC {utc:HH:mm} is {local:HH:mm} in Madrid (offset {madrid.GetUtcOffset(utc)}, DST: {madrid.IsDaylightSavingTime(utc)})")
        Dim winter = TimeZoneInfo.ConvertTimeFromUtc(DateTime.SpecifyKind(#2026-01-15 12:00:00#, DateTimeKind.Utc), madrid)
        Console.WriteLine($"In January the same UTC time is {winter:HH:mm}")

        ' DateTimeOffset keeps the offset with the instant.
        Dim stamp As New DateTimeOffset(2026, 7, 1, 14, 0, 0, TimeSpan.FromHours(2))
        Console.WriteLine($"Offset: {stamp:yyyy-MM-dd HH:mmzzz}; UtcDateTime: {stamp.UtcDateTime:HH:mm}; equal to the UTC instant: {stamp = New DateTimeOffset(utc)}")
        Console.WriteLine($"Unix seconds: {stamp.ToUnixTimeSeconds()}")
        Console.WriteLine($"In Tokyo: {TimeZoneInfo.ConvertTime(stamp, TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo")):yyyy-MM-dd HH:mmzzz}")

        ' Daylight-saving transitions
        Dim gap As Date = #2026-03-29 2:30:00#
        Dim overlap As Date = #2026-10-25 2:30:00#
        Console.WriteLine($"2026-03-29 02:30 in Madrid is invalid: {madrid.IsInvalidTime(gap)}")
        Console.WriteLine($"2026-10-25 02:30 in Madrid is ambiguous: {madrid.IsAmbiguousTime(overlap)}; offsets: {String.Join(" / ", madrid.GetAmbiguousTimeOffsets(overlap))}")

        ' DateOnly and TimeOnly
        Dim day As DateOnly = New DateOnly(2026, 1, 15)
        Dim opens As New TimeOnly(9, 30)
        Console.WriteLine($"{day:yyyy-MM-dd} {day.DayOfWeek}; {opens:HH:mm}; later: {opens.AddHours(10):HH:mm}; wraps: {opens.AddHours(20):HH:mm}")
        Console.WriteLine($"Combined: {day.ToDateTime(opens):yyyy-MM-dd HH:mm}; from Date: {DateOnly.FromDateTime(#2026-01-15 23:59:00#)}; TimeOnly.IsBetween: {opens.IsBetween(New TimeOnly(9, 0), New TimeOnly(17, 0))}")
        Console.WriteLine($"DayNumber difference: {New DateOnly(2026, 3, 1).DayNumber - day.DayNumber} days")

        ' TimeSpan
        Dim fine As TimeSpan = TimeSpan.FromDays(3) + TimeSpan.FromMinutes(90)
        Console.WriteLine($"TimeSpan: {fine} total hours {fine.TotalHours}, Days {fine.Days}, Hours {fine.Hours}, formatted {fine:d\.hh\:mm}")
        Console.WriteLine($"TimeSpan.Parse: {TimeSpan.Parse("1.02:03:04")}, negative: {TimeSpan.FromHours(-1.5)}, multiply: {fine * 2}")
    End Sub
End Module
Kinds: Utc, Unspecified; equal: True
UTC 12:00 is 14:00 in Madrid (offset 02:00:00, DST: True)
In January the same UTC time is 13:00
Offset: 2026-07-01 14:00+02:00; UtcDateTime: 12:00; equal to the UTC instant: True
Unix seconds: 1782907200
In Tokyo: 2026-07-01 21:00+09:00
2026-03-29 02:30 in Madrid is invalid: True
2026-10-25 02:30 in Madrid is ambiguous: True; offsets: 01:00:00 / 02:00:00
2026-01-15 Thursday; 09:30; later: 19:30; wraps: 05:30
Combined: 2026-01-15 09:30; from Date: 01/15/2026; TimeOnly.IsBetween: True
DayNumber difference: 45 days
TimeSpan: 3.01:30:00 total hours 73.5, Days 3, Hours 1, formatted 3.01:30
TimeSpan.Parse: 1.02:03:04, negative: -01:30:00, multiply: 6.03:00:00

Rules that follow from the example:

  • Store and exchange UTC (DateTimeOffset.UtcNow, Date.UtcNow), and convert to a time zone only to display it. TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid") accepts IANA names on Linux and macOS, and, since .NET 6 (with ICU), on Windows, where the Windows names such as Romance Standard Time also work.

  • The local time zone may not exist on every machine (a trimmed container image may lack the time-zone data); check by running TimeZoneInfo.FindSystemTimeZoneById in the environment.

  • The clock skips an hour in spring (a gap, where IsInvalidTime is True) and repeats one in autumn (an overlap, where IsAmbiguousTime is True and GetAmbiguousTimeOffsets lists both offsets). A local Date of 02:30 that day is either impossible or ambiguous; only a DateTimeOffset or a UTC value identifies the moment.

  • DateTime.SpecifyKind only changes the label: it neither converts nor adjusts the value.

  • DateOnly and TimeOnly convert with ToDateTime, DateOnly.FromDateTime and TimeOnly.FromDateTime. Neither has a VB literal; System.Text.Json and EF Core can serialise and map them.

Testable clocks with TimeProvider

Code that calls Date.Now directly is hard to test: the answer changes between runs. System.TimeProvider (.NET 8) is an abstract class with GetUtcNow(), GetLocalNow(), LocalTimeZone, GetTimestamp(), GetElapsedTime() and a CreateTimer factory; TimeProvider.System is the real clock. A class receives a provider through its constructor (defaulting to TimeProvider.System) and a test passes its own subclass, or, in a test project, FakeTimeProvider from the Microsoft.Extensions.TimeProvider.Testing package. VB can subclass it without any special syntax:

Option Strict On

' A TimeProvider makes "now" injectable, so overdue logic can be tested without waiting.
Public Class FixedTimeProvider
    Inherits TimeProvider

    Private _now As DateTimeOffset

    Public Sub New(now As DateTimeOffset)
        _now = now
    End Sub

    Public Sub Advance(by As TimeSpan)
        _now = _now.Add(by)
    End Sub

    Public Overrides Function GetUtcNow() As DateTimeOffset
        Return _now
    End Function
End Class

Public Class Loan
    Private ReadOnly _time As TimeProvider

    Public ReadOnly Property DueOn As DateTimeOffset

    Public Sub New(dueOn As DateTimeOffset, Optional time As TimeProvider = Nothing)
        Me.DueOn = dueOn
        _time = If(time, TimeProvider.System)
    End Sub

    Public ReadOnly Property IsOverdue As Boolean
        Get
            Return _time.GetUtcNow() > DueOn
        End Get
    End Property

    Public Function DaysOverdue() As Integer
        Dim late = _time.GetUtcNow() - DueOn
        Return If(late > TimeSpan.Zero, CInt(Math.Ceiling(late.TotalDays)), 0)
    End Function
End Class

Module Ex04
    Sub Main()
        Dim clock As New FixedTimeProvider(New DateTimeOffset(2026, 1, 10, 9, 0, 0, TimeSpan.Zero))
        Dim loan As New Loan(New DateTimeOffset(2026, 1, 15, 0, 0, 0, TimeSpan.Zero), clock)

        Console.WriteLine($"Day 10: overdue = {loan.IsOverdue}, days overdue = {loan.DaysOverdue()}")
        clock.Advance(TimeSpan.FromDays(5))
        Console.WriteLine($"Day 15 09:00: overdue = {loan.IsOverdue}, days overdue = {loan.DaysOverdue()}")
        clock.Advance(TimeSpan.FromDays(2))
        Console.WriteLine($"Day 17 09:00: overdue = {loan.IsOverdue}, days overdue = {loan.DaysOverdue()}")

        Dim real As New Loan(DateTimeOffset.UtcNow.AddDays(1))
        Console.WriteLine($"Loan due tomorrow with the real clock: overdue = {real.IsOverdue}")

        Dim system = TimeProvider.System
        Dim start = system.GetTimestamp()
        Dim elapsed = system.GetElapsedTime(start)
        Console.WriteLine($"Elapsed measured with GetTimestamp is non-negative: {elapsed >= TimeSpan.Zero}")
    End Sub
End Module
Day 10: overdue = False, days overdue = 0
Day 15 09:00: overdue = True, days overdue = 1
Day 17 09:00: overdue = True, days overdue = 3
Loan due tomorrow with the real clock: overdue = False
Elapsed measured with GetTimestamp is non-negative: True

Parsing dates

Dates in text are the largest source of culture bugs. Four ways to turn text into a date exist, with different risks.

Call Culture used Notes

CDate(text), DateValue(text), TimeValue(text)

Current culture of the thread

CDate throws InvalidCastException on failure ("Conversion from string "not a date" to type 'Date' is not valid."). They parse day/month order, month names and separators by the thread’s culture

Date.Parse(text), Date.TryParse(text, result)

Current culture, or the one you pass

Accept many formats, including the culture’s; TryParse returns False instead of throwing FormatException

Date.ParseExact(text, format, provider), TryParseExact

The provider, and exactly the format you give

The safe option for any text with a known shape (files, APIs, user input with a picker). TryParseExact takes an array of formats

IsDate(text)

Current culture

Equivalent to Date.TryParse; it cannot say which parse succeeded

Option Strict On
Imports System.Globalization
Imports System.Threading

Module Ex05
    Sub Main()
        Dim text = "01/02/2026"
        Dim us = CultureInfo.GetCultureInfo("en-US")
        Dim fr = CultureInfo.GetCultureInfo("fr-FR")
        Dim invariant = CultureInfo.InvariantCulture

        Console.WriteLine("-- The same text means different dates in different cultures")
        Console.WriteLine($"Parse en-US : {Date.Parse(text, us):yyyy-MM-dd}")
        Console.WriteLine($"Parse fr-FR : {Date.Parse(text, fr):yyyy-MM-dd}")
        Console.WriteLine($"Parse invariant : {Date.Parse(text, invariant):yyyy-MM-dd}")

        Console.WriteLine("-- CDate and DateValue use the current culture of the thread")
        Thread.CurrentThread.CurrentCulture = us
        Console.WriteLine($"CDate under en-US     : {CDate(text):yyyy-MM-dd}")
        Thread.CurrentThread.CurrentCulture = fr
        Console.WriteLine($"CDate under fr-FR     : {CDate(text):yyyy-MM-dd}")
        Console.WriteLine($"DateValue under fr-FR : {DateValue(text):yyyy-MM-dd}")
        Dim lenient As Date
        Console.WriteLine($"CDate(""15 January 2026"") under fr-FR: {TryCDate("15 January 2026")}; Date.TryParse with fr-FR: {Date.TryParse("15 January 2026", fr, lenient)}")
        Thread.CurrentThread.CurrentCulture = invariant

        Console.WriteLine("-- ParseExact removes the guesswork")
        Dim exact = Date.ParseExact("2026-01-15 15:45", "yyyy-MM-dd HH:mm", invariant)
        Console.WriteLine($"ParseExact: {exact:O}")
        Dim formats As String() = {"yyyy-MM-dd", "dd/MM/yyyy", "d MMM yyyy"}
        For Each candidate In {"2026-01-15", "15/01/2026", "5 Jan 2026", "tomorrow"}
            Dim result As Date
            If Date.TryParseExact(candidate, formats, invariant, DateTimeStyles.None, result) Then
                Console.WriteLine($"{candidate,-12} -> {result:yyyy-MM-dd}")
            Else
                Console.WriteLine($"{candidate,-12} -> no match")
            End If
        Next

        Console.WriteLine("-- Styles and offsets")
        Dim utcStamp = Date.Parse("2026-01-15T10:00:00Z", invariant, DateTimeStyles.AdjustToUniversal)
        Console.WriteLine($"Z suffix with AdjustToUniversal: {utcStamp:O} (Kind {utcStamp.Kind})")
        Dim withOffset = DateTimeOffset.Parse("2026-01-15T10:00:00+02:00", invariant)
        Console.WriteLine($"DateTimeOffset.Parse keeps the offset: {withOffset:O}")
        Dim dOnly = DateOnly.ParseExact("15.01.2026", "dd.MM.yyyy", invariant)
        Console.WriteLine($"DateOnly.ParseExact: {dOnly:yyyy-MM-dd}; TimeOnly.Parse: {TimeOnly.Parse("3:45 PM", us):HH:mm}")

        Console.WriteLine("-- Failures")
        Dim junk As Date
        Console.WriteLine($"TryParse(""31/02/2026"") = {Date.TryParse("31/02/2026", fr, junk)}")
        Try
            Date.Parse("not a date", invariant)
        Catch ex As FormatException
            Console.WriteLine("Date.Parse(""not a date"") throws " & ex.GetType().Name)
        End Try
        Try
            Dim x = CDate("not a date")
        Catch ex As InvalidCastException
            Console.WriteLine("CDate(""not a date"") throws " & ex.GetType().Name)
        End Try
        Console.WriteLine($"IsDate(""2026-02-30"") = {IsDate("2026-02-30")}, IsDate(""2026-02-28"") = {IsDate("2026-02-28")}")
    End Sub

    Function TryCDate(value As String) As String
        Try
            Return CDate(value).ToString("yyyy-MM-dd")
        Catch ex As InvalidCastException
            Return "InvalidCastException"
        End Try
    End Function
End Module
-- The same text means different dates in different cultures
Parse en-US : 2026-01-02
Parse fr-FR : 2026-02-01
Parse invariant : 2026-01-02
-- CDate and DateValue use the current culture of the thread
CDate under en-US     : 2026-01-02
CDate under fr-FR     : 2026-02-01
DateValue under fr-FR : 2026-02-01
CDate("15 January 2026") under fr-FR: 2026-01-15; Date.TryParse with fr-FR: True
-- ParseExact removes the guesswork
ParseExact: 2026-01-15T15:45:00.0000000
2026-01-15   -> 2026-01-15
15/01/2026   -> 2026-01-15
5 Jan 2026   -> 2026-01-05
tomorrow     -> no match
-- Styles and offsets
Z suffix with AdjustToUniversal: 2026-01-15T10:00:00.0000000Z (Kind Utc)
DateTimeOffset.Parse keeps the offset: 2026-01-15T10:00:00.0000000+02:00
DateOnly.ParseExact: 2026-01-15; TimeOnly.Parse: 15:45
-- Failures
TryParse("31/02/2026") = False
Date.Parse("not a date") throws FormatException
CDate("not a date") throws InvalidCastException
IsDate("2026-02-30") = False, IsDate("2026-02-28") = True

The first block of output is the pitfall: the text 01/02/2026 is 2 January under en-US and 1 February under fr-FR, and CDate/DateValue follow whatever culture the thread has, which for a web server or a desktop app is set by the user or by the host configuration. A program that stores such text and reads it back on another machine changes dates silently (with no exception). Rules: persist dates in ISO 8601 (yyyy-MM-ddTHH:mm:ss or "O"), parse with ParseExact and CultureInfo.InvariantCulture, and reserve culture-sensitive parsing for text that the user typed in their own culture. Use DateTimeStyles.RoundtripKind or AdjustToUniversal (or DateTimeOffset.Parse) for text that has a Z or an offset, because by default Date.Parse converts such text to local time. IsDate("2026-02-30") is False, which makes IsDate convenient for a quick check; Date.TryParseExact is preferable when the format matters.

Formatting dates

ToString(format), the interpolation format specifier and composite formatting all use the same format strings. A standard format string is a single letter (d, D, t, T, f, F, g, G, M, Y, s, u, o, R) that stands for a culture-dependent pattern, except s, o and R, which never vary: s is the sortable ISO form, o the round-trip form with 7 fractional digits and the offset or Z, R the RFC 1123 form. A custom format string is built from pattern letters.

Pattern Meaning

yy, yyyy

Year, two or four digits

M, MM, MMM, MMMM

Month number, two digits, abbreviated name, full name. Upper case M is month

m, mm

Minutes. Lower case m is minutes

d, dd, ddd, dddd

Day of month, two digits, abbreviated and full day-of-week name

H, HH, h, hh, tt

24-hour and 12-hour clock hour, and the AM/PM designator

s, ss, f..fffffff

Seconds and fractions of a second (F suppresses trailing zeros)

K, zzz

Time zone (nothing for unspecified, Z for UTC, +02:00 for local) and the offset

/, :

The culture’s date and time separators, not the characters themselves

'text', \c

Literal text

%d

A single custom letter that would otherwise be read as a standard format

Option Strict On
Imports System.Globalization

Module Ex06
    Sub Main()
        Dim moment As Date = New Date(2026, 1, 5, 15, 4, 9, 123)
        Dim us = CultureInfo.GetCultureInfo("en-US")
        Dim de = CultureInfo.GetCultureInfo("de-DE")
        Dim ja = CultureInfo.GetCultureInfo("ja-JP")
        Dim inv = CultureInfo.InvariantCulture

        Console.WriteLine("-- Standard format strings (en-US)")
        For Each f In {"d", "D", "t", "T", "f", "F", "g", "G", "M", "Y", "s", "u"}
            Console.WriteLine($"{f}  {moment.ToString(f, us)}")
        Next
        Console.WriteLine($"o  {moment.ToString("o", us)}")
        Console.WriteLine($"R  {moment.ToString("R", us)}")

        Console.WriteLine("-- The same standard format in other cultures")
        Console.WriteLine($"D  de-DE: {moment.ToString("D", de)}")
        Console.WriteLine($"D  ja-JP: {moment.ToString("D", ja)}")
        Console.WriteLine($"d  de-DE: {moment.ToString("d", de)}   d  invariant: {moment.ToString("d", inv)}")

        Console.WriteLine("-- Custom format strings")
        Console.WriteLine(moment.ToString("yyyy-MM-dd HH:mm:ss.fff", inv))
        Console.WriteLine(moment.ToString("ddd, d MMM yy h:mm tt", us))
        Console.WriteLine(moment.ToString("dddd d MMMM yyyy", de))
        Console.WriteLine(moment.ToString("MM/dd/yyyy", de) & "  <- '/' is the culture's date separator, not a literal")
        Console.WriteLine(moment.ToString("MM'/'dd'/'yyyy", de) & "  <- quoted separators stay as written")
        Console.WriteLine(moment.ToString("HH\hmm\mss\s", inv))
        Console.WriteLine(moment.ToString("%d", inv) & " " & moment.ToString("d", inv) & "  <- a single letter is a standard format; %d forces the custom one")
        Console.WriteLine(moment.ToString("yyyy-MM-dd'T'HH:mm:ssK", inv))

        Console.WriteLine("-- Interpolation and composite formatting")
        Console.WriteLine($"{moment:yyyy-MM-dd}  {moment,30:dd MMM yyyy}|  {moment:HH:mm}")
        Console.WriteLine(String.Format(de, "{0:D} / {1,12:N2}", moment, 1234.5))
        Console.WriteLine(FormattableString.Invariant($"{moment:G}"))

        Console.WriteLine("-- Round trip")
        Dim text = moment.ToString("O", inv)
        Dim back = Date.ParseExact(text, "O", inv, DateTimeStyles.RoundtripKind)
        Console.WriteLine($"{text} -> equal after parsing: {back = moment}")
    End Sub
End Module
-- Standard format strings (en-US)
d  1/5/2026
D  Monday, January 5, 2026
t  3:04 PM
T  3:04:09 PM
f  Monday, January 5, 2026 3:04 PM
F  Monday, January 5, 2026 3:04:09 PM
g  1/5/2026 3:04 PM
G  1/5/2026 3:04:09 PM
M  January 5
Y  January 2026
s  2026-01-05T15:04:09
u  2026-01-05 15:04:09Z
o  2026-01-05T15:04:09.1230000
R  Mon, 05 Jan 2026 15:04:09 GMT
-- The same standard format in other cultures
D  de-DE: Montag, 5. Januar 2026
D  ja-JP: 2026年1月5日月曜日
d  de-DE: 05.01.2026   d  invariant: 01/05/2026
-- Custom format strings
2026-01-05 15:04:09.123
Mon, 5 Jan 26 3:04 PM
Montag 5 Januar 2026
01.05.2026  <- '/' is the culture's date separator, not a literal
01/05/2026  <- quoted separators stay as written
15h04m09s
5 01/05/2026  <- a single letter is a standard format; %d forces the custom one
2026-01-05T15:04:09
-- Interpolation and composite formatting
2026-01-05                     05 Jan 2026|  15:04
Montag, 5. Januar 2026 /     1.234,50
01/05/2026 15:04:09
-- Round trip
2026-01-05T15:04:09.1230000 -> equal after parsing: True

Points to remember: the standard d, D, t, T and others differ from culture to culture (de-DE prints Montag, 5. Januar 2026 for D); the / in MM/dd/yyyy becomes . under de-DE, so quote it when you want a slash; a one-letter custom format must be written %d (or padded) because "d" alone is the standard short-date format; and the format string is case-sensitive, where mm is minutes and MM is months. Pass a culture or CultureInfo.InvariantCulture for output that other programs read.

Formatting numbers

Numbers follow the same model as dates. A standard numeric format string is a letter with an optional precision: C (currency), D (integers, padded), E (exponent), F (fixed point), G (general), N (number with group separators), P (percent, multiplies by 100), R (round trip), X (hexadecimal). A custom numeric format uses 0 (digit placeholder, pads with zeros), # (digit placeholder, no padding), . and , (decimal and group separators, and scaling when , trails the digits), %, ; (sections for positive, negative and zero), E+0 and literal text in quotes.

Option Strict On
Imports System.Globalization

Module Ex07
    Sub Main()
        Dim us = CultureInfo.GetCultureInfo("en-US")
        Dim de = CultureInfo.GetCultureInfo("de-DE")
        Dim fr = CultureInfo.GetCultureInfo("fr-FR")
        Dim inv = CultureInfo.InvariantCulture
        Dim price As Decimal = 1234.5678D
        Dim ratio As Double = 0.256789

        Console.WriteLine("-- Standard numeric formats (en-US)")
        For Each f In {"C", "C0", "N", "N0", "F1", "E2", "G", "G3", "R", "X", "D6"}
            If f = "X" OrElse f = "D6" Then
                Console.WriteLine($"{f,-3} {255.ToString(f, us)}")
            Else
                Console.WriteLine($"{f,-3} {price.ToString(f, us)}")
            End If
        Next
        Console.WriteLine($"P1  {ratio.ToString("P1", us)}   P0  {ratio.ToString("P0", us)}")

        Console.WriteLine("-- One value, several cultures")
        Console.WriteLine($"en-US {price.ToString("N2", us)}  C: {price.ToString("C", us)}")
        Console.WriteLine($"de-DE {price.ToString("N2", de)}  C: {price.ToString("C", de)}")
        Console.WriteLine($"fr-FR {price.ToString("N2", fr)}  C: {price.ToString("C", fr)}")
        Console.WriteLine($"invariant {price.ToString("N2", inv)}  C: {price.ToString("C", inv)}")

        Console.WriteLine("-- Custom numeric formats")
        Console.WriteLine(price.ToString("0.00", inv))
        Console.WriteLine(price.ToString("#,##0.0", inv))
        Console.WriteLine(5.5D.ToString("000.00", inv))
        Console.WriteLine((0.5D).ToString("#.##", inv) & " " & (0D).ToString("#.##", inv) & "|")
        Console.WriteLine((-42).ToString("+0;-0;zero", inv) & " " & 0.ToString("+0;-0;zero", inv))
        Console.WriteLine((1234567).ToString("#,##0,,'M'", inv))
        Console.WriteLine(ratio.ToString("0.0%", inv))
        Console.WriteLine((12.5).ToString("0.#"" kg""", inv))

        Console.WriteLine("-- Formatting rounds exact ties differently for Double and Decimal")
        Console.WriteLine($"Double  2.5 / 3.5 with F0: {2.5.ToString("F0", inv)} / {3.5.ToString("F0", inv)}")
        Console.WriteLine($"Decimal 2.5 / 3.5 with F0: {2.5D.ToString("F0", inv)} / {3.5D.ToString("F0", inv)}")
        Console.WriteLine($"Math.Round(2.5D) = {Math.Round(2.5D)}, Math.Round(2.5D, MidpointRounding.AwayFromZero) = {Math.Round(2.5D, MidpointRounding.AwayFromZero)}")
        Console.WriteLine($"Double 0.125 F2 = {0.125.ToString("F2", inv)}, Decimal 0.125 F2 = {0.125D.ToString("F2", inv)}")

        Console.WriteLine("-- Interpolation and composite")
        Console.WriteLine($"{price,12:N2}|{price,-12:N2}|{ratio,8:P1}|")
        Console.WriteLine(String.Format(de, "{0:N2} / {1:C} / {2:P1}", price, price, ratio))
        Console.WriteLine(FormattableString.Invariant($"{price:N2} / {ratio:E3}"))

        Console.WriteLine("-- Parsing is culture-sensitive as well")
        Dim value As Decimal
        Console.WriteLine($"Decimal.TryParse(""1.234,56"", de-DE) = {Decimal.TryParse("1.234,56", NumberStyles.Number, de, value)} {value}")
        Console.WriteLine($"Decimal.TryParse(""1.234,56"", invariant) = {Decimal.TryParse("1.234,56", NumberStyles.Number, inv, value)}")
        Console.WriteLine($"Double.Parse(""1e3"", invariant) = {Double.Parse("1e3", inv)}")
    End Sub
End Module
-- Standard numeric formats (en-US)
C   $1,234.57
C0  $1,235
N   1,234.568
N0  1,235
F1  1234.6
E2  1.23E+003
G   1234.5678
G3  1.23E+03
R   1234.5678
X   FF
D6  000255
P1  25.7%   P0  26%
-- One value, several cultures
en-US 1,234.57  C: $1,234.57
de-DE 1.234,57  C: 1.234,57 €
fr-FR 1 234,57  C: 1 234,57 €
invariant 1,234.57  C: ¤1,234.57
-- Custom numeric formats
1234.57
1,234.6
005.50
.5 |
-42 zero
1M
25.7%
12.5 kg
-- Formatting rounds exact ties differently for Double and Decimal
Double  2.5 / 3.5 with F0: 2 / 4
Decimal 2.5 / 3.5 with F0: 3 / 4
Math.Round(2.5D) = 2, Math.Round(2.5D, MidpointRounding.AwayFromZero) = 3
Double 0.125 F2 = 0.12, Decimal 0.125 F2 = 0.13
-- Interpolation and composite
    1,234.57|1,234.57    |  25.7 %|
1.234,57 / 1.234,57 € / 25,7 %
1,234.57 / 2.568E-001
-- Parsing is culture-sensitive as well
Decimal.TryParse("1.234,56", de-DE) = True 1234.56
Decimal.TryParse("1.234,56", invariant) = False
Double.Parse("1e3", invariant) = 1000

Observations from the output:

  • The currency symbol, separators and digit grouping all come from the culture: de-DE writes 1.234,57 €, fr-FR uses a narrow no-break space (U+202F) as the group separator, and the invariant culture has the generic currency sign ¤. Never build prices for a database or a file with the current culture: use CultureInfo.InvariantCulture.

  • Decimal and Double round ties differently when formatted: 2.5D.ToString("F0") is 3 but 2.5.ToString("F0") is 2, and 0.125D formats as 0.13 while 0.125 formats as 0.12. Math.Round uses ties-to-even for both types by default (Math.Round(2.5D) is 2). If a result must follow a stated rounding rule, call Math.Round(value, digits, MidpointRounding.AwayFromZero) first and format the already-rounded value (see Numbers and Math).

  • Parsing is culture-sensitive as well: Decimal.TryParse("1.234,56", NumberStyles.Number, de-DE, value) succeeds with 1234.56 and fails for the invariant culture. Always pass NumberStyles and a CultureInfo when parsing text that did not come from the current user.

  • {price,12:N2} right-aligns in 12 positions, {price,-12:N2} left-aligns; the same alignment syntax applies to String.Format("{0,12:N2}", price).

Composite formatting and interpolation

String.Format, Console.WriteLine(format, args), StringBuilder.AppendFormat and $"..." strings all use composite formatting: {index[,alignment][:format]}. With String.Format(provider, ...) and with FormattableString.Invariant($"...") the culture is explicit; with a plain interpolated string it is the thread’s culture. A custom type participates by implementing IFormattable, as DateTime, Decimal and TimeSpan do; TimeSpan has its own format strings ("c", "g", "G" and custom ones such as d\.hh\:mm, where backslashes are required before literal characters).

The VB Format functions

Format, FormatNumber, FormatCurrency, FormatPercent and FormatDateTime (all in Microsoft.VisualBasic.Strings) come from VB6. They take a value and either named formats (a name like "Currency" or an enumeration such as DateFormat.LongDate) or user-defined patterns, and read the thread’s culture.

Option Strict On
Imports System.Globalization
Imports System.Threading

Module Ex08
    Sub Main()
        Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-US")
        Dim moment As Date = #2026-01-05 15:04:09#
        Dim amount As Double = 1234.5678
        Dim loss As Double = -0.075

        Console.WriteLine("-- Format with named numeric formats")
        For Each f In {"General Number", "Currency", "Fixed", "Standard", "Percent", "Scientific"}
            Console.WriteLine($"{f,-15} {Format(amount, f)}")
        Next
        Console.WriteLine($"Yes/No {Format(0, "Yes/No")} {Format(7, "Yes/No")}; True/False {Format(True, "True/False")}; On/Off {Format(0, "On/Off")}")

        Console.WriteLine("-- Format with named date formats")
        For Each f In {"General Date", "Long Date", "Medium Date", "Short Date", "Long Time", "Medium Time", "Short Time"}
            Console.WriteLine($"{f,-12} {Format(moment, f)}")
        Next

        Console.WriteLine("-- Format with custom patterns (VB6 syntax: 0 # , . %)")
        Console.WriteLine(Format(amount, "#,##0.00") & " | " & Format(loss, "0.0%") & " | " & Format(5, "000") & " | " & Format(-3, "0;(0)"))
        Console.WriteLine(Format(moment, "yyyy-mm-dd hh:mm:ss") & " | " & Format(moment, "dddd, mmmm d") & " | " & Format(moment, "h:mm AM/PM"))
        Console.WriteLine($".NET custom pattern with the same text: {moment.ToString("yyyy-mm-dd hh:mm:ss", CultureInfo.InvariantCulture)}")

        Console.WriteLine("-- FormatNumber, FormatCurrency, FormatPercent, FormatDateTime")
        Console.WriteLine(FormatNumber(amount, 2) & " | " & FormatNumber(loss, 3, TriState.False) & " | " & FormatNumber(-1234.5, 1, TriState.True, TriState.True, TriState.True))
        Console.WriteLine(FormatCurrency(amount) & " | " & FormatCurrency(-5, 0, TriState.True, TriState.True) & " | " & FormatPercent(0.256, 1))
        Console.WriteLine(FormatDateTime(moment, DateFormat.GeneralDate) & " | " & FormatDateTime(moment, DateFormat.LongDate) & " | " & FormatDateTime(moment, DateFormat.ShortDate) & " | " & FormatDateTime(moment, DateFormat.LongTime) & " | " & FormatDateTime(moment, DateFormat.ShortTime))

        Console.WriteLine("-- Other culture, same code")
        Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("de-DE")
        Console.WriteLine(Format(amount, "Currency") & " | " & FormatNumber(amount, 1) & " | " & FormatDateTime(moment, DateFormat.LongDate) & " | " & Format(moment, "Short Date"))
    End Sub
End Module
-- Format with named numeric formats
General Number  1234.5678
Currency        $1,234.57
Fixed           1234.57
Standard        1,234.57
Percent         123456.78%
Scientific      1.23E+03
Yes/No No Yes; True/False True; On/Off Off
-- Format with named date formats
General Date 1/5/2026 3:04:09 PM
Long Date    Monday, January 5, 2026
Medium Date  Monday, January 5, 2026
Short Date   1/5/2026
Long Time    3:04:09 PM
Medium Time  3:04:09 PM
Short Time   3:04 PM
-- Format with custom patterns (VB6 syntax: 0 # , . %)
1,234.57 | -7.5% | 005 | (3)
2026-04-05 03:04:09 | Monday, 04 5 | 3:04 A1/P1
.NET custom pattern with the same text: 2026-04-05 03:04:09
-- FormatNumber, FormatCurrency, FormatPercent, FormatDateTime
1,234.57 | -.075 | (1,234.5)
$1,234.57 | ($5) | 25.6%
1/5/2026 3:04:09 PM | Monday, January 5, 2026 | 1/5/2026 | 3:04:09 PM | 15:04
-- Other culture, same code
1.234,57 € | 1.234,6 | Montag, 5. Januar 2026 | 05.01.2026

What the output shows, and where the VB6 behaviour no longer applies:

  • The named numeric formats work as documented ("Currency", "Fixed", "Standard", "Percent", "Scientific", "Yes/No", "True/False", "On/Off") and follow the culture. FormatNumber, FormatCurrency and FormatPercent take the number of digits, and TriState arguments for the leading digit, parentheses for negative numbers and digit grouping.

  • The named date formats are supported, but with .NET’s meaning: "Medium Date" is the same as the long date here, not the VB6 dd-MMM-yy, and FormatDateTime(d, DateFormat.ShortTime) is a 24-hour 15:04 while "Short Time" in Format is 3:04 PM in en-US.

  • User-defined date patterns are the .NET ones, not the VB6 ones. In VB6 Format(d, "yyyy-mm-dd hh:mm:ss") printed the month for the first mm and the minutes for the second. On .NET mm is always minutes: the program prints 2026-04-05, a date in April, for 5 January, and AM/PM is not understood (it prints A1/P1). Write yyyy-MM-dd HH:mm:ss and tt, and prefer ToString or interpolation to Format.

  • FormatDateTime takes only a DateFormat value: FormatDateTime(d, "yyyy") is BC30512 ("disallows implicit conversions from String to `DateFormat`"). It cannot format with a custom pattern, which is a common mistake in code written from older books.

  • All the functions read the thread’s culture. That is why the last line of the program changes from $1,234.57 to 1.234,57 € without any change of code.

Prefer ToString(format, culture), interpolation and String.Format in new code: they are explicit about culture, behave the same in VB, C# and F#, and need no VB-specific knowledge. Keep Format/FormatNumber for code that already uses them.

Comparison with C#

Topic Visual Basic C#

The date type

Date (alias of DateTime), literal #1/15/2026#

DateTime, no literal

Date arithmetic

d.AddDays(1), DateAdd(...); d + 1 does not compile

d.AddDays(1)

Calendar functions

DateAdd, DateDiff, DatePart, DateSerial, Weekday, Year(d) in the DateAndTime module

Only the members of the types

Text to date

CDate, DateValue, Date.Parse…​

DateTime.Parse…​

Formatting helpers

Format, FormatNumber, FormatDateTime; interpolation, ToString

ToString, interpolation

DateOnly, TimeOnly, TimeProvider

Supported as ordinary types; no literal

Supported

Interpolated string handlers

not supported

supported (C# 10)

Where the books are outdated. Both books present the VB6 date functions as the standard toolbox, and the Format and FormatDateTime functions as the way to format. Three corrections: DateValue, CDate and Date.Parse(text) depend on the current culture, so the examples in the books print different dates on different machines (and a stored DateValue result written on one culture reads back differently on another); FormatDateTime accepts only the named DateFormat values and never a custom pattern; and the VB6 patterns that depend on the context of mm do not exist on .NET. Neither book covers DateOnly, TimeOnly, DateTimeOffset, time zones or TimeProvider.

Pitfalls checklist

  • Dim d As Date = Nothing is 1 January 0001, not a missing date. Use Date? for "unknown".

  • CDate and DateValue parse by the thread’s culture; use ParseExact and CultureInfo.InvariantCulture for data.

  • Date literals are US-ordered (#1/2/2026# is 2 January) and cannot have fractions of a second.

  • DateDiff(DateInterval.Day, ...) counts whole elapsed days, not midnights crossed; Year, Quarter and Month count calendar boundaries.

  • AddMonths and DateAdd(Month, ...) clamp to the last day of the shorter month; adding a month and then subtracting it does not restore the date.

  • Weekday starts the week on Sunday (1) by default, DayOfWeek is 0-based.

  • Now, Today and DateTime.Now are local; compare and store UTC.

  • mm is minutes and MM is months in .NET format strings, including the ones passed to Format.

  • FormatDateTime accepts only DateFormat values.

  • Decimal formats ties away from zero; Double formats exact ties to even; Math.Round is ties-to-even.

  • A local time during a DST gap is invalid; during an overlap it is ambiguous.

  • The invariant culture’s currency sign is ¤; use a real culture to display money.