Convert Integer to Date in SQL Server: ddMMyyyy Values

To convert integer to date values such as 09122020, split the number into day, month and year. Then build the date from the three parts. The traps are a lost leading zero, the session language and a value that is not a real date. This post tests four methods against all three.

Gouache painting of a strand of wooden beads above three small rings of beads, the middle ring holding one vermilion bead

Why the Zero Disappears

An integer has no leading zeros. The literal 09122020 is the number 9,122,020, and that is what a table stores. A column that holds ddMMyyyy dates as integers therefore mixes eight digit and seven digit values. Any method that counts characters from the left breaks on the early days of a month.

The demo database is IntDateDemo. The table holds six shipments. The last two rows are not real dates. Row 5 is 31 April, and row 6 is zero. Run it on a test server.

IF DB_ID(N'IntDateDemo') IS NULL CREATE DATABASE IntDateDemo;
GO
USE IntDateDemo;
GO
DROP TABLE IF EXISTS dbo.Shipments;
CREATE TABLE dbo.Shipments (ShipmentID int NOT NULL PRIMARY KEY, ShipDateInt int NOT NULL);
INSERT INTO dbo.Shipments (ShipmentID, ShipDateInt)
VALUES (1, 09122020), (2, 31122020), (3, 1012021), (4, 29022020), (5, 31042020), (6, 0);
SELECT ShipmentID, ShipDateInt FROM dbo.Shipments ORDER BY ShipmentID;
ShipmentIDShipDateInt
19122020
231122020
31012021
429022020
531042020
60

Row 1 shows 9122020, with no zero. Rows 3 and 4 show the same effect for 1 January and 29 February.

Method One: Split With Arithmetic

Integer division and the modulo operator cut the number into parts. The year is the remainder after dividing by 10,000. The month sits in the middle, and the day is everything above 1,000,000. DATEFROMPARTS builds the date from them. The zero never matters, because the arithmetic does not use characters.

SELECT ShipmentID, ShipDateInt,
       DATEFROMPARTS(ShipDateInt % 10000, ShipDateInt / 10000 % 100, ShipDateInt / 1000000) AS ShipDate
FROM dbo.Shipments
WHERE ShipmentID <= 4
ORDER BY ShipmentID;
ShipmentIDShipDateIntShipDate
191220202020-12-09
2311220202020-12-31
310120212021-01-01
4290220202020-02-29

All four are right, including the leap day. Now remove the filter. DATEFROMPARTS has no mercy for an impossible date, and one bad row fails the whole query.

SELECT ShipmentID,
       DATEFROMPARTS(ShipDateInt % 10000, ShipDateInt / 10000 % 100, ShipDateInt / 1000000) AS ShipDate
FROM dbo.Shipments;

The query stops with Msg 289. The text reads as follows.

Cannot construct data type date, some of the arguments have values which are not valid.

Method Two: Pad, Format and TRY_CONVERT

When bad values exist, use a conversion that returns NULL instead of an error. Pad the number to eight digits with zeros and insert the slashes with STUFF. Then convert with style 103, which reads dd/mm/yyyy. TRY_CONVERT, DATEFROMPARTS, CONCAT and FORMAT need SQL Server 2012 or later.

SELECT ShipmentID, ShipDateInt,
       TRY_CONVERT(date, STUFF(STUFF(RIGHT(CONCAT('0000000', ShipDateInt), 8), 3, 0, '/'), 6, 0, '/'), 103) AS ShipDate
FROM dbo.Shipments
ORDER BY ShipmentID;
ShipmentIDShipDateIntShipDate
191220202020-12-09
2311220202020-12-31
310120212021-01-01
4290220202020-02-29
531042020NULL
60NULL

The bad rows come back as NULL, and the good rows are unchanged. The padding is what saves rows 1 and 3. Without it, the character positions are wrong.

You cannot filter on the alias ShipDate in the same SELECT. Wrap the query in a CTE and filter on ShipDate IS NULL to list the values to fix.

WITH Converted AS (
    SELECT ShipmentID, ShipDateInt,
           TRY_CONVERT(date, STUFF(STUFF(RIGHT(CONCAT('0000000', ShipDateInt), 8), 3, 0, '/'), 6, 0, '/'), 103) AS ShipDate
    FROM dbo.Shipments
)
SELECT ShipmentID, ShipDateInt FROM Converted WHERE ShipDate IS NULL ORDER BY ShipmentID;
ShipmentIDShipDateInt
531042020
60

Two Methods That Break

The first is a version that does the STUFF without the padding. It works for 31122020 and fails for 9122020, because the text is one character short.

SELECT CONVERT(date, STUFF(STUFF(CONVERT(char(8), ShipDateInt), 5, 0, '/'), 3, 0, '/'), 103) AS ShipDate
FROM dbo.Shipments WHERE ShipmentID = 1;

It fails with Msg 241, a conversion error. The second is the FORMAT version. It formats the number as 9-12-2020 and lets the session language decide what the text means. Check the language first, then run it.

