convert scalar function to table valued function

  • hi all, great day

    i failed to convert the below scalar function to table valued function, i tried many times and always got error

    appreciate if anybody assist in this issue

     

    ALTER FUNCTION [dbo].[fn_GetLeaveDays]

    (@DateFrom DATETIME2,@CalendarFunction INT,@DateTo AS DATETIME2,@AdjustMode BIT,@AdjustWeekEnds BIT,@AdjustHolidays BIT) RETURNS INT AS BEGIN

    IF @DateFrom>@DateTo BEGIN

    DECLARE @T DATETIME2=@DateTo,@F DATETIME2=@DateFrom

    SELECT @DateFrom=@T,@DateTo=@F

    END

    DECLARE @Count AS INT=0,@Date As DATETIME2=@DateFrom

    WHILE @Date < @DateTo BEGIN

    IF ((DATEPART(WEEKDAY,@Date)IN (6)AND @AdjustWeekEnds=1)

    OR

    EXISTS (select * from [C3DCalendar].[dbo].[PRIMAVERA_CALENDAR_HOLIDAYS] WHERE holiday_date=@Date AND calendar_key=@CalendarFunction AND @AdjustHolidays=1))

    BEGIN

    SELECT @Count = @Count + 1

    END

    SELECT @Date=DATEADD(DAY,1,@Date)

    END

    RETURN (DATEDIFF(DAY,@DateFrom,@DateTo)-(@Count))+@AdjustMode

    END

  • do you have or can you create a calendar table that contains the weekday for each date?

    that would make giving you an easier example rather than having to build it as part of the function.

  • What were the attempts you made to make it into a iTVF as well, and what were the error(s) you got?

    Thom~

    Excuse my typos and sometimes awful grammar. My fingers work faster than my brain does.
    Larnu.uk

  • i've tried the below and got this error : Cannot find either column "dbo" or the user-defined function or aggregate "dbo.fn_GetLeaveDays", or the name is ambiguous.

     

    ALTER FUNCTION [dbo].[fn_GetLeaveDays]

    (

    @DateFrom DATETIME2,

    @CalendarFunction INT,

    @DateTo AS DATETIME2,

    @AdjustMode BIT,

    @AdjustWeekEnds BIT,

    @AdjustHolidays BIT

    )

    RETURNS @tbl Table(totaldays int)

    AS

    BEGIN

    IF @DateFrom>@DateTo BEGIN

    DECLARE @T DATETIME2=@DateTo,@F DATETIME2=@DateFrom

    SELECT @DateFrom=@T,@DateTo=@F

    END

    DECLARE @Count AS INT=0,@Date As DATETIME2=@DateFrom

    WHILE @Date < @DateTo BEGIN

    IF ((DATEPART(WEEKDAY,@Date)IN (6)AND @AdjustWeekEnds=1)

    OR

    EXISTS (select * from [C3DCalendar].[dbo].[PRIMAVERA_CALENDAR_HOLIDAYS] WHERE holiday_date=@Date AND calendar_key=@CalendarFunction AND @AdjustHolidays=1))

    BEGIN

    SELECT @Count = @Count + 1

    END

    SELECT @Date=DATEADD(DAY,1,@Date)

    END

    insert into @tbl

    select (DATEDIFF(DAY,@DateFrom,@DateTo)-(@Count))+@AdjustMode

    RETURN

    END

  • Ok, that function needs a massive change anyway, it's a Multi-line table value function and it's using a WHILE; both are (with respect) absolutely terrible for performance. What is the actual goal of said function here, just to get the number of dates between 2 dates, excluding the dates specified; for example weekends and holidays?

    As mentioned above, a Calendar table would make this far easier. Can you implement one?

    Edit: For sanity (as left align, non-code formatted code is really difficult to read):

    ALTER FUNCTION [dbo].[fn_GetLeaveDays] (@DateFrom datetime2,
    @CalendarFunction int,
    @DateTo AS datetime2,
    @AdjustMode bit,
    @AdjustWeekEnds bit,
    @AdjustHolidays bit)
    RETURNS @tbl table (totaldays int)
    AS
    BEGIN

    IF @DateFrom > @DateTo
    BEGIN

    DECLARE @T datetime2 = @DateTo,
    @F datetime2 = @DateFrom;

    SELECT @DateFrom = @T,
    @DateTo = @F;

    END;

    DECLARE @Count AS int = 0,
    @Date AS datetime2 = @DateFrom;

    WHILE @Date < @DateTo
    BEGIN

    IF ((DATEPART(WEEKDAY, @Date) IN (6)
    AND @AdjustWeekEnds = 1)
    OR EXISTS (SELECT *
    FROM [C3DCalendar].[dbo].[PRIMAVERA_CALENDAR_HOLIDAYS]
    WHERE holiday_date = @Date
    AND calendar_key = @CalendarFunction
    AND @AdjustHolidays = 1))
    BEGIN

    SELECT @Count = @Count + 1;

    END;

    SELECT @Date = DATEADD(DAY, 1, @Date);

    END;

    INSERT INTO @tbl
    SELECT (DATEDIFF(DAY, @DateFrom, @DateTo) - (@Count)) + @AdjustMode;

    RETURN;

    END;

    • This reply was modified 4 years, 1 month ago by  Thom A.

    Thom~

    Excuse my typos and sometimes awful grammar. My fingers work faster than my brain does.
    Larnu.uk

  • actually i can't make that calendar table and even if i can, my concern is : that table should be created for how many years? and how to define the holidays for each year? (in my case we don't have a fixed days per year, it changed from year to year) so i created a holiday_table to insert all holidays dates in it.

  • Having a separate table for the holidays isn't a problem; I personally (at the office) use a separate table to store holidays; mainly as i store some additional meta data (such as if the business operated at reduced hours). Some prefer to have a column in the Calendar Table to denote if the day is a holiday or not but it's not required. A Calendar Table simply compliments to "Holiday Table"

    Thom~

    Excuse my typos and sometimes awful grammar. My fingers work faster than my brain does.
    Larnu.uk

  • FINALLY IT WORKS 🙂

    the issue was in how to call the function not in the mentioned above modified function

    it works fine when i used this to call it

    Select * from [dbo].[fn_GetLeaveDays] ('2020-08-19',1,'2020-08-22',1,1,1)

    instead of

    Select [dbo].[fn_GetLeaveDays] ('2020-08-19',1,'2020-08-22',1,1,1)
  • My points about the TVF you have still apply though, tanehome. A Multi-line table value function, with a WHILE is going to be very poor for performance. You really want an iTVF, not an mlTVF. Just because the above works doesn't mean you should be implementing it.

    Thom~

    Excuse my typos and sometimes awful grammar. My fingers work faster than my brain does.
    Larnu.uk

  • actually my knowledge with functions is very poor as this is first time to use function

    appropriate if you guide me to how to convert my mlTVF to iTVF and what is the different between them

  • tanehome wrote:

    FINALLY IT WORKS 🙂

    the issue was in how to call the function not in the mentioned above modified function

    it works fine when i used this to call it

    Select * from [dbo].[fn_GetLeaveDays] ('2020-08-19',1,'2020-08-22',1,1,1)

    instead of

    Select [dbo].[fn_GetLeaveDays] ('2020-08-19',1,'2020-08-22',1,1,1)

    If the function still has the word BEGIN in it, then it may be working correctly but it is not working "fine". 😉  Read what Thom A has been trying to tell you on this thread.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • hi again

    actually the function does not work properly after mant tests

    for example i've tried

    Select [dbo].[fn_GetLeaveDays_scalar] ('2020-09-24',1,'2020-09-25',1,1,1)

    and it should returns only 1 as friday is weekend but it returns 2

    could you please assist to find whats wrong in the

    ALTER FUNCTION [dbo].[fn_GetLeaveDays] (@DateFrom datetime2,
    @CalendarFunction int,
    @DateTo AS datetime2,
    @AdjustMode bit,
    @AdjustWeekEnds bit,
    @AdjustHolidays bit)
    RETURNS @tbl table (totaldays int)
    AS
    BEGIN

    IF @DateFrom > @DateTo
    BEGIN

    DECLARE @T datetime2 = @DateTo,
    @F datetime2 = @DateFrom;

    SELECT @DateFrom = @T,
    @DateTo = @F;

    END;

    DECLARE @Count AS int = 0,
    @Date AS datetime2 = @DateFrom;

    WHILE @Date < @DateTo
    BEGIN

    IF ((DATEPART(WEEKDAY, @Date) IN (6)
    AND @AdjustWeekEnds = 1)
    OR EXISTS (SELECT *
    FROM [C3DCalendar].[dbo].[PRIMAVERA_CALENDAR_HOLIDAYS]
    WHERE holiday_date = @Date
    AND calendar_key = @CalendarFunction
    AND @AdjustHolidays = 1))
    BEGIN

    SELECT @Count = @Count + 1;

    END;

    SELECT @Date = DATEADD(DAY, 1, @Date);

    END;

    INSERT INTO @tbl
    SELECT (DATEDIFF(DAY, @DateFrom, @DateTo) - (@Count)) + @AdjustMode;

    RETURN;

    END;

    function

     

  • tanehome wrote:

    hi again

    actually the function does not work properly after mant tests

    With respect, this does not surprise me; I did mention that the Function was flawed in my above posts. Perhaps now you would consider explaining the goal of said function this time, please? Then, if appropriate, you can then consider using a Calendar Table, and myself or another user can show you how to do this with an inline Table Value Funtionc.

    Thom~

    Excuse my typos and sometimes awful grammar. My fingers work faster than my brain does.
    Larnu.uk

  • this function is calculating the number of working days between two dates Excluding Weekend & Holiday

    and it takes the holidays from table exist in another database (for attendance)

    @DateFrom - The starting date for calculation

    @CalendarFunction - The holiday type function you want to use as we have two groups type

    @DateTo - The end of the date range you want to use

    @AdjustMode - Calculate the days between the dates as 0 Exclusive or 1 Inclusive of the first day.

    @AdjustWeekend - Excludes weekends from your calculations

    @AdjustHolidays - Excludes holidays if the holiday function matches

  • Ok, firstly, then, let's start by creating a Calendar Table. This is a fairly simple one that I grabbed from one of my Sandbox databases, but should be enough for what we want here. Obviously, however, feel free to add (or remove) from it:

    CREATE TABLE [dbo].[CalendarTable]([CalendarDate] [date] NOT NULL,
    [CalendarYear] AS (datepart(year,[CalendarDate])) PERSISTED,
    [CalendarMonth] AS (datepart(month,[CalendarDate])) PERSISTED,
    [CalendarDay] AS (datepart(day,[CalendarDate])) PERSISTED,
    [CalendarMonthName] varchar(15) NOT NULL,
    [CalendarWeekday] varchar(15) NOT NULL,
    [CalendarWeekdayName] varchar(15) NOT NULL,
    [Weekend] AS CONVERT(bit,CASE WHEN CalendarWeekday IN (1,7) THEN 1 ELSE 0 END) PERSISTED,
    [JulianDate] AS ((DATEPART(YEAR,[CalendarDate]) * 1000 + DATEDIFF(DAY,DATEFROMPARTS(DATEPART(YEAR,[CalendarDate]),1,1),[CalendarDate]))+ 1 ) PERSISTED);
    GO
    ALTER TABLE dbo.CalendarTable ADD CONSTRAINT PK_CalendarDate PRIMARY KEY CLUSTERED (CalendarDate);(CalendarDate);
    --I recommend making some indexes here too
    GO

    SET DATEFIRST 7; --Sunday

    WITH N AS(
    SELECT N
    FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
    Tally AS(
    SELECT TOP (DATEDIFF(DAY,'19000101','21000101'))
    ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1 AS I
    FROM N N1, N N2, N N3, N N4, N N5),
    Dates AS(
    SELECT DATEADD(DAY, I, '19000101') AS D
    FROM Tally)
    INSERT INTO dbo.CalendarTable (CalendarDate, CalenderMonthName, CalenderWeekday,CalenderWeekdayName)
    SELECT D,
    DATENAME(MONTH,D),
    DATEPART(WEEKDAY,D),
    DATENAME(WEEKDAY,D)
    FROM Dates;

    OK, now we have a Calendar Table, we can start moving onto the function. I don't have a copy of your Holiday table, so I can't completely test this. As a result my tests excluded any references to the Holiday Table, and I uncommented the code for this post.

    First, let's get the base logic down, counting the dates between 2 dates, and adding the inclusive/exclusive logic:

    DECLARE @DateFrom date,
    @DateTo date,
    @AdjustMode bit;

    SET @DateFrom = '20200117';
    SET @DateTo = '20200930';
    SET @AdjustMode = 0;

    SELECT COUNT(*)
    FROM dbo.CalendarTable CT
    WHERE CT.CalendarDate >= @DateFrom
    AND CT.CalendarDate <= @DateTo
    AND ((CT.CalendarDate NOT IN (@DateFrom,@DateTo) AND @AdjustMode = 0)
    OR @AdjustMode = 1);

    For the 2 sample dates, that brings back 258 for 1 (inclusive mode) and 256 for 0 (exclusive mode). I didn't just subtract 2, as @DateFrom or @DateTo might be a holiday or weekend, and thus the result would be wrong.

    Excluding weekends is trivial, just add another parameter, @AdjustWeekend bit and then to the WHERE add this extra clause:

      AND ((@AdjustWeekend = 1 AND CT.Weekend = 0) OR @AdjustWeekend = 0)

    The final part, which I can't test, would be adding the Holiday exclusion and the holidays to use. I would use an EXISTS for this:

      AND (@AdjustHoliday = 0
    OR (@AdjustHoliday = 1
    AND NOT EXISTS(SELECT 1
    FROM dbo.HolidayTable HT --Obviously replace with your holiday table's name
    WHERE HT.HolidayDate = CT.CalendarDate
    AND HT.HolidayFunction = @CalendarFunction)));

    Finally, we then need to wrap this into an inline Table Value Function:

    CREATE FUNCTION dbo.GetLeaveDays (@DateFrom date, @DateTo date, @AdjustMode bit, @AdjustWeekend bit, @AdjustHoliday bit, @CalendarFunction int)
    RETURNS TABLE
    AS RETURN
    SELECT COUNT(*) AS LeaveDays
    FROM dbo.CalendarTable CT
    WHERE CT.CalendarDate >= @DateFrom
    AND CT.CalendarDate <= @DateTo
    AND ((@AdjustWeekend = 1 AND CT.Weekend = 0) OR @AdjustWeekend = 0)
    --Commented out, as I cannot compile such a function
    /*
    AND (@AdjustHoliday = 0
    OR (@AdjustHoliday = 1
    AND NOT EXISTS(SELECT 1
    FROM dbo.HolidayTable HT --Obviously replace with your holiday table's name
    WHERE HT.HolidayDate = CT.CalendarDate
    AND HT.HolidayFunction = @CalendarFunction)))
    */
    AND ((CT.CalendarDate NOT IN (@DateFrom,@DateTo) AND @AdjustMode = 0)
    OR @AdjustMode = 1);

    Which we can then test with something like this (obviously my version does nothing with the Holiday logic):

    SELECT GLD.LeaveDays
    FROM dbo.GetLeaveDays('20200103','20200917',1,1,0,NULL) GLD;

    • This reply was modified 4 years, 1 month ago by  Thom A. Reason: I can't spell Calendar, apparently

    Thom~

    Excuse my typos and sometimes awful grammar. My fingers work faster than my brain does.
    Larnu.uk

Viewing 15 posts - 1 through 15 (of 27 total)

You must be logged in to reply to this topic. Login to reply