Which function is faster Abs or IIf? - sql

I ran the following test to determine the difference in efficiency between IIf and Abs:
Public Sub TestSpeed()
Dim i As Long
Dim res As Integer
Debug.Print "*** IIF ***"
Debug.Print Format(Now, "HH:mm:ss")
For i = 1 To 999999999
res = IIf(-1 = True, 1, 0)
Next
Debug.Print Format(Now, "HH:mm:ss")
Debug.Print "*** ABS **"
Debug.Print Format(Now, "HH:mm:ss")
For i = 1 To 999999999
res = Abs(-1)
Next
Debug.Print Format(Now, "HH:mm:ss")
End Sub
The results show that Abs is about 12 times faster:
TestSpeed
*** IIF ***
15:59:08
16:01:26
*** ABS **
16:01:26
16:01:37
Can anyone support this or prove that the contrary is true?
EDIT:
One situation in which one may need to decide between the two functions is for doing multiple counts based on criteria in an SQL query such as:
SELECT Sum(Abs(Colour = 'Yellow')) AS CountOfYellowItems, Sum(Abs(Votes>3) AS CountOfMoreThanThreeVotes FROM tblItems
versus
SELECT Sum(IIf(Colour = 'Yellow' ,1 ,0)) AS CountOfYellowItems, Sum(IIf(Votes > 3 ,1 ,0) AS CountOfMoreThanThreeVotes FROM tblItems

I ran a similar test that supports your findings:
Option Compare Database
Option Explicit
Public Sub SpeedTest()
Const LoopLimit = 99999999
Dim Tasked(LoopLimit) As Boolean, i As Long
Dim Total As Long, t0 As Single, Elapsed As Single
For i = 0 To LoopLimit
Tasked(i) = False
Next
Debug.Print "*** IIF ***"
Total = 0
t0 = Timer
For i = 0 To LoopLimit
Total = Total + IIf(Tasked(i) = True, 1, 0)
Next
Elapsed = Timer - t0
Debug.Print "Elapsed time: " & Format(Elapsed, "0.0") & " seconds."
Debug.Print "Average time: " & Format(Elapsed / (LoopLimit + 1) * 1000000000, "0") & " nanoseconds."
Debug.Print "*** ABS ***"
Total = 0
t0 = Timer
For i = 0 To LoopLimit
Total = Total + Abs(Tasked(i))
Next
Elapsed = Timer - t0
Debug.Print "Elapsed time: " & Format(Elapsed, "0.0") & " seconds."
Debug.Print "Average time: " & Format(Elapsed / (LoopLimit + 1) * 1000000000, "0") & " nanoseconds."
End Sub
resulting in
*** IIF ***
Elapsed time: 19.0 seconds.
Average time: 190 nanoseconds.
*** ABS ***
Elapsed time: 2.4 seconds.
Average time: 24 nanoseconds.
In terms of raw execution speed, Abs(BooleanValue) appears to be an order of magnitude faster than IIf(BooleanValue = True, 1, 0).
Whether or not that difference has a significant impact on the overall performance of the code in which the functions may be used depends very much on the context, as illustrated here.

I have had a look at this in SQL for comparison.
DECLARE #i As BIGINT = 1
DECLARE #res As INT
declare #v BIGINT = -1
Print '*** ABS **'
DECLARE #s2 datetime = GETDATE()
Print Format(#s2, 'HH:mm:ss')
SET #i = 1
WHILE #i < 9999999
begin
SET #res = Abs(#v)
SET #i = #i + 1
end
DECLARE #e2 datetime = GETDATE()
Print Format(#e2, 'HH:mm:ss')
Print DATEDIFF(MILLISECOND, #s2,#e2)
DECLARE #i As BIGINT = 1
DECLARE #res As INT
declare #v INT = -1
Print '*** IIF **'
DECLARE #s1 datetime = GETDATE()
Print Format(#s1, 'HH:mm:ss')
SET #i = 1
WHILE #i < 9999999
begin
SET #res = IIf(#v < 0 , #v*-1, #v)
SET #i = #i + 1
END
DECLARE #e1 datetime = GETDATE()
Print Format(#e1, 'HH:mm:ss')
Print DATEDIFF(MILLISECOND, #s1,#e1)
You can't run both statements together to get a fair comparison. I think there is almost no difference. One case where IIF is better is where
#v BIGINT = -2147483648 (or greater), as ABS would just fail with an overflow.

Related

Incorrect Syntax; Access VBA function to SQL Server

I am converting a large MS Access application to run almost entirely on SQL Server to increase performance as the Access DB is ancient and extremely slow. As such I am working on recreating the VBA functions into SQL functions. SQL Server is giving me syntax errors all over the place and I am trying to understand why, here is the VBA:
Function AlphaNum(prodno As String)
Dim i As Long
If Len(prodno) = 0 Then Exit Function
For i = 1 To Len(prodno)
If Mid(prodno, i, 1) Like "[0-9a-z]" Then AlphaNum = AlphaNum & Mid(prodno, i, 1)
Next i
If IsNumeric(AlphaNum) Then
While Left(AlphaNum, 1) = "0"
AlphaNum = Mid(AlphaNum, 2)
Wend
End If
End Function
I've gotten this far with SQL.. I am certainly not an expert at this either, any ideas?
CREATE FUNCTION dbo.[NAL_AlphaNum]
(
#prodno varchar(10)
)
RETURNS VarChar(10)
BEGIN
DECLARE #counter INT,
#AlphaNum varchar(10)
SET #counter='1'
CASE WHEN Len(#prodno) = 0 THEN EXIT
WHILE #counter < Len(#prodno)
BEGIN
CASE WHEN Mid(#prodno, i, 1) LIKE '[0-9a-z]'
THEN #AlphaNum = #AlphaNum + Mid(#prodno, i, 1)
SET #counter = #counter + 1
END
CASE WHEN IsNumeric(#AlphaNum) THEN
WHILE Left(#AlphaNum, 1) = '0'
#AlphaNum = Mid(#AlphaNum, 2)
END
RETURN #AlphaNum
END;
Thank you in advance.

SQL Binomial Distribution/Arithmetical Overflow

I created a function for a cumulative binomial distribution. It works well for extremely modest sample sizes, but I get a arithmetical overflow on larger samples.
The largest culprit is the n!. In excel 170! = 7.3E+306. 171! = #NUM!
Excel has an internal function that calculates binomial distribution, and it works with ns much, much larger than 170.
Is there something I can do to limit the magnitude of the #s generated?
EDIT: I played with this
SET #probout = 2*3*4*5*6*7*8*9*10*11*12
Worked fine
SET #probout = 2*3*4*5*6*7*8*9*10*11*12*13/10000
Resulted in overflow
Function below.
ALTER FUNCTION [dbo].[binomdist_cumulative]
(
#n int
,#k int
,#p float
)
RETURNS float
AS
BEGIN
-- Local Variable Declarations
-- ---------------------------
DECLARE #kfac float
,#nfac float
,#nkfac float
,#i float
,#f int
,#probout float
SET #i = 0
SET #f = 0
SET #nfac = 0
SET #kfac = 0
SET #nkfac = 0
SET #probout = 0
WHILE #i <= #k
BEGIN
--k!
SET #f = #i-1
SET #kfac = #i
IF #kfac > 0
BEGIN
WHILE #f > 0
BEGIN
SET #kfac = #kfac*#f
SET #f = #f -1
END
END
ELSE
BEGIN
SET #kfac = 1
END
--n!
SET #f = #n-1
SET #nfac = #n
IF #nfac > 0
BEGIN
WHILE #f > 0
BEGIN
SET #nfac = #nfac * #f
SET #f = #f -1
END
END
ELSE
BEGIN
SET #nfac = 1
END
--(n-k)!
SET #f = #n-#i-1
SET #nkfac = #n-#i
IF #nkfac > 0
BEGIN
WHILE #f > 0
BEGIN
SET #nkfac = #nkfac * #f
SET #f = #f -1
END
END
ELSE
BEGIN
SET #nkfac = 1
END
--Accumulate distribution
SET #probout = #probout + #nfac/(#kfac*#nkfac)*POWER(#p,#i)*POWER(1-#p,#n-#i)
SET #i = #i+1
END
RETURN #probout
END
Let me give you a hint.
If you calculate the full factorials, you are quickly going to get overflows. If you do an incremental calculation, the you won't.
For instance, instead of calculating (5 // 3) as (5*4*3*2*1) / ((3*2) * (3*2*1)), calculate it as: (5 / 3) * (4 / 2) * (3 / 3) * (2 / 2) * (1 / 1). . . oh, wait, you can see that the last three terms are all "1".
To be clear, you want to calculate the product of:
((n - i) / (n - k - i)
For i between 0 and k - 1. That is, you are dividing the product of k consecutive numbers ending in n with k consecutive numbers starting with 1.
You'll see that this incremental approach will forestall the issues with overflow.

How to calculate difference between 2 dates when it goes to past? VBA, EXCEL

I have a problem with calculating difference between 2 dates where first is older than second.
For example: I want to find difference between
5.5.2015 and 1.11.2014
I used function
=IF((A(DATEDIF(B12,$W$3,"M")<=12,RANK(Q12,Q:Q)<=11)),Q12;0)
but the function is limited only to situations where the second date is higher than the first one.
I want to know whether B12 is within last 12 months from given date. If it is, then I want to calculate with it.
Is there any way to calculate backwards in excel or VBA?
Thank you.
I know this is an old post already but for anyone who needs this...
Function FindDateDiff(myDate1 As Date, myDate2 As Date) As String
Dim myYears As Long, myMonths As Long, myDays As Long
Dim yearString As String, monthString As String, dayString As String, FinalString As String
If myDate1 > myDate2 Then
myYears = Year(myDate1) - Year(myDate2)
myMonths = Month(myDate1) - Month(myDate2)
myDays = Day(myDate1) - Day(myDate2)
If myDays < 0 Then
myMonths = myMonths - 1
myDays = Day(WorksheetFunction.EoMonth(myDate1, 0)) - Abs(myDays) - 1
End If
Else
myYears = Year(myDate2) - Year(myDate1)
myMonths = Month(myDate2) - Month(myDate1)
myDays = Day(myDate2) - Day(myDate1)
If myDays < 0 Then
myMonths = myMonths - 1
myDays = Day(WorksheetFunction.EoMonth(myDate2, 0)) - Abs(myDays) - 1
End If
End If
If myMonths < 0 Then
myYears = myYears - 1
myMonths = 12 - Abs(myMonths)
End If
If myYears = 0 Then
yearString = ""
ElseIf myYears = 1 Then
yearString = myYears & " year, "
ElseIf myYears > 1 Then
yearString = myYears & " years, "
End If
If myMonths = 0 Then
monthString = ""
ElseIf myMonths = 1 Then
monthString = myMonths & " month, "
ElseIf myMonths > 1 Then
monthString = myMonths & " months, "
End If
If myDays = 0 Then
dayString = ""
ElseIf myDays = 1 Then
dayString = myDays & " day"
ElseIf myDays > 1 Then
dayString = myDays & " days"
End If
FinalString = yearString & monthString & dayString
If Right(FinalString, 2) = ", " Then FinalString = Left(FinalString, Len(FinalString) - 2)
FindDateDiff= FinalString
End Function
Just paste this function in a new module in the workbook and you can start calling this function. '=FindDateDiff(A1,B1)'
This function only require 2 dates as arguments and the order doesn't matter.
I've tested this function with both UK and US format, both works exactly the same.
I used DateDiff before, but the calculation for days and months returns an incorrect value and could be very confuse sometimes.
In VBA use the same function.
NoOfDays = DateDiff("D", DATE1, DATE2)
NoOfDays returns either positive or negative value depending on the dates
I have it solved by using ISERROR
=IF(ISERROR(DATEDIF(RC[-16],R3C23,""M"")<=12),0,RC[-1])

A function returns a MsgBox 10 times?

Found a function on Excelguru which I changed a few things in and gonna edit some more. The idea is to use this to register worked hours and minutes.
There is one thing in this I don't understand: if I type the wrong time in the column reff I get a msg that its wrong, but it wont disappear unless I click it 10 times. I cant see what Im doing wrong. The entire code is posted and Im grateful for any help.
Use his function as part of the formula in the sheet like: TimeValue($E2;$F2;"16:00";"18:00";B2;9;C2)
Function TimeValue(FromTime As String, ToTime As String, StartTime As String, StopTime As String, Optional Weekday As String, Optional Daynr As Integer, Optional Holiday As String)
Dim x As Long
Dim F As Double
Dim T As Double
Dim Start As Double
Dim Stopp As Double
Dim Min As Long
Dim Day As Integer
Dim OverMid As Boolean
Select Case LCase(Weekday)
Case "mandag"
Day = 1
Case "tirsdag"
Day = 2
Case "onsdag"
Day = 3
Case "torsdag"
Day = 4
Case "fredag"
Day = 5
Case "lordag"
Day = 6
Case "sondag"
Day = 7
Case "x"
Day = 8
Case Else
Day = 0
End Select
OverMid = False
If LCase(Holiday) = "x" Then Day = 8
If Len(FromTime) = 0 Or Len(ToTime) = 0 Then
Exit Function
End If
If Len(FromTime) <> 5 Then
MsgBox ("Use format TT:MM - From time is wrong:" & FromTime)
Exit Function
End If
If Len(ToTime) <> 5 Then
MsgBox ("Use format TT:MM - To time is wrong:" & ToTime)
Exit Function
End If
F = Val(Left(FromTime, 2)) * 60 + Val(Right(FromTime, 2))
T = Val(Left(ToTime, 2)) * 60 + Val(Right(ToTime, 2))
Start = Val(Left(StartTime, 2)) * 60 + Val(Right(StartTime, 2))
Stopp = Val(Left(StopTime, 2)) * 60 + Val(Right(StopTime, 2))
If T = 0 Then T = 24 * 60
If T < F Then
T = T + 24 * 60
OverMid = True
End If
If Stopp = 0 Then Stopp = 24 * 60
For x = F + 1 To T
If x > Start And x <= Stopp Then
Min = Min + 1
End If
Next x
If OverMid = True Then
For x = 0 To Val(Left(ToTime, 2)) * 60 + Val(Right(ToTime, 2))
If x > Start And x <= Stopp Then
Min = Min + 1
End If
Next x
End If
'If weekday is set, equal to day
If Daynr <> 0 Then
If Daynr <> 9 Then
If Day <> Daynr Then Min = 0
End If
If Daynr = 9 And (Day > 5) Then
Min = 0
End If
End If
TimeValue = Min / 60
End Function
And the sub in the sheets
Private Sub Worksheet_Change(ByVal Target As Range)
Dim streng As String
Dim k As Long
Dim r As Long
k = Target.Column
r = Target.Row
If Cells(1, k) = "P" Then
If Cells(r, k) = "x" Then
Cells(r, 4) = "x"
Else
Cells(r, 4) = ""
End If
End If
End Sub
Message boxes really don't belong in UDFs (VBA functions meant to be used as spreadsheet functions).
Instead of the message box you could use code like:
If Len(FromTime) <> 5 Then
TimeValue = "Error! Use format TT:MM - From time is wrong:" & FromTime
Exit Function
Or perhaps:
If Len(FromTime) <> 5 Then
TimeValue = CVErr(xlErrValue)
Exit Function
This later will cause #VALUE! to display in the cell. Include enough documentation in your spreadsheet so that users can interpret such error values.

I cant get the loop right

Ok I have got this far and if you run it, it will do what you ask. Now when I type in 99999 or -99999 it will not end. Can someone tell me what I am doing wrong. I am suppose to loop until a sentinel value of -99999 is enter for previous meter reading.
Sub Main()
' program to compute a consumer’s electric bill. It will calculate the bill for one or more customers
' by looping until a sentinel value of -99999 is entered for the previous meter reading.
Dim previousReading As Integer = 0
Dim currentReading As Integer = 0
Do While (previousReading <> -99999)
Dim salesTax As Double
' prompt user to input value for previous reading then convert to integer
Console.WriteLine("Enter the value of previous meter reading")
previousReading = Convert.ToInt32(Console.ReadLine())
' prompt user to input value for current reading then convert to integer
Console.WriteLine("Enter the value of current meter reading")
currentReading = Convert.ToInt32(Console.ReadLine())
Dim kwhConsumed As Integer
Dim electricCharge, totalBill As Double
' calculate KWH consumed
kwhConsumed = currentReading - previousReading
' Use select case to determine electricCharge
Select Case kwhConsumed
Case Is < 500
electricCharge = kwhConsumed * 0.05
Case 500 To 1000
electricCharge = 25 + ((kwhConsumed - 500) * 0.055)
Case Is > 1000
electricCharge = 52.5 + ((kwhConsumed - 1000) * 0.06)
End Select
' calculate sales tax
salesTax = electricCharge * 0.085
' calculate total charges
totalBill = electricCharge + salesTax
' Output values for kwhConsumed, electricCharge, salesTax, and totalBill
Console.WriteLine("KWH consumed = " & kwhConsumed & " KWH")
Console.WriteLine("Electric charge = $" & Math.Round(electricCharge, 2))
Console.WriteLine("Sales tax = $" & Math.Round(salesTax, 2))
Console.WriteLine("Total bill = $" & Math.Round(totalBill, 2))
Loop
End Sub
You can try using string comparison instead for previousReading <> -99999. You also need to use absolute value to consider both -99999 and 99999. Do something like this
Do While (previousReading <> 99999)
//code
previousReading = Math.Abs(Convert.ToInt32(Console.ReadLine()))
//code
Loop
I'm guessing this is homework?
Instead of blurting out the answer, I wonder if you might think about inserting a Debug.Print statement and some kind of "break" statement after your previousReading = Convert.ToInt32 statement. To look for the "break" statement, search for "vb.net exit loop" and see what pops up.