Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Solve Data Problems

Download as pdf or txt
Download as pdf or txt
You are on page 1of 6

Solve date problems

SQL Server’s date and time functions make it easy to solve various date-
related problems, like finding the start and end dates of a period, finding
anniversaries in a specified period, and identifying weekdays in a period.

Tamar E. Granor, Ph.D.

In my last article, I explored the various date and use the appropriate DateTimexFromParts() func-
time types in SQL Server and the collection of func- tion for the first of the month and the first and last
tions and operations you can use with them. In this day of the year, and wrap EOMonth() with Cast()
article, I’ll look at some common date problems and or Convert() for the last day of the month.
show how to solve them.
Listing 1. There are simple ways to find the first and last day of
First, a quick review. SQL Server supports six data the month or the year containing a specified date.
types related to dates and times. Four of them are DECLARE @Date Date;
variations on a combined date and time construct.
Three of those (SmallDateTime, DateTime and SET @Date = '1/4/2017';
DateTime2) vary only in precision. The fourth, SELECT DATEFROMPARTS(DATEPART(Year, @Date),
DateTimeOffset, is the same as DateTime2, but also DATEPART(Month, @Date), 1) AS BoM,
includes a timezone offset component. There are EOMONTH(@Date) AS EoM,
also separate Date and Time types. For this article, DATEFROMPARTS(DATEPART(Year, @Date),
1, 1) AS BoY,
I’ll refer to these in the aggregate as date/time. DATEFROMPARTS(DATEPART(Year, @Date),
There are functions for retrieving the current 12, 31) AS EoY;
date/time, computing the difference between date/
time values, calculating new date/time values from
existing ones, taking date/time values apart, and
constructing date/time values from their compo-
nents. I’ll use a number of them in solving the prob- Figure 1. Finding the first and last day of the month or the
year is straightforward.
lems in this article.
The examples use the Adventureworks 2014 But this approach doesn’t work for the first and
sample database. last days of the week or the quarter, because we
don’t know what the appropriate days or months
Find the first and last days of the are. Instead, we need to calculate those, and we can
do months and years the same way, if we wish.
period
Given a date, it’s not unusual to want to find the The DateAdd() function lets us move a date/
first and last day of the containing week, month, time forward or backward by a number of years,
quarter, or year. For some of these periods, it’s easy months, days, hours, etc. The DateDiff() func-
to build the start and end dates by pulling the given tion allows us to compute the difference between
date apart and creating a new one. For other peri- two date/time values in whichever units we want
ods, it’s trickier, but the DateAdd() and DateDiff() (though see my last article to understand exactly
functions provide a generic approach. how that works). We can combine them to figure
out the first or last day of the period containing a
Let’s start with the easy way, which works for particular date, as in Listing 2, where we find the
months and years. First, as noted in my previous first and last day of the week. The code is included
article, the function EOMonth()gives you the last in this month’s downloads as FirstLastOfWeek.
day of the month for a specified date. To find the SQL.
first day of the month, you can extract the month
and year from the specified date and combine them Listing 2. You can combine DateAdd() and DateDiff() to com-
with a day of 1. A similar approach works for the pute dates such as the first and last of the month.
first and last day of the year. Listing 1 shows the DECLARE @Date Date;
code; it’s included in this month’s downloads as SET @Date = '1/4/2017';
FirstLastSimple.SQL. Figure 1 shows the results. If
you want them as one of the other date/time types, SELECT DATEADD(Week,

January 2017 FoxRockX Page 7


DATEDIFF(Week, 0, @Date), 0) AS BoW, SET @Date = '1/4/2017';
DATEADD(DAY, -1, DATEADD(Week,
DATEDIFF(Week, 0, @Date) + 1, 0)) SELECT FirstName, LastName,
AS EoW; Employee.BusinessEntityID, Birthdate
FROM [HumanResources].[Employee]
Let’s work out the first of week code first JOIN [Person].[Person]
ON Employee.BusinessEntityID =
because it’s simpler. Working from the inside out, Person.BusinessEntityID
DateDiff(Week, 0, @Date) computes the number WHERE DATEPART(Day, BirthDate) =
of weeks from SQL Server’s zero date (January 1, DATEPART(Day, @Date)
1900) to the specified date. Then, the surrounding AND DATEPART(Month, BirthDate) =
DATEPART(Month, @Date)
DateAdd() adds that many weeks to the zero date,
resulting in the first day of the week containing @
Date.
The end of week calculation is similar, but a
little more complex. It computes the first day of the
week following the one containing the specified
date by doing the same DateDiff() calculation, add- Figure 2. It’s easy to find out who shares a birthday.
ing 1 and then applying DateAdd(). Then, it uses
DateAdd() a second time to subtract one day from The alternative approach to this problem uses
that date. (For the last day of the week, you could the technique from earlier in this article to convert
also compute the first day of the week and then add the birthdate to the birthday this year and compares
6.) that to the specified date. Listing 5, included in this
You can find the first and last day of the quar- month’s downloads as SameBirthDateDateAdd.
ter containing a particular date by changing every SQL, produces the same results as Listing 4.
occurrence of “Week” in that code to “Quarter,” as
Listing 5. An alternative way to find people with a given birth-
in Listing 3 (included in this month’s downloads as day uses DateAdd() and DateDiff().
FirstLastofQuarter.SQL). DECLARE @Date Date;
Listing 3. To find the first and last day of the year, just change
SET @Date = '1/4/2017';
every occurrence of “Week” in the prior query to “Quarter”.
SELECT DATEADD(Quarter, SELECT FirstName, LastName,
DATEDIFF(Quarter, 0, @Date), 0) Employee.BusinessEntityID, BirthDate
AS BoQ, FROM [HumanResources].[Employee]
DATEADD(Day, -1, DATEADD(Quarter, JOIN [Person].[Person]
DATEDIFF(Quarter, 0, @Date) + 1, 0)) ON Employee.BusinessEntityID =
AS EoQ; Person.BusinessEntityID
WHERE DATEADD(Year, DATEDIFF(Year,
You can do use the same formulation for BirthDate, @Date), BirthDate) = @Date
months and years, too. Just specify the appropriate
datepart. We can extend this process to, for example, find
everyone with a birthday this week. The tricky part
of finding everyone with a birthday in a given week
Birthdays and anniversaries is that the week can cross the boundaries of a year.
We often need to find those having an anniversary Let’s start with a version of the query that
during a given period. It might be people with a doesn’t account for crossing the end of the year.
birthday this week, those celebrating employment Listing 6 (included in this month’s downloads as
anniversaries next month, and so on. The key point SameWeekNoYearEnd.SQL) shows how to find
here is that we’re looking for dates where the month everyone with a birthday in the week beginning
and day match, but the year is irrelevant. on a specified date, as long as the week ends in the
For simplicity, we’ll use employee birthdays to same year it starts in. It uses the same expression as
demonstrate the solutions here, but the same ideas the previous example to move the birthday into the
apply to any kind of anniversary dates. Let’s start same year as the specified date. Figure 3 shows the
with finding every employee who has a birthday results for the date specified in the example, Janu-
on a given date. There are actually two good solu- ary 4, 2017.
tions here. The first is to use DatePart() to extract
Listing 6. If you don’t have to worry about crossing the end of
the month and day and compare them, as in Listing
a year, finding all birthdays in the week beginning on a given
4; this query is included as SameBirthDateDatePart. date is fairly simple.
SQL in this month’s downloads. Figure 2 shows DECLARE @Date Date;
those employees whose birthday is January 4.
SET @Date = '1/4/2017';
Listing 4. When you want to match day and month exactly, you
can just extract them with DatePart(). SELECT FirstName, LastName,
DECLARE @Date Date; Employee.BusinessEntityID, BirthDate

