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
Source: Date data type (Visual Basic)
Things the program shows, and some related rules:
-
New Date(y, m, d)throwsArgumentOutOfRangeExceptionfor an impossible date;DateSerial(y, m, d)is lenient and rolls over (DateSerial(2026, 2, 31)is 3 March) andTimeSerial(25, 0, 0)returns aDateon 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)andWeekday(d)are the VB functions, andd.Year,d.Month,d.DayOfWeek… the properties.Weekdayreturns 1 for Sunday (and 5 for Thursday) by default,DayOfWeekis the 0-basedDayOfWeekenumeration (Sunday = 0): the numbers differ by one. -
The
-operator on two dates returns aTimeSpan; there is no+betweenDateand a number. Visual Basic 6 stored dates as aDoubleand let you writed + 1,.NETdoes not:d + 1is BC30452 ("Operator+is not defined for typesDateandInteger`"), and `CDbl(d)is BC30532 ("requires calling theDate.ToOADatemethod"). Used.AddDays(1),DateAdd(DateInterval.Day, 1, d)ord + TimeSpan.FromDays(1).ToOADateandDate.FromOADateconvert to and from the VB6/VBA/Excel serial number when you need it. -
Now(the property of theDateAndTimemodule),Date.NowandDateTime.Nowreturn the local time (Kind= Local);TodayandDate.Todaythe local date at midnight;Date.UtcNowthe current UTC instant.TimeOfDay,TimeString,DateStringandTimer(seconds since midnight, as aDouble) are legacy VB properties. PreferDateTime.UtcNowor an injectedTimeProviderfor anything stored or compared. -
A date has 100 ns resolution (
TimeSpan.TicksPerSecond= 10,000,000) but the system clock ticks coarsely, andNowis not a stopwatch: useStopwatchorTimeProvider.GetTimestampto measure durations. -
Two-digit years follow the calendar’s
TwoDigitYearMax, 2049 for the default Gregorian calendar:Date.Parse("1/1/49")is 2049 andDate.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 |
|---|---|---|
|
|
|
|
|
Not simply the elapsed time: see below |
|
|
|
|
|
Lenient: out-of-range values roll over |
|
|
Parse with the current culture of the thread; see the next sections |
|
|
|
|
|
Names come from the current culture; the week of |
|
|
Inside a class that has a member with one of these names (such as the |
|
|
Same values; |
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
Source: DateAndTime Class
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 |
|---|---|---|
|
An instant plus its UTC offset |
Anything that crosses machines or time zones: audit stamps, due times stored in a database, API payloads |
|
A calendar day, no time |
Birth dates, due dates, holidays. Its |
|
A time of day on a 24-hour clock, with wrap-around arithmetic |
Opening hours, alarm times |
|
A duration (ticks) |
Differences, timeouts, intervals. |
|
A time zone with its rules |
Converting an instant to local time, finding DST gaps and overlaps |
|
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 asRomance Standard Timealso 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.FindSystemTimeZoneByIdin the environment. -
The clock skips an hour in spring (a gap, where
IsInvalidTimeisTrue) and repeats one in autumn (an overlap, whereIsAmbiguousTimeisTrueandGetAmbiguousTimeOffsetslists both offsets). A localDateof 02:30 that day is either impossible or ambiguous; only aDateTimeOffsetor a UTC value identifies the moment. -
DateTime.SpecifyKindonly changes the label: it neither converts nor adjusts the value. -
DateOnlyandTimeOnlyconvert withToDateTime,DateOnly.FromDateTimeandTimeOnly.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
Source: TimeProvider Class
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 |
|---|---|---|
|
Current culture of the thread |
|
|
Current culture, or the one you pass |
Accept many formats, including the culture’s; |
|
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). |
|
Current culture |
Equivalent to |
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 |
|---|---|
|
Year, two or four digits |
|
Month number, two digits, abbreviated name, full name. Upper case M is month |
|
Minutes. Lower case m is minutes |
|
Day of month, two digits, abbreviated and full day-of-week name |
|
24-hour and 12-hour clock hour, and the AM/PM designator |
|
Seconds and fractions of a second ( |
|
Time zone (nothing for unspecified, |
|
The culture’s date and time separators, not the characters themselves |
|
Literal text |
|
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
Source: Standard numeric format strings
Observations from the output:
-
The currency symbol, separators and digit grouping all come from the culture:
de-DEwrites1.234,57 €,fr-FRuses 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: useCultureInfo.InvariantCulture. -
DecimalandDoubleround ties differently when formatted:2.5D.ToString("F0")is3but2.5.ToString("F0")is2, and0.125Dformats as0.13while0.125formats as0.12.Math.Rounduses ties-to-even for both types by default (Math.Round(2.5D)is2). If a result must follow a stated rounding rule, callMath.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 with1234.56and fails for the invariant culture. Always passNumberStylesand aCultureInfowhen 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 toString.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
Source: Strings.Format Method
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,FormatCurrencyandFormatPercenttake the number of digits, andTriStatearguments 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 VB6dd-MMM-yy, andFormatDateTime(d, DateFormat.ShortTime)is a 24-hour15:04while"Short Time"inFormatis3:04 PMinen-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 firstmmand the minutes for the second. On .NETmmis always minutes: the program prints2026-04-05, a date in April, for 5 January, andAM/PMis not understood (it printsA1/P1). Writeyyyy-MM-dd HH:mm:ssandtt, and preferToStringor interpolation toFormat. -
FormatDateTimetakes only aDateFormatvalue:FormatDateTime(d, "yyyy")is BC30512 ("disallows implicit conversions fromStringto `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.57to1.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 arithmetic |
|
|
Calendar functions |
|
Only the members of the types |
Text to date |
|
|
Formatting helpers |
|
|
|
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 = Nothingis 1 January 0001, not a missing date. UseDate?for "unknown". -
CDateandDateValueparse by the thread’s culture; useParseExactandCultureInfo.InvariantCulturefor 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. -
AddMonthsandDateAdd(Month, ...)clamp to the last day of the shorter month; adding a month and then subtracting it does not restore the date. -
Weekdaystarts the week on Sunday (1) by default,DayOfWeekis 0-based. -
Now,TodayandDateTime.Noware local; compare and store UTC. -
mmis minutes andMMis months in .NET format strings, including the ones passed toFormat. -
FormatDateTimeaccepts onlyDateFormatvalues. -
Decimalformats ties away from zero;Doubleformats exact ties to even;Math.Roundis 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.