Convert Excel formula (using Date and subtraction) into T-SQL - tsql

I am trying to write this Excel formula into T-SQL (to write a function).
Expected output is 0.71944444, but currently my output (using T-SQL) is 24.0000.
I am not sure why we have to add a day to same date and subtract the same date.
Bottom is a screenshot from Excel:
This is what I have so far in T-SQL:
CREATE FUNCTION [dbo].[fn_0921] (
#Punch_Start nvarchar(max)
)
RETURNS decimal(36, 8) AS
BEGIN
DECLARE #return_value nvarchar(max);
SET #return_value =
DATEDIFF(
MINUTE, CAST(#Punch_Start AS datetime2),
(
dateadd(
day, 1, CAST(#Punch_Start AS datetime2)
)
)
)
/ (60.0)
RETURN #return_value
END;
Thanks for help.

The Excel formula is returning the difference between the datetime in cell K4 & the start of the next day (i.e. 7/26/2021 00:00) as a fraction of a whole day. The following is the equivalent in T-SQL:
DECLARE #Punch_Start datetime2 = '7/25/2021 06:44';
SELECT DATEDIFF(
MINUTE,
#Punch_Start,
CAST(
CAST(
DATEADD(DAY, 1, #Punch_Start)
AS date) -- Add 1 day to #Punch_Start & cast as date to remove the time component - this is the start of the next day
AS datetime2) -- Cast back to datetime2 to get the difference in minutes
) / 1440.; -- Divide the difference in minutes by the number of minutes in a day (60 minutes per hour, 24 hours per day) to get the difference as a fraction of a day

This can probably help you:
DECLARE #date DATETIME2 = '2021-07-25 06:44'
DECLARE #seconds INT = DATEDIFF(second, CAST(#date AS date), #date)
DECLARE #secondsFromEnd FLOAT = 86400 - #seconds
SELECT #secondsFromEnd / 86400

Related

Find difference between timestamps in amount of custom intervals in PostgreSQL

I would like to find difference between two timestamps (with timezone) in amount of custom intervals. So function should be like custom_diff(timestamptz from, timestamptz to, interval custom).
Keep in mind, that it is not equivalent to (to-from)/custom (custom_diff('2016-08-01 00:00:00','2016-09-01 00:00:00','1 day') is exactly 31, but ('2016-08-01 00:00:00','2016-09-01 00:00:00')/'1 day')='1 month'/'1 day' and is ambiguous).
Also I understand that in general there is no exact result of such operation (custom_diff('2016-08-01 00:00:00','2016-09-01 00:00:00','1 month 1 day') so it is possible to have group of function (round-to-nearest, round-to-lower, round-to-upper and truncating, all of them should return integer number).
Is there any standard/common way for such calculation in PostgreSQL (PL/pgSQL)? My main interesting is round-to-nearest function.
The best way I have invented is to iteratively add/substract interval custom to/from timestamptz from and compare with timestamptz to. Also it can be optimized by initially finding approximate result (for example divide [difference in seconds between timestamps] for [approximation of interval custom in seconds]) to reduce amount of iterations.
UPD 1:
Why
SELECT EXTRACT(EPOCH FROM (timestamp '2016-08-01 10:00'
- timestamp '2016-08-01 00:00'))
/ EXTRACT(EPOCH FROM interval '1 day');
is a wrong solution: lets try yourself:
SELECT EXTRACT(EPOCH FROM ( TIMESTAMPTZ '2016-01-01 utc' -
TIMESTAMPTZ '1986-01-01 utc' ))
/ EXTRACT(EPOCH FROM INTERVAL '1 month');
Result is 365.23.... Then check result:
SELECT ( TIMESTAMPTZ '1986-01-01 utc' + 365 * INTERVAL '1 month' )
AT TIME ZONE 'utc';
Result is 2016-06-01 00:00:00.000000. Of cause 365 is wrong result, because timestamps in this example describe exactly 30 years and in any year always exactly 12 months, so right answer is 12*30=360.
UPD 2:
My solution is
CREATE OR REPLACE FUNCTION custom_diff(
_from TIMESTAMPTZ, _to TIMESTAMPTZ, _custom INTERVAL, OUT amount INTEGER)
RETURNS INTEGER
LANGUAGE plpgsql
AS $function$
DECLARE
max_iterations INTEGER :=10;
t INTEGER;
BEGIN
amount:=0;
WHILE max_iterations > 0 AND NOT (
extract(EPOCH FROM _to) <= ( extract(EPOCH FROM _from) + extract(EPOCH FROM _from + _custom) ) / 2
AND
extract(EPOCH FROM _to) >= ( extract(EPOCH FROM _from) + extract(EPOCH FROM _from - _custom) ) / 2
) LOOP
-- RAISE NOTICE 'iter: %', max_iterations;
t:=EXTRACT(EPOCH FROM ( _to - _from )) / EXTRACT(EPOCH FROM _custom);
_from:=_from + t * _custom;
amount:=amount + t;
max_iterations:=max_iterations - 1;
END LOOP;
RETURN;
END;
$function$
but I does not sure that it is correct and still waiting for sugestion about existing/common solution.
You can get exact result after extracting the epoch from both intervals:
SELECT EXTRACT(EPOCH FROM (timestamp '2016-08-01 10:00'
- timestamp '2016-08-01 00:00'))
/ EXTRACT(EPOCH FROM interval '1 day'); -- any given interval
If you want rounded (truncated) result, a simple option is to cast both to integer. Integer division cuts off the remainder.
SELECT EXTRACT(EPOCH FROM (ts_to - ts_from))::int
/ EXTRACT(EPOCH FROM interval '1 day')::int; -- any given interval
You can easily wrap the logic into a IMMUTABLE SQL function.
You are drawing the wrong conclusions from what you read in the manual. The result of a timestamp subtraction is an exact interval, storing only days and seconds (not months). So the result is exact. Try my query, it isn't "ambiguous".
You can avoid involving the data type interval:
SELECT EXTRACT(EPOCH FROM ts_to) - EXTRACT(EPOCH FROM ts_from))
/ 86400 -- = 24*60*60 -- any given interval as number of seconds
But the result is the same.
Aside:
"Exact" is an elusive term when dealing with timestamps. You may have to take DST rules and other corner cases of your time zone into consideration. You might convert to UTC time or use timestamptz before doing the math.

how to calculate WeekOfMonth in SQL

Trying to calculate holidays in a given year for loading DimDate table. There is an option in SQL to calculate the WeekOfYear Number but couldnt find a function to calculate the WeekNumberOfMonth.
How to find week number of a given month to calculate holidays in a year to fill DIMDate table
Logic
CurrentDateWeek - CurrentDateMonthBeginWeek
the difference between the week Number for a given date and the first week number for the given date provides week of a month
Simple One Line SQL Statement
Declare #CurrentDate as datetime
set #CurrentDate = '5/31/2014'
Select (Datepart(week,#CurrentDate) - (Datepart(week,cast(month(#CurrentDate) as varchar) + '/1/' + cast(Year(#CurrentDate) as varchar))) + 1) as WeekOfMonth
Stored Procedure
Declare #CurrentDate as datetime
Declare #BeginWeek as int
Declare #EndWeek as Int
set #CurrentDate = '5/31/2014'
SET #EndWeek = DATEPART(week,eomonth(#CurrentDate))
SET #BeginWeek = Datepart(week,cast(month(#CurrentDate) as varchar) + '/1/' + cast(Year(#CurrentDate) as varchar))
Select Datepart(week,#CurrentDate) - #BeginWeek + 1
Select #BeginWeek as beginweek
select #EndWeek as endweek
Select Datepart(week,#CurrentDate)
Using the above logic we could fill the DIMDATE table with holiday days

Get a list of mondays between date range

I need to show N records with each record being 1 weeks worth of summarized data. One of the inputs will be a date range.
Since each week should start with monday, I want to get a list of all the monday dates in the date range. Any suggestions to get that?
Note: This has to run on SQL 2005
declare #dt datetime
SET #dt = '2010-01-01'
declare #dtEnd datetime
SET #dtEnd = '2010-12-04'
DECLARE #day AS NVARCHAR(30)
WHILE (#dt < #dtEnd) BEGIN
-- insert into table(datefield)
-- values(#dt)
SET #day = UPPER(DATENAME(weekday,#dt))
IF #day = 'MONDAY'
--PRINT 'date is: ' + CAST(#dt AS VARCHAR(30))
BEGIN
PRINT 'date is: ' + CAST(#dt AS VARCHAR(30)) + ' ' + #day
END
SET #dt = DATEADD(day, 1, #dt)
END
The code you show seems to work, but it can be made more efficient. The code I show below will calculate the first Monday on or after the start date. Then it is used in the same loop but we simply add 7 days each time through the loop.
declare #dt datetime,
#dtEnd datetime
SET #dt = '2010-01-01'
SET #dtEnd = '2010-12-04'
Set #dt = DateAdd(Day, 7 - DateDiff(Day, 0, #dt) % 7, #dt)
WHILE (#dt < #dtEnd) BEGIN
PRINT 'date is: ' + CAST(#dt AS VARCHAR(30)) + ' ' + DateName(Weekday, #dt)
SET #dt = DATEADD(day, 7, #dt)
END
One thing I notice about your code is that you are using DateName, which works ok for displaying the weekday name, but not so good for making decisions (like you are doing). The problem with DateName is that it uses the language setting of the currently logged in user.
The following code shows this behavior.
set language 'spanish'
Select DateName(Weekday, GetDate())
set language 'us_english'
Select DateName(Weekday, GetDate())
Basically, if the language of a user was set to spanish, your code would fail but mine would not. Mine works because it uses the fact that January 1, 1900 was a Monday. It calculates the number of days and takes a mod of that and uses that mod to add the correct number of days. Since January 1, 1900 will always be a Monday, and we are not using the DateName function, this code will work without regard to the language setting.
SELECT * from table t
WHERE t.date between 'startDate' and 'endDate'
AND (SELECT DATEPART(dw,t.StudyDate))=2

T-SQL duration in hours:minutes:seconds

I have average duration between several dates (DATETIME format) ie. 1900-01-01 01:30.00.00.
How can I convert DATETIME to format hours:minutes:seconds where hours can be more that 24 - output format can be VARCHAR.
IE.
1 days 12 hours 5 minutes convert to 36:05:00
2 days 1 hour 10 minutes 5 seconds convert to 49:10:05
etc...
DECLARE #date1 DATETIME = '2011-08-03 13:30'
DECLARE #date2 DATETIME = '2011-08-03 13:00'
DECLARE #date3 DATETIME = '2011-08-03 14:00'
DECLARE #abc DATETIME = '2011-08-03 12:00'
select CAST(AVG(CAST(data-#abc as float)) as datetime)
from
(
select 'data' as label, #date1 as data
union all
select 'data' as label, #date2 as data
union all
select 'data' as label, #date3 as data
) as a
group by label
I would like to get result as 01:30:00 which means that average time is 1 hours and 30 minutes.
I tried it:
CONVERT(VARCHAR(10), CAST(AVG(CAST(data-#abc as float)) as datetime), 108)
but then I get only time portion in HH:MM:SS. When I set #abc = 2011-08-02 then the results will be the same - this is incorrect.
King regards,
Marcin
In T-SQL a datetime is precisely that, a date and a time where the hours can never exceed 24 because that moves it to the next day. You could use datepart to piece the datetime values out and treat them as integers and then rejoin them into the string you want. Depending on your final goal, you may be better of doing this type of work in your application or presentation layers where more general purpose languages often have more robust datetime libraries to work with.
I think you need to write a scalar-valued function that takes an integer argument (time difference in seconds) and format it as needed. For example,
CREATE FUNCTION intToDateTime ( #time_in_secs BIGINT) RETURNS VARCHAR(30)
AS
BEGIN
DECLARE #retval VARCHAR(30);
SET #retval = cast(#time_in_secs/(60*60) as varchar(10))+':'+
cast( (#time_in_secs-#time_in_secs/(60*60)*3600)/60 as varchar(10))+':'+
cast( (#time_in_secs-#time_in_secs/(60)*60) as varchar(10));
return #retval;
END
This function needs some changes - you may want to display leading zero for 0-9(i.g. '00' instead of '0' as this function currently does); also you need to handle negative values in a better way.
Now you can use it with DATEDIFF(second, #val1,#val2).
Hope I pointed you to the right direction.
select cast(cast(cast(t as float) *24 as int) as varchar) + right(convert(varchar,t, 20), 6)
from(
select cast(AVG(CAST(data-#abc as float)) as datetime) t
from
(
select 'data' as label, #date1 as data
union all
select 'data' as label, #date2 as data
union all
select 'data' as label, #date3 as data
) as a
group by label
) a
Result:
1:30:00
You can't convert datetime to handle non-real dates and times.
However, you can get an output that looks like a datetime, simply by concatenating an hours string with ':' with minutes, etc.
Lookup the DATEADD() and DATEDIFF() functions...

TSQL need to return last day of month only, how do I drop year, month & time?

I am writing a function in T-SQL returning the last day of the month regardless of the date input.
Here is my code:
Alter Function dbo.FN_Get_Last_Day_in_Month2
(#FN_InputDt Datetime)
Returns smalldatetime
as
Begin
Declare #Result smalldatetime
Set #Result =
case when #FN_InputDt <> 01-01-1900 then
DATEADD(m, DATEDIFF(M, 0,#FN_InputDt)+1, -1)
Else 0 End
Return #Result
End
The code is not working correctly, here is a test that shows the bad behavior:
SELECT dbo.fn_get_last_day_in_month (07-05-2010)
Here is the (incorrect) result:
2010-07-31 00:00:00
What is 07-05-2010...May 7th or July 5th? You need to use a safe date format, take a look at Setting a standard DateFormat for SQL Server
example from How to find the first and last days in years, months etc
DECLARE #d DATETIME
SET #d = '20100705' -- notice ISO format
SELECT
DATEADD(yy, DATEDIFF(yy, 0, #d), 0) AS FirstDayOfYear,
DATEADD(yy, DATEDIFF(yy, 0, #d)+1, -1) AS LastDayOfYear,
DATEADD(qq, DATEDIFF(qq, 0, #d), 0) AS FirstDayOfQuarter,
DATEADD(qq, DATEDIFF(qq, 0, #d)+1, -1) AS LastDayOfQuarter,
DATEADD(mm, DATEDIFF(mm, 0, #d), 0) AS FirstDayOfMonth,
DATEADD(mm, DATEDIFF(mm, 0, #d)+1, -1) AS LastDayOfMonth,
#d - DATEDIFF(dd, ##DATEFIRST - 1, #d) % 7 AS FirstDayOfWeek,
#d - DATEDIFF(dd, ##DATEFIRST - 1, #d) % 7 + 6 AS LastDayOfWeek
for just the day use day or datepart
select DAY(getdate()),
DATEPART(dd,GETDATE())
Cast the return value to a SQL datetime type, and then call the "DAY" function to get the day in as an integer. See the function reference here:
http://msdn.microsoft.com/en-us/library/ms176052.aspx
Not sure which database you're using, but this should be a standard function across all databases.
I'd return a DATETIME, I've had trouble with SMALLDATETIME in the past.
DECLARE #Result DATETIME
SET #Result = DATEADD(m , 1, #FN_Input);
RETURN CAST(FLOOR(CAST(DATEADD(d, DATEPART(d, #Result) * -1, #Result) AS FLOAT)) AS DATETIME)
Also, I think you may be a victim of SQL's complete disregard of date formatting. Always, always, always, when typing a string into test a SQL function use the following format;
'05 Jul 2010'
Your function probably works but it interpreted your date as 5th July - not 7th May.
DECLARE #date DATETIME = '20130624';
SELECT Day(EOMONTH ( #date )) AS LastDay;
GO