Page 8 FoxRockX January 2017


FROM [HumanResources].[Employee] Using the DateAdd(…DateDiff()…) expression
JOIN [Person].[Person]
ON Employee.BusinessEntityID =
moves the birthdate into 2017, the same year as the
Person.BusinessEntityID specified date. But if we’re looking for birthdays in
WHERE DATEADD(YEAR, DATEDIFF(Year, the week beginning December 29, we need the Janu-
BirthDate, @Date), BirthDate) ary birthdays to be translated into 2018. Doing that
BETWEEN @Date AND DATEADD(Day, 6, @Date)
requires a more complex expression using CASE;
it’s shown in Listing 8 (included in this month’s
downloads as SameWeek.SQL). The CASE expres-
sion divides into two cases, based on whether the
birthday comes earlier or later in the year than the
specified date. If it comes later in the year, we just
translate to the same year. If it comes earlier in the
year, though, we translate to the next year.
Listing 8. To work across year-end, we need to figure out
whether the birthday is earlier or later in the year than the
specified date.
DECLARE @Date Date;

SET @Date = '1/4/2017';

SELECT FirstName, LastName,


Figure 3. Moving the date into the current year lets you find all Employee.BusinessEntityID, BirthDate
birthdays in a specified week. FROM [HumanResources].[Employee]
JOIN [Person].[Person]
ON Employee.BusinessEntityID =
But what happens if we specify a date late in Person.BusinessEntityID
December? We know from the previous example WHERE CASE
WHEN DATEPART(Month, Birthdate)
that there are people with birthdays on January < DATEPART(Month, @Date)
4, but if we set @Date to ‘12/29/2017’, we get the OR (DATEPART(Month, Birthdate) =
results shown in Figure 4. DATEPART(Month, @Date) AND
DATEPART(Day, Birthdate) <
DATEPART(Day, @Date))
THEN DATEADD(Year, DATEDIFF(Year,
BirthDate, @Date) + 1, BirthDate)
Figure 4. At the end of the year, the simple test in Listing 6 ELSE DATEADD(Year, DATEDIFF(Year,
doesn’t work. BirthDate, @Date), BirthDate) END
BETWEEN @Date AND DATEADD(Day, 6, @Date)

