February 11, 2003 at 4:47 am
I have a table named TB_HOLIDAY in my database. This table contains the following columns
Column NameData TypeVALUE
YEARINT2003
JANVARCHAR1101100000110000011000001100000
FEBVARCHAR1100000110000011000001100000222
MARVARCHAR1100000110000011000001100000110
APRVARCHAR0000110000011000011110000110002
MAYVARCHAR1011000001100000110000011000101
JUNVARCHAR1000001110000110000011000001102
JULVARCHAR0000110000011000001100000110000
AUGVARCHAR1110000011000001100000110000011
SEPVARCHAR0000011000001100000110000011002
OCTVARCHAR0001100000110000011000001100000
NOVVARCHAR1100000110000011000001100000112
DECVARCHAR0000011000001100000110001111000
The data values represent 1 for a holiday and 0 for a normal working day.
My task is to calculate the number of working days from a given date to another date.
There could be records for more than 1 year.
Every 1 in the value represents a non-working day. Every 0 represents a working day. Every 2 represents a dummy value for a day which is not present in that particular month. Every value contains 31 characters. So if a particular month has 30 days, then the 31st value is represented by a 2.
This has to be a stored procedure.
This is what I have written so far.
Declare @Occurrence int
Declare @lBusinessDays int
Declare @lDaysDiff int
Declare @lBalMonth varchar(1000)
Declare @lBMonth varchar(3)
Declare @lEMonth varchar(3)
Declare @lSQLStat2 varchar(4000)
Declare @lSQLStat1 varchar(4000)
Declare @iStartDate datetime
Declare @iEndDate datetime
Declare @lMonth1Values varchar(31)
Declare @lMonth2Values varchar(31)
Declare @lBYear int
Declare @lEYear int
Declare @lBDay int
Declare @lEDay int
Declare @lValue varchar(31)
Declare @Month1Length int
Declare @Month2Length int
Declare @lDays int
Declare @lFirstDay int
Declare @dummymonth varchar(2)
Declare @dummyday varchar(2)
Declare @dummyyear varchar(4)
Declare @dummydate datetime
Declare @lBalMonths int
Declare @lBalYears int
Declare @lDiffMonth int
Set @iStartDate = '01-Aug-2003'
Set @iEndDate = '15-Sep-2003'
Set @lBYear = DatePart(yyyy,@iStartDate) --Starting Year
Set @lEYear = DatePart(yyyy,@iEndDate) --Ending Year
Set @lBMonth = SUBSTRING(DateName(mm,@iStartDate),1,3) --Starting Date Month
Set @lEMonth = SUBSTRING(DateName(mm,@iEndDate),1,3) --Ending Date Month
Set @lBDay = DatePart(dd,@iStartDate)
Set @lEDay = DatePart(dd,@iEndDate)
Set @lDaysDiff = DateDiff(dd,@iStartDate,@iEndDate)
Set @dummyyear = Convert(varchar,@lBYear)
Set @dummyday = Convert(varchar,@lEDay)
Set @lDiffMonth = DateDiff(mm,@iStartDate,@iEndDate)
Print 'The number of days difference is ' + Convert(varchar,@lDaysDiff)
If (@lEMonth = @lBMonth) And (@lEYear = @lBYear) --The start / end dates lie in the same month of the same year
Begin
Print 'Same Month Same Year. Easiest Option.'
Set @lSqlStat1 = 'Declare CurMonths1 CURSOR FOR SELECT ' + Upper(@lBMonth)
+ ' FROM TB_HOLIDAY WHERE Year = ' + Convert(varchar,@lBYear)
Exec (@lSqlStat1)
Set @lDays = 0--Initialize the days to 0
OPEN CurMonths1
FETCH NEXT FROM CurMonths1 INTO @lMonth1Values
WHILE @@FETCH_STATUS = 0
BEGIN
Set @lValue = SUBSTRING(@lMonth1Values,@lBDay,@lEDay)
Print 'String Passed To ' + @lValue
Exec OF_SP_COUNTER @lValue,'1',@Occurrence OUTPUT
FETCH NEXT FROM CurMonths1 INTO @lMonth1Values
END
CLOSE CurMonths1
DEALLOCATE CurMonths1
Set @lBusinessDays = @lDaysDiff - @Occurrence
Print 'No of Working Days' + Convert(varchar,@lBusinessDays)
Return
End
If (@lEYear = @lBYear) --Same Year
Begin
Print 'Same Year.'
Set @lSqlStat1 = 'Declare CurMonths1 CURSOR FOR SELECT ' + Upper(@lBMonth)
+ ' FROM TB_HOLIDAY WHERE Year = ' + Convert(varchar,@lBYear)
Exec (@lSqlStat1)
Set @lDays = 0--Initialize the days to 0
Set @lFirstDay = 1 --First Day of the month is always 1
OPEN CurMonths1
FETCH NEXT FROM CurMonths1 INTO @lMonth1Values
While @@FETCH_STATUS = 0
BEGIN
PRINT @lMonth1Values + '~~'
Set @Month1Length = Len(@lMonth1Values)
Set @lValue = SUBSTRING(@lMonth1Values,@lBDay,Len(@lMonth1Values))
Exec OF_SP_COUNTER @lValue,'1',@Occurrence OUTPUT
Set @lDays = @lDays + @Occurrence
FETCH NEXT FROM CurMonths1 INTO @lMonth1Values
END
Print 'No of Days in the first month ' + Convert(varchar,@lDays)
CLOSE CurMonths1
DEALLOCATE CurMonths1
--Now for the Ending month
Set @lSqlStat2 = 'Declare CurMonths2 CURSOR FOR SELECT ' + Upper(@lEMonth)
+ ' FROM TB_HOLIDAY WHERE Year = ' + Convert(varchar,@lEYear)
Exec (@lSqlStat2)
OPEN CurMonths2
FETCH NEXT FROM CurMonths2 INTO @lMonth2Values
While @@FETCH_STATUS = 0
BEGIN
--PRINT @lMonth2Values + '~Month 2 Values'
Set @Month2Length = Len(@lMonth2Values)
WHILE @lFirstDay <= @lEday /* Increment the first day till the End Day */
BEGIN
--Print 'End Day ' + Convert(Varchar,@lEDay)
Set @lValue = SUBSTRING(@lMonth2Values,@lFirstDay,@lEDay)
--PRINT 'Got The Value --> ' + @lValue
If SUBSTRING(@lValue,1,1) = '1'
BEGIN
Set @lDays = @lDays + 1 /* Increment for 1 found */
END
Set @lFirstDay = @lFirstDay + 1
END
FETCH NEXT FROM CurMonths2 INTO @lMonth2Values
END
CLOSE CurMonths2
DEALLOCATE CurMonths2
Set @lBalMonths = DateDiff(mm,@iStartdate,@iEndDate)
Print 'Balance Months -- ' +Convert(varchar,@lBalMonths)
Set @lBalMonths = @lBalMonths - 1 --Subtract Beginning Month
Set @lSqlStat1 = 'SELECT '
if @lBalMonths >= 2 --Check if gap is more than or equal to 2 months
Begin --Check if the balance months are more than 2
Print 'Gap is more than or equal to 2 months. Same Year.' + Convert(varchar,@lBalMonths)
Set @lSqlStat2 = '' --Nullify the string
While @lBalMonths != 0
Begin
--1 Added For Compensating the ending month
Set @dummymonth = Convert(varchar,@lBalMonths + Datepart(mm,@iStartDate))
--Adding 2 is essential to ensure counting from correct date
Set @dummydate = Convert(datetime,@dummyday + '/'+ @dummymonth + '/' + @dummyyear,103)
Set @lSqlStat2 = @lSqlStat2 + UPPER(SUBSTRING(DateName(mm,@dummydate),1,3)) + '+'
Set @lBalMonths = @lBalMonths - 1
Print '@lBalMonths - ' + Convert(varchar,@lBalMonths)
End
--End Of String Comma Removal
Set @lSqlStat2 = SUBSTRING(@lSqlStat2,1,Len(@lSqlStat2)-1)
Set @lSqlStat1 = @lSqlStat1 + @lSqlStat2 + ' FROM TB_HOLIDAY WHERE YEAR = '
+ Convert(varchar,@lEYear)
Print @lSqlStat1
Set @lSqlStat1 = 'Declare BalMonths Cursor FOR ' + @lSqlStat1
Exec (@lSqlStat1)
Open BalMonths
FETCH NEXT FROM BalMonths INTO @lBalMonth
WHILE @@FETCH_STATUS = 0
BEGIN
Exec OF_SP_COUNTER @lBalMonth,'1',@Occurrence OUTPUT
Set @lDays = @lDays + @Occurrence
FETCH NEXT FROM BalMonths INTO @lBalMonth
END
CLOSE BalMonths
DEALLOCATE BalMonths
End --If gap is 2 or more months
Else --If gap is only 1 month
Begin
Print 'Gap is equal to 1 month. Same Year.'
Set @lSqlStat2 = '' --Nullify the string
Set @dummyday = 1
Set @dummyyear = @lEYear
Set @dummymonth = Convert(varchar,@lBMonth) --Next Month Should be after the current month
Set @dummydate = Convert(datetime,@dummyday + '/'+ @dummymonth + '/' + @dummyyear,103)
Set @lSqlStat2 = @lSqlStat2 + UPPER(SUBSTRING(DateName(mm,@dummydate),1,3)) + '+'
--End Of String Comma Removal
Set @lSqlStat2 = SUBSTRING(@lSqlStat2,1,Len(@lSqlStat2)-1)
Set @lSqlStat1 = @lSqlStat1 + @lSqlStat2 + ' FROM TB_HOLIDAY WHERE YEAR = '
+ Convert(varchar,@lEYear)
Print @lSqlStat1
Set @lSqlStat1 = 'Declare BalMonths Cursor FOR ' + @lSqlStat1
Exec (@lSqlStat1)
Open BalMonths
FETCH NEXT FROM BalMonths INTO @lBalMonth
WHILE @@FETCH_STATUS = 0
BEGIN
Exec OF_SP_COUNTER @lBalMonth,'1',@Occurrence OUTPUT
Set @lDays = @lDays + @Occurrence
FETCH NEXT FROM BalMonths INTO @lBalMonth
END
CLOSE BalMonths
DEALLOCATE BalMonths
End
End --Same Year / Different Months
February 11, 2003 at 5:57 am
Is there a question you are asking? Not quite sure what you're looking for...Are you asking whether there is a simpler way of determining the number of days between a holiday?
One question...
Why are you storing the values in a varchar datatype? Why not store the dates in a smalldatetime field? You could structure the table like so:
CREATE TABLE TB_HOLIDAY
(dteHoliday SMALLDATETIME NOT NULL)
, then populate it with the actual dates of the holidays. It seems to me with this structure it would be easier to do DATE arithmetic. To solve the problem of the non-working days, you could either make another table to cross-reference in a query that comprises the non-working days, or use the day of week in the DATE arithmetic to filter out Saturdays and Sundays.
Hope I'm on the right track. Let me know...
February 11, 2003 at 6:05 am
Well my friend, the reason the dates are stored the way they are stored, is L E G A C Y
I do not have an interface to insert the holidays and for the user it would mean just another maintainence job. I am not authorised to do any further changes. However I could probably write a trigger which would populate the holiday dates in another table. That is certainly possible.
The existing table TB_HOLIDAY is used by a lot of VB programs, so it would not be possible to make any changes to it.
I want to know what I have written is going to work for all possible cases where the year is the same.
Cheers,
Abhijit
February 11, 2003 at 6:11 am
Here is another way to do it which looks a bit simpler.
declare @StartDate datetime,
@EndDate datetime
set @StartDate = '20030115'
set @EndDate = '20030120'
declare@Year int,
@Calender char(372),
@PreDays int,
@PostDays int,
@WorkingDays int,
@NonWorkingDays int,
@DummyDays int,
@Counter int,
@Day char(1)
set @WorkingDays = 0
set @NonWorkingDays = 0
set @DummyDays = 0
declare calender cursor local for
selectYear,
Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec
fromTB_Holiday
whereYear between datepart(yy, @StartDate) and datepart(yy, @EndDate)
open calender
fetch next from calender into @Year, @Calender
while @@fetch_status = 0
begin
if datepart(yy, @StartDate) = @Year
begin
-- trim preceeding days from @Calender
set @PreDays = ((datepart(mm, @StartDate) - 1) * 31) + (datepart(dd, @StartDate) - 1)
set @Calender = right(@Calender, len(@Calender) - @PreDays)
end
if datepart(yy, @EndDate) = @Year
begin
-- trim post days from @Calender
set @PostDays = ((12 - datepart(mm, @EndDate)) * 31) + (31 - datepart(dd, @EndDate))
set @Calender = left(@Calender, len(@Calender) - @PostDays)
end
set @Counter = 1
while @Counter < len(@Calender)
begin
set @Day = substring(@Calender, @Counter, 1)
if @Day = '0'
set @WorkingDays = @WorkingDays + 1
if @Day = '1'
set @NonWorkingDays = @NonWorkingDays + 1
if @Day = '2'
set @DummyDays = @DummyDays + 1
set @Counter = @Counter + 1
end
fetch next from calender into @Year, @Calender
end
close calender
deallocate calender
select@WorkingDays as WorkingDays,
@NonWorkingDays as NonWorkingDays,
@DummyDays as DummyDays
February 11, 2003 at 6:18 am
Paul's script does indeed look simpler, however, I don't see any OF_SP_COUNTER call in it. Is this procedure essential, or can you add it to Paul's script...
February 11, 2003 at 6:21 am
Thanks a lot Paul, I am still testing out that script.
OF_SP_COUNTER is a generic procedure which counts for the occurrence of a character in a particular string. For e.g -'JPIPES','P' will return 2 since there are 2 P's in that string. We use it for a lot of stuff. However its not essential over here.
Cheers!
Abhijit
February 11, 2003 at 7:31 am
well0549 your solution will only work for SQL2000 and I have no idea which version abhi_develops is currently using.
Everyone seems to be knocking how this data is stored but thinking about it can anyone else think of a better way to stored information for every day of the year using a total of only 376 bytes? You could substitute the 0,1,2 for any alphanumeric which gives you the ability of storing any number of single statuses for days of the year. (This one will probably open a can of worms!)
February 11, 2003 at 7:37 am
Paul,
be creative...
If a memtable doesn't work.....
Use a temp table.....
Solution is still the best......
February 11, 2003 at 7:38 am
.....if a temp table, just look at all that IO!!!
February 11, 2003 at 7:46 am
Minor correction to my code, the line below....
while @Counter < len(@Calender)
...should read......
while @Counter <= len(@Calender)
....sorry abhi_develops
February 11, 2003 at 7:57 am
Paul,
A couple of commets on your assumptions.....
We don't know if the poster of this thread uses 7. If he uses 2000, I think my solution is the best yet, because it is short and easy to maintain.
Even if he has SQL 7 we don't know what kind of harddisk he has. And all that IO ... Is this really true....(Is nothing bufferd somewhere.....) I think the server would write a few pages in Tempdb. (Remember that it is not much data !!!!!!!!) ( I could be wrong here but don't think so...... )
Furthermore, Please let the poster decide if something works or not.....
February 11, 2003 at 8:06 am
well0549,
I do not believe that I said your code would not work. The reason I made the comment regarding SQL2000 was that abhi_develops's initial post states that this has to be a stored procedure (this could imply that functions (i.e. SQL2000) is not available to him.
I have also carried out some comparisions on one of our development servers to compare your code and my own.
Your code (memtable) took 2443ms.
Your code (temp table) took 3866ms.
My code (as post) took 10ms.
Please feel free to compare them for yourself, I think that all the code you need is in the posts here.
February 11, 2003 at 8:24 am
The performance of my code could be further improved by using a STATIC cursor. This performance gain would be most evident when the date ranges spanned multiple years since all the data for the cursor is returned in a single operation instead of each FETCH.
BTW I know I spelt calendar wrong in my code....WHOOPS!
February 11, 2003 at 8:32 am
What about this option? Is this a simple, easy to follow method? The only problem with this solution is it will only handle a span of 21 years between start and end date.
SET NOCOUNT ON
drop table holiday
go
create TABLE holiday
(YEAR INT,
JAN VARCHAR(31),
FEB VARCHAR(31),
MAR VARCHAR(31),
APR VARCHAR(31),
MAY VARCHAR(31),
JUN VARCHAR(31),
JUL VARCHAR(31),
AUG VARCHAR(31),
SEP VARCHAR(31),
OCT VARCHAR(31),
NOV VARCHAR(31),
DEC VARCHAR(31))
-- STUFF SOME DATA IN IT ........
INSERT INTO HOLIDAY VALUES
(2003,'1101100000110000011000001100000',
'1100000110000011000001100000222',
'1100000110000011000001100000110',
'0000110000011000011110000110002',
'1011000001100000110000011000101',
'1000001110000110000011000001102',
'0000110000011000001100000110000',
'1110000011000001100000110000011',
'0000011000001100000110000011002',
'0001100000110000011000001100000',
'1100000110000011000001100000112',
'0000011000001100000110001111000')
INSERT INTO HOLIDAY VALUES
(2002,'1101100000110000011000001100000',
'1100000110000011000001100000222',
'1100000110000011000001100000110',
'0000110000011000011110000110002',
'1011000001100000110000011000101',
'1000001110000110000011000001102',
'0000110000011000001100000110000',
'1110000011000001100000110000011',
'0000011000001100000110000011002',
'0001100000110000011000001100000',
'1100000110000011000001100000112',
'0000011000001100000110001111000')
INSERT INTO HOLIDAY VALUES
(2001,'1101100000110000011000001100000',
'1100000110000011000001100000222',
'1100000110000011000001100000110',
'0000110000011000011110000110002',
'1011000001100000110000011000101',
'1000001110000110000011000001102',
'0000110000011000001100000110000',
'1110000011000001100000110000011',
'0000011000001100000110000011002',
'0001100000110000011000001100000',
'1100000110000011000001100000112',
'0000011000001100000110001111000')
declare @dt1 datetime
declare @dt2 datetime
declare @m1 int
declare @d1 int
declare @y1 int
declare @m2 int
declare @d2 int
declare @y2 int
declare @h1 char(8000)
-- start date
set @dt1 = '01/01/2001'
-- end date
set @dt2 = '01/01/2003'
set @m1 = datepart(mm,@dt1)
set @y1 = datepart(yy,@dt1)
set @d1 = datepart(dd,@dt1)
set @m2 = datepart(mm,@dt2)
set @y2 = datepart(yy,@dt2)
set @d2 = datepart(dd,@dt2)
set @h1 = ''
-- put together all years
select @h1 = rtrim(@h1) + jan+feb+mar+apr+may+jun+jul+aug+sep+oct+nov+dec from holiday
where year >= @y1 and year <= @y2
-- truncate beginning and end of holidays based on start and end date
set @h1=substring(@h1,(@m1-1)*31+@d1,(((@y2-@y1)* 372)+(@m2-1)*31+@d2)-((@m1-1)*31+@d1)+1)
-- remove non-work dayse
set @h1=replace(replace(@h1,'1',''),'2','')
print 'The number of work days is: ' + cast(len(@h1) as cha
Gregory Larsen, DBA
If you looking for SQL Server Examples check out my website at http://www.geocities.com/sqlserverexamples
Gregory A. Larsen, MVP
February 11, 2003 at 9:01 am
Commenting on paul.....
I don't know what kind of server you have but on my laptop.....
for a 10 year period I came to 120 for yours and 450 for mine...
so approx 4 times faster. But I don't get the big difference
you got.....
But I think Gregs solution is the best.....
( simple fast )
And because of that I removed my solution....
Edited by - well0549 on 02/11/2003 09:21:10 AM
Viewing 15 posts - 1 through 15 (of 17 total)
You must be logged in to reply to this topic. Login to reply