SELECT @@LANGUAGE AS CurrentLanguage,
       CAST(FORMAT(ShipDateInt, '##-##-####') AS date) AS ShipDate
FROM dbo.Shipments WHERE ShipmentID = 1;

On a us_english session the result is 2020-09-12. That is 12 September, not 9 December. Nothing fails, and the answer is wrong. For row 2 the same call fails with Msg 241, because 31 is not a month. A British session reads the text the other way and gets row 1 right. A method whose answer depends on the language of whoever runs it is a bug waiting for a new login.

You can see the effect on the text alone. The batch below reads the same text under the default setting. Then it reads it again with day first. The setting lasts only for the dynamic batch.

SELECT CAST('9-12-2020' AS date) AS DefaultReading;
EXEC (N'SET DATEFORMAT dmy; SELECT CAST(''9-12-2020'' AS date) AS WithDayFirst;');
SELECT CONVERT(date, '9-12-2020', 105) AS Style105;

Style 105 means dd-mm-yyyy in every language. That is why the padded method above is safe: its style fixes the order.

What the Methods Cost

On a million rows, the cost differs. The script below builds a table of one million ddMMyyyy values. It takes the largest converted date with each of three methods. The time appears in the Messages tab.

DROP TABLE IF EXISTS dbo.BigShipments;
CREATE TABLE dbo.BigShipments (ShipmentID int NOT NULL PRIMARY KEY, ShipDateInt int NOT NULL);
INSERT INTO dbo.BigShipments (ShipmentID, ShipDateInt)
SELECT x.n, DAY(y.d) * 1000000 + MONTH(y.d) * 10000 + YEAR(y.d)
FROM (SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
      FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b CROSS JOIN sys.all_objects AS c) AS x
CROSS APPLY (SELECT DATEADD(day, x.n % 3650, '2015-01-01') AS d) AS y;
GO
SET STATISTICS TIME ON;
DECLARE @d date;
SELECT @d = MAX(DATEFROMPARTS(ShipDateInt % 10000, ShipDateInt / 10000 % 100, ShipDateInt / 1000000))
FROM dbo.BigShipments OPTION (MAXDOP 1);
SELECT @d = MAX(TRY_CONVERT(date, STUFF(STUFF(RIGHT(CONCAT('0000000', ShipDateInt), 8), 3, 0, '/'), 6, 0, '/'), 103))
FROM dbo.BigShipments OPTION (MAXDOP 1);
SELECT @d = MAX(TRY_CAST(FORMAT(ShipDateInt, '##-##-####') AS date))
FROM dbo.BigShipments OPTION (MAXDOP 1);
SET STATISTICS TIME OFF;
MethodCPU time over three runs
DATEFROMPARTSabout 155 to 190 ms
Padded text with TRY_CONVERTabout 435 to 470 ms
FORMATabout 2,000 to 2,200 ms

The arithmetic is the fastest. The padded text costs about three times as much, and FORMAT costs more than ten times. To convert integer to date on clean data, use the arithmetic. Use the padded text when the data is not clean. Never put FORMAT in a conversion of many rows. On a us_english session, the FORMAT query also returns NULL for every date with a day above 12. Its answer is wrong as well as slow.

One More Case and the Better Fix

If the integer is yyyyMMdd, a single style converts it. Style 112 reads that layout directly.

SELECT CONVERT(date, CAST(20201209 AS char(8)), 112) AS ShipDate;
ShipDate
2020-12-09

You could argue that you should not convert these values at all. The better fix is a date column. Add a column of type date and fill it with the padded method. Check the NULL rows by hand, then retire the integer. A real date type validates every value and sorts correctly.

What to Remember

Convert integer to date with arithmetic and DATEFROMPARTS when the data is clean. Use padding, STUFF and TRY_CONVERT with style 103 when bad values exist. Skip FORMAT for conversions. When you finish the demo, drop the database.

USE master;
GO
DROP DATABASE IntDateDemo;

A date is not a number with a pattern, it is a value that has one reading.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL DateTime, SQL Function, SQL Scripts
Previous Post
Top N Plus Others: Grouping the Rest Into One Row
Next Post
Unquoted Procedure Parameters: Are Single Quotes Optional?

Related Posts

3 Comments. Leave new

  • Michael D Ballard
    December 9, 2020 7:22 am

    You’re working to hard:
    DECLARE @iDate INT = 31122020;
    SELECT CONVERT(DATE, STUFF(STUFF(CONVERT(CHAR(8), @iDate), 5, 0, ‘/’), 3, 0, ‘/’), 103)

    Reply
  • richard whight
    December 9, 2020 7:41 am

    I might be missing something but this is essentially yours anyway but just without having to add in the slashes .

    DECLARE @DATE INT
    SET @DATE=09122020

    SELECT DATEFROMPARTS(@DATE%10000 , @DATE/10000%100, @DATE/1000000)

    Reply
  • Carter Cordingley (@CarterCordingl1)
    December 14, 2020 7:31 am

    Select Cast(Format(09122020,’##-##-####’) as Date)

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.