To see what’s causing the problem, we can add If we run the query as shown, with a specified
the translated birthday to the field list and look for date of January 4, 2017, we get the same results as
only those records with January 4 birthdays, as in in Figure 3. Moving the date into the current year
the query in Listing 7; the results are shown in Fig- lets you find all birthdays in a specified week.. But
ure 5. if we change the specified date to December 29,
Listing 7. By including the computed birthday this year in the
2017, we get the results shown in Figure 6.
results, we can see why this version doesn’t work across the
end of a year.
SELECT FirstName, LastName,
Employee.BusinessEntityID, BirthDate,
DATEADD(YEAR, DATEDIFF(Year,
BirthDate, @Date), BirthDate)
FROM [HumanResources].[Employee]
JOIN [Person].[Person]
ON Employee.BusinessEntityID =
Person.BusinessEntityID Figure 6. With the more complicated date translation, we can
WHERE Month(BirthDate) = 1 find all the birthdays in a specified week, even across the end
AND Day(BirthDate) = 4 of the year.

The final thing we’d want here is to order the


results by birthday. To do that, we add an ORDER
BY clause using the same complex expression, as in
Listing 9; a version with this code is included in the
Figure 5. Looking at the translated birthday helps to show why month’s downloads as SameWeekOrdered.SQL.
the expression using DateAdd() and DateDiff() isn’t sufficient. The results for a start date of December 29, 2017 are
shown in Figure 7.

January 2017 FoxRockX Page 9


Listing 9. You can use the same date translation expression to BirthDate)
put the results in order. ELSE DATEADD(YEAR, DATEDIFF(YEAR,
ORDER BY CASE BirthDate, @StartDate), BirthDate)
WHEN DATEPART(Month, Birthdate) END
< DATEPART(Month, @Date)
OR (DATEPART(Month, Birthdate) =
DATEPART(Month, @Date) AND
DATEPART(Day, Birthdate)
< DATEPART(Day, @Date))
THEN DATEADD(YEAR, DATEDIFF(YEAR, BirthDate,
@Date) + 1, BirthDate)
ELSE DATEADD(YEAR, DATEDIFF(YEAR, BirthDate,
@Date), BirthDate) END

Figure 7. We can sort the birthdays by using the same ex-


pression that translates them.

We can use the approach to find anyone with


a birthday between two specified dates. The only
difference is checking whether the translated date
is between the specified start and end dates, rather
than between the specified date and a calculated
end date; Listing 10 (included as BetweenDatesOr-
dered.SQL in this month’s downloads) shows the
code. The results are shown in Figure 8.
DECLARE @StartDate Date, @EndDate Date;

SET @StartDate = '12/15/2017';


SET @EndDate = '1/15/2018';

SELECT FirstName, LastName,


Employee.BusinessEntityID, BirthDate Figure 8. It’s easy to get a list of everyone with birthdays (or
FROM [HumanResources].[Employee] other kinds of anniversaries) between two dates.
JOIN [Person].[Person]
ON Employee.BusinessEntityID =
Person.BusinessEntityID
WHERE
CASE WHEN DATEPART(Month, Birthdate) <
Identifying weekdays
DATEPART(Month, @StartDate) (and weekends)
OR (DATEPART(Month, Birthdate) = Another common date problem is determining
DATEPART(Month, @StartDate) AND
DATEPART(Day, Birthdate) < which days in a specified period are weekdays (or
DATEPART(Day, @StartDate)) alternatively, weekend days). SQL Server’s date
THEN DATEADD(YEAR, DATEDIFF(YEAR, functions make that easy.
BirthDate, @StartDate) + 1,
BirthDate) One of the dateparts you can pass to DatePart()
ELSE DATEADD(YEAR, DATEDIFF(YEAR, is weekday; the function returns a number between
BirthDate, @StartDate), BirthDate) 1 and 7 to indicate the day of the week of the date/
END time you pass in. You can also use DateName() to
BETWEEN @StartDate AND @EndDate
ORDER BY get the name of the day in the local language. So
CASE WHEN DATEPART(Month, Birthdate) < the query in Listing 11 returns today’s date, its day
DATEPART(Month, @StartDate) number, and its name; Figure 9 shows the results.
OR (DATEPART(Month, Birthdate) =
DATEPART(Month, @StartDate) AND
DATEPART(Day, Birthdate) <
DATEPART(Day, @StartDate))
THEN DATEADD(YEAR, DATEDIFF(YEAR, Figure 9. January 5, 2017 is a Thursday, the fifth day of the
BirthDate, @StartDate) + 1, week.

Page 10 FoxRockX January 2017


Listing 11. DatePart() and DateName() let you find out the day ing 12 omits days 1 and 7, Sunday and Saturday.
of the week of any date/time. But with a different value for SET DATEFIRST, you
SELECT GetDate(), have to omit different day numbers.
DATEPART(weekday, GetDate()),
DATENAME(weekday, GetDate()) To make the query locale-independent, we
need to compute the day of the week for Satur-
To get the day of the week for all dates between day and Sunday, and use the computed value. We
specified start and end dates, first we need to gen- can do that with the system variable @@DateFirst,
erate the list of dates. We can do that with a recur- which returns the current SET DATEFIRST value.
sive CTE. (See my November, 2015 article for an Listing 13 shows how to compute the day numbers
explanation of how this CTE works.) Then, we can for Saturday and Sunday. Sunday is easier; just
simply use DatePart() to find the day of the week subtract the first day from 8. So, with @@DateFirst
for each and keep only those we’re interested in. as 7 (Sunday), you get 1. When @@DateFirst is 1
Listing 12 shows how; the code is included in this (Monday), you get 7, and so forth.
month’s downloads as WeekDays.SQL. Figure 10 For Saturday, it’s a little trickier. 7-@@DateFirst
shows partial results. does the job, except when @@DateFirst is 7 (Sun-
Listing 12. To get a list of weekdays in a specified period, day). In that case, we want 7 for Saturday, but 7-@@
generate all dates and keep only those with the right weekday DateFirst gives us 0. So the code handles that case
datepart. separately.
DECLARE @StartDate Date, @EndDate Date;
Listing 13. We can calculate the day number for Saturday and
SET @StartDate = '12/15/2016'; Sunday using the @@DateFirst variable.
SET @EndDate = '1/15/2017'; SET @Sunday = 8-@@DATEFIRST;
SET @Saturday =
WITH AllDates (tDate) CASE WHEN @@DATEFIRST = 7
AS THEN 7
(SELECT @StartDate ELSE 7-@@DATEFIRST END;
UNION ALL
SELECT DATEADD(DAY, 1, tDate)
FROM AllDates
To make the query generic, we do these calcu-
WHERE tDate < @EndDate lations first and then use the variables @Saturday
) and @Sunday in the WHERE clause instead of con-
stants, as in Listing 14 (included as WeekdaysGe-
SELECT tDate, DATENAME(WEEKDAY, tDate) AS DoW
FROM AllDates
neric.SQL in this month’s downloads). To test that
WHERE DATEPART(WEEKDAY, tDate) NOT IN (1,7) this really works regardless of the DATEFIRST set-
ting, uncomment the SET DATEFIRST line in the
example and try a few different values.
Listing 14. You can get a list of weekdays without changing
your code, no matter how you’ve set DATEFIRST.
DECLARE @StartDate Date, @EndDate Date;

SET @StartDate = '12/15/2016';


SET @EndDate = '1/15/2017';

DECLARE @Saturday Int, @Sunday Int;

-- SET DateFirst 7;

SET @Sunday = 8-@@DATEFIRST;


SET @Saturday =
CASE WHEN @@DATEFIRST = 7
THEN 7
ELSE 7-@@DATEFIRST END;

WITH AllDates (tDate)


AS
(SELECT @StartDate
Figure 10. This list includes only weekdays
UNION ALL
in the specified period.
SELECT DATEADD(DAY, 1, tDate)
FROM AllDates
There’s just one wrinkle. The weekday value WHERE tDate < @EndDate
returned by DatePart() is based on the SET DATE- )
FIRST value. By default, in the US, it’s 7, which SELECT tDate, DATENAME(WEEKDAY, tDate) AS DoW
makes Sunday the first day. Thus, the query in List- FROM AllDates
WHERE DATEPART(WEEKDAY, tDate)
NOT IN (@Saturday,@Sunday)

January 2017 FoxRockX Page 11


You can just put the Saturday and Sunday cal- Author Profile
culations in the query, rather than doing them first Tamar E. Granor, Ph.D. is the owner of Tomorrow’s
and storing them in variables, but I think this ver- Solutions, LLC. She has developed and enhanced numer-
sion is more readable. ous Visual FoxPro applications for businesses and other
Of course, to see weekend days only, just organizations. Tamar is author or co-author of a dozen
change NOT IN to IN in the main query. books including the award winning Hacker’s Guide to
Visual FoxPro, Microsoft Office Automation with Visual
FoxPro and Taming Visual FoxPro’s SQL. Her latest
Try some yourself collaboration is VFPX: Open Source Treasure for the
With the techniques from this article and the previ- VFP Developer, available at www.foxrockx.com. Her
ous one in hand, you should be able to tackle lots of other books are available from Hentzenwerke Publish-
date/time challenges yourself. Let me know if you ing (www.hentzenwerke.com). Tamar was a Microsoft
come across any particularly interesting ones. Support Most Valuable Professional from the program's
inception in 1993 until 2011. She is one of the organizers
of the annual Southwest Fox conference. In 2007, Tamar
received the Visual FoxPro Community Lifetime Achieve-
ment Award. You can reach her at tamar@thegran-
ors.com or through www.tomorrowssolutionsllc.com.

DOWNLOAD
Subscribers can download FR201701code.zip in the SourceCode sub directory of the document
portal. It contains the following files:
tamargranor201701_code.zip
Source code for the article “Solve date problems” from Tamar E. Granor, Ph.D.
doughennig201701_code.zip
Source code for the article “Lessons Learned in Version Control, Part 1” from Doug Hennig
whilhentzen201701_code.zip
Source code for the article “Parsing Obnoxious Text Files – Part 2” from Whil Hentzen
ericselje201701_code.zip
Source code for the article “FoxUnit in Depth” from Eric Selje

FoxRockX™(ISSN-1866-4563)

Copyright © 2017 ISYS GmbH. This work is an independently produced


dFPUG c/o ISYS GmbH pub­ lication of ISYS GmbH, Kronberg, the content of which is the
Frankfurter Strasse 21 B property of ISYS GmbH or its affiliates or third-party licensors and which
61476 Kronberg, Germany is protected by copyright law in the U.S. and elsewhere. The right to copy
Phone +49-6173-950903 and publish the content is reserved, even for content made available for
Fax +49-6173-950904 free such as sample articles, tips, and graphics, none of which may be
Email: foxrockx@dfpug.de copied in whole or in part or further distributed in any form or medium
Editor: without the express written permission of ISYS GmbH. Requests for
Rainer Becker-Hinrichs permission to copy or republish any content may be directed to Rainer
Becker-Hinrichs.

FoxRockX, FoxTalk 2.0, FoxTalk, Visual Extend and Silverswitch are trademarks of ISYS GmbH. All product
names or services identified throughout this journal are trademarks or registered trademarks of their
respective companies.

FoxRockX is published bimonthly by ISYS GmbH

Page 12 FoxRockX January 2017

You might also like